@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.js
CHANGED
|
@@ -87,7 +87,11 @@ var init_constants = __esm({
|
|
|
87
87
|
/** Group chat: members for this address */
|
|
88
88
|
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
89
89
|
/** Group chat: processed event IDs for deduplication */
|
|
90
|
-
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
|
|
90
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
91
|
+
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
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"
|
|
91
95
|
};
|
|
92
96
|
STORAGE_KEYS = {
|
|
93
97
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3284,14 +3288,149 @@ var InstantSplitExecutor = class {
|
|
|
3284
3288
|
this.devMode = config.devMode ?? false;
|
|
3285
3289
|
}
|
|
3286
3290
|
/**
|
|
3287
|
-
*
|
|
3291
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3288
3292
|
*
|
|
3289
|
-
*
|
|
3293
|
+
* Steps 1-5 of the V5 flow:
|
|
3290
3294
|
* 1. Create and submit burn commitment
|
|
3291
3295
|
* 2. Wait for burn proof
|
|
3292
3296
|
* 3. Create mint commitments with SplitMintReason
|
|
3293
3297
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3294
|
-
* 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.
|
|
3295
3434
|
*
|
|
3296
3435
|
* @param tokenToSplit - The SDK token to split
|
|
3297
3436
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3305,117 +3444,19 @@ var InstantSplitExecutor = class {
|
|
|
3305
3444
|
*/
|
|
3306
3445
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3307
3446
|
const startTime = performance.now();
|
|
3308
|
-
const splitGroupId = crypto.randomUUID();
|
|
3309
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3310
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3311
3447
|
try {
|
|
3312
|
-
const
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3318
|
-
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3319
|
-
tokenToSplit.type,
|
|
3320
|
-
this.signingService.algorithm,
|
|
3321
|
-
this.signingService.publicKey,
|
|
3322
|
-
HashAlgorithm3.SHA256
|
|
3323
|
-
);
|
|
3324
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3325
|
-
const builder = new TokenSplitBuilder2();
|
|
3326
|
-
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3327
|
-
builder.createToken(
|
|
3328
|
-
recipientTokenId,
|
|
3329
|
-
tokenToSplit.type,
|
|
3330
|
-
new Uint8Array(0),
|
|
3331
|
-
coinDataA,
|
|
3332
|
-
senderAddress,
|
|
3333
|
-
// Mint to sender first, then transfer
|
|
3334
|
-
recipientSalt,
|
|
3335
|
-
null
|
|
3336
|
-
);
|
|
3337
|
-
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3338
|
-
builder.createToken(
|
|
3339
|
-
senderTokenId,
|
|
3340
|
-
tokenToSplit.type,
|
|
3341
|
-
new Uint8Array(0),
|
|
3342
|
-
coinDataB,
|
|
3343
|
-
senderAddress,
|
|
3344
|
-
senderSalt,
|
|
3345
|
-
null
|
|
3346
|
-
);
|
|
3347
|
-
const split = await builder.build(tokenToSplit);
|
|
3348
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3349
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3350
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3351
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3352
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3353
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3354
|
-
}
|
|
3355
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3356
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3357
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3358
|
-
const burnDuration = performance.now() - startTime;
|
|
3359
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3360
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3361
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3362
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3363
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3364
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3365
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3366
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3367
|
-
);
|
|
3368
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3369
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3370
|
-
);
|
|
3371
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3372
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3373
|
-
}
|
|
3374
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3375
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3376
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3377
|
-
recipientMintCommitment.transactionData,
|
|
3448
|
+
const buildResult = await this.buildSplitBundle(
|
|
3449
|
+
tokenToSplit,
|
|
3450
|
+
splitAmount,
|
|
3451
|
+
remainderAmount,
|
|
3452
|
+
coinIdHex,
|
|
3378
3453
|
recipientAddress,
|
|
3379
|
-
|
|
3380
|
-
this.signingService
|
|
3454
|
+
options
|
|
3381
3455
|
);
|
|
3382
|
-
|
|
3383
|
-
recipientTokenId,
|
|
3384
|
-
tokenToSplit.type,
|
|
3385
|
-
this.signingService,
|
|
3386
|
-
HashAlgorithm3.SHA256,
|
|
3387
|
-
recipientSalt
|
|
3388
|
-
);
|
|
3389
|
-
const mintedState = new TokenState3(mintedPredicate, null);
|
|
3390
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3456
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3391
3457
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3392
|
-
let nametagTokenJson;
|
|
3393
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3394
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3395
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3396
|
-
}
|
|
3397
|
-
const bundle = {
|
|
3398
|
-
version: "5.0",
|
|
3399
|
-
type: "INSTANT_SPLIT",
|
|
3400
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3401
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3402
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3403
|
-
amount: splitAmount.toString(),
|
|
3404
|
-
coinId: coinIdHex,
|
|
3405
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3406
|
-
splitGroupId,
|
|
3407
|
-
senderPubkey,
|
|
3408
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3409
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3410
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3411
|
-
finalRecipientStateJson: "",
|
|
3412
|
-
// Recipient creates their own
|
|
3413
|
-
recipientAddressJson: recipientAddressStr,
|
|
3414
|
-
nametagTokenJson
|
|
3415
|
-
};
|
|
3416
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3417
3458
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3418
|
-
token: JSON.stringify(bundle),
|
|
3459
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3419
3460
|
proof: null,
|
|
3420
3461
|
// Proof is included in the bundle
|
|
3421
3462
|
memo: options?.memo,
|
|
@@ -3426,25 +3467,13 @@ var InstantSplitExecutor = class {
|
|
|
3426
3467
|
const criticalPathDuration = performance.now() - startTime;
|
|
3427
3468
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3428
3469
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3429
|
-
|
|
3430
|
-
if (!options?.skipBackground) {
|
|
3431
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3432
|
-
signingService: this.signingService,
|
|
3433
|
-
tokenType: tokenToSplit.type,
|
|
3434
|
-
coinId,
|
|
3435
|
-
senderTokenId,
|
|
3436
|
-
senderSalt,
|
|
3437
|
-
onProgress: options?.onBackgroundProgress,
|
|
3438
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3439
|
-
onStorageSync: options?.onStorageSync
|
|
3440
|
-
});
|
|
3441
|
-
}
|
|
3470
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3442
3471
|
return {
|
|
3443
3472
|
success: true,
|
|
3444
3473
|
nostrEventId,
|
|
3445
|
-
splitGroupId,
|
|
3474
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3446
3475
|
criticalPathDurationMs: criticalPathDuration,
|
|
3447
|
-
backgroundStarted:
|
|
3476
|
+
backgroundStarted: true,
|
|
3448
3477
|
backgroundPromise
|
|
3449
3478
|
};
|
|
3450
3479
|
} catch (error) {
|
|
@@ -3453,7 +3482,6 @@ var InstantSplitExecutor = class {
|
|
|
3453
3482
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3454
3483
|
return {
|
|
3455
3484
|
success: false,
|
|
3456
|
-
splitGroupId,
|
|
3457
3485
|
criticalPathDurationMs: duration,
|
|
3458
3486
|
error: errorMessage,
|
|
3459
3487
|
backgroundStarted: false
|
|
@@ -3658,6 +3686,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3658
3686
|
function isInstantSplitBundleV5(obj) {
|
|
3659
3687
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3660
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
|
+
}
|
|
3661
3694
|
|
|
3662
3695
|
// modules/payments/InstantSplitProcessor.ts
|
|
3663
3696
|
function fromHex3(hex) {
|
|
@@ -4300,6 +4333,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4300
4333
|
// Poll every 2s
|
|
4301
4334
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4302
4335
|
// Max 30 attempts (~60s)
|
|
4336
|
+
// Periodic retry for resolveUnconfirmed (V5 lazy finalization)
|
|
4337
|
+
resolveUnconfirmedTimer = null;
|
|
4338
|
+
static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
|
|
4339
|
+
// Retry every 10s
|
|
4340
|
+
// Guard: ensure load() completes before processing incoming bundles
|
|
4341
|
+
loadedPromise = null;
|
|
4342
|
+
loaded = false;
|
|
4343
|
+
// Persistent dedup: tracks splitGroupIds that have been fully processed.
|
|
4344
|
+
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4345
|
+
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4346
|
+
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4347
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4348
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4303
4349
|
// Storage event subscriptions (push-based sync)
|
|
4304
4350
|
storageEventUnsubscribers = [];
|
|
4305
4351
|
syncDebounceTimer = null;
|
|
@@ -4385,31 +4431,53 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4385
4431
|
*/
|
|
4386
4432
|
async load() {
|
|
4387
4433
|
this.ensureInitialized();
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4434
|
+
const doLoad = async () => {
|
|
4435
|
+
await TokenRegistry.waitForReady();
|
|
4436
|
+
const providers = this.getTokenStorageProviders();
|
|
4437
|
+
for (const [id, provider] of providers) {
|
|
4438
|
+
try {
|
|
4439
|
+
const result = await provider.load();
|
|
4440
|
+
if (result.success && result.data) {
|
|
4441
|
+
this.loadFromStorageData(result.data);
|
|
4442
|
+
this.log(`Loaded metadata from provider ${id}`);
|
|
4443
|
+
break;
|
|
4444
|
+
}
|
|
4445
|
+
} catch (err) {
|
|
4446
|
+
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4397
4447
|
}
|
|
4398
|
-
} catch (err) {
|
|
4399
|
-
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4400
4448
|
}
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
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
|
+
}
|
|
4409
4460
|
}
|
|
4410
|
-
|
|
4461
|
+
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4462
|
+
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4463
|
+
await this.loadPendingV5Tokens();
|
|
4464
|
+
await this.loadProcessedSplitGroupIds();
|
|
4465
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4466
|
+
await this.loadHistory();
|
|
4467
|
+
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4468
|
+
if (pending2) {
|
|
4469
|
+
const transfers = JSON.parse(pending2);
|
|
4470
|
+
for (const transfer of transfers) {
|
|
4471
|
+
this.pendingTransfers.set(transfer.id, transfer);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
this.loaded = true;
|
|
4475
|
+
};
|
|
4476
|
+
this.loadedPromise = doLoad();
|
|
4477
|
+
await this.loadedPromise;
|
|
4411
4478
|
this.resolveUnconfirmed().catch(() => {
|
|
4412
4479
|
});
|
|
4480
|
+
this.scheduleResolveUnconfirmed();
|
|
4413
4481
|
}
|
|
4414
4482
|
/**
|
|
4415
4483
|
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
@@ -4428,6 +4496,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4428
4496
|
this.paymentRequestResponseHandlers.clear();
|
|
4429
4497
|
this.stopProofPolling();
|
|
4430
4498
|
this.proofPollingJobs.clear();
|
|
4499
|
+
this.stopResolveUnconfirmedPolling();
|
|
4431
4500
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
4432
4501
|
clearTimeout(resolver.timeout);
|
|
4433
4502
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -4485,12 +4554,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4485
4554
|
token.status = "transferring";
|
|
4486
4555
|
this.tokens.set(token.id, token);
|
|
4487
4556
|
}
|
|
4557
|
+
await this.save();
|
|
4488
4558
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4489
4559
|
result.status = "submitted";
|
|
4490
4560
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4491
4561
|
const transferMode = request.transferMode ?? "instant";
|
|
4492
|
-
if (
|
|
4493
|
-
if (
|
|
4562
|
+
if (transferMode === "conservative") {
|
|
4563
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4494
4564
|
this.log("Executing conservative split...");
|
|
4495
4565
|
const splitExecutor = new TokenSplitExecutor({
|
|
4496
4566
|
stateTransitionClient: stClient,
|
|
@@ -4534,27 +4604,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4534
4604
|
requestIdHex: splitRequestIdHex
|
|
4535
4605
|
});
|
|
4536
4606
|
this.log(`Conservative split transfer completed`);
|
|
4537
|
-
}
|
|
4538
|
-
|
|
4539
|
-
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...");
|
|
4540
4641
|
const executor = new InstantSplitExecutor({
|
|
4541
4642
|
stateTransitionClient: stClient,
|
|
4542
4643
|
trustBase,
|
|
4543
4644
|
signingService,
|
|
4544
4645
|
devMode
|
|
4545
4646
|
});
|
|
4546
|
-
|
|
4647
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4547
4648
|
splitPlan.tokenToSplit.sdkToken,
|
|
4548
4649
|
splitPlan.splitAmount,
|
|
4549
4650
|
splitPlan.remainderAmount,
|
|
4550
4651
|
splitPlan.coinId,
|
|
4551
4652
|
recipientAddress,
|
|
4552
|
-
this.deps.transport,
|
|
4553
|
-
recipientPubkey,
|
|
4554
4653
|
{
|
|
4555
4654
|
memo: request.memo,
|
|
4556
4655
|
onChangeTokenCreated: async (changeToken) => {
|
|
4557
4656
|
const changeTokenData = changeToken.toJSON();
|
|
4657
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4658
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4659
|
+
}
|
|
4558
4660
|
const uiToken = {
|
|
4559
4661
|
id: crypto.randomUUID(),
|
|
4560
4662
|
coinId: request.coinId,
|
|
@@ -4577,65 +4679,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4577
4679
|
}
|
|
4578
4680
|
}
|
|
4579
4681
|
);
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
this.
|
|
4585
|
-
|
|
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) {
|
|
4586
4747
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4587
4748
|
result.tokenTransfers.push({
|
|
4588
4749
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4589
4750
|
method: "split",
|
|
4590
|
-
splitGroupId:
|
|
4591
|
-
nostrEventId: instantResult.nostrEventId
|
|
4751
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4592
4752
|
});
|
|
4593
|
-
this.log(`Instant split transfer completed`);
|
|
4594
4753
|
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
}
|
|
4605
|
-
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4606
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4607
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4608
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4609
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4610
|
-
memo: request.memo
|
|
4611
|
-
});
|
|
4612
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4613
|
-
} else {
|
|
4614
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4615
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4616
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4617
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4618
|
-
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
|
|
4619
4763
|
});
|
|
4620
|
-
|
|
4621
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4622
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4623
|
-
);
|
|
4764
|
+
await this.removeToken(token.id);
|
|
4624
4765
|
}
|
|
4625
|
-
|
|
4626
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4627
|
-
result.tokenTransfers.push({
|
|
4628
|
-
sourceTokenId: token.id,
|
|
4629
|
-
method: "direct",
|
|
4630
|
-
requestIdHex
|
|
4631
|
-
});
|
|
4632
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4633
|
-
await this.removeToken(token.id);
|
|
4766
|
+
this.log(`V6 combined transfer completed`);
|
|
4634
4767
|
}
|
|
4635
4768
|
result.status = "delivered";
|
|
4636
4769
|
await this.save();
|
|
4637
4770
|
await this.removeFromOutbox(result.id);
|
|
4638
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
|
+
}));
|
|
4639
4779
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4640
4780
|
await this.addToHistory({
|
|
4641
4781
|
type: "SENT",
|
|
@@ -4648,7 +4788,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4648
4788
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4649
4789
|
memo: request.memo,
|
|
4650
4790
|
transferId: result.id,
|
|
4651
|
-
tokenId: sentTokenId || void 0
|
|
4791
|
+
tokenId: sentTokenId || void 0,
|
|
4792
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4652
4793
|
});
|
|
4653
4794
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4654
4795
|
return result;
|
|
@@ -4818,6 +4959,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4818
4959
|
};
|
|
4819
4960
|
}
|
|
4820
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
|
+
}
|
|
4821
5223
|
/**
|
|
4822
5224
|
* Process a received INSTANT_SPLIT bundle.
|
|
4823
5225
|
*
|
|
@@ -4834,39 +5236,17 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4834
5236
|
*/
|
|
4835
5237
|
async processInstantSplitBundle(bundle, senderPubkey, memo) {
|
|
4836
5238
|
this.ensureInitialized();
|
|
5239
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5240
|
+
await this.loadedPromise;
|
|
5241
|
+
}
|
|
4837
5242
|
if (!isInstantSplitBundleV5(bundle)) {
|
|
4838
5243
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
4839
5244
|
}
|
|
4840
5245
|
try {
|
|
4841
|
-
const
|
|
4842
|
-
if (
|
|
4843
|
-
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
5246
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5247
|
+
if (!uiToken) {
|
|
4844
5248
|
return { success: true, durationMs: 0 };
|
|
4845
5249
|
}
|
|
4846
|
-
const registry = TokenRegistry.getInstance();
|
|
4847
|
-
const pendingData = {
|
|
4848
|
-
type: "v5_bundle",
|
|
4849
|
-
stage: "RECEIVED",
|
|
4850
|
-
bundleJson: JSON.stringify(bundle),
|
|
4851
|
-
senderPubkey,
|
|
4852
|
-
savedAt: Date.now(),
|
|
4853
|
-
attemptCount: 0
|
|
4854
|
-
};
|
|
4855
|
-
const uiToken = {
|
|
4856
|
-
id: deterministicId,
|
|
4857
|
-
coinId: bundle.coinId,
|
|
4858
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4859
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4860
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4861
|
-
amount: bundle.amount,
|
|
4862
|
-
status: "submitted",
|
|
4863
|
-
// UNCONFIRMED
|
|
4864
|
-
createdAt: Date.now(),
|
|
4865
|
-
updatedAt: Date.now(),
|
|
4866
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4867
|
-
};
|
|
4868
|
-
await this.addToken(uiToken);
|
|
4869
|
-
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4870
5250
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
4871
5251
|
await this.addToHistory({
|
|
4872
5252
|
type: "RECEIVED",
|
|
@@ -4877,7 +5257,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4877
5257
|
senderPubkey,
|
|
4878
5258
|
...senderInfo,
|
|
4879
5259
|
memo,
|
|
4880
|
-
tokenId:
|
|
5260
|
+
tokenId: uiToken.id
|
|
4881
5261
|
});
|
|
4882
5262
|
this.deps.emitEvent("transfer:incoming", {
|
|
4883
5263
|
id: bundle.splitGroupId,
|
|
@@ -4890,6 +5270,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4890
5270
|
await this.save();
|
|
4891
5271
|
this.resolveUnconfirmed().catch(() => {
|
|
4892
5272
|
});
|
|
5273
|
+
this.scheduleResolveUnconfirmed();
|
|
4893
5274
|
return { success: true, durationMs: 0 };
|
|
4894
5275
|
} catch (error) {
|
|
4895
5276
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -5526,16 +5907,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5526
5907
|
}
|
|
5527
5908
|
/**
|
|
5528
5909
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5529
|
-
* 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").
|
|
5530
5912
|
*/
|
|
5531
5913
|
aggregateTokens(coinId) {
|
|
5532
5914
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5533
5915
|
for (const token of this.tokens.values()) {
|
|
5534
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
5916
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5535
5917
|
if (coinId && token.coinId !== coinId) continue;
|
|
5536
5918
|
const key = token.coinId;
|
|
5537
5919
|
const amount = BigInt(token.amount);
|
|
5538
5920
|
const isConfirmed = token.status === "confirmed";
|
|
5921
|
+
const isTransferring = token.status === "transferring";
|
|
5539
5922
|
const existing = assetsMap.get(key);
|
|
5540
5923
|
if (existing) {
|
|
5541
5924
|
if (isConfirmed) {
|
|
@@ -5545,6 +5928,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5545
5928
|
existing.unconfirmedAmount += amount;
|
|
5546
5929
|
existing.unconfirmedTokenCount++;
|
|
5547
5930
|
}
|
|
5931
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5548
5932
|
} else {
|
|
5549
5933
|
assetsMap.set(key, {
|
|
5550
5934
|
coinId: token.coinId,
|
|
@@ -5555,7 +5939,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5555
5939
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5556
5940
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5557
5941
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5558
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
5942
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
5943
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5559
5944
|
});
|
|
5560
5945
|
}
|
|
5561
5946
|
}
|
|
@@ -5573,6 +5958,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5573
5958
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5574
5959
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5575
5960
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5961
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5576
5962
|
priceUsd: null,
|
|
5577
5963
|
priceEur: null,
|
|
5578
5964
|
change24h: null,
|
|
@@ -5636,28 +6022,70 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5636
6022
|
};
|
|
5637
6023
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5638
6024
|
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5639
|
-
if (!stClient || !trustBase)
|
|
6025
|
+
if (!stClient || !trustBase) {
|
|
6026
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
|
|
6027
|
+
return result;
|
|
6028
|
+
}
|
|
5640
6029
|
const signingService = await this.createSigningService();
|
|
6030
|
+
const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
|
|
6031
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
|
|
5641
6032
|
for (const [tokenId, token] of this.tokens) {
|
|
5642
6033
|
if (token.status !== "submitted") continue;
|
|
5643
6034
|
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5644
6035
|
if (!pending2) {
|
|
6036
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
|
|
5645
6037
|
result.stillPending++;
|
|
5646
6038
|
continue;
|
|
5647
6039
|
}
|
|
5648
6040
|
if (pending2.type === "v5_bundle") {
|
|
6041
|
+
console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
|
|
5649
6042
|
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
6043
|
+
console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
|
|
5650
6044
|
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5651
6045
|
if (progress === "resolved") result.resolved++;
|
|
5652
6046
|
else if (progress === "failed") result.failed++;
|
|
5653
6047
|
else result.stillPending++;
|
|
5654
6048
|
}
|
|
5655
6049
|
}
|
|
5656
|
-
if (result.resolved > 0 || result.failed > 0) {
|
|
6050
|
+
if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
|
|
6051
|
+
console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
|
|
5657
6052
|
await this.save();
|
|
5658
6053
|
}
|
|
5659
6054
|
return result;
|
|
5660
6055
|
}
|
|
6056
|
+
/**
|
|
6057
|
+
* Start a periodic interval that retries resolveUnconfirmed() until all
|
|
6058
|
+
* tokens are confirmed or failed. Stops automatically when nothing is
|
|
6059
|
+
* pending and is cleaned up by destroy().
|
|
6060
|
+
*/
|
|
6061
|
+
scheduleResolveUnconfirmed() {
|
|
6062
|
+
if (this.resolveUnconfirmedTimer) return;
|
|
6063
|
+
const hasUnconfirmed = Array.from(this.tokens.values()).some(
|
|
6064
|
+
(t) => t.status === "submitted"
|
|
6065
|
+
);
|
|
6066
|
+
if (!hasUnconfirmed) {
|
|
6067
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
|
|
6068
|
+
return;
|
|
6069
|
+
}
|
|
6070
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
|
|
6071
|
+
this.resolveUnconfirmedTimer = setInterval(async () => {
|
|
6072
|
+
try {
|
|
6073
|
+
const result = await this.resolveUnconfirmed();
|
|
6074
|
+
if (result.stillPending === 0) {
|
|
6075
|
+
console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
|
|
6076
|
+
this.stopResolveUnconfirmedPolling();
|
|
6077
|
+
}
|
|
6078
|
+
} catch (err) {
|
|
6079
|
+
console.log(`[V5-RESOLVE] Periodic retry error:`, err);
|
|
6080
|
+
}
|
|
6081
|
+
}, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
|
|
6082
|
+
}
|
|
6083
|
+
stopResolveUnconfirmedPolling() {
|
|
6084
|
+
if (this.resolveUnconfirmedTimer) {
|
|
6085
|
+
clearInterval(this.resolveUnconfirmedTimer);
|
|
6086
|
+
this.resolveUnconfirmedTimer = null;
|
|
6087
|
+
}
|
|
6088
|
+
}
|
|
5661
6089
|
// ===========================================================================
|
|
5662
6090
|
// Private - V5 Lazy Resolution Helpers
|
|
5663
6091
|
// ===========================================================================
|
|
@@ -5670,10 +6098,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5670
6098
|
pending2.lastAttemptAt = Date.now();
|
|
5671
6099
|
try {
|
|
5672
6100
|
if (pending2.stage === "RECEIVED") {
|
|
6101
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
|
|
5673
6102
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5674
6103
|
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5675
6104
|
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5676
6105
|
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
6106
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
|
|
5677
6107
|
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5678
6108
|
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5679
6109
|
}
|
|
@@ -5681,22 +6111,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5681
6111
|
this.updatePendingFinalization(token, pending2);
|
|
5682
6112
|
}
|
|
5683
6113
|
if (pending2.stage === "MINT_SUBMITTED") {
|
|
6114
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
|
|
5684
6115
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5685
6116
|
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5686
6117
|
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5687
6118
|
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5688
6119
|
if (!proof) {
|
|
6120
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
|
|
5689
6121
|
this.updatePendingFinalization(token, pending2);
|
|
5690
6122
|
return "pending";
|
|
5691
6123
|
}
|
|
6124
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
|
|
5692
6125
|
pending2.mintProofJson = JSON.stringify(proof);
|
|
5693
6126
|
pending2.stage = "MINT_PROVEN";
|
|
5694
6127
|
this.updatePendingFinalization(token, pending2);
|
|
5695
6128
|
}
|
|
5696
6129
|
if (pending2.stage === "MINT_PROVEN") {
|
|
6130
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
|
|
5697
6131
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5698
6132
|
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5699
6133
|
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
6134
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
|
|
5700
6135
|
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5701
6136
|
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5702
6137
|
}
|
|
@@ -5704,13 +6139,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5704
6139
|
this.updatePendingFinalization(token, pending2);
|
|
5705
6140
|
}
|
|
5706
6141
|
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
6142
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
|
|
5707
6143
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5708
6144
|
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5709
6145
|
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5710
6146
|
if (!proof) {
|
|
6147
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
|
|
5711
6148
|
this.updatePendingFinalization(token, pending2);
|
|
5712
6149
|
return "pending";
|
|
5713
6150
|
}
|
|
6151
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
|
|
5714
6152
|
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5715
6153
|
const confirmedToken = {
|
|
5716
6154
|
id: token.id,
|
|
@@ -5726,6 +6164,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5726
6164
|
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5727
6165
|
};
|
|
5728
6166
|
this.tokens.set(tokenId, confirmedToken);
|
|
6167
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
6168
|
+
id: crypto.randomUUID(),
|
|
6169
|
+
status: "completed",
|
|
6170
|
+
tokens: [confirmedToken],
|
|
6171
|
+
tokenTransfers: []
|
|
6172
|
+
});
|
|
5729
6173
|
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5730
6174
|
return "resolved";
|
|
5731
6175
|
}
|
|
@@ -5867,11 +6311,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5867
6311
|
}
|
|
5868
6312
|
}
|
|
5869
6313
|
if (pendingTokens.length > 0) {
|
|
6314
|
+
const json = JSON.stringify(pendingTokens);
|
|
6315
|
+
this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
|
|
5870
6316
|
await this.deps.storage.set(
|
|
5871
6317
|
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5872
|
-
|
|
6318
|
+
json
|
|
5873
6319
|
);
|
|
6320
|
+
const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6321
|
+
if (!verify) {
|
|
6322
|
+
console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
|
|
6323
|
+
} else {
|
|
6324
|
+
this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
|
|
6325
|
+
}
|
|
5874
6326
|
} else {
|
|
6327
|
+
this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
|
|
5875
6328
|
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5876
6329
|
}
|
|
5877
6330
|
}
|
|
@@ -5881,16 +6334,47 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5881
6334
|
*/
|
|
5882
6335
|
async loadPendingV5Tokens() {
|
|
5883
6336
|
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6337
|
+
this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
|
|
5884
6338
|
if (!data) return;
|
|
5885
6339
|
try {
|
|
5886
6340
|
const pendingTokens = JSON.parse(data);
|
|
6341
|
+
this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
|
|
5887
6342
|
for (const token of pendingTokens) {
|
|
5888
6343
|
if (!this.tokens.has(token.id)) {
|
|
5889
6344
|
this.tokens.set(token.id, token);
|
|
6345
|
+
this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
|
|
6346
|
+
} else {
|
|
6347
|
+
this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
|
|
5890
6348
|
}
|
|
5891
6349
|
}
|
|
5892
|
-
|
|
5893
|
-
|
|
6350
|
+
} catch (err) {
|
|
6351
|
+
console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
/**
|
|
6355
|
+
* Persist the set of processed splitGroupIds to KV storage.
|
|
6356
|
+
* This ensures Nostr re-deliveries are ignored across page reloads,
|
|
6357
|
+
* even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
6358
|
+
*/
|
|
6359
|
+
async saveProcessedSplitGroupIds() {
|
|
6360
|
+
const ids = Array.from(this.processedSplitGroupIds);
|
|
6361
|
+
if (ids.length > 0) {
|
|
6362
|
+
await this.deps.storage.set(
|
|
6363
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
|
|
6364
|
+
JSON.stringify(ids)
|
|
6365
|
+
);
|
|
6366
|
+
}
|
|
6367
|
+
}
|
|
6368
|
+
/**
|
|
6369
|
+
* Load processed splitGroupIds from KV storage.
|
|
6370
|
+
*/
|
|
6371
|
+
async loadProcessedSplitGroupIds() {
|
|
6372
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
|
|
6373
|
+
if (!data) return;
|
|
6374
|
+
try {
|
|
6375
|
+
const ids = JSON.parse(data);
|
|
6376
|
+
for (const id of ids) {
|
|
6377
|
+
this.processedSplitGroupIds.add(id);
|
|
5894
6378
|
}
|
|
5895
6379
|
} catch {
|
|
5896
6380
|
}
|
|
@@ -6545,7 +7029,32 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6545
7029
|
try {
|
|
6546
7030
|
const result = await provider.sync(localData);
|
|
6547
7031
|
if (result.success && result.merged) {
|
|
7032
|
+
const savedTokens = new Map(this.tokens);
|
|
6548
7033
|
this.loadFromStorageData(result.merged);
|
|
7034
|
+
let restoredCount = 0;
|
|
7035
|
+
for (const [tokenId, token] of savedTokens) {
|
|
7036
|
+
if (this.tokens.has(tokenId)) continue;
|
|
7037
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7038
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7039
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
7040
|
+
continue;
|
|
7041
|
+
}
|
|
7042
|
+
if (sdkTokenId) {
|
|
7043
|
+
let hasEquivalent = false;
|
|
7044
|
+
for (const existing of this.tokens.values()) {
|
|
7045
|
+
if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
|
|
7046
|
+
hasEquivalent = true;
|
|
7047
|
+
break;
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
7050
|
+
if (hasEquivalent) continue;
|
|
7051
|
+
}
|
|
7052
|
+
this.tokens.set(tokenId, token);
|
|
7053
|
+
restoredCount++;
|
|
7054
|
+
}
|
|
7055
|
+
if (restoredCount > 0) {
|
|
7056
|
+
console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
|
|
7057
|
+
}
|
|
6549
7058
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
6550
7059
|
this.nametags = savedNametags;
|
|
6551
7060
|
}
|
|
@@ -6847,7 +7356,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6847
7356
|
/**
|
|
6848
7357
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
6849
7358
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
6850
|
-
*
|
|
7359
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
6851
7360
|
*/
|
|
6852
7361
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
6853
7362
|
try {
|
|
@@ -6857,40 +7366,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6857
7366
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
6858
7367
|
return;
|
|
6859
7368
|
}
|
|
6860
|
-
const
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
decimals: tokenInfo.decimals,
|
|
6867
|
-
iconUrl: tokenInfo.iconUrl,
|
|
6868
|
-
amount: tokenInfo.amount,
|
|
6869
|
-
status: "submitted",
|
|
6870
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
6871
|
-
createdAt: Date.now(),
|
|
6872
|
-
updatedAt: Date.now(),
|
|
6873
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
6874
|
-
};
|
|
6875
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6876
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6877
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
6878
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
6879
|
-
return;
|
|
6880
|
-
}
|
|
6881
|
-
this.tokens.set(token.id, token);
|
|
6882
|
-
await this.save();
|
|
6883
|
-
this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
|
|
7369
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7370
|
+
sourceTokenInput,
|
|
7371
|
+
commitmentInput,
|
|
7372
|
+
transfer.senderTransportPubkey
|
|
7373
|
+
);
|
|
7374
|
+
if (!token) return;
|
|
6884
7375
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
6885
|
-
|
|
7376
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
6886
7377
|
id: transfer.id,
|
|
6887
7378
|
senderPubkey: transfer.senderTransportPubkey,
|
|
6888
7379
|
senderNametag: senderInfo.senderNametag,
|
|
6889
7380
|
tokens: [token],
|
|
6890
7381
|
memo: payload.memo,
|
|
6891
7382
|
receivedAt: transfer.timestamp
|
|
6892
|
-
};
|
|
6893
|
-
|
|
7383
|
+
});
|
|
7384
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6894
7385
|
await this.addToHistory({
|
|
6895
7386
|
type: "RECEIVED",
|
|
6896
7387
|
amount: token.amount,
|
|
@@ -6902,29 +7393,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6902
7393
|
memo: payload.memo,
|
|
6903
7394
|
tokenId: nostrTokenId || token.id
|
|
6904
7395
|
});
|
|
6905
|
-
try {
|
|
6906
|
-
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
6907
|
-
const requestIdBytes = commitment.requestId;
|
|
6908
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
6909
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6910
|
-
if (stClient) {
|
|
6911
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
6912
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
6913
|
-
}
|
|
6914
|
-
this.addProofPollingJob({
|
|
6915
|
-
tokenId: token.id,
|
|
6916
|
-
requestIdHex,
|
|
6917
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
6918
|
-
startedAt: Date.now(),
|
|
6919
|
-
attemptCount: 0,
|
|
6920
|
-
lastAttemptAt: 0,
|
|
6921
|
-
onProofReceived: async (tokenId) => {
|
|
6922
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
6923
|
-
}
|
|
6924
|
-
});
|
|
6925
|
-
} catch (err) {
|
|
6926
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
6927
|
-
}
|
|
6928
7396
|
} catch (error) {
|
|
6929
7397
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
6930
7398
|
}
|
|
@@ -7037,8 +7505,34 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7037
7505
|
}
|
|
7038
7506
|
}
|
|
7039
7507
|
async handleIncomingTransfer(transfer) {
|
|
7508
|
+
if (!this.loaded && this.loadedPromise) {
|
|
7509
|
+
await this.loadedPromise;
|
|
7510
|
+
}
|
|
7040
7511
|
try {
|
|
7041
7512
|
const payload = transfer.payload;
|
|
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
|
+
}
|
|
7042
7536
|
let instantBundle = null;
|
|
7043
7537
|
if (isInstantSplitBundle(payload)) {
|
|
7044
7538
|
instantBundle = payload;
|
|
@@ -7070,7 +7564,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7070
7564
|
return;
|
|
7071
7565
|
}
|
|
7072
7566
|
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
7073
|
-
|
|
7567
|
+
console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
|
|
7074
7568
|
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
7075
7569
|
return;
|
|
7076
7570
|
}
|
|
@@ -7190,17 +7684,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7190
7684
|
memo: payload.memo,
|
|
7191
7685
|
tokenId: incomingTokenId || token.id
|
|
7192
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}`);
|
|
7193
7699
|
}
|
|
7194
|
-
const incomingTransfer = {
|
|
7195
|
-
id: transfer.id,
|
|
7196
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7197
|
-
senderNametag: senderInfo.senderNametag,
|
|
7198
|
-
tokens: [token],
|
|
7199
|
-
memo: payload.memo,
|
|
7200
|
-
receivedAt: transfer.timestamp
|
|
7201
|
-
};
|
|
7202
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7203
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7204
7700
|
} catch (error) {
|
|
7205
7701
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7206
7702
|
}
|
|
@@ -7233,17 +7729,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7233
7729
|
// ===========================================================================
|
|
7234
7730
|
async save() {
|
|
7235
7731
|
const providers = this.getTokenStorageProviders();
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
return
|
|
7239
|
-
}
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7732
|
+
const tokenStats = Array.from(this.tokens.values()).map((t) => {
|
|
7733
|
+
const txf = tokenToTxf(t);
|
|
7734
|
+
return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
|
|
7735
|
+
});
|
|
7736
|
+
console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
|
|
7737
|
+
if (providers.size > 0) {
|
|
7738
|
+
const data = await this.createStorageData();
|
|
7739
|
+
const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
|
|
7740
|
+
console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
|
|
7741
|
+
for (const [id, provider] of providers) {
|
|
7742
|
+
try {
|
|
7743
|
+
await provider.save(data);
|
|
7744
|
+
} catch (err) {
|
|
7745
|
+
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
7746
|
+
}
|
|
7246
7747
|
}
|
|
7748
|
+
} else {
|
|
7749
|
+
console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
|
|
7247
7750
|
}
|
|
7248
7751
|
await this.savePendingV5Tokens();
|
|
7249
7752
|
}
|
|
@@ -7279,6 +7782,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7279
7782
|
}
|
|
7280
7783
|
loadFromStorageData(data) {
|
|
7281
7784
|
const parsed = parseTxfStorageData(data);
|
|
7785
|
+
console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
|
|
7282
7786
|
this.tombstones = parsed.tombstones;
|
|
7283
7787
|
this.tokens.clear();
|
|
7284
7788
|
for (const token of parsed.tokens) {
|