@btc-vision/transaction 1.6.6 → 1.6.8

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,16 +1,18 @@
1
- import { initEccLib, opcodes, Psbt, script, Transaction, varuint, } from '@btc-vision/bitcoin';
1
+ import bitcoin, { getFinalScripts, initEccLib, opcodes, Psbt, script, Transaction, varuint, } from '@btc-vision/bitcoin';
2
2
  import * as ecc from '@bitcoinerlab/secp256k1';
3
3
  import { EcKeyPair } from '../../keypair/EcKeyPair.js';
4
4
  import { AddressVerificator } from '../../keypair/AddressVerificator.js';
5
5
  import { TweakedTransaction } from '../shared/TweakedTransaction.js';
6
+ import { P2WDADetector } from '../../p2wda/P2WDADetector.js';
6
7
  initEccLib(ecc);
7
- export const MINIMUM_AMOUNT_REWARD = 540n;
8
+ export const MINIMUM_AMOUNT_REWARD = 330n;
8
9
  export const MINIMUM_AMOUNT_CA = 297n;
9
10
  export const ANCHOR_SCRIPT = Buffer.from('51024e73', 'hex');
10
11
  export class TransactionBuilder extends TweakedTransaction {
11
12
  constructor(parameters) {
12
13
  super(parameters);
13
14
  this.logColor = '#785def';
15
+ this.debugFees = false;
14
16
  this.overflowFees = 0n;
15
17
  this.transactionFee = 0n;
16
18
  this.estimatedFees = 0n;
@@ -18,6 +20,7 @@ export class TransactionBuilder extends TweakedTransaction {
18
20
  this.outputs = [];
19
21
  this.feeOutput = null;
20
22
  this._maximumFeeRate = 100000000;
23
+ this.optionalOutputsAdded = false;
21
24
  if (parameters.estimatedFees) {
22
25
  this.estimatedFees = parameters.estimatedFees;
23
26
  }
@@ -29,6 +32,7 @@ export class TransactionBuilder extends TweakedTransaction {
29
32
  this.utxos = parameters.utxos;
30
33
  this.optionalInputs = parameters.optionalInputs || [];
31
34
  this.to = parameters.to || undefined;
35
+ this.debugFees = parameters.debugFees || false;
32
36
  if (parameters.note) {
33
37
  if (typeof parameters.note === 'string') {
34
38
  this.note = Buffer.from(parameters.note, 'utf8');
@@ -164,7 +168,7 @@ export class TransactionBuilder extends TweakedTransaction {
164
168
  addInput(input) {
165
169
  this.inputs.push(input);
166
170
  }
167
- addOutput(output) {
171
+ addOutput(output, bypassMinCheck = false) {
168
172
  if (output.value === 0) {
169
173
  const script = output;
170
174
  if (!script.script || script.script.length === 0) {
@@ -177,11 +181,14 @@ export class TransactionBuilder extends TweakedTransaction {
177
181
  throw new Error('Output script must start with OP_RETURN or be an ANCHOR when value is 0');
178
182
  }
179
183
  }
180
- else if (output.value < TransactionBuilder.MINIMUM_DUST) {
184
+ else if (!bypassMinCheck && output.value < TransactionBuilder.MINIMUM_DUST) {
181
185
  throw new Error(`Output value is less than the minimum dust ${output.value} < ${TransactionBuilder.MINIMUM_DUST}`);
182
186
  }
183
187
  this.outputs.push(output);
184
188
  }
189
+ getTotalOutputValue() {
190
+ return this.outputs.reduce((total, output) => total + BigInt(output.value), 0n);
191
+ }
185
192
  toAddress() {
186
193
  return this.to;
187
194
  }
@@ -189,25 +196,181 @@ export class TransactionBuilder extends TweakedTransaction {
189
196
  return this.tapData?.address;
190
197
  }
191
198
  async estimateTransactionFees() {
192
- if (!this.utxos.length) {
193
- throw new Error('No UTXOs specified');
199
+ await Promise.resolve();
200
+ const fakeTx = new Psbt({ network: this.network });
201
+ const inputs = this.getInputs();
202
+ const outputs = this.getOutputs();
203
+ fakeTx.addInputs(inputs);
204
+ fakeTx.addOutputs(outputs);
205
+ const dummySchnorrSig = Buffer.alloc(64, 0);
206
+ const dummyEcdsaSig = Buffer.alloc(72, 0);
207
+ const dummyCompressedPubkey = Buffer.alloc(33, 2);
208
+ const finalizer = (inputIndex, input) => {
209
+ if (input.isPayToAnchor || this.anchorInputIndices.has(inputIndex)) {
210
+ return {
211
+ finalScriptSig: undefined,
212
+ finalScriptWitness: Buffer.from([0]),
213
+ };
214
+ }
215
+ if (input.witnessScript && P2WDADetector.isP2WDAWitnessScript(input.witnessScript)) {
216
+ const dummyDataSlots = [];
217
+ for (let i = 0; i < 10; i++) {
218
+ dummyDataSlots.push(Buffer.alloc(0));
219
+ }
220
+ const dummyEcdsaSig = Buffer.alloc(72, 0);
221
+ return {
222
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
223
+ ...dummyDataSlots,
224
+ dummyEcdsaSig,
225
+ input.witnessScript,
226
+ ]),
227
+ };
228
+ }
229
+ if (inputIndex === 0 && this.tapLeafScript) {
230
+ const dummySecret = Buffer.alloc(32, 0);
231
+ const dummyScript = this.tapLeafScript.script;
232
+ const dummyControlBlock = Buffer.alloc(1 + 32 + 32, 0);
233
+ return {
234
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
235
+ dummySecret,
236
+ dummySchnorrSig,
237
+ dummySchnorrSig,
238
+ dummyScript,
239
+ dummyControlBlock,
240
+ ]),
241
+ };
242
+ }
243
+ if (input.witnessUtxo) {
244
+ const script = input.witnessUtxo.script;
245
+ const decompiled = bitcoin.script.decompile(script);
246
+ if (decompiled &&
247
+ decompiled.length === 5 &&
248
+ decompiled[0] === opcodes.OP_DUP &&
249
+ decompiled[1] === opcodes.OP_HASH160 &&
250
+ decompiled[3] === opcodes.OP_EQUALVERIFY &&
251
+ decompiled[4] === opcodes.OP_CHECKSIG) {
252
+ return {
253
+ finalScriptSig: bitcoin.script.compile([
254
+ dummyEcdsaSig,
255
+ dummyCompressedPubkey,
256
+ ]),
257
+ finalScriptWitness: undefined,
258
+ };
259
+ }
260
+ }
261
+ if (input.witnessScript) {
262
+ if (this.csvInputIndices.has(inputIndex)) {
263
+ return {
264
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
265
+ dummyEcdsaSig,
266
+ input.witnessScript,
267
+ ]),
268
+ };
269
+ }
270
+ if (input.redeemScript) {
271
+ const dummyWitness = [dummyEcdsaSig, input.witnessScript];
272
+ return {
273
+ finalScriptSig: input.redeemScript,
274
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(dummyWitness),
275
+ };
276
+ }
277
+ const decompiled = bitcoin.script.decompile(input.witnessScript);
278
+ if (decompiled && decompiled.length >= 4) {
279
+ const firstOp = decompiled[0];
280
+ const lastOp = decompiled[decompiled.length - 1];
281
+ if (typeof firstOp === 'number' &&
282
+ firstOp >= opcodes.OP_1 &&
283
+ lastOp === opcodes.OP_CHECKMULTISIG) {
284
+ const m = firstOp - opcodes.OP_1 + 1;
285
+ const signatures = [];
286
+ for (let i = 0; i < m; i++) {
287
+ signatures.push(dummyEcdsaSig);
288
+ }
289
+ return {
290
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
291
+ Buffer.alloc(0),
292
+ ...signatures,
293
+ input.witnessScript,
294
+ ]),
295
+ };
296
+ }
297
+ }
298
+ return {
299
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
300
+ dummyEcdsaSig,
301
+ input.witnessScript,
302
+ ]),
303
+ };
304
+ }
305
+ else if (input.redeemScript) {
306
+ const decompiled = bitcoin.script.decompile(input.redeemScript);
307
+ if (decompiled &&
308
+ decompiled.length === 2 &&
309
+ decompiled[0] === opcodes.OP_0 &&
310
+ Buffer.isBuffer(decompiled[1]) &&
311
+ decompiled[1].length === 20) {
312
+ return {
313
+ finalScriptSig: input.redeemScript,
314
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
315
+ dummyEcdsaSig,
316
+ dummyCompressedPubkey,
317
+ ]),
318
+ };
319
+ }
320
+ }
321
+ if (input.redeemScript && !input.witnessScript && !input.witnessUtxo) {
322
+ return {
323
+ finalScriptSig: bitcoin.script.compile([dummyEcdsaSig, input.redeemScript]),
324
+ finalScriptWitness: undefined,
325
+ };
326
+ }
327
+ const script = input.witnessUtxo?.script;
328
+ if (!script)
329
+ return { finalScriptSig: undefined, finalScriptWitness: undefined };
330
+ if (input.tapInternalKey) {
331
+ return {
332
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
333
+ dummySchnorrSig,
334
+ ]),
335
+ };
336
+ }
337
+ if (script.length === 22 && script[0] === opcodes.OP_0) {
338
+ return {
339
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
340
+ dummyEcdsaSig,
341
+ dummyCompressedPubkey,
342
+ ]),
343
+ };
344
+ }
345
+ if (input.redeemScript?.length === 22 && input.redeemScript[0] === opcodes.OP_0) {
346
+ return {
347
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
348
+ dummyEcdsaSig,
349
+ dummyCompressedPubkey,
350
+ ]),
351
+ };
352
+ }
353
+ return getFinalScripts(inputIndex, input, script, true, !!input.redeemScript, !!input.witnessScript);
354
+ };
355
+ try {
356
+ for (let i = 0; i < fakeTx.data.inputs.length; i++) {
357
+ const fullInput = inputs[i];
358
+ if (fullInput) {
359
+ fakeTx.finalizeInput(i, (idx) => finalizer(idx, fullInput));
360
+ }
361
+ }
194
362
  }
195
- if (this.estimatedFees)
196
- return this.estimatedFees;
197
- const fakeTx = new Psbt({
198
- network: this.network,
199
- });
200
- const builtTx = await this.internalBuildTransaction(fakeTx);
201
- if (builtTx) {
202
- const tx = fakeTx.extractTransaction(true, true);
203
- const size = tx.virtualSize();
204
- const fee = this.feeRate * size;
205
- this.estimatedFees = BigInt(Math.ceil(fee) + 1);
206
- return this.estimatedFees;
363
+ catch (e) {
364
+ this.warn(`Could not finalize dummy tx: ${e.message}`);
207
365
  }
208
- else {
209
- throw new Error(`Could not build transaction to estimate fee. Something went wrong while building the transaction.`);
366
+ const tx = fakeTx.extractTransaction(true, true);
367
+ const size = tx.virtualSize();
368
+ const fee = this.feeRate * size;
369
+ const finalFee = BigInt(Math.ceil(fee));
370
+ if (this.debugFees) {
371
+ this.log(`Estimating fees: feeRate=${this.feeRate}, accurate_vSize=${size}, fee=${finalFee}n`);
210
372
  }
373
+ return finalFee;
211
374
  }
212
375
  async rebuildFromBase64(base64) {
213
376
  this.transaction = Psbt.fromBase64(base64, {
@@ -246,34 +409,63 @@ export class TransactionBuilder extends TweakedTransaction {
246
409
  if (this.anchor) {
247
410
  this.addAnchor();
248
411
  }
249
- const sendBackAmount = this.totalInputAmount - amountSpent;
250
- if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
251
- if (AddressVerificator.isValidP2TRAddress(this.from, this.network)) {
252
- await this.setFeeOutput({
253
- value: Number(sendBackAmount),
254
- address: this.from,
255
- tapInternalKey: this.internalPubKeyToXOnly(),
256
- });
257
- }
258
- else if (AddressVerificator.isValidPublicKey(this.from, this.network)) {
259
- const pubKeyScript = script.compile([
260
- Buffer.from(this.from.replace('0x', ''), 'hex'),
261
- opcodes.OP_CHECKSIG,
262
- ]);
263
- await this.setFeeOutput({
264
- value: Number(sendBackAmount),
265
- script: pubKeyScript,
266
- });
412
+ let previousFee = -1n;
413
+ let estimatedFee = 0n;
414
+ let iterations = 0;
415
+ const maxIterations = 5;
416
+ while (iterations < maxIterations && estimatedFee !== previousFee) {
417
+ previousFee = estimatedFee;
418
+ estimatedFee = await this.estimateTransactionFees();
419
+ const totalSpent = amountSpent + estimatedFee;
420
+ const sendBackAmount = this.totalInputAmount - totalSpent;
421
+ if (this.debugFees) {
422
+ this.log(`Iteration ${iterations + 1}: inputAmount=${this.totalInputAmount}, totalSpent=${totalSpent}, sendBackAmount=${sendBackAmount}`);
423
+ }
424
+ if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
425
+ if (AddressVerificator.isValidP2TRAddress(this.from, this.network)) {
426
+ this.feeOutput = {
427
+ value: Number(sendBackAmount),
428
+ address: this.from,
429
+ tapInternalKey: this.internalPubKeyToXOnly(),
430
+ };
431
+ }
432
+ else if (AddressVerificator.isValidPublicKey(this.from, this.network)) {
433
+ const pubKeyScript = script.compile([
434
+ Buffer.from(this.from.replace('0x', ''), 'hex'),
435
+ opcodes.OP_CHECKSIG,
436
+ ]);
437
+ this.feeOutput = {
438
+ value: Number(sendBackAmount),
439
+ script: pubKeyScript,
440
+ };
441
+ }
442
+ else {
443
+ this.feeOutput = {
444
+ value: Number(sendBackAmount),
445
+ address: this.from,
446
+ };
447
+ }
448
+ this.overflowFees = sendBackAmount;
267
449
  }
268
450
  else {
269
- await this.setFeeOutput({
270
- value: Number(sendBackAmount),
271
- address: this.from,
272
- });
451
+ this.feeOutput = null;
452
+ this.overflowFees = 0n;
453
+ if (sendBackAmount < 0n) {
454
+ throw new Error(`Insufficient funds: need ${totalSpent} sats but only have ${this.totalInputAmount} sats`);
455
+ }
456
+ if (this.debugFees) {
457
+ this.warn(`Amount to send back (${sendBackAmount} sat) is less than minimum dust...`);
458
+ }
273
459
  }
274
- return;
460
+ iterations++;
461
+ }
462
+ if (iterations >= maxIterations) {
463
+ this.warn(`Fee calculation did not stabilize after ${maxIterations} iterations`);
464
+ }
465
+ this.transactionFee = estimatedFee;
466
+ if (this.debugFees) {
467
+ this.log(`Final fee: ${estimatedFee} sats, Change output: ${this.feeOutput ? `${this.feeOutput.value} sats` : 'none'}`);
275
468
  }
276
- this.warn(`Amount to send back (${sendBackAmount} sat) is less than the minimum dust (${TransactionBuilder.MINIMUM_DUST} sat), it will be consumed in fees instead.`);
277
469
  }
278
470
  addValueToToOutput(value) {
279
471
  if (value < TransactionBuilder.MINIMUM_DUST) {
@@ -315,13 +507,14 @@ export class TransactionBuilder extends TweakedTransaction {
315
507
  return total;
316
508
  }
317
509
  addOptionalOutputsAndGetAmount() {
318
- if (!this.optionalOutputs)
510
+ if (!this.optionalOutputs || this.optionalOutputsAdded)
319
511
  return 0n;
320
512
  let refundedFromOptionalOutputs = 0n;
321
513
  for (let i = 0; i < this.optionalOutputs.length; i++) {
322
514
  this.addOutput(this.optionalOutputs[i]);
323
515
  refundedFromOptionalOutputs += BigInt(this.optionalOutputs[i].value);
324
516
  }
517
+ this.optionalOutputsAdded = true;
325
518
  return refundedFromOptionalOutputs;
326
519
  }
327
520
  addInputsFromUTXO() {
@@ -350,6 +543,35 @@ export class TransactionBuilder extends TweakedTransaction {
350
543
  updateInput(input) {
351
544
  this.updateInputs.push(input);
352
545
  }
546
+ addFeeToOutput(amountSpent, contractAddress, epochChallenge, addContractOutput) {
547
+ if (addContractOutput) {
548
+ let amountToCA;
549
+ if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
550
+ amountToCA = MINIMUM_AMOUNT_CA;
551
+ }
552
+ else {
553
+ amountToCA = amountSpent;
554
+ }
555
+ this.addOutput({
556
+ value: Number(amountToCA),
557
+ address: contractAddress,
558
+ }, true);
559
+ if (amountToCA === MINIMUM_AMOUNT_CA &&
560
+ amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD) {
561
+ this.addOutput({
562
+ value: Number(amountSpent - amountToCA),
563
+ address: epochChallenge.address,
564
+ }, true);
565
+ }
566
+ }
567
+ else {
568
+ const amountToEpoch = amountSpent < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : amountSpent;
569
+ this.addOutput({
570
+ value: Number(amountToEpoch),
571
+ address: epochChallenge.address,
572
+ }, true);
573
+ }
574
+ }
353
575
  getWitness() {
354
576
  if (!this.tapData || !this.tapData.witness) {
355
577
  throw new Error('Witness is required');
@@ -379,28 +601,45 @@ export class TransactionBuilder extends TweakedTransaction {
379
601
  }
380
602
  async setFeeOutput(output) {
381
603
  const initialValue = output.value;
382
- const fee = await this.estimateTransactionFees();
383
- output.value = initialValue - Number(fee);
384
- if (output.value < TransactionBuilder.MINIMUM_DUST) {
385
- this.feeOutput = null;
386
- if (output.value < 0) {
387
- throw new Error(`setFeeOutput: Insufficient funds to pay the fees. Fee: ${fee} < Value: ${initialValue}. Total input: ${this.totalInputAmount} sat`);
604
+ this.feeOutput = null;
605
+ let estimatedFee = 0n;
606
+ let lastFee = -1n;
607
+ this.log(`setFeeOutput: Starting fee calculation for change. Initial available value: ${initialValue} sats.`);
608
+ for (let i = 0; i < 3 && estimatedFee !== lastFee; i++) {
609
+ lastFee = estimatedFee;
610
+ estimatedFee = await this.estimateTransactionFees();
611
+ const valueLeft = BigInt(initialValue) - estimatedFee;
612
+ if (this.debugFees) {
613
+ this.log(` -> Iteration ${i + 1}: Estimated fee is ${estimatedFee} sats. Value left for change: ${valueLeft} sats.`);
388
614
  }
389
- }
390
- else {
391
- this.feeOutput = output;
392
- const fee = await this.estimateTransactionFees();
393
- if (fee > BigInt(initialValue)) {
394
- throw new Error(`estimateTransactionFees: Insufficient funds to pay the fees. Fee: ${fee} > Value: ${initialValue}. Total input: ${this.totalInputAmount} sat`);
615
+ if (valueLeft >= TransactionBuilder.MINIMUM_DUST) {
616
+ this.feeOutput = { ...output, value: Number(valueLeft) };
617
+ this.overflowFees = valueLeft;
395
618
  }
396
- const valueLeft = initialValue - Number(fee);
397
- if (valueLeft < TransactionBuilder.MINIMUM_DUST) {
619
+ else {
398
620
  this.feeOutput = null;
621
+ this.overflowFees = 0n;
622
+ estimatedFee = await this.estimateTransactionFees();
623
+ if (this.debugFees) {
624
+ this.log(` -> Change is less than dust. Final fee without change output: ${estimatedFee} sats.`);
625
+ }
399
626
  }
400
- else {
401
- this.feeOutput.value = valueLeft;
627
+ }
628
+ const finalValueLeft = BigInt(initialValue) - estimatedFee;
629
+ if (finalValueLeft < 0) {
630
+ throw new Error(`setFeeOutput: Insufficient funds to pay the fees. Required fee: ${estimatedFee}, Available: ${initialValue}. Total input: ${this.totalInputAmount} sat`);
631
+ }
632
+ if (finalValueLeft >= TransactionBuilder.MINIMUM_DUST) {
633
+ this.feeOutput = { ...output, value: Number(finalValueLeft) };
634
+ this.overflowFees = finalValueLeft;
635
+ if (this.debugFees) {
636
+ this.log(`setFeeOutput: Final change output set to ${finalValueLeft} sats. Final fee: ${estimatedFee} sats.`);
402
637
  }
403
- this.overflowFees = BigInt(valueLeft);
638
+ }
639
+ else {
640
+ this.warn(`Amount to send back (${finalValueLeft} sat) is less than the minimum dust (${TransactionBuilder.MINIMUM_DUST} sat), it will be consumed in fees instead.`);
641
+ this.feeOutput = null;
642
+ this.overflowFees = 0n;
404
643
  }
405
644
  }
406
645
  async internalBuildTransaction(transaction, checkPartialSigs = false) {
@@ -432,4 +671,4 @@ TransactionBuilder.LOCK_LEAF_SCRIPT = script.compile([
432
671
  opcodes.OP_FALSE,
433
672
  opcodes.OP_VERIFY,
434
673
  ]);
435
- TransactionBuilder.MINIMUM_DUST = 50n;
674
+ TransactionBuilder.MINIMUM_DUST = 330n;
@@ -9,6 +9,7 @@ export interface LoadedStorage {
9
9
  export interface ITransactionParameters extends ITweakedTransactionData {
10
10
  readonly from?: string;
11
11
  readonly to?: string;
12
+ readonly debugFees?: boolean;
12
13
  utxos: UTXO[];
13
14
  nonWitnessUtxo?: Buffer;
14
15
  estimatedFees?: bigint;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@btc-vision/transaction",
3
3
  "type": "module",
4
- "version": "1.6.6",
4
+ "version": "1.6.8",
5
5
  "author": "BlobMaster41",
6
6
  "description": "OPNet transaction library allows you to create and sign transactions for the OPNet network.",
7
7
  "engines": {
package/src/_version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '1.6.6';
1
+ export const version = '1.6.8';