@btc-vision/transaction 1.6.5 → 1.6.7

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.
Files changed (27) hide show
  1. package/browser/_version.d.ts +1 -1
  2. package/browser/index.js +1 -1
  3. package/browser/transaction/TransactionFactory.d.ts +6 -0
  4. package/browser/transaction/builders/TransactionBuilder.d.ts +6 -1
  5. package/browser/transaction/interfaces/ITransactionParameters.d.ts +1 -0
  6. package/build/_version.d.ts +1 -1
  7. package/build/_version.js +1 -1
  8. package/build/transaction/TransactionFactory.d.ts +6 -0
  9. package/build/transaction/TransactionFactory.js +160 -70
  10. package/build/transaction/builders/DeploymentTransaction.js +2 -19
  11. package/build/transaction/builders/FundingTransaction.js +2 -1
  12. package/build/transaction/builders/InteractionTransactionP2WDA.js +2 -19
  13. package/build/transaction/builders/MultiSignTransaction.js +2 -2
  14. package/build/transaction/builders/SharedInteractionTransaction.js +6 -22
  15. package/build/transaction/builders/TransactionBuilder.d.ts +6 -1
  16. package/build/transaction/builders/TransactionBuilder.js +290 -63
  17. package/build/transaction/interfaces/ITransactionParameters.d.ts +1 -0
  18. package/package.json +9 -9
  19. package/src/_version.ts +1 -1
  20. package/src/transaction/TransactionFactory.ts +232 -102
  21. package/src/transaction/builders/DeploymentTransaction.ts +2 -29
  22. package/src/transaction/builders/FundingTransaction.ts +6 -1
  23. package/src/transaction/builders/InteractionTransactionP2WDA.ts +2 -24
  24. package/src/transaction/builders/MultiSignTransaction.ts +2 -2
  25. package/src/transaction/builders/SharedInteractionTransaction.ts +10 -26
  26. package/src/transaction/builders/TransactionBuilder.ts +432 -64
  27. package/src/transaction/interfaces/ITransactionParameters.ts +1 -0
@@ -1,6 +1,6 @@
1
1
  import { P2TRPayment, PaymentType, Psbt, PsbtInput, Signer, Taptree, toXOnly, } from '@btc-vision/bitcoin';
2
2
  import { ECPairInterface } from 'ecpair';
3
- import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder, } from './TransactionBuilder.js';
3
+ import { MINIMUM_AMOUNT_REWARD, TransactionBuilder } from './TransactionBuilder.js';
4
4
  import { TransactionType } from '../enums/TransactionType.js';
5
5
  import { CalldataGenerator } from '../../generators/builders/CalldataGenerator.js';
6
6
  import { SharedInteractionParameters } from '../interfaces/ITransactionParameters.js';
@@ -344,35 +344,19 @@ export abstract class SharedInteractionTransaction<
344
344
  protected async createMineableRewardOutputs(): Promise<void> {
345
345
  if (!this.to) throw new Error('To address is required');
346
346
 
347
- const amountSpent: bigint = this.getTransactionOPNetFee();
347
+ const opnetFee = this.getTransactionOPNetFee();
348
348
 
349
- let amountToCA: bigint;
350
- if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
351
- amountToCA = MINIMUM_AMOUNT_CA;
352
- } else {
353
- amountToCA = amountSpent;
354
- }
349
+ // Add the output to challenge address
350
+ this.addFeeToOutput(opnetFee, this.to, this.epochChallenge, false);
355
351
 
356
- // ALWAYS THE FIRST INPUT.
357
- this.addOutput({
358
- value: Number(amountToCA),
359
- address: this.to,
360
- });
361
-
362
- // ALWAYS SECOND.
363
- if (
364
- amountToCA === MINIMUM_AMOUNT_CA &&
365
- amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD
366
- ) {
367
- this.addOutput({
368
- value: Number(amountSpent - amountToCA),
369
- address: this.epochChallenge.address,
370
- });
371
- }
352
+ // Get the actual amount added to outputs (might be MINIMUM_AMOUNT_REWARD if opnetFee is too small)
353
+ const actualOutputAmount = opnetFee < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : opnetFee;
354
+
355
+ const optionalAmount = this.addOptionalOutputsAndGetAmount();
372
356
 
373
- const amount = this.addOptionalOutputsAndGetAmount();
374
357
  if (!this.disableAutoRefund) {
375
- await this.addRefundOutput(amountSpent + amount);
358
+ // Pass the TOTAL amount spent: actual output amount + optional outputs
359
+ await this.addRefundOutput(actualOutputAmount + optionalAmount);
376
360
  }
377
361
  }
378
362
 
@@ -1,4 +1,5 @@
1
- import {
1
+ import bitcoin, {
2
+ getFinalScripts,
2
3
  initEccLib,
3
4
  Network,
4
5
  opcodes,
@@ -13,20 +14,19 @@ import {
13
14
  import * as ecc from '@bitcoinerlab/secp256k1';
14
15
  import { UpdateInput } from '../interfaces/Tap.js';
15
16
  import { TransactionType } from '../enums/TransactionType.js';
16
- import {
17
- IFundingTransactionParameters,
18
- ITransactionParameters,
19
- } from '../interfaces/ITransactionParameters.js';
17
+ import { IFundingTransactionParameters, ITransactionParameters, } from '../interfaces/ITransactionParameters.js';
20
18
  import { EcKeyPair } from '../../keypair/EcKeyPair.js';
21
19
  import { UTXO } from '../../utxo/interfaces/IUTXO.js';
22
20
  import { ECPairInterface } from 'ecpair';
23
21
  import { AddressVerificator } from '../../keypair/AddressVerificator.js';
24
22
  import { TweakedTransaction } from '../shared/TweakedTransaction.js';
25
23
  import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
24
+ import { IP2WSHAddress } from '../mineable/IP2WSHAddress.js';
25
+ import { P2WDADetector } from '../../p2wda/P2WDADetector.js';
26
26
 
27
27
  initEccLib(ecc);
28
28
 
29
- export const MINIMUM_AMOUNT_REWARD: bigint = 540n;
29
+ export const MINIMUM_AMOUNT_REWARD: bigint = 330n; //540n;
30
30
  export const MINIMUM_AMOUNT_CA: bigint = 297n;
31
31
  export const ANCHOR_SCRIPT = Buffer.from('51024e73', 'hex');
32
32
 
@@ -43,10 +43,11 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
43
43
  opcodes.OP_VERIFY,
44
44
  ]);
45
45
 
46
- public static readonly MINIMUM_DUST: bigint = 50n;
46
+ public static readonly MINIMUM_DUST: bigint = 330n;
47
47
 
48
48
  public abstract readonly type: T;
49
49
  public readonly logColor: string = '#785def';
50
+ public debugFees: boolean = false;
50
51
 
51
52
  /**
52
53
  * @description The overflow fees of the transaction
@@ -157,6 +158,8 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
157
158
 
158
159
  protected note?: Buffer;
159
160
 
161
+ private optionalOutputsAdded: boolean = false;
162
+
160
163
  protected constructor(parameters: ITransactionParameters) {
161
164
  super(parameters);
162
165
 
@@ -172,6 +175,7 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
172
175
  this.utxos = parameters.utxos;
173
176
  this.optionalInputs = parameters.optionalInputs || [];
174
177
  this.to = parameters.to || undefined;
178
+ this.debugFees = parameters.debugFees || false;
175
179
 
176
180
  if (parameters.note) {
177
181
  if (typeof parameters.note === 'string') {
@@ -400,10 +404,11 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
400
404
  /**
401
405
  * Add an output to the transaction.
402
406
  * @param {PsbtOutputExtended} output - The output to add
407
+ * @param bypassMinCheck
403
408
  * @public
404
409
  * @returns {void}
405
410
  */
406
- public addOutput(output: PsbtOutputExtended): void {
411
+ public addOutput(output: PsbtOutputExtended, bypassMinCheck: boolean = false): void {
407
412
  if (output.value === 0) {
408
413
  const script = output as {
409
414
  script: Buffer;
@@ -422,7 +427,7 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
422
427
  'Output script must start with OP_RETURN or be an ANCHOR when value is 0',
423
428
  );
424
429
  }
425
- } else if (output.value < TransactionBuilder.MINIMUM_DUST) {
430
+ } else if (!bypassMinCheck && output.value < TransactionBuilder.MINIMUM_DUST) {
426
431
  throw new Error(
427
432
  `Output value is less than the minimum dust ${output.value} < ${TransactionBuilder.MINIMUM_DUST}`,
428
433
  );
@@ -431,6 +436,15 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
431
436
  this.outputs.push(output);
432
437
  }
433
438
 
439
+ /**
440
+ * Returns the total value of all outputs added so far (excluding the fee/change output).
441
+ * @public
442
+ * @returns {bigint}
443
+ */
444
+ public getTotalOutputValue(): bigint {
445
+ return this.outputs.reduce((total, output) => total + BigInt(output.value), 0n);
446
+ }
447
+
434
448
  /**
435
449
  * Receiver address.
436
450
  * @public
@@ -449,35 +463,215 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
449
463
  }
450
464
 
451
465
  /**
452
- * Estimates the transaction fees.
466
+ * Estimates the transaction fees with accurate size calculation.
453
467
  * @public
454
- * @returns {Promise<bigint>} - The estimated transaction fees
468
+ * @returns {Promise<bigint>}
455
469
  */
456
470
  public async estimateTransactionFees(): Promise<bigint> {
457
- if (!this.utxos.length) {
458
- throw new Error('No UTXOs specified');
459
- }
471
+ await Promise.resolve();
472
+
473
+ const fakeTx = new Psbt({ network: this.network });
474
+ const inputs = this.getInputs();
475
+ const outputs = this.getOutputs();
476
+ fakeTx.addInputs(inputs);
477
+ fakeTx.addOutputs(outputs);
478
+
479
+ const dummySchnorrSig = Buffer.alloc(64, 0);
480
+ const dummyEcdsaSig = Buffer.alloc(72, 0);
481
+ const dummyCompressedPubkey = Buffer.alloc(33, 2);
482
+
483
+ const finalizer = (inputIndex: number, input: PsbtInputExtended) => {
484
+ if (input.isPayToAnchor || this.anchorInputIndices.has(inputIndex)) {
485
+ return {
486
+ finalScriptSig: undefined,
487
+ finalScriptWitness: Buffer.from([0]),
488
+ };
489
+ }
460
490
 
461
- if (this.estimatedFees) return this.estimatedFees;
491
+ if (input.witnessScript && P2WDADetector.isP2WDAWitnessScript(input.witnessScript)) {
492
+ // Create dummy witness stack for P2WDA
493
+ const dummyDataSlots: Buffer[] = [];
494
+ for (let i = 0; i < 10; i++) {
495
+ dummyDataSlots.push(Buffer.alloc(0));
496
+ }
497
+
498
+ const dummyEcdsaSig = Buffer.alloc(72, 0);
499
+ return {
500
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
501
+ ...dummyDataSlots,
502
+ dummyEcdsaSig,
503
+ input.witnessScript,
504
+ ]),
505
+ };
506
+ }
462
507
 
463
- const fakeTx = new Psbt({
464
- network: this.network,
465
- });
508
+ if (inputIndex === 0 && this.tapLeafScript) {
509
+ const dummySecret = Buffer.alloc(32, 0);
510
+ const dummyScript = this.tapLeafScript.script;
511
+
512
+ // A control block for a 2-leaf tree contains one 32-byte hash.
513
+ const dummyControlBlock = Buffer.alloc(1 + 32 + 32, 0);
514
+
515
+ return {
516
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
517
+ dummySecret,
518
+ dummySchnorrSig, // It's a tapScriptSig, which is Schnorr
519
+ dummySchnorrSig, // Second Schnorr signature
520
+ dummyScript,
521
+ dummyControlBlock,
522
+ ]),
523
+ };
524
+ }
466
525
 
467
- const builtTx = await this.internalBuildTransaction(fakeTx);
468
- if (builtTx) {
469
- const tx = fakeTx.extractTransaction(true, true);
470
- const size = tx.virtualSize();
471
- const fee: number = this.feeRate * size;
526
+ if (!input.witnessUtxo && input.nonWitnessUtxo) {
527
+ return {
528
+ finalScriptSig: bitcoin.script.compile([dummyEcdsaSig, dummyCompressedPubkey]),
529
+ finalScriptWitness: undefined,
530
+ };
531
+ }
472
532
 
473
- this.estimatedFees = BigInt(Math.ceil(fee) + 1);
533
+ if (input.witnessScript) {
534
+ if (this.csvInputIndices.has(inputIndex)) {
535
+ // CSV P2WSH needs: [signature, witnessScript]
536
+ return {
537
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
538
+ dummyEcdsaSig,
539
+ input.witnessScript,
540
+ ]),
541
+ };
542
+ }
543
+
544
+ if (input.redeemScript) {
545
+ // P2SH-P2WSH needs redeemScript in scriptSig and witness data
546
+ const dummyWitness = [dummyEcdsaSig, input.witnessScript];
547
+ return {
548
+ finalScriptSig: input.redeemScript,
549
+ finalScriptWitness:
550
+ TransactionBuilder.witnessStackToScriptWitness(dummyWitness),
551
+ };
552
+ }
553
+
554
+ const decompiled = bitcoin.script.decompile(input.witnessScript);
555
+ if (decompiled && decompiled.length >= 4) {
556
+ const firstOp = decompiled[0];
557
+ const lastOp = decompiled[decompiled.length - 1];
558
+ // Check if it's M-of-N multisig
559
+ if (
560
+ typeof firstOp === 'number' &&
561
+ firstOp >= opcodes.OP_1 &&
562
+ lastOp === opcodes.OP_CHECKMULTISIG
563
+ ) {
564
+ const m = firstOp - opcodes.OP_1 + 1;
565
+ const signatures: Buffer[] = [];
566
+ for (let i = 0; i < m; i++) {
567
+ signatures.push(dummyEcdsaSig);
568
+ }
569
+
570
+ return {
571
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
572
+ Buffer.alloc(0), // OP_0 due to multisig bug
573
+ ...signatures,
574
+ input.witnessScript,
575
+ ]),
576
+ };
577
+ }
578
+ }
579
+
580
+ return {
581
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
582
+ dummyEcdsaSig,
583
+ input.witnessScript,
584
+ ]),
585
+ };
586
+ } else if (input.redeemScript) {
587
+ const decompiled = bitcoin.script.decompile(input.redeemScript);
588
+ if (
589
+ decompiled &&
590
+ decompiled.length === 2 &&
591
+ decompiled[0] === opcodes.OP_0 &&
592
+ Buffer.isBuffer(decompiled[1]) &&
593
+ decompiled[1].length === 20
594
+ ) {
595
+ // P2SH-P2WPKH
596
+ return {
597
+ finalScriptSig: input.redeemScript,
598
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
599
+ dummyEcdsaSig,
600
+ dummyCompressedPubkey,
601
+ ]),
602
+ };
603
+ }
604
+ }
474
605
 
475
- return this.estimatedFees;
476
- } else {
477
- throw new Error(
478
- `Could not build transaction to estimate fee. Something went wrong while building the transaction.`,
606
+ if (input.redeemScript && !input.witnessScript && !input.witnessUtxo) {
607
+ // Pure P2SH needs signatures + redeemScript in scriptSig
608
+ return {
609
+ finalScriptSig: bitcoin.script.compile([dummyEcdsaSig, input.redeemScript]),
610
+ finalScriptWitness: undefined,
611
+ };
612
+ }
613
+
614
+ const script = input.witnessUtxo?.script;
615
+ if (!script) return { finalScriptSig: undefined, finalScriptWitness: undefined };
616
+
617
+ if (input.tapInternalKey) {
618
+ return {
619
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
620
+ dummySchnorrSig,
621
+ ]),
622
+ };
623
+ }
624
+
625
+ if (script.length === 22 && script[0] === opcodes.OP_0) {
626
+ return {
627
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
628
+ dummyEcdsaSig,
629
+ dummyCompressedPubkey,
630
+ ]),
631
+ };
632
+ }
633
+
634
+ if (input.redeemScript?.length === 22 && input.redeemScript[0] === opcodes.OP_0) {
635
+ return {
636
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
637
+ dummyEcdsaSig,
638
+ dummyCompressedPubkey,
639
+ ]),
640
+ };
641
+ }
642
+
643
+ return getFinalScripts(
644
+ inputIndex,
645
+ input,
646
+ script,
647
+ true,
648
+ !!input.redeemScript,
649
+ !!input.witnessScript,
650
+ );
651
+ };
652
+
653
+ try {
654
+ for (let i = 0; i < fakeTx.data.inputs.length; i++) {
655
+ const fullInput = inputs[i];
656
+ if (fullInput) {
657
+ fakeTx.finalizeInput(i, (idx: number) => finalizer(idx, fullInput));
658
+ }
659
+ }
660
+ } catch (e) {
661
+ this.warn(`Could not finalize dummy tx: ${(e as Error).message}`);
662
+ }
663
+
664
+ const tx = fakeTx.extractTransaction(true, true);
665
+ const size = tx.virtualSize();
666
+ const fee = this.feeRate * size;
667
+ const finalFee = BigInt(Math.ceil(fee));
668
+
669
+ if (this.debugFees) {
670
+ this.log(
671
+ `Estimating fees: feeRate=${this.feeRate}, accurate_vSize=${size}, fee=${finalFee}n`,
479
672
  );
480
673
  }
674
+ return finalFee;
481
675
  }
482
676
 
483
677
  public async rebuildFromBase64(base64: string): Promise<Psbt> {
@@ -529,12 +723,6 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
529
723
  return total;
530
724
  }
531
725
 
532
- /**
533
- * @description Adds the refund output to the transaction
534
- * @param {bigint} amountSpent - The amount spent
535
- * @protected
536
- * @returns {Promise<void>}
537
- */
538
726
  protected async addRefundOutput(amountSpent: bigint): Promise<void> {
539
727
  if (this.note) {
540
728
  this.addOPReturn(this.note);
@@ -544,38 +732,92 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
544
732
  this.addAnchor();
545
733
  }
546
734
 
547
- /** Add the refund output */
548
- const sendBackAmount: bigint = this.totalInputAmount - amountSpent;
549
- if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
550
- if (AddressVerificator.isValidP2TRAddress(this.from, this.network)) {
551
- await this.setFeeOutput({
552
- value: Number(sendBackAmount),
553
- address: this.from,
554
- tapInternalKey: this.internalPubKeyToXOnly(),
555
- });
556
- } else if (AddressVerificator.isValidPublicKey(this.from, this.network)) {
557
- const pubKeyScript = script.compile([
558
- Buffer.from(this.from.replace('0x', ''), 'hex'),
559
- opcodes.OP_CHECKSIG,
560
- ]);
561
-
562
- await this.setFeeOutput({
563
- value: Number(sendBackAmount),
564
- script: pubKeyScript,
565
- });
735
+ // Initialize variables for iteration
736
+ let previousFee = -1n;
737
+ let estimatedFee = 0n;
738
+ let iterations = 0;
739
+ const maxIterations = 5; // Prevent infinite loops
740
+
741
+ // Iterate until fee stabilizes
742
+ while (iterations < maxIterations && estimatedFee !== previousFee) {
743
+ previousFee = estimatedFee;
744
+
745
+ // Calculate the fee with current outputs
746
+ estimatedFee = await this.estimateTransactionFees();
747
+
748
+ // Total amount that needs to be spent (outputs + fee)
749
+ const totalSpent = amountSpent + estimatedFee;
750
+
751
+ // Calculate refund
752
+ const sendBackAmount = this.totalInputAmount - totalSpent;
753
+
754
+ if (this.debugFees) {
755
+ this.log(
756
+ `Iteration ${iterations + 1}: inputAmount=${this.totalInputAmount}, totalSpent=${totalSpent}, sendBackAmount=${sendBackAmount}`,
757
+ );
758
+ }
759
+
760
+ // Determine if we should add a change output
761
+ if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
762
+ // Create the appropriate change output
763
+ if (AddressVerificator.isValidP2TRAddress(this.from, this.network)) {
764
+ this.feeOutput = {
765
+ value: Number(sendBackAmount),
766
+ address: this.from,
767
+ tapInternalKey: this.internalPubKeyToXOnly(),
768
+ };
769
+ } else if (AddressVerificator.isValidPublicKey(this.from, this.network)) {
770
+ const pubKeyScript = script.compile([
771
+ Buffer.from(this.from.replace('0x', ''), 'hex'),
772
+ opcodes.OP_CHECKSIG,
773
+ ]);
774
+
775
+ this.feeOutput = {
776
+ value: Number(sendBackAmount),
777
+ script: pubKeyScript,
778
+ };
779
+ } else {
780
+ this.feeOutput = {
781
+ value: Number(sendBackAmount),
782
+ address: this.from,
783
+ };
784
+ }
785
+
786
+ // Set overflowFees when we have a change output
787
+ this.overflowFees = sendBackAmount;
566
788
  } else {
567
- await this.setFeeOutput({
568
- value: Number(sendBackAmount),
569
- address: this.from,
570
- });
789
+ // No change output if below dust
790
+ this.feeOutput = null;
791
+ this.overflowFees = 0n;
792
+
793
+ if (sendBackAmount < 0n) {
794
+ throw new Error(
795
+ `Insufficient funds: need ${totalSpent} sats but only have ${this.totalInputAmount} sats`,
796
+ );
797
+ }
798
+
799
+ if (this.debugFees) {
800
+ this.warn(
801
+ `Amount to send back (${sendBackAmount} sat) is less than minimum dust...`,
802
+ );
803
+ }
571
804
  }
572
805
 
573
- return;
806
+ iterations++;
574
807
  }
575
808
 
576
- this.warn(
577
- `Amount to send back (${sendBackAmount} sat) is less than the minimum dust (${TransactionBuilder.MINIMUM_DUST} sat), it will be consumed in fees instead.`,
578
- );
809
+ if (iterations >= maxIterations) {
810
+ this.warn(`Fee calculation did not stabilize after ${maxIterations} iterations`);
811
+ }
812
+
813
+ // Store the final fee
814
+ this.transactionFee = estimatedFee;
815
+
816
+ if (this.debugFees) {
817
+ this.log(
818
+ `Final fee: ${estimatedFee} sats, Change output: ${this.feeOutput ? `${this.feeOutput.value} sats` : 'none'}`,
819
+ );
820
+ }
579
821
  }
580
822
 
581
823
  /**
@@ -657,14 +899,17 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
657
899
  * @returns {bigint}
658
900
  */
659
901
  protected addOptionalOutputsAndGetAmount(): bigint {
660
- if (!this.optionalOutputs) return 0n;
902
+ if (!this.optionalOutputs || this.optionalOutputsAdded) return 0n;
661
903
 
662
- let refundedFromOptionalOutputs = 0n;
904
+ let refundedFromOptionalOutputs: bigint = 0n;
663
905
 
664
906
  for (let i = 0; i < this.optionalOutputs.length; i++) {
665
907
  this.addOutput(this.optionalOutputs[i]);
666
908
  refundedFromOptionalOutputs += BigInt(this.optionalOutputs[i].value);
667
909
  }
910
+
911
+ this.optionalOutputsAdded = true;
912
+
668
913
  return refundedFromOptionalOutputs;
669
914
  }
670
915
 
@@ -732,6 +977,66 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
732
977
  this.updateInputs.push(input);
733
978
  }
734
979
 
980
+ /**
981
+ * Adds the fee to the output.
982
+ * @param amountSpent
983
+ * @param contractAddress
984
+ * @param epochChallenge
985
+ * @param addContractOutput
986
+ * @protected
987
+ */
988
+ protected addFeeToOutput(
989
+ amountSpent: bigint,
990
+ contractAddress: string,
991
+ epochChallenge: IP2WSHAddress,
992
+ addContractOutput: boolean,
993
+ ): void {
994
+ if (addContractOutput) {
995
+ let amountToCA: bigint;
996
+ if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
997
+ amountToCA = MINIMUM_AMOUNT_CA;
998
+ } else {
999
+ amountToCA = amountSpent;
1000
+ }
1001
+
1002
+ // ALWAYS THE FIRST INPUT.
1003
+ this.addOutput(
1004
+ {
1005
+ value: Number(amountToCA),
1006
+ address: contractAddress,
1007
+ },
1008
+ true,
1009
+ );
1010
+
1011
+ // ALWAYS SECOND.
1012
+ if (
1013
+ amountToCA === MINIMUM_AMOUNT_CA &&
1014
+ amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD
1015
+ ) {
1016
+ this.addOutput(
1017
+ {
1018
+ value: Number(amountSpent - amountToCA),
1019
+ address: epochChallenge.address,
1020
+ },
1021
+ true,
1022
+ );
1023
+ }
1024
+ } else {
1025
+ // When SEND_AMOUNT_TO_CA is false, always send to epochChallenge
1026
+ // Use the maximum of amountSpent or MINIMUM_AMOUNT_REWARD
1027
+ const amountToEpoch =
1028
+ amountSpent < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : amountSpent;
1029
+
1030
+ this.addOutput(
1031
+ {
1032
+ value: Number(amountToEpoch),
1033
+ address: epochChallenge.address,
1034
+ },
1035
+ true,
1036
+ );
1037
+ }
1038
+ }
1039
+
735
1040
  /**
736
1041
  * Returns the witness of the tap transaction.
737
1042
  * @protected
@@ -788,6 +1093,69 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
788
1093
  */
789
1094
  protected async setFeeOutput(output: PsbtOutputExtended): Promise<void> {
790
1095
  const initialValue = output.value;
1096
+ this.feeOutput = null; // Start with no fee output
1097
+
1098
+ let estimatedFee = 0n;
1099
+ let lastFee = -1n;
1100
+
1101
+ this.log(
1102
+ `setFeeOutput: Starting fee calculation for change. Initial available value: ${initialValue} sats.`,
1103
+ );
1104
+
1105
+ for (let i = 0; i < 3 && estimatedFee !== lastFee; i++) {
1106
+ lastFee = estimatedFee;
1107
+ estimatedFee = await this.estimateTransactionFees();
1108
+ const valueLeft = BigInt(initialValue) - estimatedFee;
1109
+
1110
+ if (this.debugFees) {
1111
+ this.log(
1112
+ ` -> Iteration ${i + 1}: Estimated fee is ${estimatedFee} sats. Value left for change: ${valueLeft} sats.`,
1113
+ );
1114
+ }
1115
+
1116
+ if (valueLeft >= TransactionBuilder.MINIMUM_DUST) {
1117
+ this.feeOutput = { ...output, value: Number(valueLeft) };
1118
+ this.overflowFees = valueLeft;
1119
+ } else {
1120
+ this.feeOutput = null;
1121
+ this.overflowFees = 0n;
1122
+ // Re-estimate fee one last time without the change output
1123
+ estimatedFee = await this.estimateTransactionFees();
1124
+
1125
+ if (this.debugFees) {
1126
+ this.log(
1127
+ ` -> Change is less than dust. Final fee without change output: ${estimatedFee} sats.`,
1128
+ );
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ const finalValueLeft = BigInt(initialValue) - estimatedFee;
1134
+
1135
+ if (finalValueLeft < 0) {
1136
+ throw new Error(
1137
+ `setFeeOutput: Insufficient funds to pay the fees. Required fee: ${estimatedFee}, Available: ${initialValue}. Total input: ${this.totalInputAmount} sat`,
1138
+ );
1139
+ }
1140
+
1141
+ if (finalValueLeft >= TransactionBuilder.MINIMUM_DUST) {
1142
+ this.feeOutput = { ...output, value: Number(finalValueLeft) };
1143
+ this.overflowFees = finalValueLeft;
1144
+ if (this.debugFees) {
1145
+ this.log(
1146
+ `setFeeOutput: Final change output set to ${finalValueLeft} sats. Final fee: ${estimatedFee} sats.`,
1147
+ );
1148
+ }
1149
+ } else {
1150
+ this.warn(
1151
+ `Amount to send back (${finalValueLeft} sat) is less than the minimum dust (${TransactionBuilder.MINIMUM_DUST} sat), it will be consumed in fees instead.`,
1152
+ );
1153
+ this.feeOutput = null;
1154
+ this.overflowFees = 0n;
1155
+ }
1156
+ }
1157
+ /*protected async setFeeOutput(output: PsbtOutputExtended): Promise<void> {
1158
+ const initialValue = output.value;
791
1159
 
792
1160
  const fee = await this.estimateTransactionFees();
793
1161
  output.value = initialValue - Number(fee);
@@ -819,7 +1187,7 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
819
1187
 
820
1188
  this.overflowFees = BigInt(valueLeft);
821
1189
  }
822
- }
1190
+ }*/
823
1191
 
824
1192
  /**
825
1193
  * Builds the transaction.