@kaspacom/kcc20-tx-builder 0.8.0-fee.13 → 0.8.0-fee.14

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "kcc20-example-compatibility/v1",
3
- "packageVersion": "0.8.0-fee.13",
3
+ "packageVersion": "0.8.0-fee.14",
4
4
  "contractsRepository": "https://github.com/KASPACOM/kaspa-covenants",
5
5
  "contractsCommit": "1a15362b35661464587cf07ffe8a42186a545897",
6
6
  "compiler": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaspacom/kcc20-tx-builder",
3
- "version": "0.8.0-fee.13",
3
+ "version": "0.8.0-fee.14",
4
4
  "description": "Runtime-independent KCC20 covenant transaction and PSKT construction primitives.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "KaspaCom",
package/src/engine.ts CHANGED
@@ -5517,101 +5517,148 @@ export function createKcc20PsktBuilderEngine(
5517
5517
  : priorityFee + 10_000n;
5518
5518
  const fundingEntry = selectFundingEntry(walletUtxos.entries, fundingNeeded,
5519
5519
  spentEntries.map(entry => entry.outpoint), params.fundingOutpoint);
5520
- const fundingChange = covenantSompi + utxoAmountSompi(fundingEntry) - outputSompi - priorityFee;
5521
- if (fundingChange <= 10_000n) throw new Error("seller group funding change is too small");
5522
- const fundingChangeIndex = outputs.length;
5523
- outputs.push(new TransactionOutput(fundingChange, payToAddressScript(walletAddress)));
5524
- const transaction = new Transaction({ version: 1, lockTime: 0n,
5525
- inputs: [
5526
- { previousOutpoint: holderEntry.outpoint, utxo: holderEntry,
5527
- sequence: 0n, sigOpCount: 0, computeBudget: 1000 },
5528
- ...bidEntries.map(({ entry }) => ({ previousOutpoint: entry.outpoint, utxo: entry,
5529
- sequence: 0n, sigOpCount: 0, computeBudget: 100 })),
5530
- ...(ticketEntry ? [{ previousOutpoint: ticketEntry.outpoint, utxo: ticketEntry,
5531
- sequence: 0n, sigOpCount: 0, computeBudget: 600 }] : []),
5532
- { previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
5533
- sequence: 0n, sigOpCount: 0, computeBudget: 30 },
5534
- ], outputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
5535
- });
5536
- // Pinned signed consensus proof uses 394 units for ten-bid leader and
5537
- // 43 for each bid delegator. Leave more than 2x headroom per role.
5538
- setVersionOneInputMassFields(transaction, 1000);
5539
- for (let slot = 0; slot < legs.length; slot++) transaction.inputs[slot + 1].computeBudget = 100;
5540
- const ticketInputIndex = ticketEntry ? legs.length + 1 : -1;
5541
- if (ticketEntry) transaction.inputs[ticketInputIndex].computeBudget = 600;
5542
- const fundingInputIndex = legs.length + 1 + (ticketEntry ? 1 : 0);
5543
- transaction.inputs[fundingInputIndex].computeBudget = 30;
5544
- assertUniqueTransactionInputOutpoints(transaction, "fee-market seller group");
5545
- const legBytes = new Uint8Array(legs.length * 32);
5546
- const view = new DataView(legBytes.buffer);
5547
- for (let slot = 0; slot < legs.length; slot++) {
5548
- const indexes = legIndexes[slot];
5549
- for (const [field, value] of [legs[slot].fill, indexes.buyerIndex,
5550
- indexes.continuationIndex < 0 ? 0 : indexes.continuationIndex,
5551
- indexes.sellerIndex].entries()) {
5552
- view.setBigInt64(slot * 32 + field * 8, BigInt(value), true);
5553
- }
5554
- }
5555
- const receiptParts = templateParts(normalizeSilverScriptArtifact(artifacts.receipt));
5556
- const scripts = [{ inputIndex: 0,
5557
- scriptHex: bytesToHex(holderPlan.holder.redeemScript), signType: 1,
5558
- signatureScript: { mode: "ordered-args", args: [
5559
- { type: "signature" }, i64TemplateArg(legs.length), dataTemplateArg(legBytes),
5560
- dataTemplateArg(vector), i64TemplateArg(ticketInputIndex), i64TemplateArg(receiptOutputIndex),
5561
- i64TemplateArg(changeIndex), i64TemplateArg(refundIndex), i64TemplateArg(ticketRefundIndex),
5562
- dataTemplateArg(feeTotal > 0n ? receiptParts.prefix : new Uint8Array()),
5563
- dataTemplateArg(feeTotal > 0n ? receiptParts.suffix : new Uint8Array()),
5564
- dataTemplateArg(dispatchTagFor(artifacts.holder, "sellIntoBids")),
5565
- ] } }];
5566
- if (ticketEntry) scripts.push(feeTicketBurnScriptHint(ticketInputIndex,
5567
- ticket.script, ticket.burnDispatchTag, ticketRefundIndex,
5568
- ticket.burnTakesRefundOutput));
5569
- for (let slot = 0; slot < legs.length; slot++) {
5570
- const delegate = covenantScriptBuilder(kw);
5571
- for (const value of [0, slot, legs[slot].remaining,
5572
- legIndexes[slot].continuationIndex]) delegate.addI64(BigInt(value));
5573
- delegate.addData(dispatchTagFor(artifacts.bid, "delegateSellerGroup"));
5574
- transaction.inputs[slot + 1].signatureScript = encodeCovenantP2shSignatureScript(
5575
- delegate.drain(), legs[slot].bidScript.redeemScript);
5576
- }
5577
- const feeResult = transactionWithPredictedSignedFeeChange(
5578
- kw, transaction, fundingChangeIndex, network, "fee-market seller group", candidate => {
5579
- const serialized = candidate.serializeToSafeJSON();
5580
- if (typeof serialized !== "string") throw new Error("seller group preflight serialization failed");
5581
- const predicted = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
5582
- predicted.inputs[fundingInputIndex].signatureScript = encodeP2pkSignatureScript(
5583
- bytesToHex(dummySignatureBytes()));
5584
- return typeof kw.Transaction.deserializeFromSafeJSON === "function"
5585
- ? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predicted))
5586
- : kw.Transaction.deserializeFromJSON(JSON.stringify(predicted));
5520
+ let estimatedFundingFeeSompi = priorityFee;
5521
+ const buildWithFunding = (fundingEntry) => {
5522
+ const fundingChange = covenantSompi + utxoAmountSompi(fundingEntry) - outputSompi - priorityFee;
5523
+ if (fundingChange <= 10_000n) throw new Error("seller group funding change is too small");
5524
+ const fundingChangeIndex = outputs.length;
5525
+ const candidateOutputs = [...outputs,
5526
+ new TransactionOutput(fundingChange, payToAddressScript(walletAddress))];
5527
+ const transaction = new Transaction({ version: 1, lockTime: 0n,
5528
+ inputs: [
5529
+ { previousOutpoint: holderEntry.outpoint, utxo: holderEntry,
5530
+ sequence: 0n, sigOpCount: 0, computeBudget: 1000 },
5531
+ ...bidEntries.map(({ entry }) => ({ previousOutpoint: entry.outpoint, utxo: entry,
5532
+ sequence: 0n, sigOpCount: 0, computeBudget: 100 })),
5533
+ ...(ticketEntry ? [{ previousOutpoint: ticketEntry.outpoint, utxo: ticketEntry,
5534
+ sequence: 0n, sigOpCount: 0, computeBudget: 600 }] : []),
5535
+ { previousOutpoint: fundingEntry.outpoint, utxo: fundingEntry,
5536
+ sequence: 0n, sigOpCount: 0, computeBudget: 30 },
5537
+ ], outputs: candidateOutputs, subnetworkId: SUBNETWORK_ID_NATIVE, gas: 0n, payload: "",
5587
5538
  });
5588
- const signedMass = calculateToccataV1NonContextualMass(
5589
- feeResult.predictedTransaction, "fee-market seller group");
5590
- const storageMass = calculateToccataStorageMass(
5591
- feeResult.predictedTransaction, "fee-market seller group");
5592
- if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
5593
- signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
5594
- storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
5595
- throw kcc20PsktBuilderError("fee-market seller group exceeds signed mass target",
5596
- "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
5597
- transientMass: signedMass.transientMass, storageMass });
5598
- }
5599
- return { schema: OUTPUT_SCHEMA,
5600
- psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
5601
- signInputs: [{ index: fundingInputIndex, sighashType: 1 }], scripts,
5602
- submitTransactionSupported: true,
5603
- metadata: { builderKey: "kcc20fee.sell-into-bids",
5604
- operation: "fee-market-sell-into-bids", network, walletAddress,
5605
- tokenId, marketId, policyId, fillCount: legs.length,
5606
- soldAmount: sold.toString(),
5607
- grossSompi: legs.reduce((sum, leg) => sum + leg.gross, 0n).toString(),
5608
- protocolFeeSompi: protocolTotal.toString(),
5609
- creatorFeeSompi: creatorTotal.toString(), receiptOutputIndex,
5610
- ticketInputIndex, ticketRefundIndex,
5611
- sellerChangeIndex: changeIndex, sellerRefundIndex: refundIndex,
5612
- signedMass: { compute: signedMass.computeMass.toString(),
5613
- transient: signedMass.transientMass.toString(), storage: storageMass.toString() } },
5539
+ // Pinned signed consensus proof uses 394 units for ten-bid leader and
5540
+ // 43 for each bid delegator. Leave more than 2x headroom per role.
5541
+ setVersionOneInputMassFields(transaction, 1000);
5542
+ for (let slot = 0; slot < legs.length; slot++) transaction.inputs[slot + 1].computeBudget = 100;
5543
+ const ticketInputIndex = ticketEntry ? legs.length + 1 : -1;
5544
+ if (ticketEntry) transaction.inputs[ticketInputIndex].computeBudget = 600;
5545
+ const fundingInputIndex = legs.length + 1 + (ticketEntry ? 1 : 0);
5546
+ transaction.inputs[fundingInputIndex].computeBudget = 30;
5547
+ assertUniqueTransactionInputOutpoints(transaction, "fee-market seller group");
5548
+ const legBytes = new Uint8Array(legs.length * 32);
5549
+ const view = new DataView(legBytes.buffer);
5550
+ for (let slot = 0; slot < legs.length; slot++) {
5551
+ const indexes = legIndexes[slot];
5552
+ for (const [field, value] of [legs[slot].fill, indexes.buyerIndex,
5553
+ indexes.continuationIndex < 0 ? 0 : indexes.continuationIndex,
5554
+ indexes.sellerIndex].entries()) {
5555
+ view.setBigInt64(slot * 32 + field * 8, BigInt(value), true);
5556
+ }
5557
+ }
5558
+ const receiptParts = templateParts(normalizeSilverScriptArtifact(artifacts.receipt));
5559
+ const scripts = [{ inputIndex: 0,
5560
+ scriptHex: bytesToHex(holderPlan.holder.redeemScript), signType: 1,
5561
+ signatureScript: { mode: "ordered-args", args: [
5562
+ { type: "signature" }, i64TemplateArg(legs.length), dataTemplateArg(legBytes),
5563
+ dataTemplateArg(vector), i64TemplateArg(ticketInputIndex), i64TemplateArg(receiptOutputIndex),
5564
+ i64TemplateArg(changeIndex), i64TemplateArg(refundIndex), i64TemplateArg(ticketRefundIndex),
5565
+ dataTemplateArg(feeTotal > 0n ? receiptParts.prefix : new Uint8Array()),
5566
+ dataTemplateArg(feeTotal > 0n ? receiptParts.suffix : new Uint8Array()),
5567
+ dataTemplateArg(dispatchTagFor(artifacts.holder, "sellIntoBids")),
5568
+ ] } }];
5569
+ if (ticketEntry) scripts.push(feeTicketBurnScriptHint(ticketInputIndex,
5570
+ ticket.script, ticket.burnDispatchTag, ticketRefundIndex,
5571
+ ticket.burnTakesRefundOutput));
5572
+ for (let slot = 0; slot < legs.length; slot++) {
5573
+ const delegate = covenantScriptBuilder(kw);
5574
+ for (const value of [0, slot, legs[slot].remaining,
5575
+ legIndexes[slot].continuationIndex]) delegate.addI64(BigInt(value));
5576
+ delegate.addData(dispatchTagFor(artifacts.bid, "delegateSellerGroup"));
5577
+ transaction.inputs[slot + 1].signatureScript = encodeCovenantP2shSignatureScript(
5578
+ delegate.drain(), legs[slot].bidScript.redeemScript);
5579
+ }
5580
+ const feeResult = transactionWithPredictedSignedFeeChange(
5581
+ kw, transaction, fundingChangeIndex, network, "fee-market seller group", candidate => {
5582
+ const serialized = candidate.serializeToSafeJSON();
5583
+ if (typeof serialized !== "string") throw new Error("seller group preflight serialization failed");
5584
+ const predicted = transactionJsonWithScriptHints(kw, JSON.parse(serialized), scripts);
5585
+ predicted.inputs[fundingInputIndex].signatureScript = encodeP2pkSignatureScript(
5586
+ bytesToHex(dummySignatureBytes()));
5587
+ return typeof kw.Transaction.deserializeFromSafeJSON === "function"
5588
+ ? kw.Transaction.deserializeFromSafeJSON(JSON.stringify(predicted))
5589
+ : kw.Transaction.deserializeFromJSON(JSON.stringify(predicted));
5590
+ });
5591
+ estimatedFundingFeeSompi = feeResult.requiredFeeSompi;
5592
+ const signedMass = calculateToccataV1NonContextualMass(
5593
+ feeResult.predictedTransaction, "fee-market seller group");
5594
+ const storageMass = calculateToccataStorageMass(
5595
+ feeResult.predictedTransaction, "fee-market seller group");
5596
+ if (signedMass.computeMass > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
5597
+ signedMass.transientMass > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
5598
+ storageMass > CONSOLIDATION_TARGET_TRANSACTION_MASS) {
5599
+ throw kcc20PsktBuilderError("fee-market seller group exceeds signed mass target",
5600
+ "TRANSACTION_MASS_EXCEEDED", { computeMass: signedMass.computeMass,
5601
+ transientMass: signedMass.transientMass, storageMass });
5602
+ }
5603
+ return { schema: OUTPUT_SCHEMA,
5604
+ psktTransactionJson: feeResult.transaction.serializeToSafeJSON(),
5605
+ signInputs: [{ index: fundingInputIndex, sighashType: 1 }], scripts,
5606
+ submitTransactionSupported: true,
5607
+ metadata: { builderKey: "kcc20fee.sell-into-bids",
5608
+ operation: "fee-market-sell-into-bids", network, walletAddress,
5609
+ tokenId, marketId, policyId, fillCount: legs.length,
5610
+ soldAmount: sold.toString(),
5611
+ grossSompi: legs.reduce((sum, leg) => sum + leg.gross, 0n).toString(),
5612
+ protocolFeeSompi: protocolTotal.toString(),
5613
+ creatorFeeSompi: creatorTotal.toString(), receiptOutputIndex,
5614
+ ticketInputIndex, ticketRefundIndex,
5615
+ sellerChangeIndex: changeIndex, sellerRefundIndex: refundIndex,
5616
+ signedMass: { compute: signedMass.computeMass.toString(),
5617
+ transient: signedMass.transientMass.toString(), storage: storageMass.toString() } },
5618
+ };
5614
5619
  };
5620
+ try {
5621
+ return buildWithFunding(fundingEntry);
5622
+ } catch (initialError) {
5623
+ // A large funding input can increase contextual storage mass. Preserve
5624
+ // caller-selected inputs; otherwise try at most seven smaller live
5625
+ // wallet UTXO amounts without changing the trade, fees, or mass limits.
5626
+ const details = initialError?.builderErrorDetails;
5627
+ if (params.fundingOutpoint != null ||
5628
+ initialError?.builderErrorCode !== "TRANSACTION_MASS_EXCEEDED" ||
5629
+ BigInt(details?.computeMass ?? 0) > CONSOLIDATION_TARGET_TRANSACTION_MASS ||
5630
+ BigInt(details?.transientMass ?? 0) > CONSOLIDATION_TARGET_TRANSACTION_MASS * 2n ||
5631
+ BigInt(details?.storageMass ?? 0) <= CONSOLIDATION_TARGET_TRANSACTION_MASS) {
5632
+ throw initialError;
5633
+ }
5634
+ const skipped = [...spentEntries.map(entry => entry.outpoint), fundingEntry.outpoint];
5635
+ const conservativeFundingNeeded = fundingNeeded +
5636
+ (estimatedFundingFeeSompi > priorityFee ? estimatedFundingFeeSompi - priorityFee : 0n);
5637
+ const candidates = walletUtxos.entries.filter(entry =>
5638
+ utxoAmountSompi(entry) >= conservativeFundingNeeded &&
5639
+ utxoAmountSompi(entry) < utxoAmountSompi(fundingEntry))
5640
+ .sort((a, b) => utxoAmountSompi(a) < utxoAmountSompi(b) ? -1 :
5641
+ utxoAmountSompi(a) > utxoAmountSompi(b) ? 1 : 0);
5642
+ let attempted = 0;
5643
+ const attemptedAmounts = new Set<string>();
5644
+ for (const candidate of candidates) {
5645
+ if (attempted === 7) break;
5646
+ const amountKey = utxoAmountSompi(candidate).toString();
5647
+ if (attemptedAmounts.has(amountKey)) continue;
5648
+ let selected;
5649
+ try { selected = selectFundingEntry([candidate], fundingNeeded, skipped, params.fundingOutpoint); }
5650
+ catch { continue; }
5651
+ skipped.push(selected.outpoint);
5652
+ attemptedAmounts.add(amountKey);
5653
+ attempted++;
5654
+ try { return buildWithFunding(selected); }
5655
+ catch (error) {
5656
+ if (!["TRANSACTION_MASS_EXCEEDED", "KAS_FUNDING_CHANGE_TOO_SMALL",
5657
+ "KAS_FUNDING_FEE_INSUFFICIENT"].includes(error?.builderErrorCode)) throw error;
5658
+ }
5659
+ }
5660
+ throw initialError;
5661
+ }
5615
5662
  } finally { await rpc.disconnect(); }
5616
5663
  }
5617
5664