@unicitylabs/sphere-sdk 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +3 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +3 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +669 -277
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +57 -2
- package/dist/core/index.d.ts +57 -2
- package/dist/core/index.js +669 -277
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +3 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +3 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +11 -3
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +11 -3
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +9 -2
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +9 -2
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +3 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +3 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +11 -3
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +7 -0
- package/dist/impl/nodejs/index.d.ts +7 -0
- package/dist/impl/nodejs/index.js +11 -3
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +671 -277
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -3
- package/dist/index.d.ts +128 -3
- package/dist/index.js +670 -277
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +3 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +3 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.cjs
CHANGED
|
@@ -105,7 +105,9 @@ var init_constants = __esm({
|
|
|
105
105
|
/** Group chat: processed event IDs for deduplication */
|
|
106
106
|
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
107
107
|
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
108
|
-
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
|
|
108
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids",
|
|
109
|
+
/** Processed V6 combined transfer IDs for Nostr re-delivery dedup */
|
|
110
|
+
PROCESSED_COMBINED_TRANSFER_IDS: "processed_combined_transfer_ids"
|
|
109
111
|
};
|
|
110
112
|
STORAGE_KEYS = {
|
|
111
113
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -2599,7 +2601,7 @@ init_constants();
|
|
|
2599
2601
|
// types/txf.ts
|
|
2600
2602
|
var ARCHIVED_PREFIX = "archived-";
|
|
2601
2603
|
var FORKED_PREFIX = "_forked_";
|
|
2602
|
-
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity"];
|
|
2604
|
+
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity", "_history"];
|
|
2603
2605
|
function isTokenKey(key) {
|
|
2604
2606
|
return key.startsWith("_") && !key.startsWith(ARCHIVED_PREFIX) && !key.startsWith(FORKED_PREFIX) && !RESERVED_KEYS.includes(key);
|
|
2605
2607
|
}
|
|
@@ -3184,6 +3186,9 @@ async function buildTxfStorageData(tokens, meta, options) {
|
|
|
3184
3186
|
if (options?.invalidatedNametags && options.invalidatedNametags.length > 0) {
|
|
3185
3187
|
storageData._invalidatedNametags = options.invalidatedNametags;
|
|
3186
3188
|
}
|
|
3189
|
+
if (options?.historyEntries && options.historyEntries.length > 0) {
|
|
3190
|
+
storageData._history = options.historyEntries;
|
|
3191
|
+
}
|
|
3187
3192
|
for (const token of tokens) {
|
|
3188
3193
|
const txf = tokenToTxf(token);
|
|
3189
3194
|
if (txf) {
|
|
@@ -3217,6 +3222,7 @@ function parseTxfStorageData(data) {
|
|
|
3217
3222
|
outboxEntries: [],
|
|
3218
3223
|
mintOutboxEntries: [],
|
|
3219
3224
|
invalidatedNametags: [],
|
|
3225
|
+
historyEntries: [],
|
|
3220
3226
|
validationErrors: []
|
|
3221
3227
|
};
|
|
3222
3228
|
if (!data || typeof data !== "object") {
|
|
@@ -3270,6 +3276,13 @@ function parseTxfStorageData(data) {
|
|
|
3270
3276
|
}
|
|
3271
3277
|
}
|
|
3272
3278
|
}
|
|
3279
|
+
if (Array.isArray(storageData._history)) {
|
|
3280
|
+
for (const entry of storageData._history) {
|
|
3281
|
+
if (typeof entry === "object" && entry !== null && typeof entry.dedupKey === "string" && typeof entry.type === "string") {
|
|
3282
|
+
result.historyEntries.push(entry);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3273
3286
|
for (const key of Object.keys(storageData)) {
|
|
3274
3287
|
if (isTokenKey(key)) {
|
|
3275
3288
|
const tokenId = tokenIdFromKey(key);
|
|
@@ -3381,14 +3394,149 @@ var InstantSplitExecutor = class {
|
|
|
3381
3394
|
this.devMode = config.devMode ?? false;
|
|
3382
3395
|
}
|
|
3383
3396
|
/**
|
|
3384
|
-
*
|
|
3397
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3385
3398
|
*
|
|
3386
|
-
*
|
|
3399
|
+
* Steps 1-5 of the V5 flow:
|
|
3387
3400
|
* 1. Create and submit burn commitment
|
|
3388
3401
|
* 2. Wait for burn proof
|
|
3389
3402
|
* 3. Create mint commitments with SplitMintReason
|
|
3390
3403
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3391
|
-
* 5.
|
|
3404
|
+
* 5. Package V5 bundle
|
|
3405
|
+
*
|
|
3406
|
+
* The caller is responsible for sending the bundle and then calling
|
|
3407
|
+
* `startBackground()` on the result to begin mint proof + change token creation.
|
|
3408
|
+
*/
|
|
3409
|
+
async buildSplitBundle(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, options) {
|
|
3410
|
+
const splitGroupId = crypto.randomUUID();
|
|
3411
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3412
|
+
console.log(`[InstantSplit] Building V5 bundle for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3413
|
+
const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
|
|
3414
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3415
|
+
const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
|
|
3416
|
+
const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
|
|
3417
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3418
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3419
|
+
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3420
|
+
tokenToSplit.type,
|
|
3421
|
+
this.signingService.algorithm,
|
|
3422
|
+
this.signingService.publicKey,
|
|
3423
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3424
|
+
);
|
|
3425
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3426
|
+
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3427
|
+
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3428
|
+
builder.createToken(
|
|
3429
|
+
recipientTokenId,
|
|
3430
|
+
tokenToSplit.type,
|
|
3431
|
+
new Uint8Array(0),
|
|
3432
|
+
coinDataA,
|
|
3433
|
+
senderAddress,
|
|
3434
|
+
// Mint to sender first, then transfer
|
|
3435
|
+
recipientSalt,
|
|
3436
|
+
null
|
|
3437
|
+
);
|
|
3438
|
+
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3439
|
+
builder.createToken(
|
|
3440
|
+
senderTokenId,
|
|
3441
|
+
tokenToSplit.type,
|
|
3442
|
+
new Uint8Array(0),
|
|
3443
|
+
coinDataB,
|
|
3444
|
+
senderAddress,
|
|
3445
|
+
senderSalt,
|
|
3446
|
+
null
|
|
3447
|
+
);
|
|
3448
|
+
const split = await builder.build(tokenToSplit);
|
|
3449
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3450
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3451
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3452
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3453
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3454
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3455
|
+
}
|
|
3456
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3457
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3458
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3459
|
+
console.log(`[InstantSplit] Burn proof received`);
|
|
3460
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3461
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3462
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3463
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3464
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3465
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3466
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3467
|
+
);
|
|
3468
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3469
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3470
|
+
);
|
|
3471
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3472
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3473
|
+
}
|
|
3474
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3475
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3476
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3477
|
+
recipientMintCommitment.transactionData,
|
|
3478
|
+
recipientAddress,
|
|
3479
|
+
transferSalt,
|
|
3480
|
+
this.signingService
|
|
3481
|
+
);
|
|
3482
|
+
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3483
|
+
recipientTokenId,
|
|
3484
|
+
tokenToSplit.type,
|
|
3485
|
+
this.signingService,
|
|
3486
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3487
|
+
recipientSalt
|
|
3488
|
+
);
|
|
3489
|
+
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3490
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3491
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3492
|
+
let nametagTokenJson;
|
|
3493
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3494
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3495
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3496
|
+
}
|
|
3497
|
+
const bundle = {
|
|
3498
|
+
version: "5.0",
|
|
3499
|
+
type: "INSTANT_SPLIT",
|
|
3500
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3501
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3502
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3503
|
+
amount: splitAmount.toString(),
|
|
3504
|
+
coinId: coinIdHex,
|
|
3505
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3506
|
+
splitGroupId,
|
|
3507
|
+
senderPubkey,
|
|
3508
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3509
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3510
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3511
|
+
finalRecipientStateJson: "",
|
|
3512
|
+
// Recipient creates their own
|
|
3513
|
+
recipientAddressJson: recipientAddressStr,
|
|
3514
|
+
nametagTokenJson
|
|
3515
|
+
};
|
|
3516
|
+
return {
|
|
3517
|
+
bundle,
|
|
3518
|
+
splitGroupId,
|
|
3519
|
+
startBackground: async () => {
|
|
3520
|
+
if (!options?.skipBackground) {
|
|
3521
|
+
await this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3522
|
+
signingService: this.signingService,
|
|
3523
|
+
tokenType: tokenToSplit.type,
|
|
3524
|
+
coinId,
|
|
3525
|
+
senderTokenId,
|
|
3526
|
+
senderSalt,
|
|
3527
|
+
onProgress: options?.onBackgroundProgress,
|
|
3528
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3529
|
+
onStorageSync: options?.onStorageSync
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3537
|
+
*
|
|
3538
|
+
* Builds the bundle via buildSplitBundle(), sends via transport,
|
|
3539
|
+
* and starts background processing.
|
|
3392
3540
|
*
|
|
3393
3541
|
* @param tokenToSplit - The SDK token to split
|
|
3394
3542
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3402,117 +3550,19 @@ var InstantSplitExecutor = class {
|
|
|
3402
3550
|
*/
|
|
3403
3551
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3404
3552
|
const startTime = performance.now();
|
|
3405
|
-
const splitGroupId = crypto.randomUUID();
|
|
3406
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3407
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3408
3553
|
try {
|
|
3409
|
-
const
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3415
|
-
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3416
|
-
tokenToSplit.type,
|
|
3417
|
-
this.signingService.algorithm,
|
|
3418
|
-
this.signingService.publicKey,
|
|
3419
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3420
|
-
);
|
|
3421
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3422
|
-
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3423
|
-
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3424
|
-
builder.createToken(
|
|
3425
|
-
recipientTokenId,
|
|
3426
|
-
tokenToSplit.type,
|
|
3427
|
-
new Uint8Array(0),
|
|
3428
|
-
coinDataA,
|
|
3429
|
-
senderAddress,
|
|
3430
|
-
// Mint to sender first, then transfer
|
|
3431
|
-
recipientSalt,
|
|
3432
|
-
null
|
|
3433
|
-
);
|
|
3434
|
-
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3435
|
-
builder.createToken(
|
|
3436
|
-
senderTokenId,
|
|
3437
|
-
tokenToSplit.type,
|
|
3438
|
-
new Uint8Array(0),
|
|
3439
|
-
coinDataB,
|
|
3440
|
-
senderAddress,
|
|
3441
|
-
senderSalt,
|
|
3442
|
-
null
|
|
3443
|
-
);
|
|
3444
|
-
const split = await builder.build(tokenToSplit);
|
|
3445
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3446
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3447
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3448
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3449
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3450
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3451
|
-
}
|
|
3452
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3453
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3454
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3455
|
-
const burnDuration = performance.now() - startTime;
|
|
3456
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3457
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3458
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3459
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3460
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3461
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3462
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3463
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3464
|
-
);
|
|
3465
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3466
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3467
|
-
);
|
|
3468
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3469
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3470
|
-
}
|
|
3471
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3472
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3473
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3474
|
-
recipientMintCommitment.transactionData,
|
|
3554
|
+
const buildResult = await this.buildSplitBundle(
|
|
3555
|
+
tokenToSplit,
|
|
3556
|
+
splitAmount,
|
|
3557
|
+
remainderAmount,
|
|
3558
|
+
coinIdHex,
|
|
3475
3559
|
recipientAddress,
|
|
3476
|
-
|
|
3477
|
-
this.signingService
|
|
3478
|
-
);
|
|
3479
|
-
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3480
|
-
recipientTokenId,
|
|
3481
|
-
tokenToSplit.type,
|
|
3482
|
-
this.signingService,
|
|
3483
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3484
|
-
recipientSalt
|
|
3560
|
+
options
|
|
3485
3561
|
);
|
|
3486
|
-
|
|
3487
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3562
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3488
3563
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3489
|
-
let nametagTokenJson;
|
|
3490
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3491
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3492
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3493
|
-
}
|
|
3494
|
-
const bundle = {
|
|
3495
|
-
version: "5.0",
|
|
3496
|
-
type: "INSTANT_SPLIT",
|
|
3497
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3498
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3499
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3500
|
-
amount: splitAmount.toString(),
|
|
3501
|
-
coinId: coinIdHex,
|
|
3502
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3503
|
-
splitGroupId,
|
|
3504
|
-
senderPubkey,
|
|
3505
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3506
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3507
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3508
|
-
finalRecipientStateJson: "",
|
|
3509
|
-
// Recipient creates their own
|
|
3510
|
-
recipientAddressJson: recipientAddressStr,
|
|
3511
|
-
nametagTokenJson
|
|
3512
|
-
};
|
|
3513
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3514
3564
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3515
|
-
token: JSON.stringify(bundle),
|
|
3565
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3516
3566
|
proof: null,
|
|
3517
3567
|
// Proof is included in the bundle
|
|
3518
3568
|
memo: options?.memo,
|
|
@@ -3523,25 +3573,13 @@ var InstantSplitExecutor = class {
|
|
|
3523
3573
|
const criticalPathDuration = performance.now() - startTime;
|
|
3524
3574
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3525
3575
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3526
|
-
|
|
3527
|
-
if (!options?.skipBackground) {
|
|
3528
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3529
|
-
signingService: this.signingService,
|
|
3530
|
-
tokenType: tokenToSplit.type,
|
|
3531
|
-
coinId,
|
|
3532
|
-
senderTokenId,
|
|
3533
|
-
senderSalt,
|
|
3534
|
-
onProgress: options?.onBackgroundProgress,
|
|
3535
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3536
|
-
onStorageSync: options?.onStorageSync
|
|
3537
|
-
});
|
|
3538
|
-
}
|
|
3576
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3539
3577
|
return {
|
|
3540
3578
|
success: true,
|
|
3541
3579
|
nostrEventId,
|
|
3542
|
-
splitGroupId,
|
|
3580
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3543
3581
|
criticalPathDurationMs: criticalPathDuration,
|
|
3544
|
-
backgroundStarted:
|
|
3582
|
+
backgroundStarted: true,
|
|
3545
3583
|
backgroundPromise
|
|
3546
3584
|
};
|
|
3547
3585
|
} catch (error) {
|
|
@@ -3550,7 +3588,6 @@ var InstantSplitExecutor = class {
|
|
|
3550
3588
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3551
3589
|
return {
|
|
3552
3590
|
success: false,
|
|
3553
|
-
splitGroupId,
|
|
3554
3591
|
criticalPathDurationMs: duration,
|
|
3555
3592
|
error: errorMessage,
|
|
3556
3593
|
backgroundStarted: false
|
|
@@ -3755,6 +3792,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3755
3792
|
function isInstantSplitBundleV5(obj) {
|
|
3756
3793
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3757
3794
|
}
|
|
3795
|
+
function isCombinedTransferBundleV6(obj) {
|
|
3796
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
3797
|
+
const b = obj;
|
|
3798
|
+
return b.version === "6.0" && b.type === "COMBINED_TRANSFER";
|
|
3799
|
+
}
|
|
3758
3800
|
|
|
3759
3801
|
// modules/payments/InstantSplitProcessor.ts
|
|
3760
3802
|
function fromHex3(hex) {
|
|
@@ -4079,6 +4121,7 @@ function computeHistoryDedupKey(type, tokenId, transferId) {
|
|
|
4079
4121
|
if (tokenId) return `${type}_${tokenId}`;
|
|
4080
4122
|
return `${type}_${crypto.randomUUID()}`;
|
|
4081
4123
|
}
|
|
4124
|
+
var MAX_SYNCED_HISTORY_ENTRIES = 5e3;
|
|
4082
4125
|
function enrichWithRegistry(info) {
|
|
4083
4126
|
const registry = TokenRegistry.getInstance();
|
|
4084
4127
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -4408,6 +4451,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4408
4451
|
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4409
4452
|
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4410
4453
|
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4454
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4455
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4411
4456
|
// Storage event subscriptions (push-based sync)
|
|
4412
4457
|
storageEventUnsubscribers = [];
|
|
4413
4458
|
syncDebounceTimer = null;
|
|
@@ -4501,6 +4546,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4501
4546
|
const result = await provider.load();
|
|
4502
4547
|
if (result.success && result.data) {
|
|
4503
4548
|
this.loadFromStorageData(result.data);
|
|
4549
|
+
const txfData = result.data;
|
|
4550
|
+
if (txfData._history && txfData._history.length > 0) {
|
|
4551
|
+
await this.importRemoteHistoryEntries(txfData._history);
|
|
4552
|
+
}
|
|
4504
4553
|
this.log(`Loaded metadata from provider ${id}`);
|
|
4505
4554
|
break;
|
|
4506
4555
|
}
|
|
@@ -4508,10 +4557,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4508
4557
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4509
4558
|
}
|
|
4510
4559
|
}
|
|
4560
|
+
for (const [id, token] of this.tokens) {
|
|
4561
|
+
try {
|
|
4562
|
+
if (token.sdkData) {
|
|
4563
|
+
const data = JSON.parse(token.sdkData);
|
|
4564
|
+
if (data?._placeholder) {
|
|
4565
|
+
this.tokens.delete(id);
|
|
4566
|
+
console.log(`[Payments] Removed stale placeholder token: ${id}`);
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
} catch {
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4511
4572
|
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4512
4573
|
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4513
4574
|
await this.loadPendingV5Tokens();
|
|
4514
4575
|
await this.loadProcessedSplitGroupIds();
|
|
4576
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4515
4577
|
await this.loadHistory();
|
|
4516
4578
|
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4517
4579
|
if (pending2) {
|
|
@@ -4603,12 +4665,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4603
4665
|
token.status = "transferring";
|
|
4604
4666
|
this.tokens.set(token.id, token);
|
|
4605
4667
|
}
|
|
4668
|
+
await this.save();
|
|
4606
4669
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4607
4670
|
result.status = "submitted";
|
|
4608
4671
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4609
4672
|
const transferMode = request.transferMode ?? "instant";
|
|
4610
|
-
if (
|
|
4611
|
-
if (
|
|
4673
|
+
if (transferMode === "conservative") {
|
|
4674
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4612
4675
|
this.log("Executing conservative split...");
|
|
4613
4676
|
const splitExecutor = new TokenSplitExecutor({
|
|
4614
4677
|
stateTransitionClient: stClient,
|
|
@@ -4652,27 +4715,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4652
4715
|
requestIdHex: splitRequestIdHex
|
|
4653
4716
|
});
|
|
4654
4717
|
this.log(`Conservative split transfer completed`);
|
|
4655
|
-
}
|
|
4656
|
-
|
|
4657
|
-
const
|
|
4718
|
+
}
|
|
4719
|
+
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4720
|
+
const token = tokenWithAmount.uiToken;
|
|
4721
|
+
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4722
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4723
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4724
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4725
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4726
|
+
}
|
|
4727
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4728
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4729
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4730
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4731
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4732
|
+
memo: request.memo
|
|
4733
|
+
});
|
|
4734
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4735
|
+
const requestIdBytes = commitment.requestId;
|
|
4736
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4737
|
+
result.tokenTransfers.push({
|
|
4738
|
+
sourceTokenId: token.id,
|
|
4739
|
+
method: "direct",
|
|
4740
|
+
requestIdHex
|
|
4741
|
+
});
|
|
4742
|
+
this.log(`Token ${token.id} sent via CONSERVATIVE, requestId: ${requestIdHex}`);
|
|
4743
|
+
await this.removeToken(token.id);
|
|
4744
|
+
}
|
|
4745
|
+
} else {
|
|
4746
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4747
|
+
const senderPubkey = this.deps.identity.chainPubkey;
|
|
4748
|
+
let changeTokenPlaceholderId = null;
|
|
4749
|
+
let builtSplit = null;
|
|
4750
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4751
|
+
this.log("Building instant split bundle...");
|
|
4658
4752
|
const executor = new InstantSplitExecutor({
|
|
4659
4753
|
stateTransitionClient: stClient,
|
|
4660
4754
|
trustBase,
|
|
4661
4755
|
signingService,
|
|
4662
4756
|
devMode
|
|
4663
4757
|
});
|
|
4664
|
-
|
|
4758
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4665
4759
|
splitPlan.tokenToSplit.sdkToken,
|
|
4666
4760
|
splitPlan.splitAmount,
|
|
4667
4761
|
splitPlan.remainderAmount,
|
|
4668
4762
|
splitPlan.coinId,
|
|
4669
4763
|
recipientAddress,
|
|
4670
|
-
this.deps.transport,
|
|
4671
|
-
recipientPubkey,
|
|
4672
4764
|
{
|
|
4673
4765
|
memo: request.memo,
|
|
4674
4766
|
onChangeTokenCreated: async (changeToken) => {
|
|
4675
4767
|
const changeTokenData = changeToken.toJSON();
|
|
4768
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4769
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4770
|
+
}
|
|
4676
4771
|
const uiToken = {
|
|
4677
4772
|
id: crypto.randomUUID(),
|
|
4678
4773
|
coinId: request.coinId,
|
|
@@ -4695,65 +4790,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4695
4790
|
}
|
|
4696
4791
|
}
|
|
4697
4792
|
);
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
this.
|
|
4703
|
-
|
|
4793
|
+
this.log(`Split bundle built: splitGroupId=${builtSplit.splitGroupId}`);
|
|
4794
|
+
}
|
|
4795
|
+
const directCommitments = await Promise.all(
|
|
4796
|
+
splitPlan.tokensToTransferDirectly.map(
|
|
4797
|
+
(tw) => this.createSdkCommitment(tw.uiToken, recipientAddress, signingService)
|
|
4798
|
+
)
|
|
4799
|
+
);
|
|
4800
|
+
const directTokenEntries = splitPlan.tokensToTransferDirectly.map(
|
|
4801
|
+
(tw, i) => ({
|
|
4802
|
+
sourceToken: JSON.stringify(tw.sdkToken.toJSON()),
|
|
4803
|
+
commitmentData: JSON.stringify(directCommitments[i].toJSON()),
|
|
4804
|
+
amount: tw.uiToken.amount,
|
|
4805
|
+
coinId: tw.uiToken.coinId,
|
|
4806
|
+
tokenId: extractTokenIdFromSdkData(tw.uiToken.sdkData) || void 0
|
|
4807
|
+
})
|
|
4808
|
+
);
|
|
4809
|
+
const combinedBundle = {
|
|
4810
|
+
version: "6.0",
|
|
4811
|
+
type: "COMBINED_TRANSFER",
|
|
4812
|
+
transferId: result.id,
|
|
4813
|
+
splitBundle: builtSplit?.bundle ?? null,
|
|
4814
|
+
directTokens: directTokenEntries,
|
|
4815
|
+
totalAmount: request.amount.toString(),
|
|
4816
|
+
coinId: request.coinId,
|
|
4817
|
+
senderPubkey,
|
|
4818
|
+
memo: request.memo
|
|
4819
|
+
};
|
|
4820
|
+
console.log(
|
|
4821
|
+
`[Payments] Sending V6 combined bundle: transfer=${result.id.slice(0, 8)}... split=${!!builtSplit} direct=${directTokenEntries.length}`
|
|
4822
|
+
);
|
|
4823
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4824
|
+
token: JSON.stringify(combinedBundle),
|
|
4825
|
+
proof: null,
|
|
4826
|
+
memo: request.memo,
|
|
4827
|
+
sender: { transportPubkey: senderPubkey }
|
|
4828
|
+
});
|
|
4829
|
+
console.log(`[Payments] V6 combined bundle sent successfully`);
|
|
4830
|
+
if (builtSplit) {
|
|
4831
|
+
const bgPromise = builtSplit.startBackground();
|
|
4832
|
+
this.pendingBackgroundTasks.push(bgPromise);
|
|
4833
|
+
}
|
|
4834
|
+
if (builtSplit && splitPlan.remainderAmount) {
|
|
4835
|
+
changeTokenPlaceholderId = crypto.randomUUID();
|
|
4836
|
+
const placeholder = {
|
|
4837
|
+
id: changeTokenPlaceholderId,
|
|
4838
|
+
coinId: request.coinId,
|
|
4839
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4840
|
+
name: this.getCoinName(request.coinId),
|
|
4841
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4842
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4843
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4844
|
+
status: "transferring",
|
|
4845
|
+
createdAt: Date.now(),
|
|
4846
|
+
updatedAt: Date.now(),
|
|
4847
|
+
sdkData: JSON.stringify({ _placeholder: true })
|
|
4848
|
+
};
|
|
4849
|
+
this.tokens.set(placeholder.id, placeholder);
|
|
4850
|
+
this.log(`Placeholder change token created: ${placeholder.id} (${placeholder.amount})`);
|
|
4851
|
+
}
|
|
4852
|
+
for (const commitment of directCommitments) {
|
|
4853
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4854
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4855
|
+
);
|
|
4856
|
+
}
|
|
4857
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4704
4858
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4705
4859
|
result.tokenTransfers.push({
|
|
4706
4860
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4707
4861
|
method: "split",
|
|
4708
|
-
splitGroupId:
|
|
4709
|
-
nostrEventId: instantResult.nostrEventId
|
|
4862
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4710
4863
|
});
|
|
4711
|
-
this.log(`Instant split transfer completed`);
|
|
4712
4864
|
}
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
}
|
|
4723
|
-
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4724
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4725
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4726
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4727
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4728
|
-
memo: request.memo
|
|
4729
|
-
});
|
|
4730
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4731
|
-
} else {
|
|
4732
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4733
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4734
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4735
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4736
|
-
memo: request.memo
|
|
4865
|
+
for (let i = 0; i < splitPlan.tokensToTransferDirectly.length; i++) {
|
|
4866
|
+
const token = splitPlan.tokensToTransferDirectly[i].uiToken;
|
|
4867
|
+
const commitment = directCommitments[i];
|
|
4868
|
+
const requestIdBytes = commitment.requestId;
|
|
4869
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4870
|
+
result.tokenTransfers.push({
|
|
4871
|
+
sourceTokenId: token.id,
|
|
4872
|
+
method: "direct",
|
|
4873
|
+
requestIdHex
|
|
4737
4874
|
});
|
|
4738
|
-
|
|
4739
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4740
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4741
|
-
);
|
|
4875
|
+
await this.removeToken(token.id);
|
|
4742
4876
|
}
|
|
4743
|
-
|
|
4744
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4745
|
-
result.tokenTransfers.push({
|
|
4746
|
-
sourceTokenId: token.id,
|
|
4747
|
-
method: "direct",
|
|
4748
|
-
requestIdHex
|
|
4749
|
-
});
|
|
4750
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4751
|
-
await this.removeToken(token.id);
|
|
4877
|
+
this.log(`V6 combined transfer completed`);
|
|
4752
4878
|
}
|
|
4753
4879
|
result.status = "delivered";
|
|
4754
4880
|
await this.save();
|
|
4755
4881
|
await this.removeFromOutbox(result.id);
|
|
4756
4882
|
result.status = "completed";
|
|
4883
|
+
const tokenMap = new Map(result.tokens.map((t) => [t.id, t]));
|
|
4884
|
+
const sentTokenIds = result.tokenTransfers.map((tt) => ({
|
|
4885
|
+
id: tt.sourceTokenId,
|
|
4886
|
+
// For split tokens, use splitAmount (the portion sent), not the original token amount
|
|
4887
|
+
amount: tt.method === "split" ? splitPlan.splitAmount?.toString() || "0" : tokenMap.get(tt.sourceTokenId)?.amount || "0",
|
|
4888
|
+
source: tt.method === "split" ? "split" : "direct"
|
|
4889
|
+
}));
|
|
4757
4890
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4758
4891
|
await this.addToHistory({
|
|
4759
4892
|
type: "SENT",
|
|
@@ -4766,7 +4899,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4766
4899
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4767
4900
|
memo: request.memo,
|
|
4768
4901
|
transferId: result.id,
|
|
4769
|
-
tokenId: sentTokenId || void 0
|
|
4902
|
+
tokenId: sentTokenId || void 0,
|
|
4903
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4770
4904
|
});
|
|
4771
4905
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4772
4906
|
return result;
|
|
@@ -4936,6 +5070,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4936
5070
|
};
|
|
4937
5071
|
}
|
|
4938
5072
|
}
|
|
5073
|
+
// ===========================================================================
|
|
5074
|
+
// Shared Helpers for V5 and V6 Receiver Processing
|
|
5075
|
+
// ===========================================================================
|
|
5076
|
+
/**
|
|
5077
|
+
* Save a V5 split bundle as an unconfirmed token (shared by V5 standalone and V6 combined).
|
|
5078
|
+
* Returns the created UI token, or null if deduped.
|
|
5079
|
+
*
|
|
5080
|
+
* @param deferPersistence - If true, skip addToken/save calls (caller batches them).
|
|
5081
|
+
* The token is still added to the in-memory map for dedup; caller must call save().
|
|
5082
|
+
*/
|
|
5083
|
+
async saveUnconfirmedV5Token(bundle, senderPubkey, deferPersistence = false) {
|
|
5084
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
5085
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
5086
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5087
|
+
return null;
|
|
5088
|
+
}
|
|
5089
|
+
const registry = TokenRegistry.getInstance();
|
|
5090
|
+
const pendingData = {
|
|
5091
|
+
type: "v5_bundle",
|
|
5092
|
+
stage: "RECEIVED",
|
|
5093
|
+
bundleJson: JSON.stringify(bundle),
|
|
5094
|
+
senderPubkey,
|
|
5095
|
+
savedAt: Date.now(),
|
|
5096
|
+
attemptCount: 0
|
|
5097
|
+
};
|
|
5098
|
+
const uiToken = {
|
|
5099
|
+
id: deterministicId,
|
|
5100
|
+
coinId: bundle.coinId,
|
|
5101
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5102
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5103
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5104
|
+
amount: bundle.amount,
|
|
5105
|
+
status: "submitted",
|
|
5106
|
+
// UNCONFIRMED
|
|
5107
|
+
createdAt: Date.now(),
|
|
5108
|
+
updatedAt: Date.now(),
|
|
5109
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5110
|
+
};
|
|
5111
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5112
|
+
if (deferPersistence) {
|
|
5113
|
+
this.tokens.set(uiToken.id, uiToken);
|
|
5114
|
+
} else {
|
|
5115
|
+
await this.addToken(uiToken);
|
|
5116
|
+
await this.saveProcessedSplitGroupIds();
|
|
5117
|
+
}
|
|
5118
|
+
return uiToken;
|
|
5119
|
+
}
|
|
5120
|
+
/**
|
|
5121
|
+
* Save a commitment-only (NOSTR-FIRST) token and start proof polling.
|
|
5122
|
+
* Shared by standalone NOSTR-FIRST handler and V6 combined handler.
|
|
5123
|
+
* Returns the created UI token, or null if deduped/tombstoned.
|
|
5124
|
+
*
|
|
5125
|
+
* @param deferPersistence - If true, skip save() and commitment submission
|
|
5126
|
+
* (caller batches them). Token is added to in-memory map + proof polling is queued.
|
|
5127
|
+
* @param skipGenesisDedup - If true, skip genesis-ID-only dedup. V6 handler sets this
|
|
5128
|
+
* because bundle-level dedup protects against replays, and split children share genesis IDs.
|
|
5129
|
+
*/
|
|
5130
|
+
async saveCommitmentOnlyToken(sourceTokenInput, commitmentInput, senderPubkey, deferPersistence = false, skipGenesisDedup = false) {
|
|
5131
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5132
|
+
const sdkData = typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput);
|
|
5133
|
+
const nostrTokenId = extractTokenIdFromSdkData(sdkData);
|
|
5134
|
+
const nostrStateHash = extractStateHashFromSdkData(sdkData);
|
|
5135
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5136
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5137
|
+
return null;
|
|
5138
|
+
}
|
|
5139
|
+
if (nostrTokenId) {
|
|
5140
|
+
for (const existing of this.tokens.values()) {
|
|
5141
|
+
const existingTokenId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5142
|
+
if (existingTokenId !== nostrTokenId) continue;
|
|
5143
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5144
|
+
if (nostrStateHash && existingStateHash === nostrStateHash) {
|
|
5145
|
+
console.log(
|
|
5146
|
+
`[Payments] NOSTR-FIRST: Skipping duplicate token state ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`
|
|
5147
|
+
);
|
|
5148
|
+
return null;
|
|
5149
|
+
}
|
|
5150
|
+
if (!skipGenesisDedup) {
|
|
5151
|
+
console.log(
|
|
5152
|
+
`[Payments] NOSTR-FIRST: Skipping replay of finalized token ${nostrTokenId.slice(0, 8)}...`
|
|
5153
|
+
);
|
|
5154
|
+
return null;
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
const token = {
|
|
5159
|
+
id: crypto.randomUUID(),
|
|
5160
|
+
coinId: tokenInfo.coinId,
|
|
5161
|
+
symbol: tokenInfo.symbol,
|
|
5162
|
+
name: tokenInfo.name,
|
|
5163
|
+
decimals: tokenInfo.decimals,
|
|
5164
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5165
|
+
amount: tokenInfo.amount,
|
|
5166
|
+
status: "submitted",
|
|
5167
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5168
|
+
createdAt: Date.now(),
|
|
5169
|
+
updatedAt: Date.now(),
|
|
5170
|
+
sdkData
|
|
5171
|
+
};
|
|
5172
|
+
this.tokens.set(token.id, token);
|
|
5173
|
+
if (!deferPersistence) {
|
|
5174
|
+
await this.save();
|
|
5175
|
+
}
|
|
5176
|
+
try {
|
|
5177
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
5178
|
+
const requestIdBytes = commitment.requestId;
|
|
5179
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5180
|
+
if (!deferPersistence) {
|
|
5181
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5182
|
+
if (stClient) {
|
|
5183
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5184
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
this.addProofPollingJob({
|
|
5188
|
+
tokenId: token.id,
|
|
5189
|
+
requestIdHex,
|
|
5190
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5191
|
+
startedAt: Date.now(),
|
|
5192
|
+
attemptCount: 0,
|
|
5193
|
+
lastAttemptAt: 0,
|
|
5194
|
+
onProofReceived: async (tokenId) => {
|
|
5195
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
5196
|
+
}
|
|
5197
|
+
});
|
|
5198
|
+
} catch (err) {
|
|
5199
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5200
|
+
}
|
|
5201
|
+
return token;
|
|
5202
|
+
}
|
|
5203
|
+
// ===========================================================================
|
|
5204
|
+
// Combined Transfer V6 — Receiver
|
|
5205
|
+
// ===========================================================================
|
|
5206
|
+
/**
|
|
5207
|
+
* Process a received COMBINED_TRANSFER V6 bundle.
|
|
5208
|
+
*
|
|
5209
|
+
* Unpacks a single Nostr message into its component tokens:
|
|
5210
|
+
* - Optional V5 split bundle (saved as unconfirmed, resolved lazily)
|
|
5211
|
+
* - Zero or more direct tokens (saved as unconfirmed, proof-polled)
|
|
5212
|
+
*
|
|
5213
|
+
* Emits ONE transfer:incoming event and records ONE history entry.
|
|
5214
|
+
*/
|
|
5215
|
+
async processCombinedTransferBundle(bundle, senderPubkey) {
|
|
5216
|
+
this.ensureInitialized();
|
|
5217
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5218
|
+
await this.loadedPromise;
|
|
5219
|
+
}
|
|
5220
|
+
if (this.processedCombinedTransferIds.has(bundle.transferId)) {
|
|
5221
|
+
console.log(`[Payments] V6 combined transfer ${bundle.transferId.slice(0, 12)}... already processed, skipping`);
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
console.log(
|
|
5225
|
+
`[Payments] Processing V6 combined transfer ${bundle.transferId.slice(0, 12)}... (split=${!!bundle.splitBundle}, direct=${bundle.directTokens.length})`
|
|
5226
|
+
);
|
|
5227
|
+
const allTokens = [];
|
|
5228
|
+
const tokenBreakdown = [];
|
|
5229
|
+
const parsedDirectEntries = bundle.directTokens.map((entry) => ({
|
|
5230
|
+
sourceToken: typeof entry.sourceToken === "string" ? JSON.parse(entry.sourceToken) : entry.sourceToken,
|
|
5231
|
+
commitment: typeof entry.commitmentData === "string" ? JSON.parse(entry.commitmentData) : entry.commitmentData
|
|
5232
|
+
}));
|
|
5233
|
+
if (bundle.splitBundle) {
|
|
5234
|
+
const splitToken = await this.saveUnconfirmedV5Token(bundle.splitBundle, senderPubkey, true);
|
|
5235
|
+
if (splitToken) {
|
|
5236
|
+
allTokens.push(splitToken);
|
|
5237
|
+
tokenBreakdown.push({ id: splitToken.id, amount: splitToken.amount, source: "split" });
|
|
5238
|
+
} else {
|
|
5239
|
+
console.warn(`[Payments] V6: split token was deduped/failed \u2014 amount=${bundle.splitBundle.amount}`);
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
const directResults = await Promise.all(
|
|
5243
|
+
parsedDirectEntries.map(
|
|
5244
|
+
({ sourceToken, commitment }) => this.saveCommitmentOnlyToken(sourceToken, commitment, senderPubkey, true, true)
|
|
5245
|
+
)
|
|
5246
|
+
);
|
|
5247
|
+
for (let i = 0; i < directResults.length; i++) {
|
|
5248
|
+
const token = directResults[i];
|
|
5249
|
+
if (token) {
|
|
5250
|
+
allTokens.push(token);
|
|
5251
|
+
tokenBreakdown.push({ id: token.id, amount: token.amount, source: "direct" });
|
|
5252
|
+
} else {
|
|
5253
|
+
const entry = bundle.directTokens[i];
|
|
5254
|
+
console.warn(
|
|
5255
|
+
`[Payments] V6: direct token #${i} dropped (amount=${entry.amount}, tokenId=${entry.tokenId?.slice(0, 12) ?? "N/A"})`
|
|
5256
|
+
);
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
if (allTokens.length === 0) {
|
|
5260
|
+
console.log(`[Payments] V6 combined transfer: all tokens deduped, nothing to save`);
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
this.processedCombinedTransferIds.add(bundle.transferId);
|
|
5264
|
+
const [senderInfo] = await Promise.all([
|
|
5265
|
+
this.resolveSenderInfo(senderPubkey),
|
|
5266
|
+
this.save(),
|
|
5267
|
+
this.saveProcessedCombinedTransferIds(),
|
|
5268
|
+
...bundle.splitBundle ? [this.saveProcessedSplitGroupIds()] : []
|
|
5269
|
+
]);
|
|
5270
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5271
|
+
if (stClient) {
|
|
5272
|
+
for (const { commitment } of parsedDirectEntries) {
|
|
5273
|
+
import_TransferCommitment4.TransferCommitment.fromJSON(commitment).then(
|
|
5274
|
+
(c) => stClient.submitTransferCommitment(c)
|
|
5275
|
+
).catch(
|
|
5276
|
+
(err) => console.error("[Payments] V6 background commitment submit failed:", err)
|
|
5277
|
+
);
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5281
|
+
id: bundle.transferId,
|
|
5282
|
+
senderPubkey,
|
|
5283
|
+
senderNametag: senderInfo.senderNametag,
|
|
5284
|
+
tokens: allTokens,
|
|
5285
|
+
memo: bundle.memo,
|
|
5286
|
+
receivedAt: Date.now()
|
|
5287
|
+
});
|
|
5288
|
+
const actualAmount = allTokens.reduce((sum, t) => sum + BigInt(t.amount || "0"), 0n).toString();
|
|
5289
|
+
await this.addToHistory({
|
|
5290
|
+
type: "RECEIVED",
|
|
5291
|
+
amount: actualAmount,
|
|
5292
|
+
coinId: bundle.coinId,
|
|
5293
|
+
symbol: allTokens[0]?.symbol || bundle.coinId,
|
|
5294
|
+
timestamp: Date.now(),
|
|
5295
|
+
senderPubkey,
|
|
5296
|
+
...senderInfo,
|
|
5297
|
+
memo: bundle.memo,
|
|
5298
|
+
transferId: bundle.transferId,
|
|
5299
|
+
tokenId: allTokens[0]?.id,
|
|
5300
|
+
tokenIds: tokenBreakdown
|
|
5301
|
+
});
|
|
5302
|
+
if (bundle.splitBundle) {
|
|
5303
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5304
|
+
});
|
|
5305
|
+
this.scheduleResolveUnconfirmed();
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
/**
|
|
5309
|
+
* Persist processed combined transfer IDs to KV storage.
|
|
5310
|
+
*/
|
|
5311
|
+
async saveProcessedCombinedTransferIds() {
|
|
5312
|
+
const ids = Array.from(this.processedCombinedTransferIds);
|
|
5313
|
+
if (ids.length > 0) {
|
|
5314
|
+
await this.deps.storage.set(
|
|
5315
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS,
|
|
5316
|
+
JSON.stringify(ids)
|
|
5317
|
+
);
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
/**
|
|
5321
|
+
* Load processed combined transfer IDs from KV storage.
|
|
5322
|
+
*/
|
|
5323
|
+
async loadProcessedCombinedTransferIds() {
|
|
5324
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS);
|
|
5325
|
+
if (!data) return;
|
|
5326
|
+
try {
|
|
5327
|
+
const ids = JSON.parse(data);
|
|
5328
|
+
for (const id of ids) {
|
|
5329
|
+
this.processedCombinedTransferIds.add(id);
|
|
5330
|
+
}
|
|
5331
|
+
} catch {
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
4939
5334
|
/**
|
|
4940
5335
|
* Process a received INSTANT_SPLIT bundle.
|
|
4941
5336
|
*
|
|
@@ -4959,36 +5354,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4959
5354
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
4960
5355
|
}
|
|
4961
5356
|
try {
|
|
4962
|
-
const
|
|
4963
|
-
if (
|
|
4964
|
-
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5357
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5358
|
+
if (!uiToken) {
|
|
4965
5359
|
return { success: true, durationMs: 0 };
|
|
4966
5360
|
}
|
|
4967
|
-
const registry = TokenRegistry.getInstance();
|
|
4968
|
-
const pendingData = {
|
|
4969
|
-
type: "v5_bundle",
|
|
4970
|
-
stage: "RECEIVED",
|
|
4971
|
-
bundleJson: JSON.stringify(bundle),
|
|
4972
|
-
senderPubkey,
|
|
4973
|
-
savedAt: Date.now(),
|
|
4974
|
-
attemptCount: 0
|
|
4975
|
-
};
|
|
4976
|
-
const uiToken = {
|
|
4977
|
-
id: deterministicId,
|
|
4978
|
-
coinId: bundle.coinId,
|
|
4979
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4980
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4981
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4982
|
-
amount: bundle.amount,
|
|
4983
|
-
status: "submitted",
|
|
4984
|
-
// UNCONFIRMED
|
|
4985
|
-
createdAt: Date.now(),
|
|
4986
|
-
updatedAt: Date.now(),
|
|
4987
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4988
|
-
};
|
|
4989
|
-
await this.addToken(uiToken);
|
|
4990
|
-
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
4991
|
-
await this.saveProcessedSplitGroupIds();
|
|
4992
5361
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
4993
5362
|
await this.addToHistory({
|
|
4994
5363
|
type: "RECEIVED",
|
|
@@ -4999,7 +5368,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4999
5368
|
senderPubkey,
|
|
5000
5369
|
...senderInfo,
|
|
5001
5370
|
memo,
|
|
5002
|
-
tokenId:
|
|
5371
|
+
tokenId: uiToken.id
|
|
5003
5372
|
});
|
|
5004
5373
|
this.deps.emitEvent("transfer:incoming", {
|
|
5005
5374
|
id: bundle.splitGroupId,
|
|
@@ -5649,16 +6018,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5649
6018
|
}
|
|
5650
6019
|
/**
|
|
5651
6020
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5652
|
-
* Excludes tokens with status 'spent'
|
|
6021
|
+
* Excludes tokens with status 'spent' or 'invalid'.
|
|
6022
|
+
* Tokens with status 'transferring' are counted as unconfirmed (visible in UI as "Sending").
|
|
5653
6023
|
*/
|
|
5654
6024
|
aggregateTokens(coinId) {
|
|
5655
6025
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5656
6026
|
for (const token of this.tokens.values()) {
|
|
5657
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6027
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5658
6028
|
if (coinId && token.coinId !== coinId) continue;
|
|
5659
6029
|
const key = token.coinId;
|
|
5660
6030
|
const amount = BigInt(token.amount);
|
|
5661
6031
|
const isConfirmed = token.status === "confirmed";
|
|
6032
|
+
const isTransferring = token.status === "transferring";
|
|
5662
6033
|
const existing = assetsMap.get(key);
|
|
5663
6034
|
if (existing) {
|
|
5664
6035
|
if (isConfirmed) {
|
|
@@ -5668,6 +6039,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5668
6039
|
existing.unconfirmedAmount += amount;
|
|
5669
6040
|
existing.unconfirmedTokenCount++;
|
|
5670
6041
|
}
|
|
6042
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5671
6043
|
} else {
|
|
5672
6044
|
assetsMap.set(key, {
|
|
5673
6045
|
coinId: token.coinId,
|
|
@@ -5678,7 +6050,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5678
6050
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5679
6051
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5680
6052
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5681
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6053
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6054
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5682
6055
|
});
|
|
5683
6056
|
}
|
|
5684
6057
|
}
|
|
@@ -5696,6 +6069,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5696
6069
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5697
6070
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5698
6071
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6072
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5699
6073
|
priceUsd: null,
|
|
5700
6074
|
priceEur: null,
|
|
5701
6075
|
change24h: null,
|
|
@@ -6548,6 +6922,33 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6548
6922
|
}
|
|
6549
6923
|
}
|
|
6550
6924
|
}
|
|
6925
|
+
/**
|
|
6926
|
+
* Import history entries from remote TXF data into local store.
|
|
6927
|
+
* Delegates to the local TokenStorageProvider's importHistoryEntries() for
|
|
6928
|
+
* persistent storage, with in-memory fallback.
|
|
6929
|
+
* Reused by both load() (initial IPFS fetch) and _doSync() (merge result).
|
|
6930
|
+
*/
|
|
6931
|
+
async importRemoteHistoryEntries(entries) {
|
|
6932
|
+
if (entries.length === 0) return 0;
|
|
6933
|
+
const provider = this.getLocalTokenStorageProvider();
|
|
6934
|
+
if (provider?.importHistoryEntries) {
|
|
6935
|
+
const imported2 = await provider.importHistoryEntries(entries);
|
|
6936
|
+
if (imported2 > 0) {
|
|
6937
|
+
this._historyCache = await provider.getHistoryEntries();
|
|
6938
|
+
}
|
|
6939
|
+
return imported2;
|
|
6940
|
+
}
|
|
6941
|
+
const existingKeys = new Set(this._historyCache.map((e) => e.dedupKey));
|
|
6942
|
+
let imported = 0;
|
|
6943
|
+
for (const entry of entries) {
|
|
6944
|
+
if (!existingKeys.has(entry.dedupKey)) {
|
|
6945
|
+
this._historyCache.push(entry);
|
|
6946
|
+
existingKeys.add(entry.dedupKey);
|
|
6947
|
+
imported++;
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
return imported;
|
|
6951
|
+
}
|
|
6551
6952
|
/**
|
|
6552
6953
|
* Get the first local token storage provider (for history operations).
|
|
6553
6954
|
*/
|
|
@@ -6795,6 +7196,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6795
7196
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
6796
7197
|
this.nametags = savedNametags;
|
|
6797
7198
|
}
|
|
7199
|
+
const txfData = result.merged;
|
|
7200
|
+
if (txfData._history && txfData._history.length > 0) {
|
|
7201
|
+
const imported = await this.importRemoteHistoryEntries(txfData._history);
|
|
7202
|
+
if (imported > 0) {
|
|
7203
|
+
this.log(`Imported ${imported} history entries from IPFS sync`);
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
6798
7206
|
totalAdded += result.added;
|
|
6799
7207
|
totalRemoved += result.removed;
|
|
6800
7208
|
}
|
|
@@ -7093,7 +7501,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7093
7501
|
/**
|
|
7094
7502
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7095
7503
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7096
|
-
*
|
|
7504
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7097
7505
|
*/
|
|
7098
7506
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7099
7507
|
try {
|
|
@@ -7103,41 +7511,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7103
7511
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7104
7512
|
return;
|
|
7105
7513
|
}
|
|
7106
|
-
const
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
decimals: tokenInfo.decimals,
|
|
7113
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7114
|
-
amount: tokenInfo.amount,
|
|
7115
|
-
status: "submitted",
|
|
7116
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7117
|
-
createdAt: Date.now(),
|
|
7118
|
-
updatedAt: Date.now(),
|
|
7119
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7120
|
-
};
|
|
7121
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7122
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7123
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7124
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7125
|
-
return;
|
|
7126
|
-
}
|
|
7127
|
-
this.tokens.set(token.id, token);
|
|
7128
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
|
|
7129
|
-
await this.save();
|
|
7130
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
|
|
7514
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7515
|
+
sourceTokenInput,
|
|
7516
|
+
commitmentInput,
|
|
7517
|
+
transfer.senderTransportPubkey
|
|
7518
|
+
);
|
|
7519
|
+
if (!token) return;
|
|
7131
7520
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7132
|
-
|
|
7521
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7133
7522
|
id: transfer.id,
|
|
7134
7523
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7135
7524
|
senderNametag: senderInfo.senderNametag,
|
|
7136
7525
|
tokens: [token],
|
|
7137
7526
|
memo: payload.memo,
|
|
7138
7527
|
receivedAt: transfer.timestamp
|
|
7139
|
-
};
|
|
7140
|
-
|
|
7528
|
+
});
|
|
7529
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7141
7530
|
await this.addToHistory({
|
|
7142
7531
|
type: "RECEIVED",
|
|
7143
7532
|
amount: token.amount,
|
|
@@ -7149,29 +7538,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7149
7538
|
memo: payload.memo,
|
|
7150
7539
|
tokenId: nostrTokenId || token.id
|
|
7151
7540
|
});
|
|
7152
|
-
try {
|
|
7153
|
-
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
7154
|
-
const requestIdBytes = commitment.requestId;
|
|
7155
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7156
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7157
|
-
if (stClient) {
|
|
7158
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7159
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7160
|
-
}
|
|
7161
|
-
this.addProofPollingJob({
|
|
7162
|
-
tokenId: token.id,
|
|
7163
|
-
requestIdHex,
|
|
7164
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7165
|
-
startedAt: Date.now(),
|
|
7166
|
-
attemptCount: 0,
|
|
7167
|
-
lastAttemptAt: 0,
|
|
7168
|
-
onProofReceived: async (tokenId) => {
|
|
7169
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7170
|
-
}
|
|
7171
|
-
});
|
|
7172
|
-
} catch (err) {
|
|
7173
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7174
|
-
}
|
|
7175
7541
|
} catch (error) {
|
|
7176
7542
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7177
7543
|
}
|
|
@@ -7290,6 +7656,28 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7290
7656
|
try {
|
|
7291
7657
|
const payload = transfer.payload;
|
|
7292
7658
|
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
7659
|
+
let combinedBundle = null;
|
|
7660
|
+
if (isCombinedTransferBundleV6(payload)) {
|
|
7661
|
+
combinedBundle = payload;
|
|
7662
|
+
} else if (payload.token) {
|
|
7663
|
+
try {
|
|
7664
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7665
|
+
if (isCombinedTransferBundleV6(inner)) {
|
|
7666
|
+
combinedBundle = inner;
|
|
7667
|
+
}
|
|
7668
|
+
} catch {
|
|
7669
|
+
}
|
|
7670
|
+
}
|
|
7671
|
+
if (combinedBundle) {
|
|
7672
|
+
this.log("Processing COMBINED_TRANSFER V6 bundle...");
|
|
7673
|
+
try {
|
|
7674
|
+
await this.processCombinedTransferBundle(combinedBundle, transfer.senderTransportPubkey);
|
|
7675
|
+
this.log("COMBINED_TRANSFER V6 processed successfully");
|
|
7676
|
+
} catch (err) {
|
|
7677
|
+
console.error("[Payments] COMBINED_TRANSFER V6 processing error:", err);
|
|
7678
|
+
}
|
|
7679
|
+
return;
|
|
7680
|
+
}
|
|
7293
7681
|
let instantBundle = null;
|
|
7294
7682
|
if (isInstantSplitBundle(payload)) {
|
|
7295
7683
|
instantBundle = payload;
|
|
@@ -7441,17 +7829,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7441
7829
|
memo: payload.memo,
|
|
7442
7830
|
tokenId: incomingTokenId || token.id
|
|
7443
7831
|
});
|
|
7832
|
+
const incomingTransfer = {
|
|
7833
|
+
id: transfer.id,
|
|
7834
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
7835
|
+
senderNametag: senderInfo.senderNametag,
|
|
7836
|
+
tokens: [token],
|
|
7837
|
+
memo: payload.memo,
|
|
7838
|
+
receivedAt: transfer.timestamp
|
|
7839
|
+
};
|
|
7840
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7841
|
+
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7842
|
+
} else {
|
|
7843
|
+
this.log(`Duplicate transfer ignored: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7444
7844
|
}
|
|
7445
|
-
const incomingTransfer = {
|
|
7446
|
-
id: transfer.id,
|
|
7447
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7448
|
-
senderNametag: senderInfo.senderNametag,
|
|
7449
|
-
tokens: [token],
|
|
7450
|
-
memo: payload.memo,
|
|
7451
|
-
receivedAt: transfer.timestamp
|
|
7452
|
-
};
|
|
7453
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7454
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7455
7845
|
} catch (error) {
|
|
7456
7846
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7457
7847
|
}
|
|
@@ -7520,6 +7910,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7520
7910
|
return data ? JSON.parse(data) : [];
|
|
7521
7911
|
}
|
|
7522
7912
|
async createStorageData() {
|
|
7913
|
+
const sorted = [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
|
|
7523
7914
|
return await buildTxfStorageData(
|
|
7524
7915
|
Array.from(this.tokens.values()),
|
|
7525
7916
|
{
|
|
@@ -7531,7 +7922,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7531
7922
|
nametags: this.nametags,
|
|
7532
7923
|
tombstones: this.tombstones,
|
|
7533
7924
|
archivedTokens: this.archivedTokens,
|
|
7534
|
-
forkedTokens: this.forkedTokens
|
|
7925
|
+
forkedTokens: this.forkedTokens,
|
|
7926
|
+
historyEntries: sorted.slice(0, MAX_SYNCED_HISTORY_ENTRIES)
|
|
7535
7927
|
}
|
|
7536
7928
|
);
|
|
7537
7929
|
}
|