@btc-vision/transaction 1.0.2 → 1.0.3

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 (83) hide show
  1. package/browser/_version.d.ts +1 -1
  2. package/browser/generators/AddressGenerator.d.ts +7 -0
  3. package/browser/generators/builders/DeploymentGenerator.d.ts +1 -0
  4. package/browser/index.js +2 -2
  5. package/browser/keypair/Wallet.d.ts +1 -0
  6. package/browser/opnet.d.ts +2 -0
  7. package/browser/tests/gen.d.ts +1 -0
  8. package/browser/tests/transfer.d.ts +1 -0
  9. package/browser/transaction/builders/TransactionBuilder.d.ts +2 -2
  10. package/browser/verification/TapscriptVerificator.d.ts +17 -0
  11. package/build/_version.d.ts +1 -1
  12. package/build/_version.js +1 -1
  13. package/build/generators/AddressGenerator.d.ts +7 -0
  14. package/build/generators/AddressGenerator.js +21 -0
  15. package/build/generators/OPNetAddressGenerator.d.ts +0 -0
  16. package/build/generators/OPNetAddressGenerator.js +1 -0
  17. package/build/generators/builders/DeploymentGenerator.d.ts +1 -0
  18. package/build/generators/builders/DeploymentGenerator.js +10 -7
  19. package/build/keypair/Wallet.d.ts +1 -0
  20. package/build/keypair/Wallet.js +6 -0
  21. package/build/opnet.d.ts +2 -0
  22. package/build/opnet.js +2 -0
  23. package/build/tests/gen.d.ts +1 -0
  24. package/build/tests/gen.js +15 -0
  25. package/build/tests/test.js +15 -38
  26. package/build/tests/transfer.d.ts +1 -0
  27. package/build/tests/transfer.js +74 -0
  28. package/build/transaction/builders/TransactionBuilder.d.ts +2 -2
  29. package/build/transaction/builders/TransactionBuilder.js +4 -1
  30. package/build/verification/TapscriptVerificator.d.ts +17 -0
  31. package/build/verification/TapscriptVerificator.js +43 -0
  32. package/docs/assets/navigation.js +1 -1
  33. package/docs/assets/search.js +1 -1
  34. package/docs/classes/AddressGenerator.html +178 -0
  35. package/docs/classes/BitcoinUtils.html +182 -182
  36. package/docs/classes/CalldataGenerator.html +210 -210
  37. package/docs/classes/Compressor.html +184 -184
  38. package/docs/classes/ContractBaseMetadata.html +181 -181
  39. package/docs/classes/DeploymentGenerator.html +200 -199
  40. package/docs/classes/EcKeyPair.html +279 -279
  41. package/docs/classes/FundingTransaction.html +315 -315
  42. package/docs/classes/Generator.html +198 -198
  43. package/docs/classes/InteractionTransaction.html +387 -387
  44. package/docs/classes/TapscriptVerificator.html +180 -0
  45. package/docs/classes/TransactionBuilder.html +325 -325
  46. package/docs/classes/TransactionFactory.html +179 -179
  47. package/docs/classes/TweakedSigner.html +180 -180
  48. package/docs/classes/UTXOManager.html +186 -186
  49. package/docs/classes/Wallet.html +192 -190
  50. package/docs/classes/wBTC.html +188 -188
  51. package/docs/enums/TransactionType.html +178 -178
  52. package/docs/interfaces/ContractAddressVerificationParams.html +179 -0
  53. package/docs/interfaces/FetchUTXOParams.html +177 -177
  54. package/docs/interfaces/IFundingTransactionParameters.html +181 -181
  55. package/docs/interfaces/IInteractionParameters.html +184 -184
  56. package/docs/interfaces/ITransactionDataContractDeployment.html +183 -183
  57. package/docs/interfaces/ITransactionDataContractInteractionWrap.html +185 -185
  58. package/docs/interfaces/ITransactionParameters.html +180 -180
  59. package/docs/interfaces/IWallet.html +180 -180
  60. package/docs/interfaces/NetworkInformation.html +175 -175
  61. package/docs/interfaces/PsbtInputExtended.html +193 -193
  62. package/docs/interfaces/PsbtOutputExtendedAddress.html +182 -182
  63. package/docs/interfaces/PsbtOutputExtendedScript.html +182 -182
  64. package/docs/interfaces/RawUTXOResponse.html +177 -177
  65. package/docs/interfaces/TapLeafScript.html +176 -176
  66. package/docs/interfaces/TweakSettings.html +178 -178
  67. package/docs/interfaces/UTXO.html +177 -177
  68. package/docs/interfaces/UpdateInput.html +174 -174
  69. package/docs/modules.html +5 -2
  70. package/docs/types/PsbtOutputExtended.html +173 -173
  71. package/docs/variables/version.html +173 -173
  72. package/package.json +2 -1
  73. package/src/_version.ts +1 -1
  74. package/src/generators/AddressGenerator.ts +29 -0
  75. package/src/generators/builders/DeploymentGenerator.ts +16 -13
  76. package/src/keypair/Wallet.ts +12 -0
  77. package/src/opnet.ts +4 -0
  78. package/src/tests/gen.ts +24 -0
  79. package/src/tests/test.ts +17 -54
  80. package/src/{scripts/test.ts → tests/transfer.ts} +102 -98
  81. package/src/transaction/builders/TransactionBuilder.ts +610 -606
  82. package/src/verification/TapscriptVerificator.ts +89 -0
  83. package/src/scripts/Regtest.ts +0 -19
@@ -1,606 +1,610 @@
1
- import { initEccLib, Network, opcodes, Payment, payments, Psbt, script, Signer, Transaction } from 'bitcoinjs-lib';
2
- import { varuint } from 'bitcoinjs-lib/src/bufferutils.js';
3
- import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371.js';
4
- import * as ecc from 'tiny-secp256k1';
5
- import { PsbtInputExtended, PsbtOutputExtended, UpdateInput } from '../interfaces/Tap.js';
6
- import { TransactionType } from '../enums/TransactionType.js';
7
- import { IFundingTransactionParameters, ITransactionParameters } from '../interfaces/ITransactionParameters.js';
8
- import { EcKeyPair } from '../../keypair/EcKeyPair.js';
9
- import { Address } from '@btc-vision/bsi-binary';
10
- import { UTXO } from '../../utxo/interfaces/IUTXO.js';
11
- import { ECPairInterface } from 'ecpair';
12
- import { Logger } from '@btc-vision/logger';
13
-
14
- /**
15
- * Allows to build a transaction like you would on Ethereum.
16
- * @description The transaction builder class
17
- * @abstract
18
- * @class TransactionBuilder
19
- */
20
- export abstract class TransactionBuilder<T extends TransactionType> extends Logger {
21
- protected static readonly LOCK_LEAF_SCRIPT: Buffer = script.compile([opcodes.OP_0]);
22
- protected static readonly MINIMUM_DUST: bigint = 330n;
23
-
24
- public abstract readonly type: T;
25
- public readonly logColor: string = '#785def';
26
-
27
- /**
28
- * @description Cost in satoshis of the transaction fee
29
- */
30
- public transactionFee: bigint = 0n;
31
- /**
32
- * @description The transaction itself.
33
- */
34
- protected readonly transaction: Psbt;
35
- /**
36
- * @description The inputs of the transaction
37
- */
38
- protected readonly inputs: PsbtInputExtended[] = [];
39
- /**
40
- * @description Inputs to update later on.
41
- */
42
- protected readonly updateInputs: UpdateInput[] = [];
43
- /**
44
- * @description The outputs of the transaction
45
- */
46
- protected readonly outputs: PsbtOutputExtended[] = [];
47
- /**
48
- * @description Output that will be used to pay the fees
49
- */
50
- protected feeOutput: PsbtOutputExtended | null = null;
51
- /**
52
- * @description Was the transaction signed?
53
- */
54
- protected signed: boolean = false;
55
- /**
56
- * @description The tap data of the transaction
57
- */
58
- protected tapData: Payment | null = null;
59
- /**
60
- * @description The script data of the transaction
61
- */
62
- protected scriptData: Payment | null = null;
63
- /**
64
- * @description The total amount of satoshis in the inputs
65
- */
66
- protected totalInputAmount: bigint;
67
- /**
68
- * @description The signer of the transaction
69
- */
70
- protected readonly signer: Signer;
71
- /**
72
- * @description The network where the transaction will be broadcasted
73
- */
74
- protected readonly network: Network;
75
- /**
76
- * @description The fee rate of the transaction
77
- */
78
- protected readonly feeRate: number;
79
- /**
80
- * @description The opnet priority fee of the transaction
81
- */
82
- protected readonly priorityFee: bigint;
83
- /**
84
- * @description The utxos used in the transaction
85
- */
86
- protected utxos: UTXO[];
87
-
88
- /**
89
- * @description The address where the transaction is sent to
90
- * @protected
91
- */
92
- protected to: Address;
93
-
94
- /**
95
- * @description The address where the transaction is sent from
96
- * @protected
97
- */
98
- protected from: Address;
99
-
100
- /**
101
- * @description The maximum fee rate of the transaction
102
- */
103
- private _maximumFeeRate: number = 100000000;
104
-
105
- /**
106
- * @param {ITransactionParameters} parameters - The transaction parameters
107
- */
108
- protected constructor(parameters: ITransactionParameters) {
109
- super();
110
-
111
- this.signer = parameters.signer;
112
- this.network = parameters.network;
113
- this.feeRate = parameters.feeRate;
114
- this.priorityFee = parameters.priorityFee;
115
- this.utxos = parameters.utxos;
116
- this.to = parameters.to;
117
- this.from =
118
- parameters.from ||
119
- EcKeyPair.getTaprootAddress(this.signer as ECPairInterface, this.network);
120
-
121
- this.totalInputAmount = this.calculateTotalUTXOAmount();
122
- const totalVOut: bigint = this.calculateTotalVOutAmount();
123
-
124
- if (totalVOut < this.totalInputAmount) {
125
- throw new Error(`Vout value is less than the value to send`);
126
- }
127
-
128
- if (this.totalInputAmount < TransactionBuilder.MINIMUM_DUST) {
129
- throw new Error(`Value is less than the minimum dust`);
130
- }
131
-
132
- this.transaction = new Psbt({
133
- network: this.network,
134
- });
135
- }
136
-
137
- public getFundingTransactionParameters(): IFundingTransactionParameters {
138
- return {
139
- utxos: this.utxos,
140
- to: this.getScriptAddress(),
141
- signer: this.signer,
142
- network: this.network,
143
- feeRate: this.feeRate,
144
- priorityFee: this.priorityFee,
145
- from: this.from,
146
- childTransactionRequiredFees: this.transactionFee,
147
- };
148
- }
149
-
150
- /**
151
- * Set the destination address of the transaction
152
- * @param {Address} address - The address to set
153
- */
154
- public setDestinationAddress(address: Address): void {
155
- this.to = address; // this.getScriptAddress()
156
- }
157
-
158
- /**
159
- * Set the maximum fee rate of the transaction in satoshis per byte
160
- * @param {number} feeRate - The fee rate to set
161
- * @public
162
- */
163
- public setMaximumFeeRate(feeRate: number): void {
164
- this._maximumFeeRate = feeRate;
165
- }
166
-
167
- /**
168
- * @description Signs the transaction
169
- * @public
170
- * @returns {Transaction} - The signed transaction in hex format
171
- * @throws {Error} - If something went wrong
172
- */
173
- public signTransaction(): Transaction {
174
- if (!this.to) throw new Error('Transaction must have a recipient');
175
-
176
- if (!EcKeyPair.verifyContractAddress(this.to, this.network)) {
177
- throw new Error(
178
- 'Invalid contract address. The contract address must be a taproot address.',
179
- );
180
- }
181
-
182
- if (this.signed) throw new Error('Transaction is already signed');
183
- this.signed = true;
184
-
185
- this.buildTransaction();
186
-
187
- const builtTx = this.internalBuildTransaction(this.transaction);
188
- if (builtTx) {
189
- return this.transaction.extractTransaction(false);
190
- }
191
-
192
- throw new Error('Could not sign transaction');
193
- }
194
-
195
- /**
196
- * @description Returns the transaction
197
- * @returns {Transaction}
198
- */
199
- public getTransaction(): Transaction {
200
- return this.transaction.extractTransaction(false);
201
- }
202
-
203
- /**
204
- * @description Returns the script address
205
- * @returns {string}
206
- */
207
- public getScriptAddress(): string {
208
- if (!this.scriptData || !this.scriptData.address) {
209
- throw new Error('Tap data is required');
210
- }
211
-
212
- return this.scriptData.address;
213
- }
214
-
215
- /**
216
- * @description Disables replace by fee on the transaction
217
- */
218
- public disableRBF(): void {
219
- if (this.signed) throw new Error('Transaction is already signed');
220
-
221
- for (let input of this.inputs) {
222
- input.sequence = 0xffffffff;
223
- }
224
- }
225
-
226
- /**
227
- * @description Returns the tap address
228
- * @returns {string}
229
- * @throws {Error} - If tap data is not set
230
- */
231
- public getTapAddress(): string {
232
- if (!this.tapData || !this.tapData.address) {
233
- throw new Error('Tap data is required');
234
- }
235
-
236
- return this.tapData.address;
237
- }
238
-
239
- /**
240
- * Add an input to the transaction.
241
- * @param {PsbtInputExtended} input - The input to add
242
- * @public
243
- * @returns {void}
244
- */
245
- public addInput(input: PsbtInputExtended): void {
246
- this.inputs.push(input);
247
- }
248
-
249
- /**
250
- * Add an output to the transaction.
251
- * @param {PsbtOutputExtended} output - The output to add
252
- * @public
253
- * @returns {void}
254
- */
255
- public addOutput(output: PsbtOutputExtended): void {
256
- if (output.value < TransactionBuilder.MINIMUM_DUST) {
257
- throw new Error(
258
- `Output value is less than the minimum dust ${output.value} < ${TransactionBuilder.MINIMUM_DUST}`,
259
- );
260
- }
261
-
262
- this.outputs.push(output);
263
- }
264
-
265
- /**
266
- * @description Adds the refund output to the transaction
267
- * @param {bigint} amountSpent - The amount spent
268
- * @protected
269
- * @returns {void}
270
- */
271
- protected addRefundOutput(amountSpent: bigint): void {
272
- /** Add the refund output */
273
- const sendBackAmount: bigint = this.totalInputAmount - amountSpent;
274
- if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
275
- this.setFeeOutput({
276
- value: Number(sendBackAmount),
277
- address: this.from,
278
- });
279
-
280
- return;
281
- }
282
-
283
- this.warn(
284
- `Amount to send back is less than the minimum dust, will be consumed in fees instead.`,
285
- );
286
- }
287
-
288
- /**
289
- * @description Returns the transaction opnet fee
290
- * @protected
291
- * @returns {bigint}
292
- */
293
- protected getTransactionOPNetFee(): bigint {
294
- if (this.priorityFee > TransactionBuilder.MINIMUM_DUST) {
295
- return this.priorityFee;
296
- }
297
-
298
- return TransactionBuilder.MINIMUM_DUST;
299
- }
300
-
301
- /**
302
- * @description Returns the total amount of satoshis in the inputs
303
- * @protected
304
- * @returns {bigint}
305
- */
306
- protected calculateTotalUTXOAmount(): bigint {
307
- let total: bigint = 0n;
308
- for (let utxo of this.utxos) {
309
- total += utxo.value;
310
- }
311
-
312
- return total;
313
- }
314
-
315
- /**
316
- * @description Returns the total amount of satoshis in the outputs
317
- * @protected
318
- * @returns {bigint}
319
- */
320
- protected calculateTotalVOutAmount(): bigint {
321
- let total: bigint = 0n;
322
- for (let utxo of this.utxos) {
323
- total += utxo.value;
324
- }
325
-
326
- return total;
327
- }
328
-
329
- /**
330
- * @description Adds the inputs from the utxos
331
- * @protected
332
- * @returns {void}
333
- */
334
- protected addInputsFromUTXO(): void {
335
- for (let utxo of this.utxos) {
336
- const input: PsbtInputExtended = {
337
- hash: utxo.transactionId,
338
- index: utxo.outputIndex,
339
- witnessUtxo: {
340
- value: Number(utxo.value),
341
- script: Buffer.from(utxo.scriptPubKey.hex, 'hex'),
342
- },
343
- sequence: 0xfffffffd,
344
- };
345
-
346
- this.addInput(input);
347
- }
348
- }
349
-
350
- /**
351
- * @description Converts the witness stack to a script witness
352
- * @param {Buffer[]} witness - The witness stack
353
- * @protected
354
- * @returns {Buffer}
355
- */
356
- protected witnessStackToScriptWitness(witness: Buffer[]): Buffer {
357
- let buffer = Buffer.allocUnsafe(0);
358
-
359
- function writeSlice(slice: Buffer) {
360
- buffer = Buffer.concat([buffer, Buffer.from(slice)]);
361
- }
362
-
363
- function writeVarInt(i: number) {
364
- const currentLen = buffer.length;
365
- const varintLen = varuint.encodingLength(i);
366
-
367
- buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
368
- varuint.encode(i, buffer, currentLen);
369
- }
370
-
371
- function writeVarSlice(slice: Buffer) {
372
- writeVarInt(slice.length);
373
- writeSlice(slice);
374
- }
375
-
376
- function writeVector(vector: Buffer[]) {
377
- writeVarInt(vector.length);
378
- vector.forEach(writeVarSlice);
379
- }
380
-
381
- writeVector(witness);
382
-
383
- return buffer;
384
- }
385
-
386
- /**
387
- * Internal init.
388
- * @protected
389
- */
390
- protected internalInit(): void {
391
- this.verifyUTXOValidity();
392
-
393
- this.scriptData = payments.p2tr(this.generateScriptAddress());
394
- this.tapData = payments.p2tr(this.generateTapData());
395
- }
396
-
397
- /**
398
- * Builds the transaction.
399
- * @protected
400
- * @returns {void}
401
- */
402
- protected abstract buildTransaction(): void;
403
-
404
- /**
405
- * Generates the script address.
406
- * @protected
407
- * @returns {Payment}
408
- */
409
- protected generateScriptAddress(): Payment {
410
- return {
411
- internalPubkey: this.internalPubKeyToXOnly(),
412
- network: this.network,
413
- };
414
- }
415
-
416
- protected generateTapData(): Payment {
417
- return {
418
- internalPubkey: this.internalPubKeyToXOnly(),
419
- network: this.network,
420
- };
421
- }
422
-
423
- /**
424
- * Add an input update
425
- * @param {UpdateInput} input - The input to update
426
- * @protected
427
- * @returns {void}
428
- */
429
- protected updateInput(input: UpdateInput): void {
430
- this.updateInputs.push(input);
431
- }
432
-
433
- /**
434
- * Returns the witness of the tap transaction.
435
- * @protected
436
- * @returns {Buffer}
437
- */
438
- protected getWitness(): Buffer {
439
- if (!this.tapData || !this.tapData.witness) {
440
- throw new Error('Witness is required');
441
- }
442
-
443
- if (this.tapData.witness.length === 0) {
444
- throw new Error('Witness is empty');
445
- }
446
-
447
- return this.tapData.witness[this.tapData.witness.length - 1];
448
- }
449
-
450
- /**
451
- * Returns the tap output.
452
- * @protected
453
- * @returns {Buffer}
454
- */
455
- protected getTapOutput(): Buffer {
456
- if (!this.tapData || !this.tapData.output) {
457
- throw new Error('Tap data is required');
458
- }
459
-
460
- return this.tapData.output;
461
- }
462
-
463
- /**
464
- * Returns the inputs of the transaction.
465
- * @protected
466
- * @returns {PsbtInputExtended[]}
467
- */
468
- protected getInputs(): PsbtInputExtended[] {
469
- return this.inputs;
470
- }
471
-
472
- /**
473
- * Returns the outputs of the transaction.
474
- * @protected
475
- * @returns {PsbtOutputExtended[]}
476
- */
477
- protected getOutputs(): PsbtOutputExtended[] {
478
- const outputs: PsbtOutputExtended[] = [...this.outputs];
479
- if (this.feeOutput) outputs.push(this.feeOutput);
480
-
481
- return outputs;
482
- }
483
-
484
- /**
485
- * Verifies that the utxos are valid.
486
- * @protected
487
- */
488
- protected verifyUTXOValidity(): void {
489
- for (let utxo of this.utxos) {
490
- if (!utxo.scriptPubKey) {
491
- throw new Error('Address is required');
492
- }
493
- }
494
- }
495
-
496
- /**
497
- * Set transaction fee output.
498
- * @param {PsbtOutputExtended} output - The output to set the fees
499
- * @protected
500
- * @returns {void}
501
- */
502
- protected setFeeOutput(output: PsbtOutputExtended): void {
503
- const initialValue = output.value;
504
-
505
- this.feeOutput = output;
506
-
507
- const fee = this.estimateTransactionFees();
508
- if (fee > BigInt(initialValue)) {
509
- throw new Error('Insufficient funds');
510
- }
511
-
512
- this.feeOutput.value = initialValue - Number(fee);
513
-
514
- if (this.feeOutput.value < TransactionBuilder.MINIMUM_DUST) {
515
- this.feeOutput = null;
516
- }
517
- }
518
-
519
- /**
520
- * Returns the signer key.
521
- * @protected
522
- * @returns {Signer}
523
- */
524
- protected abstract getSignerKey(): Signer;
525
-
526
- /**
527
- * Converts the public key to x-only.
528
- * @protected
529
- * @returns {Buffer}
530
- */
531
- protected internalPubKeyToXOnly(): Buffer {
532
- return toXOnly(this.signer.publicKey);
533
- }
534
-
535
- /**
536
- * Signs all the inputs of the transaction.
537
- * @param {Psbt} transaction - The transaction to sign
538
- * @protected
539
- * @returns {void}
540
- */
541
- protected signInputs(transaction: Psbt): void {
542
- transaction.signAllInputs(this.getSignerKey());
543
- transaction.finalizeAllInputs();
544
- }
545
-
546
- /**
547
- * Builds the transaction.
548
- * @param {Psbt} transaction - The transaction to build
549
- * @protected
550
- * @returns {boolean}
551
- * @throws {Error} - If something went wrong while building the transaction
552
- */
553
- private internalBuildTransaction(transaction: Psbt): boolean {
554
- const inputs: PsbtInputExtended[] = this.getInputs();
555
- const outputs: PsbtOutputExtended[] = this.getOutputs();
556
-
557
- transaction.setMaximumFeeRate(this._maximumFeeRate);
558
- transaction.addInputs(inputs);
559
-
560
- for (let i = 0; i < this.updateInputs.length; i++) {
561
- transaction.updateInput(i, this.updateInputs[i]);
562
- }
563
-
564
- transaction.addOutputs(outputs);
565
-
566
- try {
567
- this.signInputs(transaction);
568
- this.transactionFee = BigInt(transaction.getFee());
569
-
570
- return true;
571
- } catch (e) {
572
- const err: Error = e as Error;
573
-
574
- this.error(
575
- `[internalBuildTransaction] Something went wrong while getting building the transaction: ${err.stack}`,
576
- );
577
- }
578
-
579
- return false;
580
- }
581
-
582
- /**
583
- * Estimates the transaction fees.
584
- * @private
585
- */
586
- private estimateTransactionFees(): bigint {
587
- const fakeTx = new Psbt({
588
- network: this.network,
589
- });
590
-
591
- const builtTx = this.internalBuildTransaction(fakeTx);
592
- if (builtTx) {
593
- const tx = fakeTx.extractTransaction(false);
594
- const size = tx.virtualSize();
595
- const fee: number = this.feeRate * size + 1;
596
-
597
- return BigInt(Math.ceil(fee));
598
- } else {
599
- throw new Error(
600
- `Could not build transaction to estimate fee. Something went wrong while building the transaction.`,
601
- );
602
- }
603
- }
604
- }
605
-
606
- initEccLib(ecc);
1
+ import { initEccLib, Network, opcodes, Payment, payments, Psbt, script, Signer, Transaction } from 'bitcoinjs-lib';
2
+ import { varuint } from 'bitcoinjs-lib/src/bufferutils.js';
3
+ import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371.js';
4
+ import * as ecc from 'tiny-secp256k1';
5
+ import { PsbtInputExtended, PsbtOutputExtended, UpdateInput } from '../interfaces/Tap.js';
6
+ import { TransactionType } from '../enums/TransactionType.js';
7
+ import { IFundingTransactionParameters, ITransactionParameters } from '../interfaces/ITransactionParameters.js';
8
+ import { EcKeyPair } from '../../keypair/EcKeyPair.js';
9
+ import { Address } from '@btc-vision/bsi-binary';
10
+ import { UTXO } from '../../utxo/interfaces/IUTXO.js';
11
+ import { ECPairInterface } from 'ecpair';
12
+ import { Logger } from '@btc-vision/logger';
13
+
14
+ /**
15
+ * Allows to build a transaction like you would on Ethereum.
16
+ * @description The transaction builder class
17
+ * @abstract
18
+ * @class TransactionBuilder
19
+ */
20
+ export abstract class TransactionBuilder<T extends TransactionType> extends Logger {
21
+ public static readonly LOCK_LEAF_SCRIPT: Buffer = script.compile([
22
+ opcodes.OP_0,
23
+ opcodes.OP_VERIFY,
24
+ ]);
25
+
26
+ public static readonly MINIMUM_DUST: bigint = 330n;
27
+
28
+ public abstract readonly type: T;
29
+ public readonly logColor: string = '#785def';
30
+
31
+ /**
32
+ * @description Cost in satoshis of the transaction fee
33
+ */
34
+ public transactionFee: bigint = 0n;
35
+ /**
36
+ * @description The transaction itself.
37
+ */
38
+ protected readonly transaction: Psbt;
39
+ /**
40
+ * @description The inputs of the transaction
41
+ */
42
+ protected readonly inputs: PsbtInputExtended[] = [];
43
+ /**
44
+ * @description Inputs to update later on.
45
+ */
46
+ protected readonly updateInputs: UpdateInput[] = [];
47
+ /**
48
+ * @description The outputs of the transaction
49
+ */
50
+ protected readonly outputs: PsbtOutputExtended[] = [];
51
+ /**
52
+ * @description Output that will be used to pay the fees
53
+ */
54
+ protected feeOutput: PsbtOutputExtended | null = null;
55
+ /**
56
+ * @description Was the transaction signed?
57
+ */
58
+ protected signed: boolean = false;
59
+ /**
60
+ * @description The tap data of the transaction
61
+ */
62
+ protected tapData: Payment | null = null;
63
+ /**
64
+ * @description The script data of the transaction
65
+ */
66
+ protected scriptData: Payment | null = null;
67
+ /**
68
+ * @description The total amount of satoshis in the inputs
69
+ */
70
+ protected totalInputAmount: bigint;
71
+ /**
72
+ * @description The signer of the transaction
73
+ */
74
+ protected readonly signer: Signer;
75
+ /**
76
+ * @description The network where the transaction will be broadcasted
77
+ */
78
+ protected readonly network: Network;
79
+ /**
80
+ * @description The fee rate of the transaction
81
+ */
82
+ protected readonly feeRate: number;
83
+ /**
84
+ * @description The opnet priority fee of the transaction
85
+ */
86
+ protected readonly priorityFee: bigint;
87
+ /**
88
+ * @description The utxos used in the transaction
89
+ */
90
+ protected utxos: UTXO[];
91
+
92
+ /**
93
+ * @description The address where the transaction is sent to
94
+ * @protected
95
+ */
96
+ protected to: Address;
97
+
98
+ /**
99
+ * @description The address where the transaction is sent from
100
+ * @protected
101
+ */
102
+ protected from: Address;
103
+
104
+ /**
105
+ * @description The maximum fee rate of the transaction
106
+ */
107
+ private _maximumFeeRate: number = 100000000;
108
+
109
+ /**
110
+ * @param {ITransactionParameters} parameters - The transaction parameters
111
+ */
112
+ protected constructor(parameters: ITransactionParameters) {
113
+ super();
114
+
115
+ this.signer = parameters.signer;
116
+ this.network = parameters.network;
117
+ this.feeRate = parameters.feeRate;
118
+ this.priorityFee = parameters.priorityFee;
119
+ this.utxos = parameters.utxos;
120
+ this.to = parameters.to;
121
+ this.from =
122
+ parameters.from ||
123
+ EcKeyPair.getTaprootAddress(this.signer as ECPairInterface, this.network);
124
+
125
+ this.totalInputAmount = this.calculateTotalUTXOAmount();
126
+ const totalVOut: bigint = this.calculateTotalVOutAmount();
127
+
128
+ if (totalVOut < this.totalInputAmount) {
129
+ throw new Error(`Vout value is less than the value to send`);
130
+ }
131
+
132
+ if (this.totalInputAmount < TransactionBuilder.MINIMUM_DUST) {
133
+ throw new Error(`Value is less than the minimum dust`);
134
+ }
135
+
136
+ this.transaction = new Psbt({
137
+ network: this.network,
138
+ });
139
+ }
140
+
141
+ public getFundingTransactionParameters(): IFundingTransactionParameters {
142
+ return {
143
+ utxos: this.utxos,
144
+ to: this.getScriptAddress(),
145
+ signer: this.signer,
146
+ network: this.network,
147
+ feeRate: this.feeRate,
148
+ priorityFee: this.priorityFee,
149
+ from: this.from,
150
+ childTransactionRequiredFees: this.transactionFee,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Set the destination address of the transaction
156
+ * @param {Address} address - The address to set
157
+ */
158
+ public setDestinationAddress(address: Address): void {
159
+ this.to = address; // this.getScriptAddress()
160
+ }
161
+
162
+ /**
163
+ * Set the maximum fee rate of the transaction in satoshis per byte
164
+ * @param {number} feeRate - The fee rate to set
165
+ * @public
166
+ */
167
+ public setMaximumFeeRate(feeRate: number): void {
168
+ this._maximumFeeRate = feeRate;
169
+ }
170
+
171
+ /**
172
+ * @description Signs the transaction
173
+ * @public
174
+ * @returns {Transaction} - The signed transaction in hex format
175
+ * @throws {Error} - If something went wrong
176
+ */
177
+ public signTransaction(): Transaction {
178
+ if (!this.to) throw new Error('Transaction must have a recipient');
179
+
180
+ if (!EcKeyPair.verifyContractAddress(this.to, this.network)) {
181
+ throw new Error(
182
+ 'Invalid contract address. The contract address must be a taproot address.',
183
+ );
184
+ }
185
+
186
+ if (this.signed) throw new Error('Transaction is already signed');
187
+ this.signed = true;
188
+
189
+ this.buildTransaction();
190
+
191
+ const builtTx = this.internalBuildTransaction(this.transaction);
192
+ if (builtTx) {
193
+ return this.transaction.extractTransaction(false);
194
+ }
195
+
196
+ throw new Error('Could not sign transaction');
197
+ }
198
+
199
+ /**
200
+ * @description Returns the transaction
201
+ * @returns {Transaction}
202
+ */
203
+ public getTransaction(): Transaction {
204
+ return this.transaction.extractTransaction(false);
205
+ }
206
+
207
+ /**
208
+ * @description Returns the script address
209
+ * @returns {string}
210
+ */
211
+ public getScriptAddress(): string {
212
+ if (!this.scriptData || !this.scriptData.address) {
213
+ throw new Error('Tap data is required');
214
+ }
215
+
216
+ return this.scriptData.address;
217
+ }
218
+
219
+ /**
220
+ * @description Disables replace by fee on the transaction
221
+ */
222
+ public disableRBF(): void {
223
+ if (this.signed) throw new Error('Transaction is already signed');
224
+
225
+ for (let input of this.inputs) {
226
+ input.sequence = 0xffffffff;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * @description Returns the tap address
232
+ * @returns {string}
233
+ * @throws {Error} - If tap data is not set
234
+ */
235
+ public getTapAddress(): string {
236
+ if (!this.tapData || !this.tapData.address) {
237
+ throw new Error('Tap data is required');
238
+ }
239
+
240
+ return this.tapData.address;
241
+ }
242
+
243
+ /**
244
+ * Add an input to the transaction.
245
+ * @param {PsbtInputExtended} input - The input to add
246
+ * @public
247
+ * @returns {void}
248
+ */
249
+ public addInput(input: PsbtInputExtended): void {
250
+ this.inputs.push(input);
251
+ }
252
+
253
+ /**
254
+ * Add an output to the transaction.
255
+ * @param {PsbtOutputExtended} output - The output to add
256
+ * @public
257
+ * @returns {void}
258
+ */
259
+ public addOutput(output: PsbtOutputExtended): void {
260
+ if (output.value < TransactionBuilder.MINIMUM_DUST) {
261
+ throw new Error(
262
+ `Output value is less than the minimum dust ${output.value} < ${TransactionBuilder.MINIMUM_DUST}`,
263
+ );
264
+ }
265
+
266
+ this.outputs.push(output);
267
+ }
268
+
269
+ /**
270
+ * @description Adds the refund output to the transaction
271
+ * @param {bigint} amountSpent - The amount spent
272
+ * @protected
273
+ * @returns {void}
274
+ */
275
+ protected addRefundOutput(amountSpent: bigint): void {
276
+ /** Add the refund output */
277
+ const sendBackAmount: bigint = this.totalInputAmount - amountSpent;
278
+ if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
279
+ this.setFeeOutput({
280
+ value: Number(sendBackAmount),
281
+ address: this.from,
282
+ });
283
+
284
+ return;
285
+ }
286
+
287
+ this.warn(
288
+ `Amount to send back is less than the minimum dust, will be consumed in fees instead.`,
289
+ );
290
+ }
291
+
292
+ /**
293
+ * @description Returns the transaction opnet fee
294
+ * @protected
295
+ * @returns {bigint}
296
+ */
297
+ protected getTransactionOPNetFee(): bigint {
298
+ if (this.priorityFee > TransactionBuilder.MINIMUM_DUST) {
299
+ return this.priorityFee;
300
+ }
301
+
302
+ return TransactionBuilder.MINIMUM_DUST;
303
+ }
304
+
305
+ /**
306
+ * @description Returns the total amount of satoshis in the inputs
307
+ * @protected
308
+ * @returns {bigint}
309
+ */
310
+ protected calculateTotalUTXOAmount(): bigint {
311
+ let total: bigint = 0n;
312
+ for (let utxo of this.utxos) {
313
+ total += utxo.value;
314
+ }
315
+
316
+ return total;
317
+ }
318
+
319
+ /**
320
+ * @description Returns the total amount of satoshis in the outputs
321
+ * @protected
322
+ * @returns {bigint}
323
+ */
324
+ protected calculateTotalVOutAmount(): bigint {
325
+ let total: bigint = 0n;
326
+ for (let utxo of this.utxos) {
327
+ total += utxo.value;
328
+ }
329
+
330
+ return total;
331
+ }
332
+
333
+ /**
334
+ * @description Adds the inputs from the utxos
335
+ * @protected
336
+ * @returns {void}
337
+ */
338
+ protected addInputsFromUTXO(): void {
339
+ for (let utxo of this.utxos) {
340
+ const input: PsbtInputExtended = {
341
+ hash: utxo.transactionId,
342
+ index: utxo.outputIndex,
343
+ witnessUtxo: {
344
+ value: Number(utxo.value),
345
+ script: Buffer.from(utxo.scriptPubKey.hex, 'hex'),
346
+ },
347
+ sequence: 0xfffffffd,
348
+ };
349
+
350
+ this.addInput(input);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * @description Converts the witness stack to a script witness
356
+ * @param {Buffer[]} witness - The witness stack
357
+ * @protected
358
+ * @returns {Buffer}
359
+ */
360
+ protected witnessStackToScriptWitness(witness: Buffer[]): Buffer {
361
+ let buffer = Buffer.allocUnsafe(0);
362
+
363
+ function writeSlice(slice: Buffer) {
364
+ buffer = Buffer.concat([buffer, Buffer.from(slice)]);
365
+ }
366
+
367
+ function writeVarInt(i: number) {
368
+ const currentLen = buffer.length;
369
+ const varintLen = varuint.encodingLength(i);
370
+
371
+ buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
372
+ varuint.encode(i, buffer, currentLen);
373
+ }
374
+
375
+ function writeVarSlice(slice: Buffer) {
376
+ writeVarInt(slice.length);
377
+ writeSlice(slice);
378
+ }
379
+
380
+ function writeVector(vector: Buffer[]) {
381
+ writeVarInt(vector.length);
382
+ vector.forEach(writeVarSlice);
383
+ }
384
+
385
+ writeVector(witness);
386
+
387
+ return buffer;
388
+ }
389
+
390
+ /**
391
+ * Internal init.
392
+ * @protected
393
+ */
394
+ protected internalInit(): void {
395
+ this.verifyUTXOValidity();
396
+
397
+ this.scriptData = payments.p2tr(this.generateScriptAddress());
398
+ this.tapData = payments.p2tr(this.generateTapData());
399
+ }
400
+
401
+ /**
402
+ * Builds the transaction.
403
+ * @protected
404
+ * @returns {void}
405
+ */
406
+ protected abstract buildTransaction(): void;
407
+
408
+ /**
409
+ * Generates the script address.
410
+ * @protected
411
+ * @returns {Payment}
412
+ */
413
+ protected generateScriptAddress(): Payment {
414
+ return {
415
+ internalPubkey: this.internalPubKeyToXOnly(),
416
+ network: this.network,
417
+ };
418
+ }
419
+
420
+ protected generateTapData(): Payment {
421
+ return {
422
+ internalPubkey: this.internalPubKeyToXOnly(),
423
+ network: this.network,
424
+ };
425
+ }
426
+
427
+ /**
428
+ * Add an input update
429
+ * @param {UpdateInput} input - The input to update
430
+ * @protected
431
+ * @returns {void}
432
+ */
433
+ protected updateInput(input: UpdateInput): void {
434
+ this.updateInputs.push(input);
435
+ }
436
+
437
+ /**
438
+ * Returns the witness of the tap transaction.
439
+ * @protected
440
+ * @returns {Buffer}
441
+ */
442
+ protected getWitness(): Buffer {
443
+ if (!this.tapData || !this.tapData.witness) {
444
+ throw new Error('Witness is required');
445
+ }
446
+
447
+ if (this.tapData.witness.length === 0) {
448
+ throw new Error('Witness is empty');
449
+ }
450
+
451
+ return this.tapData.witness[this.tapData.witness.length - 1];
452
+ }
453
+
454
+ /**
455
+ * Returns the tap output.
456
+ * @protected
457
+ * @returns {Buffer}
458
+ */
459
+ protected getTapOutput(): Buffer {
460
+ if (!this.tapData || !this.tapData.output) {
461
+ throw new Error('Tap data is required');
462
+ }
463
+
464
+ return this.tapData.output;
465
+ }
466
+
467
+ /**
468
+ * Returns the inputs of the transaction.
469
+ * @protected
470
+ * @returns {PsbtInputExtended[]}
471
+ */
472
+ protected getInputs(): PsbtInputExtended[] {
473
+ return this.inputs;
474
+ }
475
+
476
+ /**
477
+ * Returns the outputs of the transaction.
478
+ * @protected
479
+ * @returns {PsbtOutputExtended[]}
480
+ */
481
+ protected getOutputs(): PsbtOutputExtended[] {
482
+ const outputs: PsbtOutputExtended[] = [...this.outputs];
483
+ if (this.feeOutput) outputs.push(this.feeOutput);
484
+
485
+ return outputs;
486
+ }
487
+
488
+ /**
489
+ * Verifies that the utxos are valid.
490
+ * @protected
491
+ */
492
+ protected verifyUTXOValidity(): void {
493
+ for (let utxo of this.utxos) {
494
+ if (!utxo.scriptPubKey) {
495
+ throw new Error('Address is required');
496
+ }
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Set transaction fee output.
502
+ * @param {PsbtOutputExtended} output - The output to set the fees
503
+ * @protected
504
+ * @returns {void}
505
+ */
506
+ protected setFeeOutput(output: PsbtOutputExtended): void {
507
+ const initialValue = output.value;
508
+
509
+ this.feeOutput = output;
510
+
511
+ const fee = this.estimateTransactionFees();
512
+ if (fee > BigInt(initialValue)) {
513
+ throw new Error('Insufficient funds');
514
+ }
515
+
516
+ this.feeOutput.value = initialValue - Number(fee);
517
+
518
+ if (this.feeOutput.value < TransactionBuilder.MINIMUM_DUST) {
519
+ this.feeOutput = null;
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Returns the signer key.
525
+ * @protected
526
+ * @returns {Signer}
527
+ */
528
+ protected abstract getSignerKey(): Signer;
529
+
530
+ /**
531
+ * Converts the public key to x-only.
532
+ * @protected
533
+ * @returns {Buffer}
534
+ */
535
+ protected internalPubKeyToXOnly(): Buffer {
536
+ return toXOnly(this.signer.publicKey);
537
+ }
538
+
539
+ /**
540
+ * Signs all the inputs of the transaction.
541
+ * @param {Psbt} transaction - The transaction to sign
542
+ * @protected
543
+ * @returns {void}
544
+ */
545
+ protected signInputs(transaction: Psbt): void {
546
+ transaction.signAllInputs(this.getSignerKey());
547
+ transaction.finalizeAllInputs();
548
+ }
549
+
550
+ /**
551
+ * Builds the transaction.
552
+ * @param {Psbt} transaction - The transaction to build
553
+ * @protected
554
+ * @returns {boolean}
555
+ * @throws {Error} - If something went wrong while building the transaction
556
+ */
557
+ private internalBuildTransaction(transaction: Psbt): boolean {
558
+ const inputs: PsbtInputExtended[] = this.getInputs();
559
+ const outputs: PsbtOutputExtended[] = this.getOutputs();
560
+
561
+ transaction.setMaximumFeeRate(this._maximumFeeRate);
562
+ transaction.addInputs(inputs);
563
+
564
+ for (let i = 0; i < this.updateInputs.length; i++) {
565
+ transaction.updateInput(i, this.updateInputs[i]);
566
+ }
567
+
568
+ transaction.addOutputs(outputs);
569
+
570
+ try {
571
+ this.signInputs(transaction);
572
+ this.transactionFee = BigInt(transaction.getFee());
573
+
574
+ return true;
575
+ } catch (e) {
576
+ const err: Error = e as Error;
577
+
578
+ this.error(
579
+ `[internalBuildTransaction] Something went wrong while getting building the transaction: ${err.stack}`,
580
+ );
581
+ }
582
+
583
+ return false;
584
+ }
585
+
586
+ /**
587
+ * Estimates the transaction fees.
588
+ * @private
589
+ */
590
+ private estimateTransactionFees(): bigint {
591
+ const fakeTx = new Psbt({
592
+ network: this.network,
593
+ });
594
+
595
+ const builtTx = this.internalBuildTransaction(fakeTx);
596
+ if (builtTx) {
597
+ const tx = fakeTx.extractTransaction(false);
598
+ const size = tx.virtualSize();
599
+ const fee: number = this.feeRate * size + 1;
600
+
601
+ return BigInt(Math.ceil(fee));
602
+ } else {
603
+ throw new Error(
604
+ `Could not build transaction to estimate fee. Something went wrong while building the transaction.`,
605
+ );
606
+ }
607
+ }
608
+ }
609
+
610
+ initEccLib(ecc);