@unicitylabs/sphere-sdk 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +5 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +5 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +813 -309
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +71 -2
- package/dist/core/index.d.ts +71 -2
- package/dist/core/index.js +813 -309
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +5 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +5 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +7 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +7 -2
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +5 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +5 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +5 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +5 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +7 -2
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +6 -0
- package/dist/impl/nodejs/index.d.ts +6 -0
- package/dist/impl/nodejs/index.js +7 -2
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +815 -309
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -3
- package/dist/index.d.ts +144 -3
- package/dist/index.js +814 -309
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +5 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +5 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.cjs
CHANGED
|
@@ -103,7 +103,11 @@ var init_constants = __esm({
|
|
|
103
103
|
/** Group chat: members for this address */
|
|
104
104
|
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
105
105
|
/** Group chat: processed event IDs for deduplication */
|
|
106
|
-
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
|
|
106
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
107
|
+
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
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"
|
|
107
111
|
};
|
|
108
112
|
STORAGE_KEYS = {
|
|
109
113
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3379,14 +3383,149 @@ var InstantSplitExecutor = class {
|
|
|
3379
3383
|
this.devMode = config.devMode ?? false;
|
|
3380
3384
|
}
|
|
3381
3385
|
/**
|
|
3382
|
-
*
|
|
3386
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3383
3387
|
*
|
|
3384
|
-
*
|
|
3388
|
+
* Steps 1-5 of the V5 flow:
|
|
3385
3389
|
* 1. Create and submit burn commitment
|
|
3386
3390
|
* 2. Wait for burn proof
|
|
3387
3391
|
* 3. Create mint commitments with SplitMintReason
|
|
3388
3392
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3389
|
-
* 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.
|
|
3390
3529
|
*
|
|
3391
3530
|
* @param tokenToSplit - The SDK token to split
|
|
3392
3531
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3400,117 +3539,19 @@ var InstantSplitExecutor = class {
|
|
|
3400
3539
|
*/
|
|
3401
3540
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3402
3541
|
const startTime = performance.now();
|
|
3403
|
-
const splitGroupId = crypto.randomUUID();
|
|
3404
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3405
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3406
3542
|
try {
|
|
3407
|
-
const
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3413
|
-
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3414
|
-
tokenToSplit.type,
|
|
3415
|
-
this.signingService.algorithm,
|
|
3416
|
-
this.signingService.publicKey,
|
|
3417
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3418
|
-
);
|
|
3419
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3420
|
-
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3421
|
-
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3422
|
-
builder.createToken(
|
|
3423
|
-
recipientTokenId,
|
|
3424
|
-
tokenToSplit.type,
|
|
3425
|
-
new Uint8Array(0),
|
|
3426
|
-
coinDataA,
|
|
3427
|
-
senderAddress,
|
|
3428
|
-
// Mint to sender first, then transfer
|
|
3429
|
-
recipientSalt,
|
|
3430
|
-
null
|
|
3431
|
-
);
|
|
3432
|
-
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3433
|
-
builder.createToken(
|
|
3434
|
-
senderTokenId,
|
|
3435
|
-
tokenToSplit.type,
|
|
3436
|
-
new Uint8Array(0),
|
|
3437
|
-
coinDataB,
|
|
3438
|
-
senderAddress,
|
|
3439
|
-
senderSalt,
|
|
3440
|
-
null
|
|
3441
|
-
);
|
|
3442
|
-
const split = await builder.build(tokenToSplit);
|
|
3443
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3444
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3445
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3446
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3447
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3448
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3449
|
-
}
|
|
3450
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3451
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3452
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3453
|
-
const burnDuration = performance.now() - startTime;
|
|
3454
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3455
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3456
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3457
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3458
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3459
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3460
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3461
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3462
|
-
);
|
|
3463
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3464
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3465
|
-
);
|
|
3466
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3467
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3468
|
-
}
|
|
3469
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3470
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3471
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3472
|
-
recipientMintCommitment.transactionData,
|
|
3543
|
+
const buildResult = await this.buildSplitBundle(
|
|
3544
|
+
tokenToSplit,
|
|
3545
|
+
splitAmount,
|
|
3546
|
+
remainderAmount,
|
|
3547
|
+
coinIdHex,
|
|
3473
3548
|
recipientAddress,
|
|
3474
|
-
|
|
3475
|
-
this.signingService
|
|
3549
|
+
options
|
|
3476
3550
|
);
|
|
3477
|
-
|
|
3478
|
-
recipientTokenId,
|
|
3479
|
-
tokenToSplit.type,
|
|
3480
|
-
this.signingService,
|
|
3481
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3482
|
-
recipientSalt
|
|
3483
|
-
);
|
|
3484
|
-
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3485
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3551
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3486
3552
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3487
|
-
let nametagTokenJson;
|
|
3488
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3489
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3490
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3491
|
-
}
|
|
3492
|
-
const bundle = {
|
|
3493
|
-
version: "5.0",
|
|
3494
|
-
type: "INSTANT_SPLIT",
|
|
3495
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3496
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3497
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3498
|
-
amount: splitAmount.toString(),
|
|
3499
|
-
coinId: coinIdHex,
|
|
3500
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3501
|
-
splitGroupId,
|
|
3502
|
-
senderPubkey,
|
|
3503
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3504
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3505
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3506
|
-
finalRecipientStateJson: "",
|
|
3507
|
-
// Recipient creates their own
|
|
3508
|
-
recipientAddressJson: recipientAddressStr,
|
|
3509
|
-
nametagTokenJson
|
|
3510
|
-
};
|
|
3511
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3512
3553
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3513
|
-
token: JSON.stringify(bundle),
|
|
3554
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3514
3555
|
proof: null,
|
|
3515
3556
|
// Proof is included in the bundle
|
|
3516
3557
|
memo: options?.memo,
|
|
@@ -3521,25 +3562,13 @@ var InstantSplitExecutor = class {
|
|
|
3521
3562
|
const criticalPathDuration = performance.now() - startTime;
|
|
3522
3563
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3523
3564
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3524
|
-
|
|
3525
|
-
if (!options?.skipBackground) {
|
|
3526
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3527
|
-
signingService: this.signingService,
|
|
3528
|
-
tokenType: tokenToSplit.type,
|
|
3529
|
-
coinId,
|
|
3530
|
-
senderTokenId,
|
|
3531
|
-
senderSalt,
|
|
3532
|
-
onProgress: options?.onBackgroundProgress,
|
|
3533
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3534
|
-
onStorageSync: options?.onStorageSync
|
|
3535
|
-
});
|
|
3536
|
-
}
|
|
3565
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3537
3566
|
return {
|
|
3538
3567
|
success: true,
|
|
3539
3568
|
nostrEventId,
|
|
3540
|
-
splitGroupId,
|
|
3569
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3541
3570
|
criticalPathDurationMs: criticalPathDuration,
|
|
3542
|
-
backgroundStarted:
|
|
3571
|
+
backgroundStarted: true,
|
|
3543
3572
|
backgroundPromise
|
|
3544
3573
|
};
|
|
3545
3574
|
} catch (error) {
|
|
@@ -3548,7 +3577,6 @@ var InstantSplitExecutor = class {
|
|
|
3548
3577
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3549
3578
|
return {
|
|
3550
3579
|
success: false,
|
|
3551
|
-
splitGroupId,
|
|
3552
3580
|
criticalPathDurationMs: duration,
|
|
3553
3581
|
error: errorMessage,
|
|
3554
3582
|
backgroundStarted: false
|
|
@@ -3753,6 +3781,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3753
3781
|
function isInstantSplitBundleV5(obj) {
|
|
3754
3782
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3755
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
|
+
}
|
|
3756
3789
|
|
|
3757
3790
|
// modules/payments/InstantSplitProcessor.ts
|
|
3758
3791
|
function fromHex3(hex) {
|
|
@@ -4395,6 +4428,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4395
4428
|
// Poll every 2s
|
|
4396
4429
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4397
4430
|
// Max 30 attempts (~60s)
|
|
4431
|
+
// Periodic retry for resolveUnconfirmed (V5 lazy finalization)
|
|
4432
|
+
resolveUnconfirmedTimer = null;
|
|
4433
|
+
static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
|
|
4434
|
+
// Retry every 10s
|
|
4435
|
+
// Guard: ensure load() completes before processing incoming bundles
|
|
4436
|
+
loadedPromise = null;
|
|
4437
|
+
loaded = false;
|
|
4438
|
+
// Persistent dedup: tracks splitGroupIds that have been fully processed.
|
|
4439
|
+
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4440
|
+
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4441
|
+
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4442
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4443
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4398
4444
|
// Storage event subscriptions (push-based sync)
|
|
4399
4445
|
storageEventUnsubscribers = [];
|
|
4400
4446
|
syncDebounceTimer = null;
|
|
@@ -4480,31 +4526,53 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4480
4526
|
*/
|
|
4481
4527
|
async load() {
|
|
4482
4528
|
this.ensureInitialized();
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4529
|
+
const doLoad = async () => {
|
|
4530
|
+
await TokenRegistry.waitForReady();
|
|
4531
|
+
const providers = this.getTokenStorageProviders();
|
|
4532
|
+
for (const [id, provider] of providers) {
|
|
4533
|
+
try {
|
|
4534
|
+
const result = await provider.load();
|
|
4535
|
+
if (result.success && result.data) {
|
|
4536
|
+
this.loadFromStorageData(result.data);
|
|
4537
|
+
this.log(`Loaded metadata from provider ${id}`);
|
|
4538
|
+
break;
|
|
4539
|
+
}
|
|
4540
|
+
} catch (err) {
|
|
4541
|
+
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4492
4542
|
}
|
|
4493
|
-
} catch (err) {
|
|
4494
|
-
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4495
4543
|
}
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
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
|
+
}
|
|
4504
4555
|
}
|
|
4505
|
-
|
|
4556
|
+
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4557
|
+
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4558
|
+
await this.loadPendingV5Tokens();
|
|
4559
|
+
await this.loadProcessedSplitGroupIds();
|
|
4560
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4561
|
+
await this.loadHistory();
|
|
4562
|
+
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4563
|
+
if (pending2) {
|
|
4564
|
+
const transfers = JSON.parse(pending2);
|
|
4565
|
+
for (const transfer of transfers) {
|
|
4566
|
+
this.pendingTransfers.set(transfer.id, transfer);
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
this.loaded = true;
|
|
4570
|
+
};
|
|
4571
|
+
this.loadedPromise = doLoad();
|
|
4572
|
+
await this.loadedPromise;
|
|
4506
4573
|
this.resolveUnconfirmed().catch(() => {
|
|
4507
4574
|
});
|
|
4575
|
+
this.scheduleResolveUnconfirmed();
|
|
4508
4576
|
}
|
|
4509
4577
|
/**
|
|
4510
4578
|
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
@@ -4523,6 +4591,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4523
4591
|
this.paymentRequestResponseHandlers.clear();
|
|
4524
4592
|
this.stopProofPolling();
|
|
4525
4593
|
this.proofPollingJobs.clear();
|
|
4594
|
+
this.stopResolveUnconfirmedPolling();
|
|
4526
4595
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
4527
4596
|
clearTimeout(resolver.timeout);
|
|
4528
4597
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -4580,12 +4649,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4580
4649
|
token.status = "transferring";
|
|
4581
4650
|
this.tokens.set(token.id, token);
|
|
4582
4651
|
}
|
|
4652
|
+
await this.save();
|
|
4583
4653
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4584
4654
|
result.status = "submitted";
|
|
4585
4655
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4586
4656
|
const transferMode = request.transferMode ?? "instant";
|
|
4587
|
-
if (
|
|
4588
|
-
if (
|
|
4657
|
+
if (transferMode === "conservative") {
|
|
4658
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4589
4659
|
this.log("Executing conservative split...");
|
|
4590
4660
|
const splitExecutor = new TokenSplitExecutor({
|
|
4591
4661
|
stateTransitionClient: stClient,
|
|
@@ -4629,27 +4699,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4629
4699
|
requestIdHex: splitRequestIdHex
|
|
4630
4700
|
});
|
|
4631
4701
|
this.log(`Conservative split transfer completed`);
|
|
4632
|
-
}
|
|
4633
|
-
|
|
4634
|
-
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...");
|
|
4635
4736
|
const executor = new InstantSplitExecutor({
|
|
4636
4737
|
stateTransitionClient: stClient,
|
|
4637
4738
|
trustBase,
|
|
4638
4739
|
signingService,
|
|
4639
4740
|
devMode
|
|
4640
4741
|
});
|
|
4641
|
-
|
|
4742
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4642
4743
|
splitPlan.tokenToSplit.sdkToken,
|
|
4643
4744
|
splitPlan.splitAmount,
|
|
4644
4745
|
splitPlan.remainderAmount,
|
|
4645
4746
|
splitPlan.coinId,
|
|
4646
4747
|
recipientAddress,
|
|
4647
|
-
this.deps.transport,
|
|
4648
|
-
recipientPubkey,
|
|
4649
4748
|
{
|
|
4650
4749
|
memo: request.memo,
|
|
4651
4750
|
onChangeTokenCreated: async (changeToken) => {
|
|
4652
4751
|
const changeTokenData = changeToken.toJSON();
|
|
4752
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4753
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4754
|
+
}
|
|
4653
4755
|
const uiToken = {
|
|
4654
4756
|
id: crypto.randomUUID(),
|
|
4655
4757
|
coinId: request.coinId,
|
|
@@ -4672,65 +4774,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4672
4774
|
}
|
|
4673
4775
|
}
|
|
4674
4776
|
);
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
this.
|
|
4680
|
-
|
|
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) {
|
|
4681
4842
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4682
4843
|
result.tokenTransfers.push({
|
|
4683
4844
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4684
4845
|
method: "split",
|
|
4685
|
-
splitGroupId:
|
|
4686
|
-
nostrEventId: instantResult.nostrEventId
|
|
4846
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4687
4847
|
});
|
|
4688
|
-
this.log(`Instant split transfer completed`);
|
|
4689
4848
|
}
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
}
|
|
4700
|
-
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4701
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4702
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4703
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4704
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4705
|
-
memo: request.memo
|
|
4706
|
-
});
|
|
4707
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4708
|
-
} else {
|
|
4709
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4710
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4711
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4712
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4713
|
-
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
|
|
4714
4858
|
});
|
|
4715
|
-
|
|
4716
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4717
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4718
|
-
);
|
|
4859
|
+
await this.removeToken(token.id);
|
|
4719
4860
|
}
|
|
4720
|
-
|
|
4721
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4722
|
-
result.tokenTransfers.push({
|
|
4723
|
-
sourceTokenId: token.id,
|
|
4724
|
-
method: "direct",
|
|
4725
|
-
requestIdHex
|
|
4726
|
-
});
|
|
4727
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4728
|
-
await this.removeToken(token.id);
|
|
4861
|
+
this.log(`V6 combined transfer completed`);
|
|
4729
4862
|
}
|
|
4730
4863
|
result.status = "delivered";
|
|
4731
4864
|
await this.save();
|
|
4732
4865
|
await this.removeFromOutbox(result.id);
|
|
4733
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
|
+
}));
|
|
4734
4874
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4735
4875
|
await this.addToHistory({
|
|
4736
4876
|
type: "SENT",
|
|
@@ -4743,7 +4883,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4743
4883
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4744
4884
|
memo: request.memo,
|
|
4745
4885
|
transferId: result.id,
|
|
4746
|
-
tokenId: sentTokenId || void 0
|
|
4886
|
+
tokenId: sentTokenId || void 0,
|
|
4887
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4747
4888
|
});
|
|
4748
4889
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4749
4890
|
return result;
|
|
@@ -4913,6 +5054,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4913
5054
|
};
|
|
4914
5055
|
}
|
|
4915
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
|
+
}
|
|
4916
5318
|
/**
|
|
4917
5319
|
* Process a received INSTANT_SPLIT bundle.
|
|
4918
5320
|
*
|
|
@@ -4929,39 +5331,17 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4929
5331
|
*/
|
|
4930
5332
|
async processInstantSplitBundle(bundle, senderPubkey, memo) {
|
|
4931
5333
|
this.ensureInitialized();
|
|
5334
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5335
|
+
await this.loadedPromise;
|
|
5336
|
+
}
|
|
4932
5337
|
if (!isInstantSplitBundleV5(bundle)) {
|
|
4933
5338
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
4934
5339
|
}
|
|
4935
5340
|
try {
|
|
4936
|
-
const
|
|
4937
|
-
if (
|
|
4938
|
-
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
5341
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5342
|
+
if (!uiToken) {
|
|
4939
5343
|
return { success: true, durationMs: 0 };
|
|
4940
5344
|
}
|
|
4941
|
-
const registry = TokenRegistry.getInstance();
|
|
4942
|
-
const pendingData = {
|
|
4943
|
-
type: "v5_bundle",
|
|
4944
|
-
stage: "RECEIVED",
|
|
4945
|
-
bundleJson: JSON.stringify(bundle),
|
|
4946
|
-
senderPubkey,
|
|
4947
|
-
savedAt: Date.now(),
|
|
4948
|
-
attemptCount: 0
|
|
4949
|
-
};
|
|
4950
|
-
const uiToken = {
|
|
4951
|
-
id: deterministicId,
|
|
4952
|
-
coinId: bundle.coinId,
|
|
4953
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4954
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4955
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4956
|
-
amount: bundle.amount,
|
|
4957
|
-
status: "submitted",
|
|
4958
|
-
// UNCONFIRMED
|
|
4959
|
-
createdAt: Date.now(),
|
|
4960
|
-
updatedAt: Date.now(),
|
|
4961
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4962
|
-
};
|
|
4963
|
-
await this.addToken(uiToken);
|
|
4964
|
-
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4965
5345
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
4966
5346
|
await this.addToHistory({
|
|
4967
5347
|
type: "RECEIVED",
|
|
@@ -4972,7 +5352,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4972
5352
|
senderPubkey,
|
|
4973
5353
|
...senderInfo,
|
|
4974
5354
|
memo,
|
|
4975
|
-
tokenId:
|
|
5355
|
+
tokenId: uiToken.id
|
|
4976
5356
|
});
|
|
4977
5357
|
this.deps.emitEvent("transfer:incoming", {
|
|
4978
5358
|
id: bundle.splitGroupId,
|
|
@@ -4985,6 +5365,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4985
5365
|
await this.save();
|
|
4986
5366
|
this.resolveUnconfirmed().catch(() => {
|
|
4987
5367
|
});
|
|
5368
|
+
this.scheduleResolveUnconfirmed();
|
|
4988
5369
|
return { success: true, durationMs: 0 };
|
|
4989
5370
|
} catch (error) {
|
|
4990
5371
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -5621,16 +6002,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5621
6002
|
}
|
|
5622
6003
|
/**
|
|
5623
6004
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5624
|
-
* 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").
|
|
5625
6007
|
*/
|
|
5626
6008
|
aggregateTokens(coinId) {
|
|
5627
6009
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5628
6010
|
for (const token of this.tokens.values()) {
|
|
5629
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6011
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5630
6012
|
if (coinId && token.coinId !== coinId) continue;
|
|
5631
6013
|
const key = token.coinId;
|
|
5632
6014
|
const amount = BigInt(token.amount);
|
|
5633
6015
|
const isConfirmed = token.status === "confirmed";
|
|
6016
|
+
const isTransferring = token.status === "transferring";
|
|
5634
6017
|
const existing = assetsMap.get(key);
|
|
5635
6018
|
if (existing) {
|
|
5636
6019
|
if (isConfirmed) {
|
|
@@ -5640,6 +6023,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5640
6023
|
existing.unconfirmedAmount += amount;
|
|
5641
6024
|
existing.unconfirmedTokenCount++;
|
|
5642
6025
|
}
|
|
6026
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5643
6027
|
} else {
|
|
5644
6028
|
assetsMap.set(key, {
|
|
5645
6029
|
coinId: token.coinId,
|
|
@@ -5650,7 +6034,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5650
6034
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5651
6035
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5652
6036
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5653
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6037
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6038
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5654
6039
|
});
|
|
5655
6040
|
}
|
|
5656
6041
|
}
|
|
@@ -5668,6 +6053,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5668
6053
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5669
6054
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5670
6055
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6056
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5671
6057
|
priceUsd: null,
|
|
5672
6058
|
priceEur: null,
|
|
5673
6059
|
change24h: null,
|
|
@@ -5731,28 +6117,70 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5731
6117
|
};
|
|
5732
6118
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5733
6119
|
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5734
|
-
if (!stClient || !trustBase)
|
|
6120
|
+
if (!stClient || !trustBase) {
|
|
6121
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
|
|
6122
|
+
return result;
|
|
6123
|
+
}
|
|
5735
6124
|
const signingService = await this.createSigningService();
|
|
6125
|
+
const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
|
|
6126
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
|
|
5736
6127
|
for (const [tokenId, token] of this.tokens) {
|
|
5737
6128
|
if (token.status !== "submitted") continue;
|
|
5738
6129
|
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5739
6130
|
if (!pending2) {
|
|
6131
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
|
|
5740
6132
|
result.stillPending++;
|
|
5741
6133
|
continue;
|
|
5742
6134
|
}
|
|
5743
6135
|
if (pending2.type === "v5_bundle") {
|
|
6136
|
+
console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
|
|
5744
6137
|
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
6138
|
+
console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
|
|
5745
6139
|
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5746
6140
|
if (progress === "resolved") result.resolved++;
|
|
5747
6141
|
else if (progress === "failed") result.failed++;
|
|
5748
6142
|
else result.stillPending++;
|
|
5749
6143
|
}
|
|
5750
6144
|
}
|
|
5751
|
-
if (result.resolved > 0 || result.failed > 0) {
|
|
6145
|
+
if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
|
|
6146
|
+
console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
|
|
5752
6147
|
await this.save();
|
|
5753
6148
|
}
|
|
5754
6149
|
return result;
|
|
5755
6150
|
}
|
|
6151
|
+
/**
|
|
6152
|
+
* Start a periodic interval that retries resolveUnconfirmed() until all
|
|
6153
|
+
* tokens are confirmed or failed. Stops automatically when nothing is
|
|
6154
|
+
* pending and is cleaned up by destroy().
|
|
6155
|
+
*/
|
|
6156
|
+
scheduleResolveUnconfirmed() {
|
|
6157
|
+
if (this.resolveUnconfirmedTimer) return;
|
|
6158
|
+
const hasUnconfirmed = Array.from(this.tokens.values()).some(
|
|
6159
|
+
(t) => t.status === "submitted"
|
|
6160
|
+
);
|
|
6161
|
+
if (!hasUnconfirmed) {
|
|
6162
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
|
|
6163
|
+
return;
|
|
6164
|
+
}
|
|
6165
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
|
|
6166
|
+
this.resolveUnconfirmedTimer = setInterval(async () => {
|
|
6167
|
+
try {
|
|
6168
|
+
const result = await this.resolveUnconfirmed();
|
|
6169
|
+
if (result.stillPending === 0) {
|
|
6170
|
+
console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
|
|
6171
|
+
this.stopResolveUnconfirmedPolling();
|
|
6172
|
+
}
|
|
6173
|
+
} catch (err) {
|
|
6174
|
+
console.log(`[V5-RESOLVE] Periodic retry error:`, err);
|
|
6175
|
+
}
|
|
6176
|
+
}, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
|
|
6177
|
+
}
|
|
6178
|
+
stopResolveUnconfirmedPolling() {
|
|
6179
|
+
if (this.resolveUnconfirmedTimer) {
|
|
6180
|
+
clearInterval(this.resolveUnconfirmedTimer);
|
|
6181
|
+
this.resolveUnconfirmedTimer = null;
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
5756
6184
|
// ===========================================================================
|
|
5757
6185
|
// Private - V5 Lazy Resolution Helpers
|
|
5758
6186
|
// ===========================================================================
|
|
@@ -5765,10 +6193,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5765
6193
|
pending2.lastAttemptAt = Date.now();
|
|
5766
6194
|
try {
|
|
5767
6195
|
if (pending2.stage === "RECEIVED") {
|
|
6196
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
|
|
5768
6197
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5769
6198
|
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5770
6199
|
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5771
6200
|
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
6201
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
|
|
5772
6202
|
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5773
6203
|
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5774
6204
|
}
|
|
@@ -5776,22 +6206,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5776
6206
|
this.updatePendingFinalization(token, pending2);
|
|
5777
6207
|
}
|
|
5778
6208
|
if (pending2.stage === "MINT_SUBMITTED") {
|
|
6209
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
|
|
5779
6210
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5780
6211
|
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5781
6212
|
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5782
6213
|
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5783
6214
|
if (!proof) {
|
|
6215
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
|
|
5784
6216
|
this.updatePendingFinalization(token, pending2);
|
|
5785
6217
|
return "pending";
|
|
5786
6218
|
}
|
|
6219
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
|
|
5787
6220
|
pending2.mintProofJson = JSON.stringify(proof);
|
|
5788
6221
|
pending2.stage = "MINT_PROVEN";
|
|
5789
6222
|
this.updatePendingFinalization(token, pending2);
|
|
5790
6223
|
}
|
|
5791
6224
|
if (pending2.stage === "MINT_PROVEN") {
|
|
6225
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
|
|
5792
6226
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5793
6227
|
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5794
6228
|
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
6229
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
|
|
5795
6230
|
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5796
6231
|
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5797
6232
|
}
|
|
@@ -5799,13 +6234,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5799
6234
|
this.updatePendingFinalization(token, pending2);
|
|
5800
6235
|
}
|
|
5801
6236
|
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
6237
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
|
|
5802
6238
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5803
6239
|
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5804
6240
|
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5805
6241
|
if (!proof) {
|
|
6242
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
|
|
5806
6243
|
this.updatePendingFinalization(token, pending2);
|
|
5807
6244
|
return "pending";
|
|
5808
6245
|
}
|
|
6246
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
|
|
5809
6247
|
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5810
6248
|
const confirmedToken = {
|
|
5811
6249
|
id: token.id,
|
|
@@ -5821,6 +6259,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5821
6259
|
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5822
6260
|
};
|
|
5823
6261
|
this.tokens.set(tokenId, confirmedToken);
|
|
6262
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
6263
|
+
id: crypto.randomUUID(),
|
|
6264
|
+
status: "completed",
|
|
6265
|
+
tokens: [confirmedToken],
|
|
6266
|
+
tokenTransfers: []
|
|
6267
|
+
});
|
|
5824
6268
|
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5825
6269
|
return "resolved";
|
|
5826
6270
|
}
|
|
@@ -5962,11 +6406,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5962
6406
|
}
|
|
5963
6407
|
}
|
|
5964
6408
|
if (pendingTokens.length > 0) {
|
|
6409
|
+
const json = JSON.stringify(pendingTokens);
|
|
6410
|
+
this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
|
|
5965
6411
|
await this.deps.storage.set(
|
|
5966
6412
|
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5967
|
-
|
|
6413
|
+
json
|
|
5968
6414
|
);
|
|
6415
|
+
const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6416
|
+
if (!verify) {
|
|
6417
|
+
console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
|
|
6418
|
+
} else {
|
|
6419
|
+
this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
|
|
6420
|
+
}
|
|
5969
6421
|
} else {
|
|
6422
|
+
this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
|
|
5970
6423
|
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5971
6424
|
}
|
|
5972
6425
|
}
|
|
@@ -5976,16 +6429,47 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5976
6429
|
*/
|
|
5977
6430
|
async loadPendingV5Tokens() {
|
|
5978
6431
|
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6432
|
+
this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
|
|
5979
6433
|
if (!data) return;
|
|
5980
6434
|
try {
|
|
5981
6435
|
const pendingTokens = JSON.parse(data);
|
|
6436
|
+
this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
|
|
5982
6437
|
for (const token of pendingTokens) {
|
|
5983
6438
|
if (!this.tokens.has(token.id)) {
|
|
5984
6439
|
this.tokens.set(token.id, token);
|
|
6440
|
+
this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
|
|
6441
|
+
} else {
|
|
6442
|
+
this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
|
|
5985
6443
|
}
|
|
5986
6444
|
}
|
|
5987
|
-
|
|
5988
|
-
|
|
6445
|
+
} catch (err) {
|
|
6446
|
+
console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
/**
|
|
6450
|
+
* Persist the set of processed splitGroupIds to KV storage.
|
|
6451
|
+
* This ensures Nostr re-deliveries are ignored across page reloads,
|
|
6452
|
+
* even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
6453
|
+
*/
|
|
6454
|
+
async saveProcessedSplitGroupIds() {
|
|
6455
|
+
const ids = Array.from(this.processedSplitGroupIds);
|
|
6456
|
+
if (ids.length > 0) {
|
|
6457
|
+
await this.deps.storage.set(
|
|
6458
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
|
|
6459
|
+
JSON.stringify(ids)
|
|
6460
|
+
);
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
/**
|
|
6464
|
+
* Load processed splitGroupIds from KV storage.
|
|
6465
|
+
*/
|
|
6466
|
+
async loadProcessedSplitGroupIds() {
|
|
6467
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
|
|
6468
|
+
if (!data) return;
|
|
6469
|
+
try {
|
|
6470
|
+
const ids = JSON.parse(data);
|
|
6471
|
+
for (const id of ids) {
|
|
6472
|
+
this.processedSplitGroupIds.add(id);
|
|
5989
6473
|
}
|
|
5990
6474
|
} catch {
|
|
5991
6475
|
}
|
|
@@ -6640,7 +7124,32 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6640
7124
|
try {
|
|
6641
7125
|
const result = await provider.sync(localData);
|
|
6642
7126
|
if (result.success && result.merged) {
|
|
7127
|
+
const savedTokens = new Map(this.tokens);
|
|
6643
7128
|
this.loadFromStorageData(result.merged);
|
|
7129
|
+
let restoredCount = 0;
|
|
7130
|
+
for (const [tokenId, token] of savedTokens) {
|
|
7131
|
+
if (this.tokens.has(tokenId)) continue;
|
|
7132
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7133
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7134
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
7135
|
+
continue;
|
|
7136
|
+
}
|
|
7137
|
+
if (sdkTokenId) {
|
|
7138
|
+
let hasEquivalent = false;
|
|
7139
|
+
for (const existing of this.tokens.values()) {
|
|
7140
|
+
if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
|
|
7141
|
+
hasEquivalent = true;
|
|
7142
|
+
break;
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
if (hasEquivalent) continue;
|
|
7146
|
+
}
|
|
7147
|
+
this.tokens.set(tokenId, token);
|
|
7148
|
+
restoredCount++;
|
|
7149
|
+
}
|
|
7150
|
+
if (restoredCount > 0) {
|
|
7151
|
+
console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
|
|
7152
|
+
}
|
|
6644
7153
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
6645
7154
|
this.nametags = savedNametags;
|
|
6646
7155
|
}
|
|
@@ -6942,7 +7451,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6942
7451
|
/**
|
|
6943
7452
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
6944
7453
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
6945
|
-
*
|
|
7454
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
6946
7455
|
*/
|
|
6947
7456
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
6948
7457
|
try {
|
|
@@ -6952,40 +7461,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6952
7461
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
6953
7462
|
return;
|
|
6954
7463
|
}
|
|
6955
|
-
const
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
decimals: tokenInfo.decimals,
|
|
6962
|
-
iconUrl: tokenInfo.iconUrl,
|
|
6963
|
-
amount: tokenInfo.amount,
|
|
6964
|
-
status: "submitted",
|
|
6965
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
6966
|
-
createdAt: Date.now(),
|
|
6967
|
-
updatedAt: Date.now(),
|
|
6968
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
6969
|
-
};
|
|
6970
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6971
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6972
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
6973
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
6974
|
-
return;
|
|
6975
|
-
}
|
|
6976
|
-
this.tokens.set(token.id, token);
|
|
6977
|
-
await this.save();
|
|
6978
|
-
this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
|
|
7464
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7465
|
+
sourceTokenInput,
|
|
7466
|
+
commitmentInput,
|
|
7467
|
+
transfer.senderTransportPubkey
|
|
7468
|
+
);
|
|
7469
|
+
if (!token) return;
|
|
6979
7470
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
6980
|
-
|
|
7471
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
6981
7472
|
id: transfer.id,
|
|
6982
7473
|
senderPubkey: transfer.senderTransportPubkey,
|
|
6983
7474
|
senderNametag: senderInfo.senderNametag,
|
|
6984
7475
|
tokens: [token],
|
|
6985
7476
|
memo: payload.memo,
|
|
6986
7477
|
receivedAt: transfer.timestamp
|
|
6987
|
-
};
|
|
6988
|
-
|
|
7478
|
+
});
|
|
7479
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6989
7480
|
await this.addToHistory({
|
|
6990
7481
|
type: "RECEIVED",
|
|
6991
7482
|
amount: token.amount,
|
|
@@ -6997,29 +7488,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6997
7488
|
memo: payload.memo,
|
|
6998
7489
|
tokenId: nostrTokenId || token.id
|
|
6999
7490
|
});
|
|
7000
|
-
try {
|
|
7001
|
-
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
7002
|
-
const requestIdBytes = commitment.requestId;
|
|
7003
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7004
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7005
|
-
if (stClient) {
|
|
7006
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7007
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7008
|
-
}
|
|
7009
|
-
this.addProofPollingJob({
|
|
7010
|
-
tokenId: token.id,
|
|
7011
|
-
requestIdHex,
|
|
7012
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7013
|
-
startedAt: Date.now(),
|
|
7014
|
-
attemptCount: 0,
|
|
7015
|
-
lastAttemptAt: 0,
|
|
7016
|
-
onProofReceived: async (tokenId) => {
|
|
7017
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7018
|
-
}
|
|
7019
|
-
});
|
|
7020
|
-
} catch (err) {
|
|
7021
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7022
|
-
}
|
|
7023
7491
|
} catch (error) {
|
|
7024
7492
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7025
7493
|
}
|
|
@@ -7132,8 +7600,34 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7132
7600
|
}
|
|
7133
7601
|
}
|
|
7134
7602
|
async handleIncomingTransfer(transfer) {
|
|
7603
|
+
if (!this.loaded && this.loadedPromise) {
|
|
7604
|
+
await this.loadedPromise;
|
|
7605
|
+
}
|
|
7135
7606
|
try {
|
|
7136
7607
|
const payload = transfer.payload;
|
|
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
|
+
}
|
|
7137
7631
|
let instantBundle = null;
|
|
7138
7632
|
if (isInstantSplitBundle(payload)) {
|
|
7139
7633
|
instantBundle = payload;
|
|
@@ -7165,7 +7659,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7165
7659
|
return;
|
|
7166
7660
|
}
|
|
7167
7661
|
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
7168
|
-
|
|
7662
|
+
console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
|
|
7169
7663
|
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
7170
7664
|
return;
|
|
7171
7665
|
}
|
|
@@ -7285,17 +7779,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7285
7779
|
memo: payload.memo,
|
|
7286
7780
|
tokenId: incomingTokenId || token.id
|
|
7287
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}`);
|
|
7288
7794
|
}
|
|
7289
|
-
const incomingTransfer = {
|
|
7290
|
-
id: transfer.id,
|
|
7291
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7292
|
-
senderNametag: senderInfo.senderNametag,
|
|
7293
|
-
tokens: [token],
|
|
7294
|
-
memo: payload.memo,
|
|
7295
|
-
receivedAt: transfer.timestamp
|
|
7296
|
-
};
|
|
7297
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7298
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7299
7795
|
} catch (error) {
|
|
7300
7796
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7301
7797
|
}
|
|
@@ -7328,17 +7824,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7328
7824
|
// ===========================================================================
|
|
7329
7825
|
async save() {
|
|
7330
7826
|
const providers = this.getTokenStorageProviders();
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
return
|
|
7334
|
-
}
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7827
|
+
const tokenStats = Array.from(this.tokens.values()).map((t) => {
|
|
7828
|
+
const txf = tokenToTxf(t);
|
|
7829
|
+
return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
|
|
7830
|
+
});
|
|
7831
|
+
console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
|
|
7832
|
+
if (providers.size > 0) {
|
|
7833
|
+
const data = await this.createStorageData();
|
|
7834
|
+
const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
|
|
7835
|
+
console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
|
|
7836
|
+
for (const [id, provider] of providers) {
|
|
7837
|
+
try {
|
|
7838
|
+
await provider.save(data);
|
|
7839
|
+
} catch (err) {
|
|
7840
|
+
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
7841
|
+
}
|
|
7341
7842
|
}
|
|
7843
|
+
} else {
|
|
7844
|
+
console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
|
|
7342
7845
|
}
|
|
7343
7846
|
await this.savePendingV5Tokens();
|
|
7344
7847
|
}
|
|
@@ -7374,6 +7877,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7374
7877
|
}
|
|
7375
7878
|
loadFromStorageData(data) {
|
|
7376
7879
|
const parsed = parseTxfStorageData(data);
|
|
7880
|
+
console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
|
|
7377
7881
|
this.tombstones = parsed.tombstones;
|
|
7378
7882
|
this.tokens.clear();
|
|
7379
7883
|
for (const token of parsed.tokens) {
|