@btc-vision/transaction 1.7.16 → 1.7.18

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.
@@ -32,9 +32,6 @@ export class P2WDAGenerator extends Generator {
32
32
  maxWitnessFields: number = 10,
33
33
  maxBytesPerWitness: number = 80,
34
34
  ): boolean {
35
- // Account for Schnorr signature (64 bytes) and compression
36
- // Assume 30% compression ratio (conservative estimate)
37
-
38
35
  const signatureSize = 64;
39
36
  const compressionRatio = 0.7;
40
37
 
@@ -79,12 +76,10 @@ export class P2WDAGenerator extends Generator {
79
76
 
80
77
  const writer = new BinaryWriter();
81
78
 
82
- // Version byte
83
79
  writer.writeU8(P2WDAGenerator.P2WDA_VERSION);
84
80
 
85
81
  const features: Feature<Features>[] = featuresRaw.sort((a, b) => a.priority - b.priority);
86
82
 
87
- // Header
88
83
  writer.writeBytes(
89
84
  this.getHeader(
90
85
  maxPriority,
@@ -92,33 +87,19 @@ export class P2WDAGenerator extends Generator {
92
87
  ),
93
88
  );
94
89
 
95
- // Contract secret
96
90
  writer.writeBytes(contractSecret);
97
91
 
98
- // Challenge components for epoch rewards
99
92
  writer.writeBytes(challenge.publicKey.toBuffer());
100
93
  writer.writeBytes(challenge.solution);
101
94
 
102
- // Calldata with length prefix
103
95
  writer.writeU32(calldata.length);
104
96
  writer.writeBytes(calldata);
105
97
 
106
- // Features
107
98
  this.writeFeatures(writer, features);
108
99
 
109
100
  return Buffer.from(writer.getBuffer());
110
101
  }
111
102
 
112
- /**
113
- * Create a minimal header for P2WDA operations
114
- *
115
- * The header contains essential transaction metadata in a compact format:
116
- * [sender_pubkey_prefix(1)] [feature_flags(3)] [max_priority(8)]
117
- *
118
- * @param maxPriority Maximum priority fee
119
- * @param features Feature opcodes to set in flags
120
- * @returns 12-byte header
121
- */
122
103
  public override getHeader(maxPriority: bigint, features: Features[] = []): Buffer {
123
104
  return super.getHeader(maxPriority, features);
124
105
  }
@@ -133,48 +114,11 @@ export class P2WDAGenerator extends Generator {
133
114
  * @param features Array of features to encode
134
115
  */
135
116
  private writeFeatures(writer: BinaryWriter, features: Feature<Features>[]): void {
136
- // Write feature count
137
117
  writer.writeU16(features.length);
138
118
 
139
119
  for (const feature of features) {
140
- // Write feature opcode
141
120
  writer.writeU8(feature.opcode);
142
-
143
- // Encode feature data
144
- const encodedData = this.encodeFeatureData(feature);
145
-
146
- // Write feature data with length prefix
147
- writer.writeU32(encodedData.length);
148
- writer.writeBytes(encodedData);
149
- }
150
- }
151
-
152
- /**
153
- * Encode a single feature's data
154
- *
155
- * Unlike the base Generator class, we don't split into chunks here
156
- * since P2WDA handles chunking at the witness level
157
- *
158
- * @param feature The feature to encode
159
- * @returns Encoded feature data
160
- */
161
- private encodeFeatureData(feature: Feature<Features>): Buffer {
162
- switch (feature.opcode) {
163
- case Features.ACCESS_LIST: {
164
- // Access lists are already encoded efficiently by the parent class
165
- const chunks = this.encodeFeature(feature);
166
- // Flatten chunks since P2WDA doesn't need script-level chunking
167
- return Buffer.concat(chunks.flat());
168
- }
169
-
170
- case Features.EPOCH_SUBMISSION: {
171
- // Epoch submissions are also handled by parent
172
- const chunks = this.encodeFeature(feature);
173
- return Buffer.concat(chunks.flat());
174
- }
175
-
176
- default:
177
- throw new Error(`Unknown feature type: ${feature.opcode}`);
121
+ this.encodeFeature(feature, writer);
178
122
  }
179
123
  }
180
124
  }
@@ -1,10 +1,7 @@
1
1
  import { Transaction, TxOutput } from '@btc-vision/bitcoin';
2
2
  import { currentConsensus } from '../consensus/ConsensusConfig.js';
3
3
  import { UTXO } from '../utxo/interfaces/IUTXO.js';
4
- import {
5
- CustomScriptTransaction,
6
- ICustomTransactionParameters,
7
- } from './builders/CustomScriptTransaction.js';
4
+ import { CustomScriptTransaction, ICustomTransactionParameters, } from './builders/CustomScriptTransaction.js';
8
5
  import { DeploymentTransaction } from './builders/DeploymentTransaction.js';
9
6
  import { FundingTransaction } from './builders/FundingTransaction.js';
10
7
  import { InteractionTransaction } from './builders/InteractionTransaction.js';
@@ -34,12 +31,11 @@ import { CancelTransaction, ICancelTransactionParameters } from './builders/Canc
34
31
 
35
32
  export interface DeploymentResult {
36
33
  readonly transaction: [string, string];
37
-
38
34
  readonly contractAddress: string;
39
35
  readonly contractPubKey: string;
40
36
  readonly challenge: RawChallenge;
41
-
42
37
  readonly utxos: UTXO[];
38
+ readonly inputUtxos: UTXO[];
43
39
  }
44
40
 
45
41
  export interface FundingTransactionResponse {
@@ -47,12 +43,14 @@ export interface FundingTransactionResponse {
47
43
  readonly original: FundingTransaction;
48
44
  readonly estimatedFees: bigint;
49
45
  readonly nextUTXOs: UTXO[];
46
+ readonly inputUtxos: UTXO[];
50
47
  }
51
48
 
52
49
  export interface BitcoinTransferBase {
53
50
  readonly tx: string;
54
51
  readonly estimatedFees: bigint;
55
52
  readonly nextUTXOs: UTXO[];
53
+ readonly inputUtxos: UTXO[];
56
54
  }
57
55
 
58
56
  export interface InteractionResponse {
@@ -61,6 +59,7 @@ export interface InteractionResponse {
61
59
  readonly estimatedFees: bigint;
62
60
  readonly nextUTXOs: UTXO[];
63
61
  readonly fundingUTXOs: UTXO[];
62
+ readonly fundingInputUtxos: UTXO[];
64
63
  readonly challenge: RawChallenge;
65
64
  readonly interactionAddress: string | null;
66
65
  readonly compiledTargetScript: string | null;
@@ -73,19 +72,22 @@ export interface BitcoinTransferResponse extends BitcoinTransferBase {
73
72
  export interface CancelledTransaction {
74
73
  readonly transaction: string;
75
74
  readonly nextUTXOs: UTXO[];
75
+ readonly inputUtxos: UTXO[];
76
76
  }
77
77
 
78
78
  export class TransactionFactory {
79
79
  public debug: boolean = false;
80
80
 
81
81
  private readonly DUMMY_PUBKEY = Buffer.alloc(32, 1);
82
- private readonly P2TR_SCRIPT = Buffer.concat([
83
- Buffer.from([0x51, 0x20]), // OP_1 + 32 bytes
84
- this.DUMMY_PUBKEY,
85
- ]);
82
+ private readonly P2TR_SCRIPT = Buffer.concat([Buffer.from([0x51, 0x20]), this.DUMMY_PUBKEY]);
86
83
  private readonly INITIAL_FUNDING_ESTIMATE = 2000n;
87
84
  private readonly MAX_ITERATIONS = 10;
88
85
 
86
+ /**
87
+ * @description Creates a cancellable transaction.
88
+ * @param {ICancelTransactionParameters | ICancelTransactionParametersWithoutSigner} params - The cancel transaction parameters
89
+ * @returns {Promise<CancelledTransaction>} - The cancelled transaction result
90
+ */
89
91
  public async createCancellableTransaction(
90
92
  params: ICancelTransactionParameters | ICancelTransactionParametersWithoutSigner,
91
93
  ): Promise<CancelledTransaction> {
@@ -115,16 +117,18 @@ export class TransactionFactory {
115
117
  return {
116
118
  transaction: rawTx,
117
119
  nextUTXOs: this.getUTXOAsTransaction(signed, params.from, 0),
120
+ inputUtxos: params.utxos,
118
121
  };
119
122
  }
120
123
 
121
124
  /**
122
125
  * @description Generate a transaction with a custom script.
123
- * @returns {Promise<[string, string]>} - The signed transaction
126
+ * @param {ICustomTransactionParameters | ICustomTransactionWithoutSigner} interactionParameters - The custom transaction parameters
127
+ * @returns {Promise<[string, string, UTXO[], UTXO[]]>} - The signed transaction tuple [fundingTx, customTx, nextUTXOs, inputUtxos]
124
128
  */
125
129
  public async createCustomScriptTransaction(
126
130
  interactionParameters: ICustomTransactionParameters | ICustomTransactionWithoutSigner,
127
- ): Promise<[string, string, UTXO[]]> {
131
+ ): Promise<[string, string, UTXO[], UTXO[]]> {
128
132
  if (!interactionParameters.to) {
129
133
  throw new Error('Field "to" not provided.');
130
134
  }
@@ -140,7 +144,6 @@ export class TransactionFactory {
140
144
 
141
145
  const inputs = this.parseOptionalInputs(interactionParameters.optionalInputs);
142
146
 
143
- // Use common iteration logic
144
147
  const { finalTransaction, estimatedAmount, challenge } = await this.iterateFundingAmount(
145
148
  { ...interactionParameters, optionalInputs: inputs },
146
149
  CustomScriptTransaction,
@@ -159,7 +162,6 @@ export class TransactionFactory {
159
162
  parameters.utxos = interactionParameters.utxos;
160
163
  parameters.amount = estimatedAmount;
161
164
 
162
- // Create funding transaction
163
165
  const feeEstimationFunding = await this.createFundTransaction({
164
166
  ...parameters,
165
167
  optionalOutputs: [],
@@ -199,11 +201,13 @@ export class TransactionFactory {
199
201
  signedTransaction.tx.toHex(),
200
202
  outTx.toHex(),
201
203
  this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.from, 1),
204
+ interactionParameters.utxos,
202
205
  ];
203
206
  }
204
207
 
205
208
  /**
206
209
  * @description Generates the required transactions.
210
+ * @param {IInteractionParameters | InteractionParametersWithoutSigner} interactionParameters - The interaction parameters
207
211
  * @returns {Promise<InteractionResponse>} - The signed transaction
208
212
  */
209
213
  public async signInteraction(
@@ -235,7 +239,6 @@ export class TransactionFactory {
235
239
 
236
240
  const inputs = this.parseOptionalInputs(interactionParameters.optionalInputs);
237
241
 
238
- // Use common iteration logic
239
242
  const { finalTransaction, estimatedAmount, challenge } = await this.iterateFundingAmount(
240
243
  { ...interactionParameters, optionalInputs: inputs },
241
244
  InteractionTransaction,
@@ -310,6 +313,7 @@ export class TransactionFactory {
310
313
  ),
311
314
  challenge: challenge.toRaw(),
312
315
  fundingUTXOs: fundingUTXO,
316
+ fundingInputUtxos: interactionParameters.utxos,
313
317
  compiledTargetScript: interactionTx.exportCompiledTargetScript().toString('hex'),
314
318
  };
315
319
  }
@@ -333,7 +337,6 @@ export class TransactionFactory {
333
337
 
334
338
  const inputs = this.parseOptionalInputs(deploymentParameters.optionalInputs);
335
339
 
336
- // Use common iteration logic
337
340
  const { finalTransaction, estimatedAmount, challenge } = await this.iterateFundingAmount(
338
341
  { ...deploymentParameters, optionalInputs: inputs },
339
342
  DeploymentTransaction,
@@ -421,13 +424,14 @@ export class TransactionFactory {
421
424
  contractPubKey: deploymentTx.contractPubKey,
422
425
  utxos: [refundUTXO],
423
426
  challenge: challenge.toRaw(),
427
+ inputUtxos: deploymentParameters.utxos,
424
428
  };
425
429
  }
426
430
 
427
431
  /**
428
432
  * @description Creates a funding transaction.
429
433
  * @param {IFundingTransactionParameters} parameters - The funding transaction parameters
430
- * @returns {Promise<{ estimatedFees: bigint; tx: string }>} - The signed transaction
434
+ * @returns {Promise<BitcoinTransferResponse>} - The signed transaction
431
435
  */
432
436
  public async createBTCTransfer(
433
437
  parameters: IFundingTransactionParameters,
@@ -442,6 +446,7 @@ export class TransactionFactory {
442
446
  original: resp.original,
443
447
  tx: resp.tx.toHex(),
444
448
  nextUTXOs: this.getAllNewUTXOs(resp.original, resp.tx, parameters.from),
449
+ inputUtxos: parameters.utxos,
445
450
  };
446
451
  }
447
452
 
@@ -450,6 +455,7 @@ export class TransactionFactory {
450
455
  * @param {TransactionBuilder<TransactionType>} original - The original transaction
451
456
  * @param {Transaction} tx - The transaction
452
457
  * @param {string} to - The address to filter
458
+ * @returns {UTXO[]} - The new UTXOs belonging to the specified address
453
459
  */
454
460
  public getAllNewUTXOs(
455
461
  original: TransactionBuilder<TransactionType>,
@@ -473,6 +479,11 @@ export class TransactionFactory {
473
479
  return utxos;
474
480
  }
475
481
 
482
+ /**
483
+ * Parse optional inputs and normalize nonWitnessUtxo format.
484
+ * @param {UTXO[]} optionalInputs - The optional inputs to parse
485
+ * @returns {UTXO[]} - The parsed inputs with normalized nonWitnessUtxo
486
+ */
476
487
  private parseOptionalInputs(optionalInputs?: UTXO[]): UTXO[] {
477
488
  return (optionalInputs || []).map((input) => {
478
489
  let nonWitness = input.nonWitnessUtxo;
@@ -495,6 +506,11 @@ export class TransactionFactory {
495
506
  });
496
507
  }
497
508
 
509
+ /**
510
+ * Detect and use OP_WALLET for cancel transactions if available.
511
+ * @param {ICancelTransactionParameters | ICancelTransactionParametersWithoutSigner} interactionParameters - The cancel parameters
512
+ * @returns {Promise<CancelledTransaction | null>} - The cancelled transaction or null if OP_WALLET not available
513
+ */
498
514
  private async detectCancelOPWallet(
499
515
  interactionParameters:
500
516
  | ICancelTransactionParameters
@@ -512,7 +528,6 @@ export class TransactionFactory {
512
528
  const opnet = _window.opnet.web3;
513
529
  const interaction = await opnet.cancelTransaction({
514
530
  ...interactionParameters,
515
-
516
531
  // @ts-expect-error no, this is ok
517
532
  signer: undefined,
518
533
  });
@@ -521,9 +536,17 @@ export class TransactionFactory {
521
536
  throw new Error('Could not sign interaction transaction.');
522
537
  }
523
538
 
524
- return interaction;
539
+ return {
540
+ ...interaction,
541
+ inputUtxos: interaction.inputUtxos ?? interactionParameters.utxos,
542
+ };
525
543
  }
526
544
 
545
+ /**
546
+ * Detect and use OP_WALLET for interaction transactions if available.
547
+ * @param {IInteractionParameters | InteractionParametersWithoutSigner} interactionParameters - The interaction parameters
548
+ * @returns {Promise<InteractionResponse | null>} - The interaction response or null if OP_WALLET not available
549
+ */
527
550
  private async detectInteractionOPWallet(
528
551
  interactionParameters: IInteractionParameters | InteractionParametersWithoutSigner,
529
552
  ): Promise<InteractionResponse | null> {
@@ -539,7 +562,6 @@ export class TransactionFactory {
539
562
  const opnet = _window.opnet.web3;
540
563
  const interaction = await opnet.signInteraction({
541
564
  ...interactionParameters,
542
-
543
565
  // @ts-expect-error no, this is ok
544
566
  signer: undefined,
545
567
  });
@@ -548,9 +570,17 @@ export class TransactionFactory {
548
570
  throw new Error('Could not sign interaction transaction.');
549
571
  }
550
572
 
551
- return interaction;
573
+ return {
574
+ ...interaction,
575
+ fundingInputUtxos: interaction.fundingInputUtxos ?? interactionParameters.utxos,
576
+ };
552
577
  }
553
578
 
579
+ /**
580
+ * Detect and use OP_WALLET for deployment transactions if available.
581
+ * @param {IDeploymentParameters | IDeploymentParametersWithoutSigner} deploymentParameters - The deployment parameters
582
+ * @returns {Promise<DeploymentResult | null>} - The deployment result or null if OP_WALLET not available
583
+ */
554
584
  private async detectDeploymentOPWallet(
555
585
  deploymentParameters: IDeploymentParameters | IDeploymentParametersWithoutSigner,
556
586
  ): Promise<DeploymentResult | null> {
@@ -566,7 +596,6 @@ export class TransactionFactory {
566
596
  const opnet = _window.opnet.web3;
567
597
  const deployment = await opnet.deployContract({
568
598
  ...deploymentParameters,
569
-
570
599
  // @ts-expect-error no, this is ok
571
600
  signer: undefined,
572
601
  });
@@ -575,9 +604,17 @@ export class TransactionFactory {
575
604
  throw new Error('Could not sign interaction transaction.');
576
605
  }
577
606
 
578
- return deployment;
607
+ return {
608
+ ...deployment,
609
+ inputUtxos: deployment.inputUtxos ?? deploymentParameters.utxos,
610
+ };
579
611
  }
580
612
 
613
+ /**
614
+ * Create and sign a funding transaction.
615
+ * @param {IFundingTransactionParameters} parameters - The funding transaction parameters
616
+ * @returns {Promise<FundingTransactionResponse>} - The funding transaction response
617
+ */
581
618
  private async createFundTransaction(
582
619
  parameters: IFundingTransactionParameters,
583
620
  ): Promise<FundingTransactionResponse> {
@@ -594,6 +631,7 @@ export class TransactionFactory {
594
631
  original: fundingTransaction,
595
632
  estimatedFees: fundingTransaction.estimatedFees,
596
633
  nextUTXOs: this.getUTXOAsTransaction(signedTransaction, parameters.to, 0),
634
+ inputUtxos: parameters.utxos,
597
635
  };
598
636
  }
599
637
 
@@ -604,13 +642,19 @@ export class TransactionFactory {
604
642
  * if any of them are P2WDA addresses. P2WDA detection is based on the
605
643
  * witness script pattern: (OP_2DROP * 5) <pubkey> OP_CHECKSIG
606
644
  *
607
- * @param utxos The main UTXOs to check
608
- * @returns true if any UTXO is P2WDA, false otherwise
645
+ * @param {UTXO[]} utxos - The main UTXOs to check
646
+ * @returns {boolean} - true if any UTXO is P2WDA, false otherwise
609
647
  */
610
648
  private hasP2WDAInputs(utxos: UTXO[]): boolean {
611
649
  return utxos.some((utxo) => P2WDADetector.isP2WDAUTXO(utxo));
612
650
  }
613
651
 
652
+ /**
653
+ * Write PSBT header with type and consensus version.
654
+ * @param {PSBTTypes} type - The PSBT type
655
+ * @param {string} psbt - The base64 encoded PSBT
656
+ * @returns {string} - The hex encoded PSBT with header
657
+ */
614
658
  private writePSBTHeader(type: PSBTTypes, psbt: string): string {
615
659
  const buf = Buffer.from(psbt, 'base64');
616
660
 
@@ -635,8 +679,8 @@ export class TransactionFactory {
635
679
  * - 75% cost reduction for data storage
636
680
  * - No separate funding transaction needed
637
681
  *
638
- * @param interactionParameters The interaction parameters
639
- * @returns The signed P2WDA interaction response
682
+ * @param {IInteractionParameters | InteractionParametersWithoutSigner} interactionParameters - The interaction parameters
683
+ * @returns {Promise<InteractionResponse>} - The signed P2WDA interaction response
640
684
  */
641
685
  private async signP2WDAInteraction(
642
686
  interactionParameters: IInteractionParameters | InteractionParametersWithoutSigner,
@@ -645,7 +689,6 @@ export class TransactionFactory {
645
689
  throw new Error('Field "from" not provided.');
646
690
  }
647
691
 
648
- // Ensure we have a signer for P2WDA
649
692
  if (!('signer' in interactionParameters)) {
650
693
  throw new Error(
651
694
  'P2WDA interactions require a signer. OP_WALLET is not supported for P2WDA.',
@@ -669,14 +712,20 @@ export class TransactionFactory {
669
712
  nextUTXOs: this.getUTXOAsTransaction(
670
713
  signedTx,
671
714
  interactionParameters.from,
672
- signedTx.outs.length - 1, // Last output is typically the change
715
+ signedTx.outs.length - 1,
673
716
  ),
674
717
  fundingUTXOs: [...interactionParameters.utxos, ...inputs],
718
+ fundingInputUtxos: interactionParameters.utxos,
675
719
  challenge: interactionParameters.challenge.toRaw(),
676
720
  compiledTargetScript: null,
677
721
  };
678
722
  }
679
723
 
724
+ /**
725
+ * Get the priority fee from transaction parameters.
726
+ * @param {ITransactionParameters} params - The transaction parameters
727
+ * @returns {bigint} - The priority fee, minimum dust if below threshold
728
+ */
680
729
  private getPriorityFee(params: ITransactionParameters): bigint {
681
730
  const totalFee = params.priorityFee + params.gasSatFee;
682
731
  if (totalFee < TransactionBuilder.MINIMUM_DUST) {
@@ -687,7 +736,16 @@ export class TransactionFactory {
687
736
  }
688
737
 
689
738
  /**
690
- * Common iteration logic for finding the correct funding amount
739
+ * Common iteration logic for finding the correct funding amount.
740
+ *
741
+ * This method iteratively estimates the required funding amount by simulating
742
+ * transactions until the amount converges or max iterations is reached.
743
+ *
744
+ * @param {P} params - The transaction parameters
745
+ * @param {new (params: P) => T} TransactionClass - The transaction class constructor
746
+ * @param {(tx: T) => Promise<bigint>} calculateAmount - Function to calculate required amount
747
+ * @param {string} debugPrefix - Prefix for debug logging
748
+ * @returns {Promise<{finalTransaction: T, estimatedAmount: bigint, challenge: ChallengeSolution | null}>} - The final transaction and estimated amount
691
749
  */
692
750
  private async iterateFundingAmount<
693
751
  T extends InteractionTransaction | DeploymentTransaction | CustomScriptTransaction,
@@ -732,14 +790,13 @@ export class TransactionFactory {
732
790
  nonWitnessUtxo: dummyTx.toBuffer(),
733
791
  };
734
792
 
735
- // Build transaction params - TypeScript needs explicit typing here
736
793
  let txParams: P;
737
794
  if ('challenge' in params && params.challenge) {
738
795
  const withChallenge = {
739
796
  ...params,
740
797
  utxos: [simulatedFundedUtxo],
741
798
  randomBytes: randomBytes,
742
- challenge: challenge ?? params.challenge, // Use existing or original
799
+ challenge: challenge ?? params.challenge,
743
800
  };
744
801
  txParams = withChallenge as P;
745
802
  } else {
@@ -776,7 +833,6 @@ export class TransactionFactory {
776
833
 
777
834
  finalPreTransaction = preTransaction;
778
835
 
779
- // Extract challenge with explicit typing
780
836
  if (
781
837
  'getChallenge' in preTransaction &&
782
838
  typeof preTransaction.getChallenge === 'function'
@@ -804,6 +860,13 @@ export class TransactionFactory {
804
860
  };
805
861
  }
806
862
 
863
+ /**
864
+ * Convert a transaction output to a UTXO.
865
+ * @param {Transaction} tx - The transaction
866
+ * @param {string} to - The address
867
+ * @param {number} index - The output index
868
+ * @returns {UTXO[]} - The UTXO array (empty if output doesn't exist)
869
+ */
807
870
  private getUTXOAsTransaction(tx: Transaction, to: string, index: number): UTXO[] {
808
871
  if (!tx.outs[index]) return [];
809
872