@btc-vision/transaction 1.6.6 → 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.
@@ -25,6 +25,9 @@ import { WindowWithWallets } from './browser/extensions/UnisatSigner.js';
25
25
  import { RawChallenge } from '../epoch/interfaces/IChallengeSolution.js';
26
26
  import { P2WDADetector } from '../p2wda/P2WDADetector.js';
27
27
  import { InteractionTransactionP2WDA } from './builders/InteractionTransactionP2WDA.js';
28
+ import { ChallengeSolution } from '../epoch/ChallengeSolution.js';
29
+ import { Address } from '../keypair/Address.js';
30
+ import { BitcoinUtils } from '../utils/BitcoinUtils.js';
28
31
 
29
32
  export interface DeploymentResult {
30
33
  readonly transaction: [string, string];
@@ -62,6 +65,16 @@ export interface BitcoinTransferResponse extends BitcoinTransferBase {
62
65
  }
63
66
 
64
67
  export class TransactionFactory {
68
+ public debug: boolean = false;
69
+
70
+ private readonly DUMMY_PUBKEY = Buffer.alloc(32, 1);
71
+ private readonly P2TR_SCRIPT = Buffer.concat([
72
+ Buffer.from([0x51, 0x20]), // OP_1 + 32 bytes
73
+ this.DUMMY_PUBKEY,
74
+ ]);
75
+ private readonly INITIAL_FUNDING_ESTIMATE = 2000n;
76
+ private readonly MAX_ITERATIONS = 10;
77
+
65
78
  /**
66
79
  * @description Generate a transaction with a custom script.
67
80
  * @returns {Promise<[string, string]>} - The signed transaction
@@ -72,49 +85,49 @@ export class TransactionFactory {
72
85
  if (!interactionParameters.to) {
73
86
  throw new Error('Field "to" not provided.');
74
87
  }
75
-
76
88
  if (!interactionParameters.from) {
77
89
  throw new Error('Field "from" not provided.');
78
90
  }
79
-
80
91
  if (!interactionParameters.utxos[0]) {
81
92
  throw new Error('Missing at least one UTXO.');
82
93
  }
83
-
84
94
  if (!('signer' in interactionParameters)) {
85
95
  throw new Error('Field "signer" not provided, OP_WALLET not detected.');
86
96
  }
87
97
 
88
98
  const inputs = this.parseOptionalInputs(interactionParameters.optionalInputs);
89
- const preTransaction: CustomScriptTransaction = new CustomScriptTransaction({
90
- ...interactionParameters,
91
- utxos: [interactionParameters.utxos[0]], // we simulate one input here.
92
- optionalInputs: inputs,
93
- });
94
99
 
95
- // we don't sign that transaction, we just need the parameters.
96
- await preTransaction.generateTransactionMinimalSignatures();
100
+ // Use common iteration logic
101
+ const { finalTransaction, estimatedAmount, challenge } = await this.iterateFundingAmount(
102
+ { ...interactionParameters, optionalInputs: inputs },
103
+ CustomScriptTransaction,
104
+ async (tx) => {
105
+ const fee = await tx.estimateTransactionFees();
106
+ const priorityFee = this.getPriorityFee(interactionParameters);
107
+ const optionalValue = tx.getOptionalOutputValue();
108
+ return fee + priorityFee + optionalValue;
109
+ },
110
+ 'CustomScript',
111
+ );
97
112
 
98
113
  const parameters: IFundingTransactionParameters =
99
- await preTransaction.getFundingTransactionParameters();
114
+ await finalTransaction.getFundingTransactionParameters();
100
115
 
101
116
  parameters.utxos = interactionParameters.utxos;
102
- parameters.amount =
103
- (await preTransaction.estimateTransactionFees()) +
104
- this.getPriorityFee(interactionParameters) +
105
- preTransaction.getOptionalOutputValue();
117
+ parameters.amount = estimatedAmount;
106
118
 
107
- const feeEstimationFundingTransaction = await this.createFundTransaction({
119
+ // Create funding transaction
120
+ const feeEstimationFunding = await this.createFundTransaction({
108
121
  ...parameters,
109
122
  optionalOutputs: [],
110
123
  optionalInputs: [],
111
124
  });
112
125
 
113
- if (!feeEstimationFundingTransaction) {
126
+ if (!feeEstimationFunding) {
114
127
  throw new Error('Could not sign funding transaction.');
115
128
  }
116
129
 
117
- parameters.estimatedFees = feeEstimationFundingTransaction.estimatedFees;
130
+ parameters.estimatedFees = feeEstimationFunding.estimatedFees;
118
131
 
119
132
  const signedTransaction = await this.createFundTransaction({
120
133
  ...parameters,
@@ -126,34 +139,29 @@ export class TransactionFactory {
126
139
  throw new Error('Could not sign funding transaction.');
127
140
  }
128
141
 
129
- interactionParameters.utxos = this.getUTXOAsTransaction(
130
- signedTransaction.tx,
131
- interactionParameters.to,
132
- 0,
133
- );
134
-
135
142
  const newParams: ICustomTransactionParameters = {
136
143
  ...interactionParameters,
137
- utxos: [
138
- ...this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.to, 0),
139
- ], // always 0
140
- randomBytes: preTransaction.getRndBytes(),
144
+ utxos: this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.to, 0),
145
+ randomBytes: finalTransaction.getRndBytes(),
141
146
  nonWitnessUtxo: signedTransaction.tx.toBuffer(),
142
- estimatedFees: preTransaction.estimatedFees,
147
+ estimatedFees: finalTransaction.estimatedFees,
143
148
  optionalInputs: inputs,
144
149
  };
145
150
 
146
- const finalTransaction: CustomScriptTransaction = new CustomScriptTransaction(newParams);
151
+ const customTransaction = new CustomScriptTransaction(newParams);
152
+ const outTx = await customTransaction.signTransaction();
147
153
 
148
- // We have to regenerate using the new utxo
149
- const outTx: Transaction = await finalTransaction.signTransaction();
150
154
  return [
151
155
  signedTransaction.tx.toHex(),
152
156
  outTx.toHex(),
153
- this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.from, 1), // always 1
157
+ this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.from, 1),
154
158
  ];
155
159
  }
156
160
 
161
+ /**
162
+ * @description Generates the required transactions.
163
+ * @returns {Promise<InteractionResponse>} - The signed transaction
164
+ */
157
165
  /**
158
166
  * @description Generates the required transactions.
159
167
  * @returns {Promise<InteractionResponse>} - The signed transaction
@@ -164,16 +172,13 @@ export class TransactionFactory {
164
172
  if (!interactionParameters.to) {
165
173
  throw new Error('Field "to" not provided.');
166
174
  }
167
-
168
175
  if (!interactionParameters.from) {
169
176
  throw new Error('Field "from" not provided.');
170
177
  }
171
-
172
178
  if (!interactionParameters.utxos[0]) {
173
179
  throw new Error('Missing at least one UTXO.');
174
180
  }
175
181
 
176
- // If OP_WALLET is used...
177
182
  const opWalletInteraction = await this.detectInteractionOPWallet(interactionParameters);
178
183
  if (opWalletInteraction) {
179
184
  return opWalletInteraction;
@@ -189,35 +194,40 @@ export class TransactionFactory {
189
194
  }
190
195
 
191
196
  const inputs = this.parseOptionalInputs(interactionParameters.optionalInputs);
192
- const preTransaction: InteractionTransaction = new InteractionTransaction({
193
- ...interactionParameters,
194
- utxos: [interactionParameters.utxos[0]], // we simulate one input here.
195
- optionalInputs: inputs,
196
- });
197
197
 
198
- // we don't sign that transaction, we just need the parameters.
199
- await preTransaction.generateTransactionMinimalSignatures();
198
+ // Use common iteration logic
199
+ const { finalTransaction, estimatedAmount, challenge } = await this.iterateFundingAmount(
200
+ { ...interactionParameters, optionalInputs: inputs },
201
+ InteractionTransaction,
202
+ async (tx) => {
203
+ const fee = await tx.estimateTransactionFees();
204
+ const outputsValue = tx.getTotalOutputValue();
205
+ return fee + outputsValue;
206
+ },
207
+ 'Interaction',
208
+ );
209
+
210
+ if (!challenge) {
211
+ throw new Error('Failed to get challenge from interaction transaction');
212
+ }
200
213
 
201
214
  const parameters: IFundingTransactionParameters =
202
- await preTransaction.getFundingTransactionParameters();
215
+ await finalTransaction.getFundingTransactionParameters();
203
216
 
204
217
  parameters.utxos = interactionParameters.utxos;
205
- parameters.amount =
206
- (await preTransaction.estimateTransactionFees()) +
207
- this.getPriorityFee(interactionParameters) +
208
- preTransaction.getOptionalOutputValue();
218
+ parameters.amount = estimatedAmount;
209
219
 
210
- const feeEstimationFundingTransaction = await this.createFundTransaction({
220
+ const feeEstimationFunding = await this.createFundTransaction({
211
221
  ...parameters,
212
222
  optionalOutputs: [],
213
223
  optionalInputs: [],
214
224
  });
215
225
 
216
- if (!feeEstimationFundingTransaction) {
226
+ if (!feeEstimationFunding) {
217
227
  throw new Error('Could not sign funding transaction.');
218
228
  }
219
229
 
220
- parameters.estimatedFees = feeEstimationFundingTransaction.estimatedFees;
230
+ parameters.estimatedFees = feeEstimationFunding.estimatedFees;
221
231
 
222
232
  const signedTransaction = await this.createFundTransaction({
223
233
  ...parameters,
@@ -229,38 +239,33 @@ export class TransactionFactory {
229
239
  throw new Error('Could not sign funding transaction.');
230
240
  }
231
241
 
232
- interactionParameters.utxos = this.getUTXOAsTransaction(
233
- signedTransaction.tx,
234
- interactionParameters.to,
235
- 0,
236
- );
237
-
238
242
  const newParams: IInteractionParameters = {
239
243
  ...interactionParameters,
240
- utxos: [
241
- ...this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.to, 0),
242
- ], // always 0
243
- randomBytes: preTransaction.getRndBytes(),
244
- challenge: preTransaction.getChallenge(),
244
+ utxos: this.getUTXOAsTransaction(
245
+ signedTransaction.tx,
246
+ finalTransaction.getScriptAddress(),
247
+ 0,
248
+ ),
249
+ randomBytes: finalTransaction.getRndBytes(),
250
+ challenge: challenge,
245
251
  nonWitnessUtxo: signedTransaction.tx.toBuffer(),
246
- estimatedFees: preTransaction.estimatedFees,
252
+ estimatedFees: finalTransaction.estimatedFees,
247
253
  optionalInputs: inputs,
248
254
  };
249
255
 
250
- const finalTransaction: InteractionTransaction = new InteractionTransaction(newParams);
256
+ const interactionTx = new InteractionTransaction(newParams);
257
+ const outTx = await interactionTx.signTransaction();
251
258
 
252
- // We have to regenerate using the new utxo
253
- const outTx: Transaction = await finalTransaction.signTransaction();
254
259
  return {
255
260
  fundingTransaction: signedTransaction.tx.toHex(),
256
261
  interactionTransaction: outTx.toHex(),
257
- estimatedFees: preTransaction.estimatedFees,
262
+ estimatedFees: interactionTx.transactionFee,
258
263
  nextUTXOs: this.getUTXOAsTransaction(
259
264
  signedTransaction.tx,
260
265
  interactionParameters.from,
261
266
  1,
262
- ), // always 1
263
- challenge: preTransaction.getChallenge().toRaw(),
267
+ ),
268
+ challenge: challenge.toRaw(),
264
269
  };
265
270
  }
266
271
 
@@ -282,77 +287,81 @@ export class TransactionFactory {
282
287
  }
283
288
 
284
289
  const inputs = this.parseOptionalInputs(deploymentParameters.optionalInputs);
285
- const preTransaction: DeploymentTransaction = new DeploymentTransaction({
286
- ...deploymentParameters,
287
- utxos: [deploymentParameters.utxos[0]], // we simulate one input here.
288
- optionalInputs: inputs,
289
- });
290
290
 
291
- // we don't sign that transaction, we just need the parameters.
292
- await preTransaction.generateTransactionMinimalSignatures();
291
+ // Use common iteration logic
292
+ const { finalTransaction, estimatedAmount, challenge } = await this.iterateFundingAmount(
293
+ { ...deploymentParameters, optionalInputs: inputs },
294
+ DeploymentTransaction,
295
+ async (tx) => {
296
+ const fee = await tx.estimateTransactionFees();
297
+ const priorityFee = this.getPriorityFee(deploymentParameters);
298
+ const optionalValue = tx.getOptionalOutputValue();
299
+ return fee + priorityFee + optionalValue;
300
+ },
301
+ 'Deployment',
302
+ );
303
+
304
+ if (!challenge) {
305
+ throw new Error('Failed to get challenge from deployment transaction');
306
+ }
293
307
 
294
308
  const parameters: IFundingTransactionParameters =
295
- await preTransaction.getFundingTransactionParameters();
309
+ await finalTransaction.getFundingTransactionParameters();
296
310
 
297
311
  parameters.utxos = deploymentParameters.utxos;
298
- parameters.amount =
299
- (await preTransaction.estimateTransactionFees()) +
300
- this.getPriorityFee(deploymentParameters) +
301
- preTransaction.getOptionalOutputValue();
312
+ parameters.amount = estimatedAmount;
302
313
 
303
- const feeEstimationFundingTransaction = await this.createFundTransaction({
314
+ const feeEstimationFunding = await this.createFundTransaction({
304
315
  ...parameters,
305
316
  optionalOutputs: [],
306
317
  optionalInputs: [],
307
318
  });
308
319
 
309
- if (!feeEstimationFundingTransaction) {
320
+ if (!feeEstimationFunding) {
310
321
  throw new Error('Could not sign funding transaction.');
311
322
  }
312
323
 
313
- parameters.estimatedFees = feeEstimationFundingTransaction.estimatedFees;
324
+ parameters.estimatedFees = feeEstimationFunding.estimatedFees;
314
325
 
315
- const fundingTransaction: FundingTransaction = new FundingTransaction({
326
+ const fundingTransaction = new FundingTransaction({
316
327
  ...parameters,
317
328
  optionalInputs: [],
318
329
  optionalOutputs: [],
319
330
  });
320
331
 
321
- const signedTransaction: Transaction = await fundingTransaction.signTransaction();
332
+ const signedTransaction = await fundingTransaction.signTransaction();
322
333
  if (!signedTransaction) {
323
334
  throw new Error('Could not sign funding transaction.');
324
335
  }
325
336
 
326
- const out: TxOutput = signedTransaction.outs[0];
337
+ const out = signedTransaction.outs[0];
327
338
  const newUtxo: UTXO = {
328
339
  transactionId: signedTransaction.getId(),
329
- outputIndex: 0, // always 0
340
+ outputIndex: 0,
330
341
  scriptPubKey: {
331
342
  hex: out.script.toString('hex'),
332
- address: preTransaction.getScriptAddress(),
343
+ address: finalTransaction.getScriptAddress(),
333
344
  },
334
345
  value: BigInt(out.value),
335
346
  };
336
347
 
337
348
  const newParams: IDeploymentParameters = {
338
349
  ...deploymentParameters,
339
- utxos: [newUtxo], // always 0
340
- randomBytes: preTransaction.getRndBytes(),
341
- challenge: preTransaction.getChallenge(),
350
+ utxos: [newUtxo],
351
+ randomBytes: finalTransaction.getRndBytes(),
352
+ challenge: challenge,
342
353
  nonWitnessUtxo: signedTransaction.toBuffer(),
343
- estimatedFees: preTransaction.estimatedFees,
354
+ estimatedFees: finalTransaction.estimatedFees,
344
355
  optionalInputs: inputs,
345
356
  };
346
357
 
347
- const finalTransaction: DeploymentTransaction = new DeploymentTransaction(newParams);
358
+ const deploymentTx = new DeploymentTransaction(newParams);
359
+ const outTx = await deploymentTx.signTransaction();
348
360
 
349
- // We have to regenerate using the new utxo
350
- const outTx: Transaction = await finalTransaction.signTransaction();
351
-
352
- const out2: TxOutput = signedTransaction.outs[1];
361
+ const out2 = signedTransaction.outs[1];
353
362
  const refundUTXO: UTXO = {
354
363
  transactionId: signedTransaction.getId(),
355
- outputIndex: 1, // always 1
364
+ outputIndex: 1,
356
365
  scriptPubKey: {
357
366
  hex: out2.script.toString('hex'),
358
367
  address: deploymentParameters.from,
@@ -362,10 +371,10 @@ export class TransactionFactory {
362
371
 
363
372
  return {
364
373
  transaction: [signedTransaction.toHex(), outTx.toHex()],
365
- contractAddress: finalTransaction.getContractAddress(), //finalTransaction.contractAddress.p2tr(deploymentParameters.network),
366
- contractPubKey: finalTransaction.contractPubKey,
374
+ contractAddress: deploymentTx.getContractAddress(),
375
+ contractPubKey: deploymentTx.contractPubKey,
367
376
  utxos: [refundUTXO],
368
- challenge: preTransaction.getChallenge().toRaw(),
377
+ challenge: challenge.toRaw(),
369
378
  };
370
379
  }
371
380
 
@@ -599,6 +608,127 @@ export class TransactionFactory {
599
608
  return totalFee;
600
609
  }
601
610
 
611
+ /**
612
+ * Common iteration logic for finding the correct funding amount
613
+ */
614
+ private async iterateFundingAmount<
615
+ T extends InteractionTransaction | DeploymentTransaction | CustomScriptTransaction,
616
+ P extends IInteractionParameters | IDeploymentParameters | ICustomTransactionParameters,
617
+ >(
618
+ params: P,
619
+ TransactionClass: new (params: P) => T,
620
+ calculateAmount: (tx: T) => Promise<bigint>,
621
+ debugPrefix: string,
622
+ ): Promise<{
623
+ finalTransaction: T;
624
+ estimatedAmount: bigint;
625
+ challenge: ChallengeSolution | null;
626
+ }> {
627
+ const randomBytes =
628
+ 'randomBytes' in params
629
+ ? (params.randomBytes ?? BitcoinUtils.rndBytes())
630
+ : BitcoinUtils.rndBytes();
631
+
632
+ const dummyAddress = Address.dead().p2tr(params.network);
633
+
634
+ let estimatedFundingAmount = this.INITIAL_FUNDING_ESTIMATE;
635
+ let previousAmount = 0n;
636
+ let iterations = 0;
637
+ let finalPreTransaction: T | null = null;
638
+ let challenge: ChallengeSolution | null = null;
639
+
640
+ while (iterations < this.MAX_ITERATIONS && estimatedFundingAmount !== previousAmount) {
641
+ previousAmount = estimatedFundingAmount;
642
+
643
+ const dummyTx = new Transaction();
644
+ dummyTx.addOutput(this.P2TR_SCRIPT, Number(estimatedFundingAmount));
645
+
646
+ const simulatedFundedUtxo: UTXO = {
647
+ transactionId: Buffer.alloc(32, 0).toString('hex'),
648
+ outputIndex: 0,
649
+ scriptPubKey: {
650
+ hex: this.P2TR_SCRIPT.toString('hex'),
651
+ address: dummyAddress,
652
+ },
653
+ value: estimatedFundingAmount,
654
+ nonWitnessUtxo: dummyTx.toBuffer(),
655
+ };
656
+
657
+ // Build transaction params - TypeScript needs explicit typing here
658
+ let txParams: P;
659
+ if ('challenge' in params && params.challenge) {
660
+ const withChallenge = {
661
+ ...params,
662
+ utxos: [simulatedFundedUtxo],
663
+ randomBytes: randomBytes,
664
+ challenge: challenge ?? params.challenge, // Use existing or original
665
+ };
666
+ txParams = withChallenge as P;
667
+ } else {
668
+ const withoutChallenge = {
669
+ ...params,
670
+ utxos: [simulatedFundedUtxo],
671
+ randomBytes: randomBytes,
672
+ };
673
+ txParams = withoutChallenge as P;
674
+ }
675
+
676
+ const preTransaction: T = new TransactionClass(txParams);
677
+
678
+ try {
679
+ await preTransaction.generateTransactionMinimalSignatures();
680
+ estimatedFundingAmount = await calculateAmount(preTransaction);
681
+ } catch (error: unknown) {
682
+ if (error instanceof Error) {
683
+ const match = error.message.match(/need (\d+) sats but only have (\d+) sats/);
684
+ if (match) {
685
+ estimatedFundingAmount = BigInt(match[1]);
686
+ if (this.debug) {
687
+ console.log(
688
+ `${debugPrefix}: Caught insufficient funds, updating to ${estimatedFundingAmount}`,
689
+ );
690
+ }
691
+ } else {
692
+ throw error;
693
+ }
694
+ } else {
695
+ throw new Error('Unknown error during fee estimation');
696
+ }
697
+ }
698
+
699
+ finalPreTransaction = preTransaction;
700
+
701
+ // Extract challenge with explicit typing
702
+ if (
703
+ 'getChallenge' in preTransaction &&
704
+ typeof preTransaction.getChallenge === 'function'
705
+ ) {
706
+ const result = preTransaction.getChallenge();
707
+ if (result instanceof ChallengeSolution) {
708
+ challenge = result;
709
+ }
710
+ }
711
+
712
+ iterations++;
713
+
714
+ if (this.debug) {
715
+ console.log(
716
+ `${debugPrefix} Iteration ${iterations}: Previous=${previousAmount}, New=${estimatedFundingAmount}`,
717
+ );
718
+ }
719
+ }
720
+
721
+ if (!finalPreTransaction) {
722
+ throw new Error(`Failed to converge on ${debugPrefix} funding amount`);
723
+ }
724
+
725
+ return {
726
+ finalTransaction: finalPreTransaction,
727
+ estimatedAmount: estimatedFundingAmount,
728
+ challenge,
729
+ };
730
+ }
731
+
602
732
  private getUTXOAsTransaction(tx: Transaction, to: string, index: number): UTXO[] {
603
733
  if (!tx.outs[index]) return [];
604
734
 
@@ -10,11 +10,7 @@ import {
10
10
  Taptree,
11
11
  toXOnly,
12
12
  } from '@btc-vision/bitcoin';
13
- import {
14
- MINIMUM_AMOUNT_CA,
15
- MINIMUM_AMOUNT_REWARD,
16
- TransactionBuilder,
17
- } from './TransactionBuilder.js';
13
+ import { TransactionBuilder } from './TransactionBuilder.js';
18
14
  import { TapLeafScript } from '../interfaces/Tap.js';
19
15
  import {
20
16
  DeploymentGenerator,
@@ -255,30 +251,7 @@ export class DeploymentTransaction extends TransactionBuilder<TransactionType.DE
255
251
  this.addInputsFromUTXO();
256
252
 
257
253
  const amountSpent: bigint = this.getTransactionOPNetFee();
258
-
259
- let amountToCA: bigint;
260
- if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
261
- amountToCA = MINIMUM_AMOUNT_CA;
262
- } else {
263
- amountToCA = amountSpent;
264
- }
265
-
266
- // ALWAYS THE FIRST INPUT.
267
- this.addOutput({
268
- value: Number(amountToCA),
269
- address: this.getContractAddress(),
270
- });
271
-
272
- // ALWAYS SECOND.
273
- if (
274
- amountToCA === MINIMUM_AMOUNT_CA &&
275
- amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD
276
- ) {
277
- this.addOutput({
278
- value: Number(amountSpent - amountToCA),
279
- address: this.epochChallenge.address,
280
- });
281
- }
254
+ this.addFeeToOutput(amountSpent, this.getContractAddress(), this.epochChallenge, true);
282
255
 
283
256
  await this.addRefundOutput(amountSpent + this.addOptionalOutputsAndGetAmount());
284
257
  }
@@ -26,6 +26,7 @@ export class FundingTransaction extends TransactionBuilder<TransactionType.FUNDI
26
26
 
27
27
  this.addInputsFromUTXO();
28
28
 
29
+ // Add the primary output(s) first
29
30
  if (this.splitInputsInto > 1) {
30
31
  this.splitInputs(this.amount);
31
32
  } else if (this.isPubKeyDestination) {
@@ -45,7 +46,11 @@ export class FundingTransaction extends TransactionBuilder<TransactionType.FUNDI
45
46
  });
46
47
  }
47
48
 
48
- await this.addRefundOutput(this.amount + this.addOptionalOutputsAndGetAmount());
49
+ // Calculate total amount needed for all outputs (including optional)
50
+ const totalOutputAmount = this.amount + this.addOptionalOutputsAndGetAmount();
51
+
52
+ // Add refund output - this will handle fee calculation properly
53
+ await this.addRefundOutput(totalOutputAmount);
49
54
  }
50
55
 
51
56
  protected splitInputs(amountSpent: bigint): void {
@@ -2,11 +2,7 @@ import { Buffer } from 'buffer';
2
2
  import { Psbt, PsbtInput, toXOnly } from '@btc-vision/bitcoin';
3
3
  import { TransactionType } from '../enums/TransactionType.js';
4
4
  import { IInteractionParameters } from '../interfaces/ITransactionParameters.js';
5
- import {
6
- MINIMUM_AMOUNT_CA,
7
- MINIMUM_AMOUNT_REWARD,
8
- TransactionBuilder,
9
- } from './TransactionBuilder.js';
5
+ import { TransactionBuilder } from './TransactionBuilder.js';
10
6
  import { MessageSigner } from '../../keypair/MessageSigner.js';
11
7
  import { Compressor } from '../../bytecode/Compressor.js';
12
8
  import { P2WDAGenerator } from '../../generators/builders/P2WDAGenerator.js';
@@ -153,29 +149,7 @@ export class InteractionTransactionP2WDA extends TransactionBuilder<TransactionT
153
149
 
154
150
  const amountSpent: bigint = this.getTransactionOPNetFee();
155
151
 
156
- let amountToCA: bigint;
157
- if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
158
- amountToCA = MINIMUM_AMOUNT_CA;
159
- } else {
160
- amountToCA = amountSpent;
161
- }
162
-
163
- // ALWAYS THE FIRST INPUT.
164
- this.addOutput({
165
- value: Number(amountToCA),
166
- address: this.to,
167
- });
168
-
169
- // ALWAYS SECOND.
170
- if (
171
- amountToCA === MINIMUM_AMOUNT_CA &&
172
- amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD
173
- ) {
174
- this.addOutput({
175
- value: Number(amountSpent - amountToCA),
176
- address: this.epochChallenge.address,
177
- });
178
- }
152
+ this.addFeeToOutput(amountSpent, this.to, this.epochChallenge, false);
179
153
 
180
154
  const amount = this.addOptionalOutputsAndGetAmount();
181
155
  if (!this.disableAutoRefund) {
@@ -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