@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.js
CHANGED
|
@@ -89,7 +89,9 @@ var init_constants = __esm({
|
|
|
89
89
|
/** Group chat: processed event IDs for deduplication */
|
|
90
90
|
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
91
91
|
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
92
|
-
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
|
|
92
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids",
|
|
93
|
+
/** Processed V6 combined transfer IDs for Nostr re-delivery dedup */
|
|
94
|
+
PROCESSED_COMBINED_TRANSFER_IDS: "processed_combined_transfer_ids"
|
|
93
95
|
};
|
|
94
96
|
STORAGE_KEYS = {
|
|
95
97
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3286,14 +3288,149 @@ var InstantSplitExecutor = class {
|
|
|
3286
3288
|
this.devMode = config.devMode ?? false;
|
|
3287
3289
|
}
|
|
3288
3290
|
/**
|
|
3289
|
-
*
|
|
3291
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3290
3292
|
*
|
|
3291
|
-
*
|
|
3293
|
+
* Steps 1-5 of the V5 flow:
|
|
3292
3294
|
* 1. Create and submit burn commitment
|
|
3293
3295
|
* 2. Wait for burn proof
|
|
3294
3296
|
* 3. Create mint commitments with SplitMintReason
|
|
3295
3297
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3296
|
-
* 5.
|
|
3298
|
+
* 5. Package V5 bundle
|
|
3299
|
+
*
|
|
3300
|
+
* The caller is responsible for sending the bundle and then calling
|
|
3301
|
+
* `startBackground()` on the result to begin mint proof + change token creation.
|
|
3302
|
+
*/
|
|
3303
|
+
async buildSplitBundle(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, options) {
|
|
3304
|
+
const splitGroupId = crypto.randomUUID();
|
|
3305
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3306
|
+
console.log(`[InstantSplit] Building V5 bundle for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3307
|
+
const coinId = new CoinId3(fromHex2(coinIdHex));
|
|
3308
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3309
|
+
const recipientTokenId = new TokenId3(await sha2563(seedString));
|
|
3310
|
+
const senderTokenId = new TokenId3(await sha2563(seedString + "_sender"));
|
|
3311
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3312
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3313
|
+
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3314
|
+
tokenToSplit.type,
|
|
3315
|
+
this.signingService.algorithm,
|
|
3316
|
+
this.signingService.publicKey,
|
|
3317
|
+
HashAlgorithm3.SHA256
|
|
3318
|
+
);
|
|
3319
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3320
|
+
const builder = new TokenSplitBuilder2();
|
|
3321
|
+
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3322
|
+
builder.createToken(
|
|
3323
|
+
recipientTokenId,
|
|
3324
|
+
tokenToSplit.type,
|
|
3325
|
+
new Uint8Array(0),
|
|
3326
|
+
coinDataA,
|
|
3327
|
+
senderAddress,
|
|
3328
|
+
// Mint to sender first, then transfer
|
|
3329
|
+
recipientSalt,
|
|
3330
|
+
null
|
|
3331
|
+
);
|
|
3332
|
+
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3333
|
+
builder.createToken(
|
|
3334
|
+
senderTokenId,
|
|
3335
|
+
tokenToSplit.type,
|
|
3336
|
+
new Uint8Array(0),
|
|
3337
|
+
coinDataB,
|
|
3338
|
+
senderAddress,
|
|
3339
|
+
senderSalt,
|
|
3340
|
+
null
|
|
3341
|
+
);
|
|
3342
|
+
const split = await builder.build(tokenToSplit);
|
|
3343
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3344
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3345
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3346
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3347
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3348
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3349
|
+
}
|
|
3350
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3351
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3352
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3353
|
+
console.log(`[InstantSplit] Burn proof received`);
|
|
3354
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3355
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3356
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3357
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3358
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3359
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3360
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3361
|
+
);
|
|
3362
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3363
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3364
|
+
);
|
|
3365
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3366
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3367
|
+
}
|
|
3368
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3369
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3370
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3371
|
+
recipientMintCommitment.transactionData,
|
|
3372
|
+
recipientAddress,
|
|
3373
|
+
transferSalt,
|
|
3374
|
+
this.signingService
|
|
3375
|
+
);
|
|
3376
|
+
const mintedPredicate = await UnmaskedPredicate3.create(
|
|
3377
|
+
recipientTokenId,
|
|
3378
|
+
tokenToSplit.type,
|
|
3379
|
+
this.signingService,
|
|
3380
|
+
HashAlgorithm3.SHA256,
|
|
3381
|
+
recipientSalt
|
|
3382
|
+
);
|
|
3383
|
+
const mintedState = new TokenState3(mintedPredicate, null);
|
|
3384
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3385
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3386
|
+
let nametagTokenJson;
|
|
3387
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3388
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3389
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3390
|
+
}
|
|
3391
|
+
const bundle = {
|
|
3392
|
+
version: "5.0",
|
|
3393
|
+
type: "INSTANT_SPLIT",
|
|
3394
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3395
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3396
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3397
|
+
amount: splitAmount.toString(),
|
|
3398
|
+
coinId: coinIdHex,
|
|
3399
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3400
|
+
splitGroupId,
|
|
3401
|
+
senderPubkey,
|
|
3402
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3403
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3404
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3405
|
+
finalRecipientStateJson: "",
|
|
3406
|
+
// Recipient creates their own
|
|
3407
|
+
recipientAddressJson: recipientAddressStr,
|
|
3408
|
+
nametagTokenJson
|
|
3409
|
+
};
|
|
3410
|
+
return {
|
|
3411
|
+
bundle,
|
|
3412
|
+
splitGroupId,
|
|
3413
|
+
startBackground: async () => {
|
|
3414
|
+
if (!options?.skipBackground) {
|
|
3415
|
+
await this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3416
|
+
signingService: this.signingService,
|
|
3417
|
+
tokenType: tokenToSplit.type,
|
|
3418
|
+
coinId,
|
|
3419
|
+
senderTokenId,
|
|
3420
|
+
senderSalt,
|
|
3421
|
+
onProgress: options?.onBackgroundProgress,
|
|
3422
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3423
|
+
onStorageSync: options?.onStorageSync
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
/**
|
|
3430
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3431
|
+
*
|
|
3432
|
+
* Builds the bundle via buildSplitBundle(), sends via transport,
|
|
3433
|
+
* and starts background processing.
|
|
3297
3434
|
*
|
|
3298
3435
|
* @param tokenToSplit - The SDK token to split
|
|
3299
3436
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3307,117 +3444,19 @@ var InstantSplitExecutor = class {
|
|
|
3307
3444
|
*/
|
|
3308
3445
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3309
3446
|
const startTime = performance.now();
|
|
3310
|
-
const splitGroupId = crypto.randomUUID();
|
|
3311
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3312
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3313
3447
|
try {
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3320
|
-
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3321
|
-
tokenToSplit.type,
|
|
3322
|
-
this.signingService.algorithm,
|
|
3323
|
-
this.signingService.publicKey,
|
|
3324
|
-
HashAlgorithm3.SHA256
|
|
3325
|
-
);
|
|
3326
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3327
|
-
const builder = new TokenSplitBuilder2();
|
|
3328
|
-
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3329
|
-
builder.createToken(
|
|
3330
|
-
recipientTokenId,
|
|
3331
|
-
tokenToSplit.type,
|
|
3332
|
-
new Uint8Array(0),
|
|
3333
|
-
coinDataA,
|
|
3334
|
-
senderAddress,
|
|
3335
|
-
// Mint to sender first, then transfer
|
|
3336
|
-
recipientSalt,
|
|
3337
|
-
null
|
|
3338
|
-
);
|
|
3339
|
-
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3340
|
-
builder.createToken(
|
|
3341
|
-
senderTokenId,
|
|
3342
|
-
tokenToSplit.type,
|
|
3343
|
-
new Uint8Array(0),
|
|
3344
|
-
coinDataB,
|
|
3345
|
-
senderAddress,
|
|
3346
|
-
senderSalt,
|
|
3347
|
-
null
|
|
3348
|
-
);
|
|
3349
|
-
const split = await builder.build(tokenToSplit);
|
|
3350
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3351
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3352
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3353
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3354
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3355
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3356
|
-
}
|
|
3357
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3358
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3359
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3360
|
-
const burnDuration = performance.now() - startTime;
|
|
3361
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3362
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3363
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3364
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3365
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3366
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3367
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3368
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3369
|
-
);
|
|
3370
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3371
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3372
|
-
);
|
|
3373
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3374
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3375
|
-
}
|
|
3376
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3377
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3378
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3379
|
-
recipientMintCommitment.transactionData,
|
|
3448
|
+
const buildResult = await this.buildSplitBundle(
|
|
3449
|
+
tokenToSplit,
|
|
3450
|
+
splitAmount,
|
|
3451
|
+
remainderAmount,
|
|
3452
|
+
coinIdHex,
|
|
3380
3453
|
recipientAddress,
|
|
3381
|
-
|
|
3382
|
-
this.signingService
|
|
3383
|
-
);
|
|
3384
|
-
const mintedPredicate = await UnmaskedPredicate3.create(
|
|
3385
|
-
recipientTokenId,
|
|
3386
|
-
tokenToSplit.type,
|
|
3387
|
-
this.signingService,
|
|
3388
|
-
HashAlgorithm3.SHA256,
|
|
3389
|
-
recipientSalt
|
|
3454
|
+
options
|
|
3390
3455
|
);
|
|
3391
|
-
|
|
3392
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3456
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3393
3457
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3394
|
-
let nametagTokenJson;
|
|
3395
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3396
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3397
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3398
|
-
}
|
|
3399
|
-
const bundle = {
|
|
3400
|
-
version: "5.0",
|
|
3401
|
-
type: "INSTANT_SPLIT",
|
|
3402
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3403
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3404
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3405
|
-
amount: splitAmount.toString(),
|
|
3406
|
-
coinId: coinIdHex,
|
|
3407
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3408
|
-
splitGroupId,
|
|
3409
|
-
senderPubkey,
|
|
3410
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3411
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3412
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3413
|
-
finalRecipientStateJson: "",
|
|
3414
|
-
// Recipient creates their own
|
|
3415
|
-
recipientAddressJson: recipientAddressStr,
|
|
3416
|
-
nametagTokenJson
|
|
3417
|
-
};
|
|
3418
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3419
3458
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3420
|
-
token: JSON.stringify(bundle),
|
|
3459
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3421
3460
|
proof: null,
|
|
3422
3461
|
// Proof is included in the bundle
|
|
3423
3462
|
memo: options?.memo,
|
|
@@ -3428,25 +3467,13 @@ var InstantSplitExecutor = class {
|
|
|
3428
3467
|
const criticalPathDuration = performance.now() - startTime;
|
|
3429
3468
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3430
3469
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3431
|
-
|
|
3432
|
-
if (!options?.skipBackground) {
|
|
3433
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3434
|
-
signingService: this.signingService,
|
|
3435
|
-
tokenType: tokenToSplit.type,
|
|
3436
|
-
coinId,
|
|
3437
|
-
senderTokenId,
|
|
3438
|
-
senderSalt,
|
|
3439
|
-
onProgress: options?.onBackgroundProgress,
|
|
3440
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3441
|
-
onStorageSync: options?.onStorageSync
|
|
3442
|
-
});
|
|
3443
|
-
}
|
|
3470
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3444
3471
|
return {
|
|
3445
3472
|
success: true,
|
|
3446
3473
|
nostrEventId,
|
|
3447
|
-
splitGroupId,
|
|
3474
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3448
3475
|
criticalPathDurationMs: criticalPathDuration,
|
|
3449
|
-
backgroundStarted:
|
|
3476
|
+
backgroundStarted: true,
|
|
3450
3477
|
backgroundPromise
|
|
3451
3478
|
};
|
|
3452
3479
|
} catch (error) {
|
|
@@ -3455,7 +3482,6 @@ var InstantSplitExecutor = class {
|
|
|
3455
3482
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3456
3483
|
return {
|
|
3457
3484
|
success: false,
|
|
3458
|
-
splitGroupId,
|
|
3459
3485
|
criticalPathDurationMs: duration,
|
|
3460
3486
|
error: errorMessage,
|
|
3461
3487
|
backgroundStarted: false
|
|
@@ -3660,6 +3686,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3660
3686
|
function isInstantSplitBundleV5(obj) {
|
|
3661
3687
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3662
3688
|
}
|
|
3689
|
+
function isCombinedTransferBundleV6(obj) {
|
|
3690
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
3691
|
+
const b = obj;
|
|
3692
|
+
return b.version === "6.0" && b.type === "COMBINED_TRANSFER";
|
|
3693
|
+
}
|
|
3663
3694
|
|
|
3664
3695
|
// modules/payments/InstantSplitProcessor.ts
|
|
3665
3696
|
function fromHex3(hex) {
|
|
@@ -4313,6 +4344,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4313
4344
|
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4314
4345
|
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4315
4346
|
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4347
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4348
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4316
4349
|
// Storage event subscriptions (push-based sync)
|
|
4317
4350
|
storageEventUnsubscribers = [];
|
|
4318
4351
|
syncDebounceTimer = null;
|
|
@@ -4413,10 +4446,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4413
4446
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4414
4447
|
}
|
|
4415
4448
|
}
|
|
4449
|
+
for (const [id, token] of this.tokens) {
|
|
4450
|
+
try {
|
|
4451
|
+
if (token.sdkData) {
|
|
4452
|
+
const data = JSON.parse(token.sdkData);
|
|
4453
|
+
if (data?._placeholder) {
|
|
4454
|
+
this.tokens.delete(id);
|
|
4455
|
+
console.log(`[Payments] Removed stale placeholder token: ${id}`);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
} catch {
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4416
4461
|
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4417
4462
|
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4418
4463
|
await this.loadPendingV5Tokens();
|
|
4419
4464
|
await this.loadProcessedSplitGroupIds();
|
|
4465
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4420
4466
|
await this.loadHistory();
|
|
4421
4467
|
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4422
4468
|
if (pending2) {
|
|
@@ -4508,12 +4554,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4508
4554
|
token.status = "transferring";
|
|
4509
4555
|
this.tokens.set(token.id, token);
|
|
4510
4556
|
}
|
|
4557
|
+
await this.save();
|
|
4511
4558
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4512
4559
|
result.status = "submitted";
|
|
4513
4560
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4514
4561
|
const transferMode = request.transferMode ?? "instant";
|
|
4515
|
-
if (
|
|
4516
|
-
if (
|
|
4562
|
+
if (transferMode === "conservative") {
|
|
4563
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4517
4564
|
this.log("Executing conservative split...");
|
|
4518
4565
|
const splitExecutor = new TokenSplitExecutor({
|
|
4519
4566
|
stateTransitionClient: stClient,
|
|
@@ -4557,27 +4604,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4557
4604
|
requestIdHex: splitRequestIdHex
|
|
4558
4605
|
});
|
|
4559
4606
|
this.log(`Conservative split transfer completed`);
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
|
-
const
|
|
4607
|
+
}
|
|
4608
|
+
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4609
|
+
const token = tokenWithAmount.uiToken;
|
|
4610
|
+
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4611
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4612
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4613
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4614
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4615
|
+
}
|
|
4616
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4617
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4618
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4619
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4620
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4621
|
+
memo: request.memo
|
|
4622
|
+
});
|
|
4623
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4624
|
+
const requestIdBytes = commitment.requestId;
|
|
4625
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4626
|
+
result.tokenTransfers.push({
|
|
4627
|
+
sourceTokenId: token.id,
|
|
4628
|
+
method: "direct",
|
|
4629
|
+
requestIdHex
|
|
4630
|
+
});
|
|
4631
|
+
this.log(`Token ${token.id} sent via CONSERVATIVE, requestId: ${requestIdHex}`);
|
|
4632
|
+
await this.removeToken(token.id);
|
|
4633
|
+
}
|
|
4634
|
+
} else {
|
|
4635
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4636
|
+
const senderPubkey = this.deps.identity.chainPubkey;
|
|
4637
|
+
let changeTokenPlaceholderId = null;
|
|
4638
|
+
let builtSplit = null;
|
|
4639
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4640
|
+
this.log("Building instant split bundle...");
|
|
4563
4641
|
const executor = new InstantSplitExecutor({
|
|
4564
4642
|
stateTransitionClient: stClient,
|
|
4565
4643
|
trustBase,
|
|
4566
4644
|
signingService,
|
|
4567
4645
|
devMode
|
|
4568
4646
|
});
|
|
4569
|
-
|
|
4647
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4570
4648
|
splitPlan.tokenToSplit.sdkToken,
|
|
4571
4649
|
splitPlan.splitAmount,
|
|
4572
4650
|
splitPlan.remainderAmount,
|
|
4573
4651
|
splitPlan.coinId,
|
|
4574
4652
|
recipientAddress,
|
|
4575
|
-
this.deps.transport,
|
|
4576
|
-
recipientPubkey,
|
|
4577
4653
|
{
|
|
4578
4654
|
memo: request.memo,
|
|
4579
4655
|
onChangeTokenCreated: async (changeToken) => {
|
|
4580
4656
|
const changeTokenData = changeToken.toJSON();
|
|
4657
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4658
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4659
|
+
}
|
|
4581
4660
|
const uiToken = {
|
|
4582
4661
|
id: crypto.randomUUID(),
|
|
4583
4662
|
coinId: request.coinId,
|
|
@@ -4600,65 +4679,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4600
4679
|
}
|
|
4601
4680
|
}
|
|
4602
4681
|
);
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
this.
|
|
4608
|
-
|
|
4682
|
+
this.log(`Split bundle built: splitGroupId=${builtSplit.splitGroupId}`);
|
|
4683
|
+
}
|
|
4684
|
+
const directCommitments = await Promise.all(
|
|
4685
|
+
splitPlan.tokensToTransferDirectly.map(
|
|
4686
|
+
(tw) => this.createSdkCommitment(tw.uiToken, recipientAddress, signingService)
|
|
4687
|
+
)
|
|
4688
|
+
);
|
|
4689
|
+
const directTokenEntries = splitPlan.tokensToTransferDirectly.map(
|
|
4690
|
+
(tw, i) => ({
|
|
4691
|
+
sourceToken: JSON.stringify(tw.sdkToken.toJSON()),
|
|
4692
|
+
commitmentData: JSON.stringify(directCommitments[i].toJSON()),
|
|
4693
|
+
amount: tw.uiToken.amount,
|
|
4694
|
+
coinId: tw.uiToken.coinId,
|
|
4695
|
+
tokenId: extractTokenIdFromSdkData(tw.uiToken.sdkData) || void 0
|
|
4696
|
+
})
|
|
4697
|
+
);
|
|
4698
|
+
const combinedBundle = {
|
|
4699
|
+
version: "6.0",
|
|
4700
|
+
type: "COMBINED_TRANSFER",
|
|
4701
|
+
transferId: result.id,
|
|
4702
|
+
splitBundle: builtSplit?.bundle ?? null,
|
|
4703
|
+
directTokens: directTokenEntries,
|
|
4704
|
+
totalAmount: request.amount.toString(),
|
|
4705
|
+
coinId: request.coinId,
|
|
4706
|
+
senderPubkey,
|
|
4707
|
+
memo: request.memo
|
|
4708
|
+
};
|
|
4709
|
+
console.log(
|
|
4710
|
+
`[Payments] Sending V6 combined bundle: transfer=${result.id.slice(0, 8)}... split=${!!builtSplit} direct=${directTokenEntries.length}`
|
|
4711
|
+
);
|
|
4712
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4713
|
+
token: JSON.stringify(combinedBundle),
|
|
4714
|
+
proof: null,
|
|
4715
|
+
memo: request.memo,
|
|
4716
|
+
sender: { transportPubkey: senderPubkey }
|
|
4717
|
+
});
|
|
4718
|
+
console.log(`[Payments] V6 combined bundle sent successfully`);
|
|
4719
|
+
if (builtSplit) {
|
|
4720
|
+
const bgPromise = builtSplit.startBackground();
|
|
4721
|
+
this.pendingBackgroundTasks.push(bgPromise);
|
|
4722
|
+
}
|
|
4723
|
+
if (builtSplit && splitPlan.remainderAmount) {
|
|
4724
|
+
changeTokenPlaceholderId = crypto.randomUUID();
|
|
4725
|
+
const placeholder = {
|
|
4726
|
+
id: changeTokenPlaceholderId,
|
|
4727
|
+
coinId: request.coinId,
|
|
4728
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4729
|
+
name: this.getCoinName(request.coinId),
|
|
4730
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4731
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4732
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4733
|
+
status: "transferring",
|
|
4734
|
+
createdAt: Date.now(),
|
|
4735
|
+
updatedAt: Date.now(),
|
|
4736
|
+
sdkData: JSON.stringify({ _placeholder: true })
|
|
4737
|
+
};
|
|
4738
|
+
this.tokens.set(placeholder.id, placeholder);
|
|
4739
|
+
this.log(`Placeholder change token created: ${placeholder.id} (${placeholder.amount})`);
|
|
4740
|
+
}
|
|
4741
|
+
for (const commitment of directCommitments) {
|
|
4742
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4743
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4744
|
+
);
|
|
4745
|
+
}
|
|
4746
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4609
4747
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4610
4748
|
result.tokenTransfers.push({
|
|
4611
4749
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4612
4750
|
method: "split",
|
|
4613
|
-
splitGroupId:
|
|
4614
|
-
nostrEventId: instantResult.nostrEventId
|
|
4751
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4615
4752
|
});
|
|
4616
|
-
this.log(`Instant split transfer completed`);
|
|
4617
4753
|
}
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
}
|
|
4628
|
-
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4629
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4630
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4631
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4632
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4633
|
-
memo: request.memo
|
|
4634
|
-
});
|
|
4635
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4636
|
-
} else {
|
|
4637
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4638
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4639
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4640
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4641
|
-
memo: request.memo
|
|
4754
|
+
for (let i = 0; i < splitPlan.tokensToTransferDirectly.length; i++) {
|
|
4755
|
+
const token = splitPlan.tokensToTransferDirectly[i].uiToken;
|
|
4756
|
+
const commitment = directCommitments[i];
|
|
4757
|
+
const requestIdBytes = commitment.requestId;
|
|
4758
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4759
|
+
result.tokenTransfers.push({
|
|
4760
|
+
sourceTokenId: token.id,
|
|
4761
|
+
method: "direct",
|
|
4762
|
+
requestIdHex
|
|
4642
4763
|
});
|
|
4643
|
-
|
|
4644
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4645
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4646
|
-
);
|
|
4764
|
+
await this.removeToken(token.id);
|
|
4647
4765
|
}
|
|
4648
|
-
|
|
4649
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4650
|
-
result.tokenTransfers.push({
|
|
4651
|
-
sourceTokenId: token.id,
|
|
4652
|
-
method: "direct",
|
|
4653
|
-
requestIdHex
|
|
4654
|
-
});
|
|
4655
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4656
|
-
await this.removeToken(token.id);
|
|
4766
|
+
this.log(`V6 combined transfer completed`);
|
|
4657
4767
|
}
|
|
4658
4768
|
result.status = "delivered";
|
|
4659
4769
|
await this.save();
|
|
4660
4770
|
await this.removeFromOutbox(result.id);
|
|
4661
4771
|
result.status = "completed";
|
|
4772
|
+
const tokenMap = new Map(result.tokens.map((t) => [t.id, t]));
|
|
4773
|
+
const sentTokenIds = result.tokenTransfers.map((tt) => ({
|
|
4774
|
+
id: tt.sourceTokenId,
|
|
4775
|
+
// For split tokens, use splitAmount (the portion sent), not the original token amount
|
|
4776
|
+
amount: tt.method === "split" ? splitPlan.splitAmount?.toString() || "0" : tokenMap.get(tt.sourceTokenId)?.amount || "0",
|
|
4777
|
+
source: tt.method === "split" ? "split" : "direct"
|
|
4778
|
+
}));
|
|
4662
4779
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4663
4780
|
await this.addToHistory({
|
|
4664
4781
|
type: "SENT",
|
|
@@ -4671,7 +4788,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4671
4788
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4672
4789
|
memo: request.memo,
|
|
4673
4790
|
transferId: result.id,
|
|
4674
|
-
tokenId: sentTokenId || void 0
|
|
4791
|
+
tokenId: sentTokenId || void 0,
|
|
4792
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4675
4793
|
});
|
|
4676
4794
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4677
4795
|
return result;
|
|
@@ -4841,6 +4959,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4841
4959
|
};
|
|
4842
4960
|
}
|
|
4843
4961
|
}
|
|
4962
|
+
// ===========================================================================
|
|
4963
|
+
// Shared Helpers for V5 and V6 Receiver Processing
|
|
4964
|
+
// ===========================================================================
|
|
4965
|
+
/**
|
|
4966
|
+
* Save a V5 split bundle as an unconfirmed token (shared by V5 standalone and V6 combined).
|
|
4967
|
+
* Returns the created UI token, or null if deduped.
|
|
4968
|
+
*
|
|
4969
|
+
* @param deferPersistence - If true, skip addToken/save calls (caller batches them).
|
|
4970
|
+
* The token is still added to the in-memory map for dedup; caller must call save().
|
|
4971
|
+
*/
|
|
4972
|
+
async saveUnconfirmedV5Token(bundle, senderPubkey, deferPersistence = false) {
|
|
4973
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4974
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
4975
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
4976
|
+
return null;
|
|
4977
|
+
}
|
|
4978
|
+
const registry = TokenRegistry.getInstance();
|
|
4979
|
+
const pendingData = {
|
|
4980
|
+
type: "v5_bundle",
|
|
4981
|
+
stage: "RECEIVED",
|
|
4982
|
+
bundleJson: JSON.stringify(bundle),
|
|
4983
|
+
senderPubkey,
|
|
4984
|
+
savedAt: Date.now(),
|
|
4985
|
+
attemptCount: 0
|
|
4986
|
+
};
|
|
4987
|
+
const uiToken = {
|
|
4988
|
+
id: deterministicId,
|
|
4989
|
+
coinId: bundle.coinId,
|
|
4990
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4991
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4992
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4993
|
+
amount: bundle.amount,
|
|
4994
|
+
status: "submitted",
|
|
4995
|
+
// UNCONFIRMED
|
|
4996
|
+
createdAt: Date.now(),
|
|
4997
|
+
updatedAt: Date.now(),
|
|
4998
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4999
|
+
};
|
|
5000
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5001
|
+
if (deferPersistence) {
|
|
5002
|
+
this.tokens.set(uiToken.id, uiToken);
|
|
5003
|
+
} else {
|
|
5004
|
+
await this.addToken(uiToken);
|
|
5005
|
+
await this.saveProcessedSplitGroupIds();
|
|
5006
|
+
}
|
|
5007
|
+
return uiToken;
|
|
5008
|
+
}
|
|
5009
|
+
/**
|
|
5010
|
+
* Save a commitment-only (NOSTR-FIRST) token and start proof polling.
|
|
5011
|
+
* Shared by standalone NOSTR-FIRST handler and V6 combined handler.
|
|
5012
|
+
* Returns the created UI token, or null if deduped/tombstoned.
|
|
5013
|
+
*
|
|
5014
|
+
* @param deferPersistence - If true, skip save() and commitment submission
|
|
5015
|
+
* (caller batches them). Token is added to in-memory map + proof polling is queued.
|
|
5016
|
+
* @param skipGenesisDedup - If true, skip genesis-ID-only dedup. V6 handler sets this
|
|
5017
|
+
* because bundle-level dedup protects against replays, and split children share genesis IDs.
|
|
5018
|
+
*/
|
|
5019
|
+
async saveCommitmentOnlyToken(sourceTokenInput, commitmentInput, senderPubkey, deferPersistence = false, skipGenesisDedup = false) {
|
|
5020
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5021
|
+
const sdkData = typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput);
|
|
5022
|
+
const nostrTokenId = extractTokenIdFromSdkData(sdkData);
|
|
5023
|
+
const nostrStateHash = extractStateHashFromSdkData(sdkData);
|
|
5024
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5025
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5026
|
+
return null;
|
|
5027
|
+
}
|
|
5028
|
+
if (nostrTokenId) {
|
|
5029
|
+
for (const existing of this.tokens.values()) {
|
|
5030
|
+
const existingTokenId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5031
|
+
if (existingTokenId !== nostrTokenId) continue;
|
|
5032
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5033
|
+
if (nostrStateHash && existingStateHash === nostrStateHash) {
|
|
5034
|
+
console.log(
|
|
5035
|
+
`[Payments] NOSTR-FIRST: Skipping duplicate token state ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`
|
|
5036
|
+
);
|
|
5037
|
+
return null;
|
|
5038
|
+
}
|
|
5039
|
+
if (!skipGenesisDedup) {
|
|
5040
|
+
console.log(
|
|
5041
|
+
`[Payments] NOSTR-FIRST: Skipping replay of finalized token ${nostrTokenId.slice(0, 8)}...`
|
|
5042
|
+
);
|
|
5043
|
+
return null;
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
const token = {
|
|
5048
|
+
id: crypto.randomUUID(),
|
|
5049
|
+
coinId: tokenInfo.coinId,
|
|
5050
|
+
symbol: tokenInfo.symbol,
|
|
5051
|
+
name: tokenInfo.name,
|
|
5052
|
+
decimals: tokenInfo.decimals,
|
|
5053
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5054
|
+
amount: tokenInfo.amount,
|
|
5055
|
+
status: "submitted",
|
|
5056
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5057
|
+
createdAt: Date.now(),
|
|
5058
|
+
updatedAt: Date.now(),
|
|
5059
|
+
sdkData
|
|
5060
|
+
};
|
|
5061
|
+
this.tokens.set(token.id, token);
|
|
5062
|
+
if (!deferPersistence) {
|
|
5063
|
+
await this.save();
|
|
5064
|
+
}
|
|
5065
|
+
try {
|
|
5066
|
+
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
5067
|
+
const requestIdBytes = commitment.requestId;
|
|
5068
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5069
|
+
if (!deferPersistence) {
|
|
5070
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5071
|
+
if (stClient) {
|
|
5072
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5073
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5076
|
+
this.addProofPollingJob({
|
|
5077
|
+
tokenId: token.id,
|
|
5078
|
+
requestIdHex,
|
|
5079
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5080
|
+
startedAt: Date.now(),
|
|
5081
|
+
attemptCount: 0,
|
|
5082
|
+
lastAttemptAt: 0,
|
|
5083
|
+
onProofReceived: async (tokenId) => {
|
|
5084
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
5085
|
+
}
|
|
5086
|
+
});
|
|
5087
|
+
} catch (err) {
|
|
5088
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5089
|
+
}
|
|
5090
|
+
return token;
|
|
5091
|
+
}
|
|
5092
|
+
// ===========================================================================
|
|
5093
|
+
// Combined Transfer V6 — Receiver
|
|
5094
|
+
// ===========================================================================
|
|
5095
|
+
/**
|
|
5096
|
+
* Process a received COMBINED_TRANSFER V6 bundle.
|
|
5097
|
+
*
|
|
5098
|
+
* Unpacks a single Nostr message into its component tokens:
|
|
5099
|
+
* - Optional V5 split bundle (saved as unconfirmed, resolved lazily)
|
|
5100
|
+
* - Zero or more direct tokens (saved as unconfirmed, proof-polled)
|
|
5101
|
+
*
|
|
5102
|
+
* Emits ONE transfer:incoming event and records ONE history entry.
|
|
5103
|
+
*/
|
|
5104
|
+
async processCombinedTransferBundle(bundle, senderPubkey) {
|
|
5105
|
+
this.ensureInitialized();
|
|
5106
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5107
|
+
await this.loadedPromise;
|
|
5108
|
+
}
|
|
5109
|
+
if (this.processedCombinedTransferIds.has(bundle.transferId)) {
|
|
5110
|
+
console.log(`[Payments] V6 combined transfer ${bundle.transferId.slice(0, 12)}... already processed, skipping`);
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
console.log(
|
|
5114
|
+
`[Payments] Processing V6 combined transfer ${bundle.transferId.slice(0, 12)}... (split=${!!bundle.splitBundle}, direct=${bundle.directTokens.length})`
|
|
5115
|
+
);
|
|
5116
|
+
const allTokens = [];
|
|
5117
|
+
const tokenBreakdown = [];
|
|
5118
|
+
const parsedDirectEntries = bundle.directTokens.map((entry) => ({
|
|
5119
|
+
sourceToken: typeof entry.sourceToken === "string" ? JSON.parse(entry.sourceToken) : entry.sourceToken,
|
|
5120
|
+
commitment: typeof entry.commitmentData === "string" ? JSON.parse(entry.commitmentData) : entry.commitmentData
|
|
5121
|
+
}));
|
|
5122
|
+
if (bundle.splitBundle) {
|
|
5123
|
+
const splitToken = await this.saveUnconfirmedV5Token(bundle.splitBundle, senderPubkey, true);
|
|
5124
|
+
if (splitToken) {
|
|
5125
|
+
allTokens.push(splitToken);
|
|
5126
|
+
tokenBreakdown.push({ id: splitToken.id, amount: splitToken.amount, source: "split" });
|
|
5127
|
+
} else {
|
|
5128
|
+
console.warn(`[Payments] V6: split token was deduped/failed \u2014 amount=${bundle.splitBundle.amount}`);
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
const directResults = await Promise.all(
|
|
5132
|
+
parsedDirectEntries.map(
|
|
5133
|
+
({ sourceToken, commitment }) => this.saveCommitmentOnlyToken(sourceToken, commitment, senderPubkey, true, true)
|
|
5134
|
+
)
|
|
5135
|
+
);
|
|
5136
|
+
for (let i = 0; i < directResults.length; i++) {
|
|
5137
|
+
const token = directResults[i];
|
|
5138
|
+
if (token) {
|
|
5139
|
+
allTokens.push(token);
|
|
5140
|
+
tokenBreakdown.push({ id: token.id, amount: token.amount, source: "direct" });
|
|
5141
|
+
} else {
|
|
5142
|
+
const entry = bundle.directTokens[i];
|
|
5143
|
+
console.warn(
|
|
5144
|
+
`[Payments] V6: direct token #${i} dropped (amount=${entry.amount}, tokenId=${entry.tokenId?.slice(0, 12) ?? "N/A"})`
|
|
5145
|
+
);
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
if (allTokens.length === 0) {
|
|
5149
|
+
console.log(`[Payments] V6 combined transfer: all tokens deduped, nothing to save`);
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
5152
|
+
this.processedCombinedTransferIds.add(bundle.transferId);
|
|
5153
|
+
const [senderInfo] = await Promise.all([
|
|
5154
|
+
this.resolveSenderInfo(senderPubkey),
|
|
5155
|
+
this.save(),
|
|
5156
|
+
this.saveProcessedCombinedTransferIds(),
|
|
5157
|
+
...bundle.splitBundle ? [this.saveProcessedSplitGroupIds()] : []
|
|
5158
|
+
]);
|
|
5159
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5160
|
+
if (stClient) {
|
|
5161
|
+
for (const { commitment } of parsedDirectEntries) {
|
|
5162
|
+
TransferCommitment4.fromJSON(commitment).then(
|
|
5163
|
+
(c) => stClient.submitTransferCommitment(c)
|
|
5164
|
+
).catch(
|
|
5165
|
+
(err) => console.error("[Payments] V6 background commitment submit failed:", err)
|
|
5166
|
+
);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5170
|
+
id: bundle.transferId,
|
|
5171
|
+
senderPubkey,
|
|
5172
|
+
senderNametag: senderInfo.senderNametag,
|
|
5173
|
+
tokens: allTokens,
|
|
5174
|
+
memo: bundle.memo,
|
|
5175
|
+
receivedAt: Date.now()
|
|
5176
|
+
});
|
|
5177
|
+
const actualAmount = allTokens.reduce((sum, t) => sum + BigInt(t.amount || "0"), 0n).toString();
|
|
5178
|
+
await this.addToHistory({
|
|
5179
|
+
type: "RECEIVED",
|
|
5180
|
+
amount: actualAmount,
|
|
5181
|
+
coinId: bundle.coinId,
|
|
5182
|
+
symbol: allTokens[0]?.symbol || bundle.coinId,
|
|
5183
|
+
timestamp: Date.now(),
|
|
5184
|
+
senderPubkey,
|
|
5185
|
+
...senderInfo,
|
|
5186
|
+
memo: bundle.memo,
|
|
5187
|
+
transferId: bundle.transferId,
|
|
5188
|
+
tokenId: allTokens[0]?.id,
|
|
5189
|
+
tokenIds: tokenBreakdown
|
|
5190
|
+
});
|
|
5191
|
+
if (bundle.splitBundle) {
|
|
5192
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5193
|
+
});
|
|
5194
|
+
this.scheduleResolveUnconfirmed();
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
/**
|
|
5198
|
+
* Persist processed combined transfer IDs to KV storage.
|
|
5199
|
+
*/
|
|
5200
|
+
async saveProcessedCombinedTransferIds() {
|
|
5201
|
+
const ids = Array.from(this.processedCombinedTransferIds);
|
|
5202
|
+
if (ids.length > 0) {
|
|
5203
|
+
await this.deps.storage.set(
|
|
5204
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS,
|
|
5205
|
+
JSON.stringify(ids)
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
/**
|
|
5210
|
+
* Load processed combined transfer IDs from KV storage.
|
|
5211
|
+
*/
|
|
5212
|
+
async loadProcessedCombinedTransferIds() {
|
|
5213
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS);
|
|
5214
|
+
if (!data) return;
|
|
5215
|
+
try {
|
|
5216
|
+
const ids = JSON.parse(data);
|
|
5217
|
+
for (const id of ids) {
|
|
5218
|
+
this.processedCombinedTransferIds.add(id);
|
|
5219
|
+
}
|
|
5220
|
+
} catch {
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
4844
5223
|
/**
|
|
4845
5224
|
* Process a received INSTANT_SPLIT bundle.
|
|
4846
5225
|
*
|
|
@@ -4864,36 +5243,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4864
5243
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
4865
5244
|
}
|
|
4866
5245
|
try {
|
|
4867
|
-
const
|
|
4868
|
-
if (
|
|
4869
|
-
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5246
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5247
|
+
if (!uiToken) {
|
|
4870
5248
|
return { success: true, durationMs: 0 };
|
|
4871
5249
|
}
|
|
4872
|
-
const registry = TokenRegistry.getInstance();
|
|
4873
|
-
const pendingData = {
|
|
4874
|
-
type: "v5_bundle",
|
|
4875
|
-
stage: "RECEIVED",
|
|
4876
|
-
bundleJson: JSON.stringify(bundle),
|
|
4877
|
-
senderPubkey,
|
|
4878
|
-
savedAt: Date.now(),
|
|
4879
|
-
attemptCount: 0
|
|
4880
|
-
};
|
|
4881
|
-
const uiToken = {
|
|
4882
|
-
id: deterministicId,
|
|
4883
|
-
coinId: bundle.coinId,
|
|
4884
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4885
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4886
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4887
|
-
amount: bundle.amount,
|
|
4888
|
-
status: "submitted",
|
|
4889
|
-
// UNCONFIRMED
|
|
4890
|
-
createdAt: Date.now(),
|
|
4891
|
-
updatedAt: Date.now(),
|
|
4892
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4893
|
-
};
|
|
4894
|
-
await this.addToken(uiToken);
|
|
4895
|
-
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
4896
|
-
await this.saveProcessedSplitGroupIds();
|
|
4897
5250
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
4898
5251
|
await this.addToHistory({
|
|
4899
5252
|
type: "RECEIVED",
|
|
@@ -4904,7 +5257,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4904
5257
|
senderPubkey,
|
|
4905
5258
|
...senderInfo,
|
|
4906
5259
|
memo,
|
|
4907
|
-
tokenId:
|
|
5260
|
+
tokenId: uiToken.id
|
|
4908
5261
|
});
|
|
4909
5262
|
this.deps.emitEvent("transfer:incoming", {
|
|
4910
5263
|
id: bundle.splitGroupId,
|
|
@@ -5554,16 +5907,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5554
5907
|
}
|
|
5555
5908
|
/**
|
|
5556
5909
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5557
|
-
* Excludes tokens with status 'spent'
|
|
5910
|
+
* Excludes tokens with status 'spent' or 'invalid'.
|
|
5911
|
+
* Tokens with status 'transferring' are counted as unconfirmed (visible in UI as "Sending").
|
|
5558
5912
|
*/
|
|
5559
5913
|
aggregateTokens(coinId) {
|
|
5560
5914
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5561
5915
|
for (const token of this.tokens.values()) {
|
|
5562
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
5916
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5563
5917
|
if (coinId && token.coinId !== coinId) continue;
|
|
5564
5918
|
const key = token.coinId;
|
|
5565
5919
|
const amount = BigInt(token.amount);
|
|
5566
5920
|
const isConfirmed = token.status === "confirmed";
|
|
5921
|
+
const isTransferring = token.status === "transferring";
|
|
5567
5922
|
const existing = assetsMap.get(key);
|
|
5568
5923
|
if (existing) {
|
|
5569
5924
|
if (isConfirmed) {
|
|
@@ -5573,6 +5928,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5573
5928
|
existing.unconfirmedAmount += amount;
|
|
5574
5929
|
existing.unconfirmedTokenCount++;
|
|
5575
5930
|
}
|
|
5931
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5576
5932
|
} else {
|
|
5577
5933
|
assetsMap.set(key, {
|
|
5578
5934
|
coinId: token.coinId,
|
|
@@ -5583,7 +5939,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5583
5939
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5584
5940
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5585
5941
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5586
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
5942
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
5943
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5587
5944
|
});
|
|
5588
5945
|
}
|
|
5589
5946
|
}
|
|
@@ -5601,6 +5958,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5601
5958
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5602
5959
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5603
5960
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5961
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5604
5962
|
priceUsd: null,
|
|
5605
5963
|
priceEur: null,
|
|
5606
5964
|
change24h: null,
|
|
@@ -6998,7 +7356,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6998
7356
|
/**
|
|
6999
7357
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7000
7358
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7001
|
-
*
|
|
7359
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7002
7360
|
*/
|
|
7003
7361
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7004
7362
|
try {
|
|
@@ -7008,41 +7366,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7008
7366
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7009
7367
|
return;
|
|
7010
7368
|
}
|
|
7011
|
-
const
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
decimals: tokenInfo.decimals,
|
|
7018
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7019
|
-
amount: tokenInfo.amount,
|
|
7020
|
-
status: "submitted",
|
|
7021
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7022
|
-
createdAt: Date.now(),
|
|
7023
|
-
updatedAt: Date.now(),
|
|
7024
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7025
|
-
};
|
|
7026
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7027
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7028
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7029
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7030
|
-
return;
|
|
7031
|
-
}
|
|
7032
|
-
this.tokens.set(token.id, token);
|
|
7033
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
|
|
7034
|
-
await this.save();
|
|
7035
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
|
|
7369
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7370
|
+
sourceTokenInput,
|
|
7371
|
+
commitmentInput,
|
|
7372
|
+
transfer.senderTransportPubkey
|
|
7373
|
+
);
|
|
7374
|
+
if (!token) return;
|
|
7036
7375
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7037
|
-
|
|
7376
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7038
7377
|
id: transfer.id,
|
|
7039
7378
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7040
7379
|
senderNametag: senderInfo.senderNametag,
|
|
7041
7380
|
tokens: [token],
|
|
7042
7381
|
memo: payload.memo,
|
|
7043
7382
|
receivedAt: transfer.timestamp
|
|
7044
|
-
};
|
|
7045
|
-
|
|
7383
|
+
});
|
|
7384
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7046
7385
|
await this.addToHistory({
|
|
7047
7386
|
type: "RECEIVED",
|
|
7048
7387
|
amount: token.amount,
|
|
@@ -7054,29 +7393,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7054
7393
|
memo: payload.memo,
|
|
7055
7394
|
tokenId: nostrTokenId || token.id
|
|
7056
7395
|
});
|
|
7057
|
-
try {
|
|
7058
|
-
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
7059
|
-
const requestIdBytes = commitment.requestId;
|
|
7060
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7061
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7062
|
-
if (stClient) {
|
|
7063
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7064
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7065
|
-
}
|
|
7066
|
-
this.addProofPollingJob({
|
|
7067
|
-
tokenId: token.id,
|
|
7068
|
-
requestIdHex,
|
|
7069
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7070
|
-
startedAt: Date.now(),
|
|
7071
|
-
attemptCount: 0,
|
|
7072
|
-
lastAttemptAt: 0,
|
|
7073
|
-
onProofReceived: async (tokenId) => {
|
|
7074
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7075
|
-
}
|
|
7076
|
-
});
|
|
7077
|
-
} catch (err) {
|
|
7078
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7079
|
-
}
|
|
7080
7396
|
} catch (error) {
|
|
7081
7397
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7082
7398
|
}
|
|
@@ -7195,6 +7511,28 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7195
7511
|
try {
|
|
7196
7512
|
const payload = transfer.payload;
|
|
7197
7513
|
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
7514
|
+
let combinedBundle = null;
|
|
7515
|
+
if (isCombinedTransferBundleV6(payload)) {
|
|
7516
|
+
combinedBundle = payload;
|
|
7517
|
+
} else if (payload.token) {
|
|
7518
|
+
try {
|
|
7519
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7520
|
+
if (isCombinedTransferBundleV6(inner)) {
|
|
7521
|
+
combinedBundle = inner;
|
|
7522
|
+
}
|
|
7523
|
+
} catch {
|
|
7524
|
+
}
|
|
7525
|
+
}
|
|
7526
|
+
if (combinedBundle) {
|
|
7527
|
+
this.log("Processing COMBINED_TRANSFER V6 bundle...");
|
|
7528
|
+
try {
|
|
7529
|
+
await this.processCombinedTransferBundle(combinedBundle, transfer.senderTransportPubkey);
|
|
7530
|
+
this.log("COMBINED_TRANSFER V6 processed successfully");
|
|
7531
|
+
} catch (err) {
|
|
7532
|
+
console.error("[Payments] COMBINED_TRANSFER V6 processing error:", err);
|
|
7533
|
+
}
|
|
7534
|
+
return;
|
|
7535
|
+
}
|
|
7198
7536
|
let instantBundle = null;
|
|
7199
7537
|
if (isInstantSplitBundle(payload)) {
|
|
7200
7538
|
instantBundle = payload;
|
|
@@ -7346,17 +7684,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7346
7684
|
memo: payload.memo,
|
|
7347
7685
|
tokenId: incomingTokenId || token.id
|
|
7348
7686
|
});
|
|
7687
|
+
const incomingTransfer = {
|
|
7688
|
+
id: transfer.id,
|
|
7689
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
7690
|
+
senderNametag: senderInfo.senderNametag,
|
|
7691
|
+
tokens: [token],
|
|
7692
|
+
memo: payload.memo,
|
|
7693
|
+
receivedAt: transfer.timestamp
|
|
7694
|
+
};
|
|
7695
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7696
|
+
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7697
|
+
} else {
|
|
7698
|
+
this.log(`Duplicate transfer ignored: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7349
7699
|
}
|
|
7350
|
-
const incomingTransfer = {
|
|
7351
|
-
id: transfer.id,
|
|
7352
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7353
|
-
senderNametag: senderInfo.senderNametag,
|
|
7354
|
-
tokens: [token],
|
|
7355
|
-
memo: payload.memo,
|
|
7356
|
-
receivedAt: transfer.timestamp
|
|
7357
|
-
};
|
|
7358
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7359
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7360
7700
|
} catch (error) {
|
|
7361
7701
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7362
7702
|
}
|