@btc-vision/transaction 1.1.7 → 1.1.9

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,503 +0,0 @@
1
- import { Taptree } from '@btc-vision/bitcoin/src/types.js';
2
- import { TransactionType } from '../enums/TransactionType.js';
3
- import { IUnwrapParameters } from '../interfaces/ITransactionParameters.js';
4
- import { SharedInteractionTransaction } from './SharedInteractionTransaction.js';
5
- import { TransactionBuilder } from './TransactionBuilder.js';
6
- import { wBTC } from '../../metadata/contracts/wBTC.js';
7
- import {
8
- Network,
9
- Payment,
10
- payments,
11
- Psbt,
12
- PsbtInput,
13
- PsbtInputExtended,
14
- PsbtOutputExtended,
15
- } from '@btc-vision/bitcoin';
16
- import { EcKeyPair } from '../../keypair/EcKeyPair.js';
17
- import { IWBTCUTXODocument, PsbtTransaction, VaultUTXOs } from '../processor/PsbtTransaction.js';
18
- import { MultiSignGenerator } from '../../generators/builders/MultiSignGenerator.js';
19
- import { MultiSignTransaction } from './MultiSignTransaction.js';
20
- import { toXOnly } from '@btc-vision/bitcoin/src/psbt/bip371.js';
21
- import { CalldataGenerator } from '../../generators/builders/CalldataGenerator.js';
22
- import { currentConsensusConfig } from '../../consensus/ConsensusConfig.js';
23
- import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
24
- import { Features } from '../../generators/Features.js';
25
- import { ABICoder } from '../../abi/ABICoder.js';
26
- import { Selector } from '../../utils/types.js';
27
- import { BinaryWriter } from '../../buffer/BinaryWriter.js';
28
-
29
- const abiCoder: ABICoder = new ABICoder();
30
- const numsPoint: Buffer = Buffer.from(
31
- '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0',
32
- 'hex',
33
- );
34
-
35
- /**
36
- * Unwrap transaction
37
- * @class UnwrapTransaction
38
- */
39
- export class UnwrapTransaction extends SharedInteractionTransaction<TransactionType.WBTC_UNWRAP> {
40
- private static readonly UNWRAP_SELECTOR: Selector = Number(
41
- '0x' + abiCoder.encodeSelector('burn'),
42
- );
43
-
44
- public type: TransactionType.WBTC_UNWRAP = TransactionType.WBTC_UNWRAP;
45
-
46
- /**
47
- * The amount to wrap
48
- * @private
49
- */
50
- public readonly amount: bigint;
51
-
52
- /**
53
- * The compiled target script
54
- * @protected
55
- */
56
- protected readonly compiledTargetScript: Buffer;
57
- /**
58
- * The script tree
59
- * @protected
60
- */
61
- protected readonly scriptTree: Taptree;
62
- /**
63
- * The sighash types for the transaction
64
- * @protected
65
- */
66
- protected sighashTypes: number[] = [];
67
- /**
68
- * Contract secret for the interaction
69
- * @protected
70
- */
71
- protected readonly contractSecret: Buffer;
72
- /**
73
- * The vault UTXOs
74
- * @protected
75
- */
76
- protected readonly vaultUTXOs: VaultUTXOs[];
77
-
78
- /**
79
- * Estimated unwrap loss due to bitcoin fees in satoshis.
80
- * @protected
81
- */
82
- protected readonly estimatedFeeLoss: bigint = 0n;
83
-
84
- /**
85
- * The wBTC contract
86
- * @private
87
- */
88
- private readonly wbtc: wBTC;
89
- private readonly calculatedSignHash: number = PsbtTransaction.calculateSignHash(
90
- this.sighashTypes,
91
- );
92
-
93
- public constructor(parameters: IUnwrapParameters) {
94
- if (parameters.amount < TransactionBuilder.MINIMUM_DUST) {
95
- throw new Error('Amount is below dust limit');
96
- }
97
-
98
- parameters.disableAutoRefund = true; // we have to disable auto refund for this transaction, so it does not create an unwanted output.
99
- parameters.calldata = UnwrapTransaction.generateBurnCalldata(parameters.amount);
100
-
101
- super(parameters);
102
-
103
- this.wbtc = new wBTC(parameters.network, parameters.chainId);
104
- this.to = this.wbtc.getAddress();
105
-
106
- this.vaultUTXOs = parameters.unwrapUTXOs;
107
- this.estimatedFeeLoss = UnwrapTransaction.preEstimateTaprootTransactionFees(
108
- BigInt(this.feeRate),
109
- this.calculateNumInputs(this.vaultUTXOs),
110
- 2n,
111
- this.calculateNumSignatures(this.vaultUTXOs),
112
- 65n,
113
- this.calculateNumEmptyWitnesses(this.vaultUTXOs),
114
- );
115
-
116
- this.amount = parameters.amount;
117
- this.contractSecret = this.generateSecret();
118
-
119
- this.calldataGenerator = new CalldataGenerator(
120
- Buffer.from(this.signer.publicKey),
121
- this.scriptSignerXOnlyPubKey(),
122
- this.network,
123
- );
124
-
125
- this.compiledTargetScript = this.calldataGenerator.compile(
126
- this.calldata,
127
- this.contractSecret,
128
- [Features.UNWRAP],
129
- );
130
-
131
- this.scriptTree = this.getScriptTree();
132
- this.internalInit();
133
- }
134
-
135
- /**
136
- * Generate a valid wBTC calldata
137
- * @param {bigint} amount - The amount to wrap
138
- * @private
139
- * @returns {Buffer} - The calldata
140
- */
141
- public static generateBurnCalldata(amount: bigint): Buffer {
142
- if (!amount) throw new Error('Amount is required');
143
-
144
- const bufWriter: BinaryWriter = new BinaryWriter();
145
- bufWriter.writeSelector(UnwrapTransaction.UNWRAP_SELECTOR);
146
- bufWriter.writeU256(amount);
147
-
148
- return Buffer.from(bufWriter.getBuffer());
149
- }
150
-
151
- /**
152
- * @description Signs the transaction
153
- * @public
154
- * @returns {Promise<Psbt>} - The signed transaction in hex format
155
- * @throws {Error} - If something went wrong
156
- */
157
- public async signPSBT(): Promise<Psbt> {
158
- if (this.to && !EcKeyPair.verifyContractAddress(this.to, this.network)) {
159
- throw new Error(
160
- 'Invalid contract address. The contract address must be a taproot address.',
161
- );
162
- }
163
-
164
- if (!this.vaultUTXOs.length) {
165
- throw new Error('No vault UTXOs provided');
166
- }
167
-
168
- await this.buildTransaction();
169
- this.ignoreSignatureError();
170
- this.mergeVaults();
171
-
172
- const builtTx = await this.internalBuildTransaction(this.transaction);
173
- if (builtTx) {
174
- return this.transaction;
175
- }
176
-
177
- throw new Error('Could not sign transaction');
178
- }
179
-
180
- public getRefund(): bigint {
181
- let losses: bigint = -currentConsensusConfig.UNWRAP_CONSOLIDATION_PREPAID_FEES_SAT;
182
-
183
- for (const vault of this.vaultUTXOs) {
184
- for (let i = 0; i < vault.utxos.length; i++) {
185
- losses += currentConsensusConfig.UNWRAP_CONSOLIDATION_PREPAID_FEES_SAT;
186
- }
187
- }
188
-
189
- // Since we are creating one output when consolidating, we need to add the fee for that output.
190
- return losses;
191
- }
192
-
193
- /**
194
- * @description Get the estimated unwrap loss due to bitcoin fees in satoshis.
195
- * @description If the number is negative, it means the user will get a refund.
196
- * @description If the number is positive, it means the user will lose that amount.
197
- * @public
198
- * @returns {bigint} - The estimated fee loss or refund
199
- */
200
- public getFeeLossOrRefund(): bigint {
201
- const refund: bigint = this.getRefund();
202
-
203
- return refund - this.estimatedFeeLoss;
204
- }
205
-
206
- /**
207
- * @description Merge vault UTXOs into the transaction
208
- * @protected
209
- */
210
- protected mergeVaults(): void {
211
- const totalInputAmount: bigint = this.getVaultTotalOutputAmount(this.vaultUTXOs);
212
-
213
- let refund: bigint = this.getRefund();
214
- const outputLeftAmount = totalInputAmount - refund - this.amount;
215
-
216
- if (outputLeftAmount === currentConsensusConfig.UNWRAP_CONSOLIDATION_PREPAID_FEES_SAT) {
217
- refund += currentConsensusConfig.UNWRAP_CONSOLIDATION_PREPAID_FEES_SAT;
218
- } else if (outputLeftAmount < currentConsensusConfig.VAULT_MINIMUM_AMOUNT) {
219
- throw new Error(
220
- `Output left amount is below the minimum amount: ${outputLeftAmount} below ${currentConsensusConfig.VAULT_MINIMUM_AMOUNT}`,
221
- );
222
- }
223
-
224
- const outAmount: bigint = this.amount + refund - this.estimatedFeeLoss;
225
- const bestVault = BitcoinUtils.findVaultWithMostPublicKeys(this.vaultUTXOs);
226
- if (!bestVault) {
227
- throw new Error('No vaults provided');
228
- }
229
-
230
- const hasConsolidation: boolean =
231
- outputLeftAmount > currentConsensusConfig.VAULT_MINIMUM_AMOUNT &&
232
- outputLeftAmount - currentConsensusConfig.UNWRAP_CONSOLIDATION_PREPAID_FEES_SAT !== 0n;
233
-
234
- if (hasConsolidation) {
235
- this.success(`Consolidating output with ${outputLeftAmount} sat.`);
236
- } else {
237
- this.warn(`No consolidation in this transaction.`);
238
- }
239
-
240
- if (
241
- outputLeftAmount - currentConsensusConfig.UNWRAP_CONSOLIDATION_PREPAID_FEES_SAT !==
242
- 0n
243
- ) {
244
- // If the amount left is 0, we don't consolidate the output.
245
- this.addOutput({
246
- address: bestVault.vault,
247
- value: Number(outputLeftAmount),
248
- });
249
- }
250
-
251
- if (outAmount < TransactionBuilder.MINIMUM_DUST) {
252
- throw new Error(
253
- `Amount is below dust limit. The requested amount can not be unwrapped since, after fees, it is below the dust limit. Dust: ${outAmount} sat. Are your bitcoin fees too high?`,
254
- );
255
- }
256
-
257
- const percentageLossOverInitialAmount = (outAmount * 100n) / this.amount;
258
- if (percentageLossOverInitialAmount <= 60n) {
259
- // For user safety, we don't allow more than 60% loss over the initial amount.
260
- throw new Error(
261
- `For user safety, OPNet will decline this transaction since you will lose ${100n - percentageLossOverInitialAmount}% of your btc by doing this transaction due to bitcoin fees. Are your bitcoin fees too high?`,
262
- );
263
- }
264
-
265
- this.addOutput({
266
- address: this.from,
267
- value: Number(outAmount),
268
- });
269
-
270
- for (const vault of this.vaultUTXOs) {
271
- this.addVaultInputs(vault);
272
- }
273
- }
274
-
275
- protected calculateNumEmptyWitnesses(vault: VaultUTXOs[]): bigint {
276
- let numSignatures = 0n;
277
- for (const v of vault) {
278
- numSignatures += BigInt(v.publicKeys.length - v.minimum) * BigInt(v.utxos.length);
279
- }
280
-
281
- return numSignatures;
282
- }
283
-
284
- protected calculateNumSignatures(vault: VaultUTXOs[]): bigint {
285
- let numSignatures = 0n;
286
- for (const v of vault) {
287
- numSignatures += BigInt(v.minimum * v.utxos.length);
288
- }
289
-
290
- return numSignatures;
291
- }
292
-
293
- protected calculateNumInputs(vault: VaultUTXOs[]): bigint {
294
- let numSignatures = 0n;
295
- for (const v of vault) {
296
- numSignatures += BigInt(v.utxos.length);
297
- }
298
-
299
- return numSignatures;
300
- }
301
-
302
- /**
303
- * Converts the public key to x-only.
304
- * @protected
305
- * @returns {Buffer}
306
- */
307
- protected internalPubKeyToXOnly(): Buffer {
308
- return toXOnly(numsPoint);
309
- }
310
-
311
- /**
312
- * Generate an input for a vault UTXO
313
- * @param {Buffer[]} pubkeys The public keys
314
- * @param {number} minimumSignatures The minimum number of signatures
315
- * @protected
316
- * @returns {Taptree} The tap tree
317
- * @throws {Error} If something went wrong
318
- */
319
- protected generateTapDataForInput(
320
- pubkeys: Buffer[],
321
- minimumSignatures: number,
322
- ): {
323
- internalPubkey: Buffer;
324
- network: Network;
325
- scriptTree: Taptree;
326
- redeem: Payment;
327
- } {
328
- const compiledTargetScript = MultiSignGenerator.compile(pubkeys, minimumSignatures);
329
- const scriptTree: Taptree = [
330
- {
331
- output: compiledTargetScript,
332
- version: 192,
333
- },
334
- {
335
- output: MultiSignTransaction.LOCK_LEAF_SCRIPT,
336
- version: 192,
337
- },
338
- ];
339
-
340
- const redeem: Payment = {
341
- output: compiledTargetScript,
342
- redeemVersion: 192,
343
- };
344
-
345
- return {
346
- internalPubkey: this.internalPubKeyToXOnly(),
347
- network: this.network,
348
- scriptTree: scriptTree,
349
- redeem: redeem,
350
- };
351
- }
352
-
353
- /**
354
- * Generate the script solution
355
- * @param {PsbtInput} input The input
356
- * @protected
357
- *
358
- * @returns {Buffer[]} The script solution
359
- */
360
- protected getScriptSolution(input: PsbtInput): Buffer[] {
361
- if (!input.tapScriptSig) {
362
- throw new Error('Tap script signature is required');
363
- }
364
-
365
- return [
366
- this.contractSecret,
367
- toXOnly(Buffer.from(this.signer.publicKey)),
368
- input.tapScriptSig[0].signature,
369
- input.tapScriptSig[1].signature,
370
- ];
371
- }
372
-
373
- /**
374
- * Builds the transaction.
375
- * @param {Psbt} transaction - The transaction to build
376
- * @param checkPartialSigs
377
- * @protected
378
- * @returns {Promise<boolean>}
379
- * @throws {Error} - If something went wrong while building the transaction
380
- */
381
- protected async internalBuildTransaction(
382
- transaction: Psbt,
383
- checkPartialSigs: boolean = false,
384
- ): Promise<boolean> {
385
- if (transaction.data.inputs.length === 0) {
386
- const inputs: PsbtInputExtended[] = this.getInputs();
387
- const outputs: PsbtOutputExtended[] = this.getOutputs();
388
-
389
- transaction.setMaximumFeeRate(this._maximumFeeRate);
390
- transaction.addInputs(inputs, checkPartialSigs);
391
-
392
- for (let i = 0; i < this.updateInputs.length; i++) {
393
- transaction.updateInput(i, this.updateInputs[i]);
394
- }
395
-
396
- transaction.addOutputs(outputs);
397
- }
398
-
399
- try {
400
- try {
401
- await this.signInputs(transaction);
402
- } catch (e) {
403
- console.log(e);
404
- }
405
-
406
- if (this.finalized) {
407
- this.transactionFee = BigInt(transaction.getFee());
408
- }
409
-
410
- return true;
411
- } catch (e) {
412
- const err: Error = e as Error;
413
-
414
- this.error(
415
- `[internalBuildTransaction] Something went wrong while getting building the transaction: ${err.stack}`,
416
- );
417
- }
418
-
419
- return false;
420
- }
421
-
422
- /**
423
- * @description Add a vault UTXO to the transaction
424
- * @private
425
- */
426
- private addVaultUTXO(
427
- utxo: IWBTCUTXODocument,
428
- pubkeys: Buffer[],
429
- minimumSignatures: number,
430
- ): void {
431
- const tapInput = this.generateTapDataForInput(pubkeys, minimumSignatures);
432
- const tap = payments.p2tr(tapInput);
433
-
434
- if (!tap.witness) throw new Error('Failed to generate taproot witness');
435
-
436
- const controlBlock = tap.witness[tap.witness.length - 1];
437
- const input: PsbtInputExtended = {
438
- hash: utxo.hash,
439
- index: utxo.outputIndex,
440
- witnessUtxo: {
441
- script: Buffer.from(utxo.output, 'base64'),
442
- value: Number(utxo.value),
443
- },
444
- sequence: this.sequence,
445
- tapLeafScript: [
446
- {
447
- leafVersion: tapInput.redeem.redeemVersion as number,
448
- script: tapInput.redeem.output as Buffer,
449
- controlBlock: controlBlock,
450
- },
451
- ],
452
- };
453
-
454
- if (this.calculatedSignHash) {
455
- input.sighashType = this.calculatedSignHash;
456
- }
457
-
458
- this.addInput(input);
459
- }
460
-
461
- /**
462
- * @description Add vault inputs to the transaction
463
- * @param {VaultUTXOs} vault The vault UTXOs
464
- * @private
465
- */
466
- private addVaultInputs(vault: VaultUTXOs): void {
467
- const pubKeys = vault.publicKeys.map((key) => Buffer.from(key, 'base64'));
468
-
469
- for (const utxo of vault.utxos) {
470
- this.addVaultUTXO(utxo, pubKeys, vault.minimum);
471
- }
472
- }
473
-
474
- /**
475
- * @description Calculate the amount left to refund to the first vault.
476
- * @param {VaultUTXOs[]} vaults The vaults
477
- * @private
478
- * @returns {bigint} The amount left
479
- */
480
- private calculateOutputLeftAmountFromVaults(vaults: VaultUTXOs[]): bigint {
481
- const total = this.getVaultTotalOutputAmount(vaults);
482
-
483
- return total - this.amount;
484
- }
485
-
486
- /**
487
- * Get the total output amount from the vaults
488
- * @description Get the total output amount from the vaults
489
- * @param {VaultUTXOs[]} vaults The vaults
490
- * @private
491
- * @returns {bigint} The total output amount
492
- */
493
- private getVaultTotalOutputAmount(vaults: VaultUTXOs[]): bigint {
494
- let total = BigInt(0);
495
- for (const vault of vaults) {
496
- for (const utxo of vault.utxos) {
497
- total += BigInt(utxo.value);
498
- }
499
- }
500
-
501
- return total;
502
- }
503
- }