@bitgo-beta/babylonlabs-io-btc-staking-ts 0.4.0-beta.74 → 0.4.0-beta.740

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.
package/dist/index.js CHANGED
@@ -1,231 +1,3 @@
1
- // src/staking/stakingScript.ts
2
- import { opcodes, script } from "bitcoinjs-lib";
3
-
4
- // src/constants/keys.ts
5
- var NO_COORD_PK_BYTE_LENGTH = 32;
6
-
7
- // src/staking/stakingScript.ts
8
- var MAGIC_BYTES_LEN = 4;
9
- var StakingScriptData = class {
10
- constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock) {
11
- if (!stakerKey || !finalityProviderKeys || !covenantKeys || !covenantThreshold || !stakingTimelock || !unbondingTimelock) {
12
- throw new Error("Missing required input values");
13
- }
14
- this.stakerKey = stakerKey;
15
- this.finalityProviderKeys = finalityProviderKeys;
16
- this.covenantKeys = covenantKeys;
17
- this.covenantThreshold = covenantThreshold;
18
- this.stakingTimeLock = stakingTimelock;
19
- this.unbondingTimeLock = unbondingTimelock;
20
- if (!this.validate()) {
21
- throw new Error("Invalid script data provided");
22
- }
23
- }
24
- /**
25
- * Validates the staking script.
26
- * @returns {boolean} Returns true if the staking script is valid, otherwise false.
27
- */
28
- validate() {
29
- if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {
30
- return false;
31
- }
32
- if (this.finalityProviderKeys.some(
33
- (finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH
34
- )) {
35
- return false;
36
- }
37
- if (this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)) {
38
- return false;
39
- }
40
- const allPks = [
41
- this.stakerKey,
42
- ...this.finalityProviderKeys,
43
- ...this.covenantKeys
44
- ];
45
- const allPksSet = new Set(allPks);
46
- if (allPks.length !== allPksSet.size) {
47
- return false;
48
- }
49
- if (this.covenantThreshold == 0 || this.covenantThreshold > this.covenantKeys.length) {
50
- return false;
51
- }
52
- if (this.stakingTimeLock == 0 || this.stakingTimeLock > 65535) {
53
- return false;
54
- }
55
- if (this.unbondingTimeLock == 0 || this.unbondingTimeLock > 65535) {
56
- return false;
57
- }
58
- return true;
59
- }
60
- // The staking script allows for multiple finality provider public keys
61
- // to support (re)stake to multiple finality providers
62
- // Covenant members are going to have multiple keys
63
- /**
64
- * Builds a timelock script.
65
- * @param timelock - The timelock value to encode in the script.
66
- * @returns {Buffer} containing the compiled timelock script.
67
- */
68
- buildTimelockScript(timelock) {
69
- return script.compile([
70
- this.stakerKey,
71
- opcodes.OP_CHECKSIGVERIFY,
72
- script.number.encode(timelock),
73
- opcodes.OP_CHECKSEQUENCEVERIFY
74
- ]);
75
- }
76
- /**
77
- * Builds the staking timelock script.
78
- * Only holder of private key for given pubKey can spend after relative lock time
79
- * Creates the timelock script in the form:
80
- * <stakerPubKey>
81
- * OP_CHECKSIGVERIFY
82
- * <stakingTimeBlocks>
83
- * OP_CHECKSEQUENCEVERIFY
84
- * @returns {Buffer} The staking timelock script.
85
- */
86
- buildStakingTimelockScript() {
87
- return this.buildTimelockScript(this.stakingTimeLock);
88
- }
89
- /**
90
- * Builds the unbonding timelock script.
91
- * Creates the unbonding timelock script in the form:
92
- * <stakerPubKey>
93
- * OP_CHECKSIGVERIFY
94
- * <unbondingTimeBlocks>
95
- * OP_CHECKSEQUENCEVERIFY
96
- * @returns {Buffer} The unbonding timelock script.
97
- */
98
- buildUnbondingTimelockScript() {
99
- return this.buildTimelockScript(this.unbondingTimeLock);
100
- }
101
- /**
102
- * Builds the unbonding script in the form:
103
- * buildSingleKeyScript(stakerPk, true) ||
104
- * buildMultiKeyScript(covenantPks, covenantThreshold, false)
105
- * || means combining the scripts
106
- * @returns {Buffer} The unbonding script.
107
- */
108
- buildUnbondingScript() {
109
- return Buffer.concat([
110
- this.buildSingleKeyScript(this.stakerKey, true),
111
- this.buildMultiKeyScript(
112
- this.covenantKeys,
113
- this.covenantThreshold,
114
- false
115
- )
116
- ]);
117
- }
118
- /**
119
- * Builds the slashing script for staking in the form:
120
- * buildSingleKeyScript(stakerPk, true) ||
121
- * buildMultiKeyScript(finalityProviderPKs, 1, true) ||
122
- * buildMultiKeyScript(covenantPks, covenantThreshold, false)
123
- * || means combining the scripts
124
- * The slashing script is a combination of single-key and multi-key scripts.
125
- * The single-key script is used for staker key verification.
126
- * The multi-key script is used for finality provider key verification and covenant key verification.
127
- * @returns {Buffer} The slashing script as a Buffer.
128
- */
129
- buildSlashingScript() {
130
- return Buffer.concat([
131
- this.buildSingleKeyScript(this.stakerKey, true),
132
- this.buildMultiKeyScript(
133
- this.finalityProviderKeys,
134
- // The threshold is always 1 as we only need one
135
- // finalityProvider signature to perform slashing
136
- // (only one finalityProvider performs an offence)
137
- 1,
138
- // OP_VERIFY/OP_CHECKSIGVERIFY is added at the end
139
- true
140
- ),
141
- this.buildMultiKeyScript(
142
- this.covenantKeys,
143
- this.covenantThreshold,
144
- // No need to add verify since covenants are at the end of the script
145
- false
146
- )
147
- ]);
148
- }
149
- /**
150
- * Builds the staking scripts.
151
- * @returns {StakingScripts} The staking scripts.
152
- */
153
- buildScripts() {
154
- return {
155
- timelockScript: this.buildStakingTimelockScript(),
156
- unbondingScript: this.buildUnbondingScript(),
157
- slashingScript: this.buildSlashingScript(),
158
- unbondingTimelockScript: this.buildUnbondingTimelockScript()
159
- };
160
- }
161
- // buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality
162
- // for creating Bitcoin scripts for the unbonding script and the slashing script
163
- /**
164
- * Builds a single key script in the form:
165
- * buildSingleKeyScript creates a single key script
166
- * <pk> OP_CHECKSIGVERIFY (if withVerify is true)
167
- * <pk> OP_CHECKSIG (if withVerify is false)
168
- * @param pk - The public key buffer.
169
- * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode.
170
- * @returns The compiled script buffer.
171
- */
172
- buildSingleKeyScript(pk, withVerify) {
173
- if (pk.length != NO_COORD_PK_BYTE_LENGTH) {
174
- throw new Error("Invalid key length");
175
- }
176
- return script.compile([
177
- pk,
178
- withVerify ? opcodes.OP_CHECKSIGVERIFY : opcodes.OP_CHECKSIG
179
- ]);
180
- }
181
- /**
182
- * Builds a multi-key script in the form:
183
- * <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL
184
- * <withVerify -> OP_NUMEQUALVERIFY>
185
- * It validates whether provided keys are unique and the threshold is not greater than number of keys
186
- * If there is only one key provided it will return single key sig script
187
- * @param pks - An array of public keys.
188
- * @param threshold - The required number of valid signers.
189
- * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode.
190
- * @returns The compiled multi-key script as a Buffer.
191
- * @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided.
192
- */
193
- buildMultiKeyScript(pks, threshold, withVerify) {
194
- if (!pks || pks.length === 0) {
195
- throw new Error("No keys provided");
196
- }
197
- if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) {
198
- throw new Error("Invalid key length");
199
- }
200
- if (threshold > pks.length) {
201
- throw new Error(
202
- "Required number of valid signers is greater than number of provided keys"
203
- );
204
- }
205
- if (pks.length === 1) {
206
- return this.buildSingleKeyScript(pks[0], withVerify);
207
- }
208
- const sortedPks = [...pks].sort(Buffer.compare);
209
- for (let i = 0; i < sortedPks.length - 1; ++i) {
210
- if (sortedPks[i].equals(sortedPks[i + 1])) {
211
- throw new Error("Duplicate keys provided");
212
- }
213
- }
214
- const scriptElements = [sortedPks[0], opcodes.OP_CHECKSIG];
215
- for (let i = 1; i < sortedPks.length; i++) {
216
- scriptElements.push(sortedPks[i]);
217
- scriptElements.push(opcodes.OP_CHECKSIGADD);
218
- }
219
- scriptElements.push(script.number.encode(threshold));
220
- if (withVerify) {
221
- scriptElements.push(opcodes.OP_NUMEQUALVERIFY);
222
- } else {
223
- scriptElements.push(opcodes.OP_NUMEQUAL);
224
- }
225
- return script.compile(scriptElements);
226
- }
227
- };
228
-
229
1
  // src/error/index.ts
230
2
  var StakingError = class _StakingError extends Error {
231
3
  constructor(code, message) {
@@ -244,19 +16,14 @@ var StakingError = class _StakingError extends Error {
244
16
  }
245
17
  };
246
18
 
247
- // src/staking/transactions.ts
248
- import { Psbt, Transaction as Transaction2, payments as payments3, script as script2, address as address3 } from "bitcoinjs-lib";
249
-
250
- // src/constants/dustSat.ts
251
- var BTC_DUST_SAT = 546;
19
+ // src/utils/btc.ts
20
+ import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
21
+ import { address, initEccLib, networks } from "bitcoinjs-lib";
252
22
 
253
- // src/constants/internalPubkey.ts
254
- var key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
255
- var internalPubkey = Buffer.from(key, "hex").subarray(1, 33);
23
+ // src/constants/keys.ts
24
+ var NO_COORD_PK_BYTE_LENGTH = 32;
256
25
 
257
26
  // src/utils/btc.ts
258
- import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
259
- import { initEccLib, address, networks } from "bitcoinjs-lib";
260
27
  var initBTCCurve = () => {
261
28
  initEccLib(ecc);
262
29
  };
@@ -273,14 +40,28 @@ var isTaproot = (taprootAddress, network) => {
273
40
  if (decoded.version !== 1) {
274
41
  return false;
275
42
  }
276
- switch (network) {
277
- case networks.bitcoin:
278
- return taprootAddress.startsWith("bc1p");
279
- case networks.testnet:
280
- return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p");
281
- default:
282
- return false;
43
+ if (network.bech32 === networks.bitcoin.bech32) {
44
+ return taprootAddress.startsWith("bc1p");
45
+ } else if (network.bech32 === networks.testnet.bech32) {
46
+ return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p");
47
+ }
48
+ return false;
49
+ } catch (error) {
50
+ return false;
51
+ }
52
+ };
53
+ var isNativeSegwit = (segwitAddress, network) => {
54
+ try {
55
+ const decoded = address.fromBech32(segwitAddress);
56
+ if (decoded.version !== 0) {
57
+ return false;
58
+ }
59
+ if (network.bech32 === networks.bitcoin.bech32) {
60
+ return segwitAddress.startsWith("bc1q");
61
+ } else if (network.bech32 === networks.testnet.bech32) {
62
+ return segwitAddress.startsWith("tb1q");
283
63
  }
64
+ return false;
284
65
  } catch (error) {
285
66
  return false;
286
67
  }
@@ -316,124 +97,12 @@ var transactionIdToHash = (txId) => {
316
97
  return Buffer.from(txId, "hex").reverse();
317
98
  };
318
99
 
319
- // src/utils/fee/index.ts
320
- import { script as bitcoinScript2 } from "bitcoinjs-lib";
100
+ // src/utils/staking/index.ts
101
+ import { address as address2, payments } from "bitcoinjs-lib";
321
102
 
322
- // src/constants/fee.ts
323
- var DEFAULT_INPUT_SIZE = 180;
324
- var P2WPKH_INPUT_SIZE = 68;
325
- var P2TR_INPUT_SIZE = 58;
326
- var TX_BUFFER_SIZE_OVERHEAD = 11;
327
- var LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30;
328
- var MAX_NON_LEGACY_OUTPUT_SIZE = 43;
329
- var WITHDRAW_TX_BUFFER_SIZE = 17;
330
- var WALLET_RELAY_FEE_RATE_THRESHOLD = 2;
331
- var OP_RETURN_OUTPUT_VALUE_SIZE = 8;
332
- var OP_RETURN_VALUE_SERIALIZE_SIZE = 1;
333
-
334
- // src/utils/fee/utils.ts
335
- import { script as bitcoinScript, opcodes as opcodes2, payments } from "bitcoinjs-lib";
336
- var isOP_RETURN = (script4) => {
337
- const decompiled = bitcoinScript.decompile(script4);
338
- return !!decompiled && decompiled[0] === opcodes2.OP_RETURN;
339
- };
340
- var getInputSizeByScript = (script4) => {
341
- try {
342
- const { address: p2wpkhAddress } = payments.p2wpkh({
343
- output: script4
344
- });
345
- if (p2wpkhAddress) {
346
- return P2WPKH_INPUT_SIZE;
347
- }
348
- } catch (error) {
349
- }
350
- try {
351
- const { address: p2trAddress } = payments.p2tr({
352
- output: script4
353
- });
354
- if (p2trAddress) {
355
- return P2TR_INPUT_SIZE;
356
- }
357
- } catch (error) {
358
- }
359
- return DEFAULT_INPUT_SIZE;
360
- };
361
- var getEstimatedChangeOutputSize = () => {
362
- return MAX_NON_LEGACY_OUTPUT_SIZE;
363
- };
364
- var inputValueSum = (inputUTXOs) => {
365
- return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0);
366
- };
367
-
368
- // src/utils/fee/index.ts
369
- var getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => {
370
- if (availableUTXOs.length === 0) {
371
- throw new Error("Insufficient funds");
372
- }
373
- const validUTXOs = availableUTXOs.filter((utxo) => {
374
- const script4 = Buffer.from(utxo.scriptPubKey, "hex");
375
- return !!bitcoinScript2.decompile(script4);
376
- });
377
- if (validUTXOs.length === 0) {
378
- throw new Error("Insufficient funds: no valid UTXOs available for staking");
379
- }
380
- const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value);
381
- const selectedUTXOs = [];
382
- let accumulatedValue = 0;
383
- let estimatedFee = 0;
384
- for (const utxo of sortedUTXOs) {
385
- selectedUTXOs.push(utxo);
386
- accumulatedValue += utxo.value;
387
- const estimatedSize = getEstimatedSize(selectedUTXOs, outputs);
388
- estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate);
389
- if (accumulatedValue - (stakingAmount + estimatedFee) > BTC_DUST_SAT) {
390
- estimatedFee += getEstimatedChangeOutputSize() * feeRate;
391
- }
392
- if (accumulatedValue >= stakingAmount + estimatedFee) {
393
- break;
394
- }
395
- }
396
- if (accumulatedValue < stakingAmount + estimatedFee) {
397
- throw new Error(
398
- "Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees"
399
- );
400
- }
401
- return {
402
- selectedUTXOs,
403
- fee: estimatedFee
404
- };
405
- };
406
- var getWithdrawTxFee = (feeRate) => {
407
- const inputSize = P2TR_INPUT_SIZE;
408
- const outputSize = getEstimatedChangeOutputSize();
409
- return feeRate * (inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD + WITHDRAW_TX_BUFFER_SIZE) + rateBasedTxBufferFee(feeRate);
410
- };
411
- var getEstimatedSize = (inputUtxos, outputs) => {
412
- const inputSize = inputUtxos.reduce((acc, u) => {
413
- const script4 = Buffer.from(u.scriptPubKey, "hex");
414
- const decompiledScript = bitcoinScript2.decompile(script4);
415
- if (!decompiledScript) {
416
- return acc;
417
- }
418
- return acc + getInputSizeByScript(script4);
419
- }, 0);
420
- const outputSize = outputs.reduce((acc, output) => {
421
- if (isOP_RETURN(output.scriptPubKey)) {
422
- return acc + output.scriptPubKey.length + OP_RETURN_OUTPUT_VALUE_SIZE + OP_RETURN_VALUE_SERIALIZE_SIZE;
423
- }
424
- return acc + MAX_NON_LEGACY_OUTPUT_SIZE;
425
- }, 0);
426
- return inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD;
427
- };
428
- var rateBasedTxBufferFee = (feeRate) => {
429
- return feeRate <= WALLET_RELAY_FEE_RATE_THRESHOLD ? LOW_RATE_ESTIMATION_ACCURACY_BUFFER : 0;
430
- };
431
-
432
- // src/utils/staking/index.ts
433
- import { address as address2, payments as payments2 } from "bitcoinjs-lib";
434
-
435
- // src/constants/unbonding.ts
436
- var MIN_UNBONDING_OUTPUT_VALUE = 1e3;
103
+ // src/constants/internalPubkey.ts
104
+ var key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
105
+ var internalPubkey = Buffer.from(key, "hex").subarray(1, 33);
437
106
 
438
107
  // src/utils/staking/index.ts
439
108
  var buildStakingTransactionOutputs = (scripts, network, amount) => {
@@ -459,7 +128,7 @@ var deriveStakingOutputInfo = (scripts, network) => {
459
128
  },
460
129
  [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
461
130
  ];
462
- const stakingOutput = payments2.p2tr({
131
+ const stakingOutput = payments.p2tr({
463
132
  internalPubkey,
464
133
  scriptTree,
465
134
  network
@@ -482,7 +151,7 @@ var deriveUnbondingOutputInfo = (scripts, network) => {
482
151
  },
483
152
  { output: scripts.unbondingTimelockScript }
484
153
  ];
485
- const unbondingOutput = payments2.p2tr({
154
+ const unbondingOutput = payments.p2tr({
486
155
  internalPubkey,
487
156
  scriptTree: outputScriptTree,
488
157
  network
@@ -499,7 +168,7 @@ var deriveUnbondingOutputInfo = (scripts, network) => {
499
168
  };
500
169
  };
501
170
  var deriveSlashingOutput = (scripts, network) => {
502
- const slashingOutput = payments2.p2tr({
171
+ const slashingOutput = payments.p2tr({
503
172
  internalPubkey,
504
173
  scriptTree: { output: scripts.unbondingTimelockScript },
505
174
  network
@@ -518,7 +187,11 @@ var deriveSlashingOutput = (scripts, network) => {
518
187
  };
519
188
  var findMatchingTxOutputIndex = (tx, outputAddress, network) => {
520
189
  const index = tx.outs.findIndex((output) => {
521
- return address2.fromOutputScript(output.script, network) === outputAddress;
190
+ try {
191
+ return address2.fromOutputScript(output.script, network) === outputAddress;
192
+ } catch (error) {
193
+ return false;
194
+ }
522
195
  });
523
196
  if (index === -1) {
524
197
  throw new StakingError(
@@ -528,694 +201,1302 @@ var findMatchingTxOutputIndex = (tx, outputAddress, network) => {
528
201
  }
529
202
  return index;
530
203
  };
531
- var validateStakingTxInputData = (stakingAmountSat, timelock, params, inputUTXOs, feeRate) => {
532
- if (stakingAmountSat < params.minStakingAmountSat || stakingAmountSat > params.maxStakingAmountSat) {
533
- throw new StakingError(
534
- "INVALID_INPUT" /* INVALID_INPUT */,
535
- "Invalid staking amount"
536
- );
537
- }
538
- if (timelock < params.minStakingTimeBlocks || timelock > params.maxStakingTimeBlocks) {
539
- throw new StakingError(
540
- "INVALID_INPUT" /* INVALID_INPUT */,
541
- "Invalid timelock"
542
- );
543
- }
544
- if (inputUTXOs.length == 0) {
545
- throw new StakingError(
546
- "INVALID_INPUT" /* INVALID_INPUT */,
547
- "No input UTXOs provided"
548
- );
549
- }
550
- if (feeRate <= 0) {
551
- throw new StakingError(
204
+ var toBuffers = (inputs) => {
205
+ try {
206
+ return inputs.map((i) => Buffer.from(i, "hex"));
207
+ } catch (error) {
208
+ throw StakingError.fromUnknown(
209
+ error,
552
210
  "INVALID_INPUT" /* INVALID_INPUT */,
553
- "Invalid fee rate"
211
+ "Cannot convert values to buffers"
554
212
  );
555
213
  }
556
214
  };
557
- var validateParams = (params) => {
558
- if (params.covenantNoCoordPks.length == 0) {
559
- throw new StakingError(
560
- "INVALID_PARAMS" /* INVALID_PARAMS */,
561
- "Could not find any covenant public keys"
562
- );
563
- }
564
- if (params.covenantNoCoordPks.length < params.covenantQuorum) {
565
- throw new StakingError(
566
- "INVALID_PARAMS" /* INVALID_PARAMS */,
567
- "Covenant public keys must be greater than or equal to the quorum"
568
- );
569
- }
570
- params.covenantNoCoordPks.forEach((pk) => {
571
- if (!isValidNoCoordPublicKey(pk)) {
572
- throw new StakingError(
573
- "INVALID_PARAMS" /* INVALID_PARAMS */,
574
- "Covenant public key should contains no coordinate"
575
- );
576
- }
215
+ var clearTxSignatures = (tx) => {
216
+ tx.ins.forEach((input) => {
217
+ input.script = Buffer.alloc(0);
218
+ input.witness = [];
577
219
  });
578
- if (params.unbondingTime <= 0) {
579
- throw new StakingError(
580
- "INVALID_PARAMS" /* INVALID_PARAMS */,
581
- "Unbonding time must be greater than 0"
582
- );
220
+ return tx;
221
+ };
222
+ var deriveMerkleProof = (merkle) => {
223
+ const proofHex = merkle.reduce((acc, m) => {
224
+ return acc + Buffer.from(m, "hex").reverse().toString("hex");
225
+ }, "");
226
+ return proofHex;
227
+ };
228
+ var extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => {
229
+ for (const input of singedTransaction.ins) {
230
+ if (input.witness && input.witness.length > 0) {
231
+ const schnorrSignature = input.witness[0];
232
+ if (schnorrSignature.length === 64) {
233
+ return schnorrSignature;
234
+ }
235
+ }
583
236
  }
584
- if (params.unbondingFeeSat <= 0) {
585
- throw new StakingError(
586
- "INVALID_PARAMS" /* INVALID_PARAMS */,
587
- "Unbonding fee must be greater than 0"
237
+ return void 0;
238
+ };
239
+
240
+ // src/staking/psbt.ts
241
+ import { Psbt, payments as payments3 } from "bitcoinjs-lib";
242
+
243
+ // src/constants/transaction.ts
244
+ var REDEEM_VERSION = 192;
245
+
246
+ // src/utils/utxo/findInputUTXO.ts
247
+ var findInputUTXO = (inputUTXOs, input) => {
248
+ const inputUTXO = inputUTXOs.find(
249
+ (u) => transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") && u.vout === input.index
250
+ );
251
+ if (!inputUTXO) {
252
+ throw new Error(
253
+ `Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString("hex")} and vout: ${input.index}`
588
254
  );
589
255
  }
590
- if (params.maxStakingAmountSat < params.minStakingAmountSat) {
591
- throw new StakingError(
592
- "INVALID_PARAMS" /* INVALID_PARAMS */,
593
- "Max staking amount must be greater or equal to min staking amount"
594
- );
256
+ return inputUTXO;
257
+ };
258
+
259
+ // src/utils/utxo/getScriptType.ts
260
+ import { payments as payments2 } from "bitcoinjs-lib";
261
+ var BitcoinScriptType = /* @__PURE__ */ ((BitcoinScriptType2) => {
262
+ BitcoinScriptType2["P2PKH"] = "pubkeyhash";
263
+ BitcoinScriptType2["P2SH"] = "scripthash";
264
+ BitcoinScriptType2["P2WPKH"] = "witnesspubkeyhash";
265
+ BitcoinScriptType2["P2WSH"] = "witnessscripthash";
266
+ BitcoinScriptType2["P2TR"] = "taproot";
267
+ return BitcoinScriptType2;
268
+ })(BitcoinScriptType || {});
269
+ var getScriptType = (script4) => {
270
+ try {
271
+ payments2.p2pkh({ output: script4 });
272
+ return "pubkeyhash" /* P2PKH */;
273
+ } catch {
595
274
  }
596
- if (params.minStakingAmountSat < params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE) {
597
- throw new StakingError(
598
- "INVALID_PARAMS" /* INVALID_PARAMS */,
599
- `Min staking amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`
600
- );
275
+ try {
276
+ payments2.p2sh({ output: script4 });
277
+ return "scripthash" /* P2SH */;
278
+ } catch {
601
279
  }
602
- if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) {
603
- throw new StakingError(
604
- "INVALID_PARAMS" /* INVALID_PARAMS */,
605
- "Max staking time must be greater or equal to min staking time"
606
- );
280
+ try {
281
+ payments2.p2wpkh({ output: script4 });
282
+ return "witnesspubkeyhash" /* P2WPKH */;
283
+ } catch {
607
284
  }
608
- if (params.minStakingTimeBlocks <= 0) {
609
- throw new StakingError(
610
- "INVALID_PARAMS" /* INVALID_PARAMS */,
611
- "Min staking time must be greater than 0"
612
- );
285
+ try {
286
+ payments2.p2wsh({ output: script4 });
287
+ return "witnessscripthash" /* P2WSH */;
288
+ } catch {
613
289
  }
614
- if (params.covenantQuorum <= 0) {
615
- throw new StakingError(
616
- "INVALID_PARAMS" /* INVALID_PARAMS */,
617
- "Covenant quorum must be greater than 0"
618
- );
290
+ try {
291
+ payments2.p2tr({ output: script4 });
292
+ return "taproot" /* P2TR */;
293
+ } catch {
619
294
  }
620
- if (params.slashing) {
621
- if (params.slashing.slashingRate <= 0) {
622
- throw new StakingError(
623
- "INVALID_PARAMS" /* INVALID_PARAMS */,
624
- "Slashing rate must be greater than 0"
625
- );
295
+ throw new Error("Unknown script type");
296
+ };
297
+
298
+ // src/utils/utxo/getPsbtInputFields.ts
299
+ var getPsbtInputFields = (utxo, publicKeyNoCoord) => {
300
+ const scriptPubKey = Buffer.from(utxo.scriptPubKey, "hex");
301
+ const type = getScriptType(scriptPubKey);
302
+ switch (type) {
303
+ case "pubkeyhash" /* P2PKH */: {
304
+ if (!utxo.rawTxHex) {
305
+ throw new Error("Missing rawTxHex for legacy P2PKH input");
306
+ }
307
+ return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex") };
626
308
  }
627
- if (params.slashing.slashingRate > 1) {
628
- throw new StakingError(
629
- "INVALID_PARAMS" /* INVALID_PARAMS */,
630
- "Slashing rate must be less or equal to 1"
631
- );
309
+ case "scripthash" /* P2SH */: {
310
+ if (!utxo.rawTxHex) {
311
+ throw new Error("Missing rawTxHex for P2SH input");
312
+ }
313
+ if (!utxo.redeemScript) {
314
+ throw new Error("Missing redeemScript for P2SH input");
315
+ }
316
+ return {
317
+ nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex"),
318
+ redeemScript: Buffer.from(utxo.redeemScript, "hex")
319
+ };
632
320
  }
633
- if (params.slashing.slashingPkScriptHex.length == 0) {
634
- throw new StakingError(
635
- "INVALID_PARAMS" /* INVALID_PARAMS */,
636
- "Slashing public key script is missing"
637
- );
321
+ case "witnesspubkeyhash" /* P2WPKH */: {
322
+ return {
323
+ witnessUtxo: {
324
+ script: scriptPubKey,
325
+ value: utxo.value
326
+ }
327
+ };
638
328
  }
639
- if (params.slashing.minSlashingTxFeeSat <= 0) {
640
- throw new StakingError(
641
- "INVALID_PARAMS" /* INVALID_PARAMS */,
642
- "Minimum slashing transaction fee must be greater than 0"
643
- );
329
+ case "witnessscripthash" /* P2WSH */: {
330
+ if (!utxo.witnessScript) {
331
+ throw new Error("Missing witnessScript for P2WSH input");
332
+ }
333
+ return {
334
+ witnessUtxo: {
335
+ script: scriptPubKey,
336
+ value: utxo.value
337
+ },
338
+ witnessScript: Buffer.from(utxo.witnessScript, "hex")
339
+ };
644
340
  }
341
+ case "taproot" /* P2TR */: {
342
+ return {
343
+ witnessUtxo: {
344
+ script: scriptPubKey,
345
+ value: utxo.value
346
+ },
347
+ // this is needed only if the wallet is in taproot mode
348
+ ...publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord }
349
+ };
350
+ }
351
+ default:
352
+ throw new Error(`Unsupported script type: ${type}`);
645
353
  }
646
354
  };
647
- var validateStakingTimelock = (stakingTimelock, params) => {
648
- if (stakingTimelock < params.minStakingTimeBlocks || stakingTimelock > params.maxStakingTimeBlocks) {
649
- throw new StakingError(
650
- "INVALID_INPUT" /* INVALID_INPUT */,
651
- "Staking transaction timelock is out of range"
652
- );
653
- }
654
- };
655
- var toBuffers = (inputs) => {
656
- try {
657
- return inputs.map(
658
- (i) => Buffer.from(i, "hex")
659
- );
660
- } catch (error) {
661
- throw StakingError.fromUnknown(
662
- error,
663
- "INVALID_INPUT" /* INVALID_INPUT */,
664
- "Cannot convert values to buffers"
665
- );
355
+
356
+ // src/staking/psbt.ts
357
+ var stakingPsbt = (stakingTx, network, inputUTXOs, publicKeyNoCoord) => {
358
+ if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {
359
+ throw new Error("Invalid public key");
666
360
  }
361
+ const psbt = new Psbt({ network });
362
+ if (stakingTx.version !== void 0) psbt.setVersion(stakingTx.version);
363
+ if (stakingTx.locktime !== void 0) psbt.setLocktime(stakingTx.locktime);
364
+ stakingTx.ins.forEach((input) => {
365
+ const inputUTXO = findInputUTXO(inputUTXOs, input);
366
+ const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);
367
+ psbt.addInput({
368
+ hash: input.hash,
369
+ index: input.index,
370
+ sequence: input.sequence,
371
+ ...psbtInputData
372
+ });
373
+ });
374
+ stakingTx.outs.forEach((o) => {
375
+ psbt.addOutput({ script: o.script, value: o.value });
376
+ });
377
+ return psbt;
667
378
  };
668
-
669
- // src/constants/psbt.ts
670
- var NON_RBF_SEQUENCE = 4294967295;
671
- var TRANSACTION_VERSION = 2;
672
-
673
- // src/constants/transaction.ts
674
- var REDEEM_VERSION = 192;
675
-
676
- // src/staking/transactions.ts
677
- var BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 5e8;
678
- function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) {
679
- if (amount <= 0 || feeRate <= 0) {
680
- throw new Error("Amount and fee rate must be bigger than 0");
379
+ var stakingExpansionPsbt = (network, stakingTx, previousStakingTxInfo, inputUTXOs, previousScripts, publicKeyNoCoord) => {
380
+ const psbt = new Psbt({ network });
381
+ if (stakingTx.version !== void 0) psbt.setVersion(stakingTx.version);
382
+ if (stakingTx.locktime !== void 0) psbt.setLocktime(stakingTx.locktime);
383
+ if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {
384
+ throw new Error("Invalid public key");
681
385
  }
682
- if (!isValidBitcoinAddress(changeAddress, network)) {
683
- throw new Error("Invalid change address");
386
+ const previousStakingOutput = previousStakingTxInfo.stakingTx.outs[previousStakingTxInfo.outputIndex];
387
+ if (!previousStakingOutput) {
388
+ throw new Error("Previous staking output not found");
684
389
  }
685
- const stakingOutputs = buildStakingTransactionOutputs(scripts, network, amount);
686
- const { selectedUTXOs, fee } = getStakingTxInputUTXOsAndFees(
687
- inputUTXOs,
688
- amount,
689
- feeRate,
690
- stakingOutputs
691
- );
692
- const tx = new Transaction2();
693
- tx.version = TRANSACTION_VERSION;
694
- for (let i = 0; i < selectedUTXOs.length; ++i) {
695
- const input = selectedUTXOs[i];
696
- tx.addInput(
697
- transactionIdToHash(input.txid),
698
- input.vout,
699
- NON_RBF_SEQUENCE
700
- );
390
+ ;
391
+ if (getScriptType(previousStakingOutput.script) !== "taproot" /* P2TR */) {
392
+ throw new Error("Previous staking output script type is not P2TR");
701
393
  }
702
- stakingOutputs.forEach((o) => {
703
- tx.addOutput(o.scriptPubKey, o.value);
704
- });
705
- const inputsSum = inputValueSum(selectedUTXOs);
706
- if (inputsSum - (amount + fee) > BTC_DUST_SAT) {
707
- tx.addOutput(
708
- address3.toOutputScript(changeAddress, network),
709
- inputsSum - (amount + fee)
394
+ if (stakingTx.ins.length !== 2) {
395
+ throw new Error(
396
+ "Staking expansion transaction must have exactly 2 inputs"
710
397
  );
711
398
  }
712
- if (lockHeight) {
713
- if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) {
714
- throw new Error("Invalid lock height");
715
- }
716
- tx.locktime = lockHeight;
399
+ const txInputs = stakingTx.ins;
400
+ if (Buffer.from(txInputs[0].hash).reverse().toString("hex") !== previousStakingTxInfo.stakingTx.getId()) {
401
+ throw new Error("Previous staking input hash does not match");
402
+ } else if (txInputs[0].index !== previousStakingTxInfo.outputIndex) {
403
+ throw new Error("Previous staking input index does not match");
717
404
  }
718
- return {
719
- transaction: tx,
720
- fee
721
- };
722
- }
723
- function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) {
724
- const scriptTree = [
725
- {
726
- output: scripts.slashingScript
727
- },
728
- { output: scripts.unbondingTimelockScript }
729
- ];
730
- return withdrawalTransaction(
731
- {
732
- timelockScript: scripts.unbondingTimelockScript
733
- },
734
- scriptTree,
735
- unbondingTx,
736
- withdrawalAddress,
737
- network,
738
- feeRate,
739
- 0
740
- // unbonding always has a single output
741
- );
742
- }
743
- function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
744
- const scriptTree = [
745
- {
746
- output: scripts.slashingScript
747
- },
748
- [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
405
+ const inputScriptTree = [
406
+ { output: previousScripts.slashingScript },
407
+ [{ output: previousScripts.unbondingScript }, { output: previousScripts.timelockScript }]
749
408
  ];
750
- return withdrawalTransaction(
751
- scripts,
752
- scriptTree,
753
- tx,
754
- withdrawalAddress,
755
- network,
756
- feeRate,
757
- outputIndex
758
- );
759
- }
760
- function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) {
761
- const scriptTree = { output: scripts.unbondingTimelockScript };
762
- return withdrawalTransaction(
763
- {
764
- timelockScript: scripts.unbondingTimelockScript
765
- },
766
- scriptTree,
767
- slashingTx,
768
- withdrawalAddress,
769
- network,
770
- feeRate,
771
- outputIndex
772
- );
773
- }
774
- function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
775
- if (feeRate <= 0) {
776
- throw new Error("Withdrawal feeRate must be bigger than 0");
777
- }
778
- if (outputIndex < 0) {
779
- throw new Error("Output index must be bigger or equal to 0");
780
- }
781
- const timePosition = 2;
782
- const decompiled = script2.decompile(scripts.timelockScript);
783
- if (!decompiled) {
784
- throw new Error("Timelock script is not valid");
785
- }
786
- let timelock = 0;
787
- if (typeof decompiled[timePosition] !== "number") {
788
- const timeBuffer = decompiled[timePosition];
789
- timelock = script2.number.decode(timeBuffer);
790
- } else {
791
- const wrap = decompiled[timePosition] % 16;
792
- timelock = wrap === 0 ? 16 : wrap;
793
- }
794
- const redeem = {
795
- output: scripts.timelockScript,
409
+ const inputRedeem = {
410
+ output: previousScripts.unbondingScript,
796
411
  redeemVersion: REDEEM_VERSION
797
412
  };
798
413
  const p2tr = payments3.p2tr({
799
414
  internalPubkey,
800
- scriptTree,
801
- redeem,
415
+ scriptTree: inputScriptTree,
416
+ redeem: inputRedeem,
802
417
  network
803
418
  });
804
- const tapLeafScript = {
805
- leafVersion: redeem.redeemVersion,
806
- script: redeem.output,
419
+ if (!p2tr.witness || p2tr.witness.length === 0) {
420
+ throw new Error(
421
+ "Failed to create P2TR witness for expansion transaction input"
422
+ );
423
+ }
424
+ const inputTapLeafScript = {
425
+ leafVersion: inputRedeem.redeemVersion,
426
+ script: inputRedeem.output,
807
427
  controlBlock: p2tr.witness[p2tr.witness.length - 1]
808
428
  };
809
- const psbt = new Psbt({ network });
810
- psbt.setVersion(TRANSACTION_VERSION);
811
429
  psbt.addInput({
812
- hash: tx.getHash(),
813
- index: outputIndex,
814
- tapInternalKey: internalPubkey,
430
+ hash: txInputs[0].hash,
431
+ index: txInputs[0].index,
432
+ sequence: txInputs[0].sequence,
815
433
  witnessUtxo: {
816
- value: tx.outs[outputIndex].value,
817
- script: tx.outs[outputIndex].script
434
+ script: previousStakingOutput.script,
435
+ value: previousStakingOutput.value
818
436
  },
819
- tapLeafScript: [tapLeafScript],
820
- sequence: timelock
437
+ tapInternalKey: internalPubkey,
438
+ tapLeafScript: [inputTapLeafScript]
821
439
  });
822
- const estimatedFee = getWithdrawTxFee(feeRate);
823
- const outputValue = tx.outs[outputIndex].value - estimatedFee;
824
- if (outputValue < 0) {
825
- throw new Error(
826
- "Not enough funds to cover the fee for withdrawal transaction"
827
- );
828
- }
829
- if (outputValue < BTC_DUST_SAT) {
830
- throw new Error("Output value is less than dust limit");
831
- }
832
- psbt.addOutput({
833
- address: withdrawalAddress,
834
- value: outputValue
440
+ const inputUTXO = findInputUTXO(inputUTXOs, txInputs[1]);
441
+ const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);
442
+ psbt.addInput({
443
+ hash: txInputs[1].hash,
444
+ index: txInputs[1].index,
445
+ sequence: txInputs[1].sequence,
446
+ ...psbtInputData
835
447
  });
836
- psbt.setLocktime(0);
837
- return {
838
- psbt,
839
- fee: estimatedFee
840
- };
841
- }
842
- function slashTimelockUnbondedTransaction(scripts, stakingTransaction2, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
843
- const slashingScriptTree = [
844
- {
845
- output: scripts.slashingScript
846
- },
847
- [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
848
- ];
849
- return slashingTransaction(
850
- {
851
- unbondingTimelockScript: scripts.unbondingTimelockScript,
852
- slashingScript: scripts.slashingScript
853
- },
854
- slashingScriptTree,
855
- stakingTransaction2,
856
- slashingPkScriptHex,
857
- slashingRate,
858
- minimumFee,
859
- network,
860
- outputIndex
861
- );
862
- }
863
- function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) {
864
- const unbondingScriptTree = [
865
- {
866
- output: scripts.slashingScript
867
- },
868
- {
869
- output: scripts.unbondingTimelockScript
870
- }
871
- ];
872
- return slashingTransaction(
873
- {
874
- unbondingTimelockScript: scripts.unbondingTimelockScript,
875
- slashingScript: scripts.slashingScript
876
- },
877
- unbondingScriptTree,
878
- unbondingTx,
879
- slashingPkScriptHex,
880
- slashingRate,
881
- minimumSlashingFee,
882
- network,
883
- 0
884
- // unbonding always has a single output
885
- );
886
- }
887
- function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
888
- if (slashingRate <= 0 || slashingRate >= 1) {
889
- throw new Error("Slashing rate must be between 0 and 1");
448
+ stakingTx.outs.forEach((o) => {
449
+ psbt.addOutput({ script: o.script, value: o.value });
450
+ });
451
+ return psbt;
452
+ };
453
+ var unbondingPsbt = (scripts, unbondingTx, stakingTx, network) => {
454
+ if (unbondingTx.outs.length !== 1) {
455
+ throw new Error("Unbonding transaction must have exactly one output");
890
456
  }
891
- slashingRate = parseFloat(slashingRate.toFixed(2));
892
- if (minimumFee <= 0 || !Number.isInteger(minimumFee)) {
893
- throw new Error("Minimum fee must be a positve integer");
457
+ if (unbondingTx.ins.length !== 1) {
458
+ throw new Error("Unbonding transaction must have exactly one input");
894
459
  }
895
- if (outputIndex < 0 || !Number.isInteger(outputIndex)) {
896
- throw new Error("Output index must be an integer bigger or equal to 0");
460
+ validateUnbondingOutput(scripts, unbondingTx, network);
461
+ const psbt = new Psbt({ network });
462
+ if (unbondingTx.version !== void 0) {
463
+ psbt.setVersion(unbondingTx.version);
897
464
  }
898
- if (!transaction.outs[outputIndex]) {
899
- throw new Error("Output index is out of range");
465
+ if (unbondingTx.locktime !== void 0) {
466
+ psbt.setLocktime(unbondingTx.locktime);
900
467
  }
901
- const redeem = {
902
- output: scripts.slashingScript,
468
+ const input = unbondingTx.ins[0];
469
+ const outputIndex = input.index;
470
+ const inputScriptTree = [
471
+ { output: scripts.slashingScript },
472
+ [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
473
+ ];
474
+ const inputRedeem = {
475
+ output: scripts.unbondingScript,
903
476
  redeemVersion: REDEEM_VERSION
904
477
  };
905
478
  const p2tr = payments3.p2tr({
906
479
  internalPubkey,
907
- scriptTree,
908
- redeem,
480
+ scriptTree: inputScriptTree,
481
+ redeem: inputRedeem,
909
482
  network
910
483
  });
911
- const tapLeafScript = {
912
- leafVersion: redeem.redeemVersion,
913
- script: redeem.output,
484
+ const inputTapLeafScript = {
485
+ leafVersion: inputRedeem.redeemVersion,
486
+ script: inputRedeem.output,
914
487
  controlBlock: p2tr.witness[p2tr.witness.length - 1]
915
488
  };
916
- const stakingAmount = transaction.outs[outputIndex].value;
917
- const slashingAmount = Math.floor(stakingAmount * slashingRate);
918
- if (slashingAmount <= BTC_DUST_SAT) {
919
- throw new Error("Slashing amount is less than dust limit");
920
- }
921
- const userFunds = stakingAmount - slashingAmount - minimumFee;
922
- if (userFunds <= BTC_DUST_SAT) {
923
- throw new Error("User funds are less than dust limit");
924
- }
925
- const psbt = new Psbt({ network });
926
- psbt.setVersion(TRANSACTION_VERSION);
927
489
  psbt.addInput({
928
- hash: transaction.getHash(),
929
- index: outputIndex,
490
+ hash: input.hash,
491
+ index: input.index,
492
+ sequence: input.sequence,
930
493
  tapInternalKey: internalPubkey,
931
494
  witnessUtxo: {
932
- value: stakingAmount,
933
- script: transaction.outs[outputIndex].script
495
+ value: stakingTx.outs[outputIndex].value,
496
+ script: stakingTx.outs[outputIndex].script
934
497
  },
935
- tapLeafScript: [tapLeafScript],
936
- // not RBF-able
937
- sequence: NON_RBF_SEQUENCE
938
- });
939
- psbt.addOutput({
940
- script: Buffer.from(slashingPkScriptHex, "hex"),
941
- value: slashingAmount
942
- });
943
- const changeOutput = payments3.p2tr({
944
- internalPubkey,
945
- scriptTree: { output: scripts.unbondingTimelockScript },
946
- network
498
+ tapLeafScript: [inputTapLeafScript]
947
499
  });
948
500
  psbt.addOutput({
949
- address: changeOutput.address,
950
- value: userFunds
951
- });
952
- psbt.setLocktime(0);
953
- return { psbt };
954
- }
955
- function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) {
956
- if (unbondingFee <= 0) {
957
- throw new Error("Unbonding fee must be bigger than 0");
958
- }
959
- if (outputIndex < 0) {
960
- throw new Error("Output index must be bigger or equal to 0");
961
- }
962
- const tx = new Transaction2();
963
- tx.version = TRANSACTION_VERSION;
964
- tx.addInput(
965
- stakingTx.getHash(),
966
- outputIndex,
967
- NON_RBF_SEQUENCE
968
- // not RBF-able
969
- );
970
- const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
971
- const outputValue = stakingTx.outs[outputIndex].value - unbondingFee;
972
- if (outputValue < BTC_DUST_SAT) {
973
- throw new Error("Output value is less than dust limit for unbonding transaction");
974
- }
975
- if (!unbondingOutputInfo.outputAddress) {
976
- throw new Error("Unbonding output address is not defined");
977
- }
978
- tx.addOutput(
979
- unbondingOutputInfo.scriptPubKey,
980
- outputValue
981
- );
982
- tx.locktime = 0;
983
- return {
984
- transaction: tx,
985
- fee: unbondingFee
986
- };
987
- }
988
- var createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => {
989
- if (covenantSigs.length < covenantQuorum) {
990
- throw new Error(
991
- `Not enough covenant signatures. Required: ${covenantQuorum}, got: ${covenantSigs.length}`
992
- );
993
- }
994
- for (const sig of covenantSigs) {
995
- const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex");
996
- if (!paramsCovenants.some((covenant) => covenant.equals(btcPkHexBuf))) {
997
- throw new Error(
998
- `Covenant signature public key ${sig.btcPkHex} not found in params covenants`
999
- );
1000
- }
1001
- }
1002
- const covenantSigsBuffers = covenantSigs.slice(0, covenantQuorum).map((sig) => ({
1003
- btcPkHex: Buffer.from(sig.btcPkHex, "hex"),
1004
- sigHex: Buffer.from(sig.sigHex, "hex")
1005
- }));
1006
- const paramsCovenantsSorted = [...paramsCovenants].sort(Buffer.compare).reverse();
1007
- const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => {
1008
- const covenantSig = covenantSigsBuffers.find(
1009
- (sig) => sig.btcPkHex.compare(covenant) === 0
1010
- );
1011
- return covenantSig?.sigHex || Buffer.alloc(0);
501
+ script: unbondingTx.outs[0].script,
502
+ value: unbondingTx.outs[0].value
1012
503
  });
1013
- return [...composedCovenantSigs, ...originalWitness];
504
+ return psbt;
1014
505
  };
1015
-
1016
- // src/staking/psbt.ts
1017
- import { Psbt as Psbt2, payments as payments5 } from "bitcoinjs-lib";
1018
-
1019
- // src/utils/utxo/findInputUTXO.ts
1020
- var findInputUTXO = (inputUTXOs, input) => {
1021
- const inputUTXO = inputUTXOs.find(
1022
- (u) => transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") && u.vout === input.index
1023
- );
1024
- if (!inputUTXO) {
506
+ var validateUnbondingOutput = (scripts, unbondingTx, network) => {
507
+ const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
508
+ if (unbondingOutputInfo.scriptPubKey.toString("hex") !== unbondingTx.outs[0].script.toString("hex")) {
1025
509
  throw new Error(
1026
- `Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString("hex")} and vout: ${input.index}`
510
+ "Unbonding output script does not match the expected script while building psbt"
1027
511
  );
1028
512
  }
1029
- return inputUTXO;
1030
513
  };
1031
514
 
1032
- // src/utils/utxo/getScriptType.ts
1033
- import { payments as payments4 } from "bitcoinjs-lib";
1034
- var BitcoinScriptType = /* @__PURE__ */ ((BitcoinScriptType2) => {
1035
- BitcoinScriptType2["P2PKH"] = "pubkeyhash";
1036
- BitcoinScriptType2["P2SH"] = "scripthash";
1037
- BitcoinScriptType2["P2WPKH"] = "witnesspubkeyhash";
1038
- BitcoinScriptType2["P2WSH"] = "witnessscripthash";
1039
- BitcoinScriptType2["P2TR"] = "taproot";
1040
- return BitcoinScriptType2;
1041
- })(BitcoinScriptType || {});
1042
- var getScriptType = (script4) => {
1043
- try {
1044
- payments4.p2pkh({ output: script4 });
1045
- return "pubkeyhash" /* P2PKH */;
1046
- } catch {
1047
- }
1048
- try {
1049
- payments4.p2sh({ output: script4 });
1050
- return "scripthash" /* P2SH */;
1051
- } catch {
515
+ // src/staking/stakingScript.ts
516
+ import { opcodes, script } from "bitcoinjs-lib";
517
+ var MAGIC_BYTES_LEN = 4;
518
+ var StakingScriptData = class {
519
+ constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock) {
520
+ if (!stakerKey || !finalityProviderKeys || !covenantKeys || !covenantThreshold || !stakingTimelock || !unbondingTimelock) {
521
+ throw new Error("Missing required input values");
522
+ }
523
+ this.stakerKey = stakerKey;
524
+ this.finalityProviderKeys = finalityProviderKeys;
525
+ this.covenantKeys = covenantKeys;
526
+ this.covenantThreshold = covenantThreshold;
527
+ this.stakingTimeLock = stakingTimelock;
528
+ this.unbondingTimeLock = unbondingTimelock;
529
+ if (!this.validate()) {
530
+ throw new Error("Invalid script data provided");
531
+ }
1052
532
  }
1053
- try {
1054
- payments4.p2wpkh({ output: script4 });
1055
- return "witnesspubkeyhash" /* P2WPKH */;
1056
- } catch {
533
+ /**
534
+ * Validates the staking script.
535
+ * @returns {boolean} Returns true if the staking script is valid, otherwise false.
536
+ */
537
+ validate() {
538
+ if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {
539
+ return false;
540
+ }
541
+ if (this.finalityProviderKeys.some(
542
+ (finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH
543
+ )) {
544
+ return false;
545
+ }
546
+ if (this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)) {
547
+ return false;
548
+ }
549
+ const allPks = [
550
+ this.stakerKey,
551
+ ...this.finalityProviderKeys,
552
+ ...this.covenantKeys
553
+ ];
554
+ const allPksSet = new Set(allPks);
555
+ if (allPks.length !== allPksSet.size) {
556
+ return false;
557
+ }
558
+ if (this.covenantThreshold <= 0 || this.covenantThreshold > this.covenantKeys.length) {
559
+ return false;
560
+ }
561
+ if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) {
562
+ return false;
563
+ }
564
+ if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) {
565
+ return false;
566
+ }
567
+ return true;
1057
568
  }
1058
- try {
1059
- payments4.p2wsh({ output: script4 });
1060
- return "witnessscripthash" /* P2WSH */;
1061
- } catch {
569
+ // The staking script allows for multiple finality provider public keys
570
+ // to support (re)stake to multiple finality providers
571
+ // Covenant members are going to have multiple keys
572
+ /**
573
+ * Builds a timelock script.
574
+ * @param timelock - The timelock value to encode in the script.
575
+ * @returns {Buffer} containing the compiled timelock script.
576
+ */
577
+ buildTimelockScript(timelock) {
578
+ return script.compile([
579
+ this.stakerKey,
580
+ opcodes.OP_CHECKSIGVERIFY,
581
+ script.number.encode(timelock),
582
+ opcodes.OP_CHECKSEQUENCEVERIFY
583
+ ]);
1062
584
  }
1063
- try {
1064
- payments4.p2tr({ output: script4 });
1065
- return "taproot" /* P2TR */;
1066
- } catch {
585
+ /**
586
+ * Builds the staking timelock script.
587
+ * Only holder of private key for given pubKey can spend after relative lock time
588
+ * Creates the timelock script in the form:
589
+ * <stakerPubKey>
590
+ * OP_CHECKSIGVERIFY
591
+ * <stakingTimeBlocks>
592
+ * OP_CHECKSEQUENCEVERIFY
593
+ * @returns {Buffer} The staking timelock script.
594
+ */
595
+ buildStakingTimelockScript() {
596
+ return this.buildTimelockScript(this.stakingTimeLock);
1067
597
  }
1068
- throw new Error("Unknown script type");
1069
- };
1070
-
1071
- // src/utils/utxo/getPsbtInputFields.ts
1072
- var getPsbtInputFields = (utxo, publicKeyNoCoord) => {
1073
- const scriptPubKey = Buffer.from(utxo.scriptPubKey, "hex");
1074
- const type = getScriptType(scriptPubKey);
1075
- switch (type) {
1076
- case "pubkeyhash" /* P2PKH */: {
1077
- if (!utxo.rawTxHex) {
1078
- throw new Error("Missing rawTxHex for legacy P2PKH input");
1079
- }
1080
- return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex") };
598
+ /**
599
+ * Builds the unbonding timelock script.
600
+ * Creates the unbonding timelock script in the form:
601
+ * <stakerPubKey>
602
+ * OP_CHECKSIGVERIFY
603
+ * <unbondingTimeBlocks>
604
+ * OP_CHECKSEQUENCEVERIFY
605
+ * @returns {Buffer} The unbonding timelock script.
606
+ */
607
+ buildUnbondingTimelockScript() {
608
+ return this.buildTimelockScript(this.unbondingTimeLock);
609
+ }
610
+ /**
611
+ * Builds the unbonding script in the form:
612
+ * buildSingleKeyScript(stakerPk, true) ||
613
+ * buildMultiKeyScript(covenantPks, covenantThreshold, false)
614
+ * || means combining the scripts
615
+ * @returns {Buffer} The unbonding script.
616
+ */
617
+ buildUnbondingScript() {
618
+ return Buffer.concat([
619
+ this.buildSingleKeyScript(this.stakerKey, true),
620
+ this.buildMultiKeyScript(
621
+ this.covenantKeys,
622
+ this.covenantThreshold,
623
+ false
624
+ )
625
+ ]);
626
+ }
627
+ /**
628
+ * Builds the slashing script for staking in the form:
629
+ * buildSingleKeyScript(stakerPk, true) ||
630
+ * buildMultiKeyScript(finalityProviderPKs, 1, true) ||
631
+ * buildMultiKeyScript(covenantPks, covenantThreshold, false)
632
+ * || means combining the scripts
633
+ * The slashing script is a combination of single-key and multi-key scripts.
634
+ * The single-key script is used for staker key verification.
635
+ * The multi-key script is used for finality provider key verification and covenant key verification.
636
+ * @returns {Buffer} The slashing script as a Buffer.
637
+ */
638
+ buildSlashingScript() {
639
+ return Buffer.concat([
640
+ this.buildSingleKeyScript(this.stakerKey, true),
641
+ this.buildMultiKeyScript(
642
+ this.finalityProviderKeys,
643
+ // The threshold is always 1 as we only need one
644
+ // finalityProvider signature to perform slashing
645
+ // (only one finalityProvider performs an offence)
646
+ 1,
647
+ // OP_VERIFY/OP_CHECKSIGVERIFY is added at the end
648
+ true
649
+ ),
650
+ this.buildMultiKeyScript(
651
+ this.covenantKeys,
652
+ this.covenantThreshold,
653
+ // No need to add verify since covenants are at the end of the script
654
+ false
655
+ )
656
+ ]);
657
+ }
658
+ /**
659
+ * Builds the staking scripts.
660
+ * @returns {StakingScripts} The staking scripts.
661
+ */
662
+ buildScripts() {
663
+ return {
664
+ timelockScript: this.buildStakingTimelockScript(),
665
+ unbondingScript: this.buildUnbondingScript(),
666
+ slashingScript: this.buildSlashingScript(),
667
+ unbondingTimelockScript: this.buildUnbondingTimelockScript()
668
+ };
669
+ }
670
+ // buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality
671
+ // for creating Bitcoin scripts for the unbonding script and the slashing script
672
+ /**
673
+ * Builds a single key script in the form:
674
+ * buildSingleKeyScript creates a single key script
675
+ * <pk> OP_CHECKSIGVERIFY (if withVerify is true)
676
+ * <pk> OP_CHECKSIG (if withVerify is false)
677
+ * @param pk - The public key buffer.
678
+ * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode.
679
+ * @returns The compiled script buffer.
680
+ */
681
+ buildSingleKeyScript(pk, withVerify) {
682
+ if (pk.length != NO_COORD_PK_BYTE_LENGTH) {
683
+ throw new Error("Invalid key length");
1081
684
  }
1082
- case "scripthash" /* P2SH */: {
1083
- if (!utxo.rawTxHex) {
1084
- throw new Error("Missing rawTxHex for P2SH input");
1085
- }
1086
- if (!utxo.redeemScript) {
1087
- throw new Error("Missing redeemScript for P2SH input");
1088
- }
1089
- return {
1090
- nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex"),
1091
- redeemScript: Buffer.from(utxo.redeemScript, "hex")
1092
- };
685
+ return script.compile([
686
+ pk,
687
+ withVerify ? opcodes.OP_CHECKSIGVERIFY : opcodes.OP_CHECKSIG
688
+ ]);
689
+ }
690
+ /**
691
+ * Builds a multi-key script in the form:
692
+ * <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL
693
+ * <withVerify -> OP_NUMEQUALVERIFY>
694
+ * It validates whether provided keys are unique and the threshold is not greater than number of keys
695
+ * If there is only one key provided it will return single key sig script
696
+ * @param pks - An array of public keys.
697
+ * @param threshold - The required number of valid signers.
698
+ * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode.
699
+ * @returns The compiled multi-key script as a Buffer.
700
+ * @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided.
701
+ */
702
+ buildMultiKeyScript(pks, threshold, withVerify) {
703
+ if (!pks || pks.length === 0) {
704
+ throw new Error("No keys provided");
1093
705
  }
1094
- case "witnesspubkeyhash" /* P2WPKH */: {
1095
- return {
1096
- witnessUtxo: {
1097
- script: scriptPubKey,
1098
- value: utxo.value
1099
- }
1100
- };
706
+ if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) {
707
+ throw new Error("Invalid key length");
1101
708
  }
1102
- case "witnessscripthash" /* P2WSH */: {
1103
- if (!utxo.witnessScript) {
1104
- throw new Error("Missing witnessScript for P2WSH input");
709
+ if (threshold > pks.length) {
710
+ throw new Error(
711
+ "Required number of valid signers is greater than number of provided keys"
712
+ );
713
+ }
714
+ if (pks.length === 1) {
715
+ return this.buildSingleKeyScript(pks[0], withVerify);
716
+ }
717
+ const sortedPks = [...pks].sort(Buffer.compare);
718
+ for (let i = 0; i < sortedPks.length - 1; ++i) {
719
+ if (sortedPks[i].equals(sortedPks[i + 1])) {
720
+ throw new Error("Duplicate keys provided");
1105
721
  }
1106
- return {
1107
- witnessUtxo: {
1108
- script: scriptPubKey,
1109
- value: utxo.value
1110
- },
1111
- witnessScript: Buffer.from(utxo.witnessScript, "hex")
1112
- };
1113
722
  }
1114
- case "taproot" /* P2TR */: {
1115
- return {
1116
- witnessUtxo: {
1117
- script: scriptPubKey,
1118
- value: utxo.value
1119
- },
1120
- // this is needed only if the wallet is in taproot mode
1121
- ...publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord }
1122
- };
723
+ const scriptElements = [sortedPks[0], opcodes.OP_CHECKSIG];
724
+ for (let i = 1; i < sortedPks.length; i++) {
725
+ scriptElements.push(sortedPks[i]);
726
+ scriptElements.push(opcodes.OP_CHECKSIGADD);
1123
727
  }
1124
- default:
1125
- throw new Error(`Unsupported script type: ${type}`);
728
+ scriptElements.push(script.number.encode(threshold));
729
+ if (withVerify) {
730
+ scriptElements.push(opcodes.OP_NUMEQUALVERIFY);
731
+ } else {
732
+ scriptElements.push(opcodes.OP_NUMEQUAL);
733
+ }
734
+ return script.compile(scriptElements);
1126
735
  }
1127
736
  };
1128
737
 
1129
- // src/staking/psbt.ts
1130
- var stakingPsbt = (stakingTx, network, inputUTXOs, publicKeyNoCoord) => {
1131
- if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {
1132
- throw new Error("Invalid public key");
1133
- }
1134
- const psbt = new Psbt2({ network });
1135
- if (stakingTx.version !== void 0)
1136
- psbt.setVersion(stakingTx.version);
1137
- if (stakingTx.locktime !== void 0)
1138
- psbt.setLocktime(stakingTx.locktime);
1139
- stakingTx.ins.forEach((input) => {
1140
- const inputUTXO = findInputUTXO(inputUTXOs, input);
1141
- const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);
1142
- psbt.addInput({
1143
- hash: input.hash,
1144
- index: input.index,
1145
- sequence: input.sequence,
1146
- ...psbtInputData
738
+ // src/staking/transactions.ts
739
+ import {
740
+ Psbt as Psbt2,
741
+ Transaction as Transaction3,
742
+ payments as payments5,
743
+ script as script2,
744
+ address as address3,
745
+ opcodes as opcodes3
746
+ } from "bitcoinjs-lib";
747
+
748
+ // src/constants/dustSat.ts
749
+ var BTC_DUST_SAT = 546;
750
+
751
+ // src/utils/fee/index.ts
752
+ import { script as bitcoinScript2 } from "bitcoinjs-lib";
753
+
754
+ // src/constants/fee.ts
755
+ var DEFAULT_INPUT_SIZE = 180;
756
+ var P2WPKH_INPUT_SIZE = 68;
757
+ var P2TR_INPUT_SIZE = 58;
758
+ var P2TR_STAKING_EXPANSION_INPUT_SIZE = 268;
759
+ var TX_BUFFER_SIZE_OVERHEAD = 11;
760
+ var LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30;
761
+ var MAX_NON_LEGACY_OUTPUT_SIZE = 43;
762
+ var WITHDRAW_TX_BUFFER_SIZE = 17;
763
+ var WALLET_RELAY_FEE_RATE_THRESHOLD = 2;
764
+ var OP_RETURN_OUTPUT_VALUE_SIZE = 8;
765
+ var OP_RETURN_VALUE_SERIALIZE_SIZE = 1;
766
+
767
+ // src/utils/fee/utils.ts
768
+ import { script as bitcoinScript, opcodes as opcodes2, payments as payments4 } from "bitcoinjs-lib";
769
+ var isOP_RETURN = (script4) => {
770
+ const decompiled = bitcoinScript.decompile(script4);
771
+ return !!decompiled && decompiled[0] === opcodes2.OP_RETURN;
772
+ };
773
+ var getInputSizeByScript = (script4) => {
774
+ try {
775
+ const { address: p2wpkhAddress } = payments4.p2wpkh({
776
+ output: script4
1147
777
  });
778
+ if (p2wpkhAddress) {
779
+ return P2WPKH_INPUT_SIZE;
780
+ }
781
+ } catch (error) {
782
+ }
783
+ try {
784
+ const { address: p2trAddress } = payments4.p2tr({
785
+ output: script4
786
+ });
787
+ if (p2trAddress) {
788
+ return P2TR_INPUT_SIZE;
789
+ }
790
+ } catch (error) {
791
+ }
792
+ return DEFAULT_INPUT_SIZE;
793
+ };
794
+ var getEstimatedChangeOutputSize = () => {
795
+ return MAX_NON_LEGACY_OUTPUT_SIZE;
796
+ };
797
+ var inputValueSum = (inputUTXOs) => {
798
+ return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0);
799
+ };
800
+
801
+ // src/utils/fee/index.ts
802
+ var getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => {
803
+ if (availableUTXOs.length === 0) {
804
+ throw new Error("Insufficient funds");
805
+ }
806
+ const validUTXOs = availableUTXOs.filter((utxo) => {
807
+ const script4 = Buffer.from(utxo.scriptPubKey, "hex");
808
+ return !!bitcoinScript2.decompile(script4);
1148
809
  });
1149
- stakingTx.outs.forEach((o) => {
1150
- psbt.addOutput({ script: o.script, value: o.value });
810
+ if (validUTXOs.length === 0) {
811
+ throw new Error("Insufficient funds: no valid UTXOs available for staking");
812
+ }
813
+ const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value);
814
+ const selectedUTXOs = [];
815
+ let accumulatedValue = 0;
816
+ let estimatedFee = 0;
817
+ for (const utxo of sortedUTXOs) {
818
+ selectedUTXOs.push(utxo);
819
+ accumulatedValue += utxo.value;
820
+ const estimatedSize = getEstimatedSize(selectedUTXOs, outputs);
821
+ estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate);
822
+ if (accumulatedValue - (stakingAmount + estimatedFee) > BTC_DUST_SAT) {
823
+ estimatedFee += getEstimatedChangeOutputSize() * feeRate;
824
+ }
825
+ if (accumulatedValue >= stakingAmount + estimatedFee) {
826
+ break;
827
+ }
828
+ }
829
+ if (accumulatedValue < stakingAmount + estimatedFee) {
830
+ throw new Error(
831
+ "Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees"
832
+ );
833
+ }
834
+ return {
835
+ selectedUTXOs,
836
+ fee: estimatedFee
837
+ };
838
+ };
839
+ var getStakingExpansionTxFundingUTXOAndFees = (availableUTXOs, feeRate, outputs) => {
840
+ if (availableUTXOs.length === 0) {
841
+ throw new Error("Insufficient funds");
842
+ }
843
+ const validUTXOs = availableUTXOs.filter((utxo) => {
844
+ const script4 = Buffer.from(utxo.scriptPubKey, "hex");
845
+ const decompiledScript = bitcoinScript2.decompile(script4);
846
+ return decompiledScript && decompiledScript.length > 0;
1151
847
  });
1152
- return psbt;
848
+ if (validUTXOs.length === 0) {
849
+ throw new Error("Insufficient funds: no valid UTXOs available for staking");
850
+ }
851
+ const sortedUTXOs = validUTXOs.sort((a, b) => a.value - b.value);
852
+ for (const utxo of sortedUTXOs) {
853
+ const estimatedSize = getEstimatedSize(
854
+ [utxo],
855
+ outputs
856
+ ) + P2TR_STAKING_EXPANSION_INPUT_SIZE;
857
+ let estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate);
858
+ if (utxo.value >= estimatedFee) {
859
+ if (utxo.value - estimatedFee > BTC_DUST_SAT) {
860
+ estimatedFee += getEstimatedChangeOutputSize() * feeRate;
861
+ }
862
+ if (utxo.value >= estimatedFee) {
863
+ return {
864
+ selectedUTXO: utxo,
865
+ fee: estimatedFee
866
+ };
867
+ }
868
+ }
869
+ }
870
+ throw new Error(
871
+ "Insufficient funds: unable to find a UTXO to cover the fees for the staking expansion transaction."
872
+ );
1153
873
  };
1154
- var unbondingPsbt = (scripts, unbondingTx, stakingTx, network) => {
1155
- if (unbondingTx.outs.length !== 1) {
1156
- throw new Error("Unbonding transaction must have exactly one output");
874
+ var getWithdrawTxFee = (feeRate) => {
875
+ const inputSize = P2TR_INPUT_SIZE;
876
+ const outputSize = getEstimatedChangeOutputSize();
877
+ return feeRate * (inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD + WITHDRAW_TX_BUFFER_SIZE) + rateBasedTxBufferFee(feeRate);
878
+ };
879
+ var getEstimatedSize = (inputUtxos, outputs) => {
880
+ const inputSize = inputUtxos.reduce((acc, u) => {
881
+ const script4 = Buffer.from(u.scriptPubKey, "hex");
882
+ const decompiledScript = bitcoinScript2.decompile(script4);
883
+ if (!decompiledScript) {
884
+ return acc;
885
+ }
886
+ return acc + getInputSizeByScript(script4);
887
+ }, 0);
888
+ const outputSize = outputs.reduce((acc, output) => {
889
+ if (isOP_RETURN(output.scriptPubKey)) {
890
+ return acc + output.scriptPubKey.length + OP_RETURN_OUTPUT_VALUE_SIZE + OP_RETURN_VALUE_SERIALIZE_SIZE;
891
+ }
892
+ return acc + MAX_NON_LEGACY_OUTPUT_SIZE;
893
+ }, 0);
894
+ return inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD;
895
+ };
896
+ var rateBasedTxBufferFee = (feeRate) => {
897
+ return feeRate <= WALLET_RELAY_FEE_RATE_THRESHOLD ? LOW_RATE_ESTIMATION_ACCURACY_BUFFER : 0;
898
+ };
899
+
900
+ // src/constants/psbt.ts
901
+ var NON_RBF_SEQUENCE = 4294967295;
902
+ var TRANSACTION_VERSION = 2;
903
+
904
+ // src/staking/transactions.ts
905
+ var BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 5e8;
906
+ var BTC_SLASHING_FRACTION_DIGITS = 4;
907
+ function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) {
908
+ if (amount <= 0 || feeRate <= 0) {
909
+ throw new Error("Amount and fee rate must be bigger than 0");
1157
910
  }
1158
- if (unbondingTx.ins.length !== 1) {
1159
- throw new Error("Unbonding transaction must have exactly one input");
911
+ if (!isValidBitcoinAddress(changeAddress, network)) {
912
+ throw new Error("Invalid change address");
1160
913
  }
1161
- validateUnbondingOutput(scripts, unbondingTx, network);
914
+ const stakingOutputs = buildStakingTransactionOutputs(scripts, network, amount);
915
+ const { selectedUTXOs, fee } = getStakingTxInputUTXOsAndFees(
916
+ inputUTXOs,
917
+ amount,
918
+ feeRate,
919
+ stakingOutputs
920
+ );
921
+ const tx = new Transaction3();
922
+ tx.version = TRANSACTION_VERSION;
923
+ for (let i = 0; i < selectedUTXOs.length; ++i) {
924
+ const input = selectedUTXOs[i];
925
+ tx.addInput(
926
+ transactionIdToHash(input.txid),
927
+ input.vout,
928
+ NON_RBF_SEQUENCE
929
+ );
930
+ }
931
+ stakingOutputs.forEach((o) => {
932
+ tx.addOutput(o.scriptPubKey, o.value);
933
+ });
934
+ const inputsSum = inputValueSum(selectedUTXOs);
935
+ if (inputsSum - (amount + fee) > BTC_DUST_SAT) {
936
+ tx.addOutput(
937
+ address3.toOutputScript(changeAddress, network),
938
+ inputsSum - (amount + fee)
939
+ );
940
+ }
941
+ if (lockHeight) {
942
+ if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) {
943
+ throw new Error("Invalid lock height");
944
+ }
945
+ tx.locktime = lockHeight;
946
+ }
947
+ return {
948
+ transaction: tx,
949
+ fee
950
+ };
951
+ }
952
+ function stakingExpansionTransaction(network, scripts, amount, changeAddress, feeRate, inputUTXOs, previousStakingTxInfo) {
953
+ if (amount <= 0 || feeRate <= 0) {
954
+ throw new Error("Amount and fee rate must be bigger than 0");
955
+ } else if (!isValidBitcoinAddress(changeAddress, network)) {
956
+ throw new Error("Invalid BTC change address");
957
+ }
958
+ const previousStakingOutputInfo = deriveStakingOutputInfo(
959
+ previousStakingTxInfo.scripts,
960
+ network
961
+ );
962
+ const previousStakingOutputIndex = findMatchingTxOutputIndex(
963
+ previousStakingTxInfo.stakingTx,
964
+ previousStakingOutputInfo.outputAddress,
965
+ network
966
+ );
967
+ const previousStakingAmount = previousStakingTxInfo.stakingTx.outs[previousStakingOutputIndex].value;
968
+ if (amount !== previousStakingAmount) {
969
+ throw new Error(
970
+ "Expansion staking transaction amount must be equal to the previous staking amount. Increase of the staking amount is not supported yet."
971
+ );
972
+ }
973
+ const stakingOutputs = buildStakingTransactionOutputs(
974
+ scripts,
975
+ network,
976
+ amount
977
+ );
978
+ const { selectedUTXO, fee } = getStakingExpansionTxFundingUTXOAndFees(
979
+ inputUTXOs,
980
+ feeRate,
981
+ stakingOutputs
982
+ );
983
+ const tx = new Transaction3();
984
+ tx.version = TRANSACTION_VERSION;
985
+ tx.addInput(
986
+ previousStakingTxInfo.stakingTx.getHash(),
987
+ previousStakingOutputIndex,
988
+ NON_RBF_SEQUENCE
989
+ );
990
+ tx.addInput(
991
+ transactionIdToHash(selectedUTXO.txid),
992
+ selectedUTXO.vout,
993
+ NON_RBF_SEQUENCE
994
+ );
995
+ stakingOutputs.forEach((o) => {
996
+ tx.addOutput(o.scriptPubKey, o.value);
997
+ });
998
+ if (selectedUTXO.value - fee > BTC_DUST_SAT) {
999
+ tx.addOutput(
1000
+ address3.toOutputScript(changeAddress, network),
1001
+ selectedUTXO.value - fee
1002
+ );
1003
+ }
1004
+ return {
1005
+ transaction: tx,
1006
+ fee,
1007
+ fundingUTXO: selectedUTXO
1008
+ };
1009
+ }
1010
+ function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) {
1011
+ const scriptTree = [
1012
+ {
1013
+ output: scripts.slashingScript
1014
+ },
1015
+ { output: scripts.unbondingTimelockScript }
1016
+ ];
1017
+ return withdrawalTransaction(
1018
+ {
1019
+ timelockScript: scripts.unbondingTimelockScript
1020
+ },
1021
+ scriptTree,
1022
+ unbondingTx,
1023
+ withdrawalAddress,
1024
+ network,
1025
+ feeRate,
1026
+ 0
1027
+ // unbonding always has a single output
1028
+ );
1029
+ }
1030
+ function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
1031
+ const scriptTree = [
1032
+ {
1033
+ output: scripts.slashingScript
1034
+ },
1035
+ [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
1036
+ ];
1037
+ return withdrawalTransaction(
1038
+ scripts,
1039
+ scriptTree,
1040
+ tx,
1041
+ withdrawalAddress,
1042
+ network,
1043
+ feeRate,
1044
+ outputIndex
1045
+ );
1046
+ }
1047
+ function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) {
1048
+ const scriptTree = { output: scripts.unbondingTimelockScript };
1049
+ return withdrawalTransaction(
1050
+ {
1051
+ timelockScript: scripts.unbondingTimelockScript
1052
+ },
1053
+ scriptTree,
1054
+ slashingTx,
1055
+ withdrawalAddress,
1056
+ network,
1057
+ feeRate,
1058
+ outputIndex
1059
+ );
1060
+ }
1061
+ function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
1062
+ if (feeRate <= 0) {
1063
+ throw new Error("Withdrawal feeRate must be bigger than 0");
1064
+ }
1065
+ if (outputIndex < 0) {
1066
+ throw new Error("Output index must be bigger or equal to 0");
1067
+ }
1068
+ const timePosition = 2;
1069
+ const decompiled = script2.decompile(scripts.timelockScript);
1070
+ if (!decompiled) {
1071
+ throw new Error("Timelock script is not valid");
1072
+ }
1073
+ let timelock = 0;
1074
+ if (typeof decompiled[timePosition] !== "number") {
1075
+ const timeBuffer = decompiled[timePosition];
1076
+ timelock = script2.number.decode(timeBuffer);
1077
+ } else {
1078
+ const wrap = decompiled[timePosition] % 16;
1079
+ timelock = wrap === 0 ? 16 : wrap;
1080
+ }
1081
+ const redeem = {
1082
+ output: scripts.timelockScript,
1083
+ redeemVersion: REDEEM_VERSION
1084
+ };
1085
+ const p2tr = payments5.p2tr({
1086
+ internalPubkey,
1087
+ scriptTree,
1088
+ redeem,
1089
+ network
1090
+ });
1091
+ const tapLeafScript = {
1092
+ leafVersion: redeem.redeemVersion,
1093
+ script: redeem.output,
1094
+ controlBlock: p2tr.witness[p2tr.witness.length - 1]
1095
+ };
1162
1096
  const psbt = new Psbt2({ network });
1163
- if (unbondingTx.version !== void 0) {
1164
- psbt.setVersion(unbondingTx.version);
1097
+ psbt.setVersion(TRANSACTION_VERSION);
1098
+ psbt.addInput({
1099
+ hash: tx.getHash(),
1100
+ index: outputIndex,
1101
+ tapInternalKey: internalPubkey,
1102
+ witnessUtxo: {
1103
+ value: tx.outs[outputIndex].value,
1104
+ script: tx.outs[outputIndex].script
1105
+ },
1106
+ tapLeafScript: [tapLeafScript],
1107
+ sequence: timelock
1108
+ });
1109
+ const estimatedFee = getWithdrawTxFee(feeRate);
1110
+ const outputValue = tx.outs[outputIndex].value - estimatedFee;
1111
+ if (outputValue < 0) {
1112
+ throw new Error(
1113
+ "Not enough funds to cover the fee for withdrawal transaction"
1114
+ );
1115
+ }
1116
+ if (outputValue < BTC_DUST_SAT) {
1117
+ throw new Error("Output value is less than dust limit");
1118
+ }
1119
+ psbt.addOutput({
1120
+ address: withdrawalAddress,
1121
+ value: outputValue
1122
+ });
1123
+ psbt.setLocktime(0);
1124
+ return {
1125
+ psbt,
1126
+ fee: estimatedFee
1127
+ };
1128
+ }
1129
+ function slashTimelockUnbondedTransaction(scripts, stakingTransaction2, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
1130
+ const slashingScriptTree = [
1131
+ {
1132
+ output: scripts.slashingScript
1133
+ },
1134
+ [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
1135
+ ];
1136
+ return slashingTransaction(
1137
+ {
1138
+ unbondingTimelockScript: scripts.unbondingTimelockScript,
1139
+ slashingScript: scripts.slashingScript
1140
+ },
1141
+ slashingScriptTree,
1142
+ stakingTransaction2,
1143
+ slashingPkScriptHex,
1144
+ slashingRate,
1145
+ minimumFee,
1146
+ network,
1147
+ outputIndex
1148
+ );
1149
+ }
1150
+ function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) {
1151
+ const unbondingScriptTree = [
1152
+ {
1153
+ output: scripts.slashingScript
1154
+ },
1155
+ {
1156
+ output: scripts.unbondingTimelockScript
1157
+ }
1158
+ ];
1159
+ return slashingTransaction(
1160
+ {
1161
+ unbondingTimelockScript: scripts.unbondingTimelockScript,
1162
+ slashingScript: scripts.slashingScript
1163
+ },
1164
+ unbondingScriptTree,
1165
+ unbondingTx,
1166
+ slashingPkScriptHex,
1167
+ slashingRate,
1168
+ minimumSlashingFee,
1169
+ network,
1170
+ 0
1171
+ // unbonding always has a single output
1172
+ );
1173
+ }
1174
+ function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
1175
+ if (slashingRate <= 0 || slashingRate >= 1) {
1176
+ throw new Error("Slashing rate must be between 0 and 1");
1177
+ }
1178
+ slashingRate = parseFloat(slashingRate.toFixed(BTC_SLASHING_FRACTION_DIGITS));
1179
+ if (minimumFee <= 0 || !Number.isInteger(minimumFee)) {
1180
+ throw new Error("Minimum fee must be a positve integer");
1181
+ }
1182
+ if (outputIndex < 0 || !Number.isInteger(outputIndex)) {
1183
+ throw new Error("Output index must be an integer bigger or equal to 0");
1165
1184
  }
1166
- if (unbondingTx.locktime !== void 0) {
1167
- psbt.setLocktime(unbondingTx.locktime);
1185
+ if (!transaction.outs[outputIndex]) {
1186
+ throw new Error("Output index is out of range");
1168
1187
  }
1169
- const input = unbondingTx.ins[0];
1170
- const outputIndex = input.index;
1171
- const inputScriptTree = [
1172
- { output: scripts.slashingScript },
1173
- [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
1174
- ];
1175
- const inputRedeem = {
1176
- output: scripts.unbondingScript,
1188
+ const redeem = {
1189
+ output: scripts.slashingScript,
1177
1190
  redeemVersion: REDEEM_VERSION
1178
1191
  };
1179
1192
  const p2tr = payments5.p2tr({
1180
1193
  internalPubkey,
1181
- scriptTree: inputScriptTree,
1182
- redeem: inputRedeem,
1194
+ scriptTree,
1195
+ redeem,
1183
1196
  network
1184
1197
  });
1185
- const inputTapLeafScript = {
1186
- leafVersion: inputRedeem.redeemVersion,
1187
- script: inputRedeem.output,
1198
+ const tapLeafScript = {
1199
+ leafVersion: redeem.redeemVersion,
1200
+ script: redeem.output,
1188
1201
  controlBlock: p2tr.witness[p2tr.witness.length - 1]
1189
1202
  };
1203
+ const stakingAmount = transaction.outs[outputIndex].value;
1204
+ const slashingAmount = Math.round(stakingAmount * slashingRate);
1205
+ const slashingOutput = Buffer.from(slashingPkScriptHex, "hex");
1206
+ if (opcodes3.OP_RETURN != slashingOutput[0]) {
1207
+ if (slashingAmount <= BTC_DUST_SAT) {
1208
+ throw new Error("Slashing amount is less than dust limit");
1209
+ }
1210
+ }
1211
+ const userFunds = stakingAmount - slashingAmount - minimumFee;
1212
+ if (userFunds <= BTC_DUST_SAT) {
1213
+ throw new Error("User funds are less than dust limit");
1214
+ }
1215
+ const psbt = new Psbt2({ network });
1216
+ psbt.setVersion(TRANSACTION_VERSION);
1190
1217
  psbt.addInput({
1191
- hash: input.hash,
1192
- index: input.index,
1193
- sequence: input.sequence,
1218
+ hash: transaction.getHash(),
1219
+ index: outputIndex,
1194
1220
  tapInternalKey: internalPubkey,
1195
1221
  witnessUtxo: {
1196
- value: stakingTx.outs[outputIndex].value,
1197
- script: stakingTx.outs[outputIndex].script
1222
+ value: stakingAmount,
1223
+ script: transaction.outs[outputIndex].script
1198
1224
  },
1199
- tapLeafScript: [inputTapLeafScript]
1225
+ tapLeafScript: [tapLeafScript],
1226
+ // not RBF-able
1227
+ sequence: NON_RBF_SEQUENCE
1200
1228
  });
1201
1229
  psbt.addOutput({
1202
- script: unbondingTx.outs[0].script,
1203
- value: unbondingTx.outs[0].value
1230
+ script: slashingOutput,
1231
+ value: slashingAmount
1204
1232
  });
1205
- return psbt;
1233
+ const changeOutput = payments5.p2tr({
1234
+ internalPubkey,
1235
+ scriptTree: { output: scripts.unbondingTimelockScript },
1236
+ network
1237
+ });
1238
+ psbt.addOutput({
1239
+ address: changeOutput.address,
1240
+ value: userFunds
1241
+ });
1242
+ psbt.setLocktime(0);
1243
+ return { psbt };
1244
+ }
1245
+ function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) {
1246
+ if (unbondingFee <= 0) {
1247
+ throw new Error("Unbonding fee must be bigger than 0");
1248
+ }
1249
+ if (outputIndex < 0) {
1250
+ throw new Error("Output index must be bigger or equal to 0");
1251
+ }
1252
+ const tx = new Transaction3();
1253
+ tx.version = TRANSACTION_VERSION;
1254
+ tx.addInput(
1255
+ stakingTx.getHash(),
1256
+ outputIndex,
1257
+ NON_RBF_SEQUENCE
1258
+ // not RBF-able
1259
+ );
1260
+ const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
1261
+ const outputValue = stakingTx.outs[outputIndex].value - unbondingFee;
1262
+ if (outputValue < BTC_DUST_SAT) {
1263
+ throw new Error("Output value is less than dust limit for unbonding transaction");
1264
+ }
1265
+ if (!unbondingOutputInfo.outputAddress) {
1266
+ throw new Error("Unbonding output address is not defined");
1267
+ }
1268
+ tx.addOutput(
1269
+ unbondingOutputInfo.scriptPubKey,
1270
+ outputValue
1271
+ );
1272
+ tx.locktime = 0;
1273
+ return {
1274
+ transaction: tx,
1275
+ fee: unbondingFee
1276
+ };
1277
+ }
1278
+ var createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => {
1279
+ if (covenantSigs.length < covenantQuorum) {
1280
+ throw new Error(
1281
+ `Not enough covenant signatures. Required: ${covenantQuorum}, got: ${covenantSigs.length}`
1282
+ );
1283
+ }
1284
+ const filteredCovenantSigs = covenantSigs.filter((sig) => {
1285
+ const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex");
1286
+ return paramsCovenants.some((covenant) => covenant.equals(btcPkHexBuf));
1287
+ });
1288
+ if (filteredCovenantSigs.length < covenantQuorum) {
1289
+ throw new Error(
1290
+ `Not enough valid covenant signatures. Required: ${covenantQuorum}, got: ${filteredCovenantSigs.length}`
1291
+ );
1292
+ }
1293
+ const covenantSigsBuffers = covenantSigs.slice(0, covenantQuorum).map((sig) => ({
1294
+ btcPkHex: Buffer.from(sig.btcPkHex, "hex"),
1295
+ sigHex: Buffer.from(sig.sigHex, "hex")
1296
+ }));
1297
+ const paramsCovenantsSorted = [...paramsCovenants].sort(Buffer.compare).reverse();
1298
+ const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => {
1299
+ const covenantSig = covenantSigsBuffers.find(
1300
+ (sig) => sig.btcPkHex.compare(covenant) === 0
1301
+ );
1302
+ return covenantSig?.sigHex || Buffer.alloc(0);
1303
+ });
1304
+ return [...composedCovenantSigs, ...originalWitness];
1305
+ };
1306
+
1307
+ // src/constants/unbonding.ts
1308
+ var MIN_UNBONDING_OUTPUT_VALUE = 1e3;
1309
+
1310
+ // src/utils/babylon.ts
1311
+ import { fromBech32 } from "@cosmjs/encoding";
1312
+ var isValidBabylonAddress = (address4) => {
1313
+ try {
1314
+ const { prefix } = fromBech32(address4);
1315
+ return prefix === "bbn";
1316
+ } catch (error) {
1317
+ return false;
1318
+ }
1319
+ };
1320
+
1321
+ // src/utils/staking/validation.ts
1322
+ var validateStakingExpansionInputs = ({
1323
+ babylonBtcTipHeight,
1324
+ inputUTXOs,
1325
+ stakingInput,
1326
+ previousStakingInput,
1327
+ babylonAddress
1328
+ }) => {
1329
+ if (babylonBtcTipHeight === 0) {
1330
+ throw new StakingError(
1331
+ "INVALID_INPUT" /* INVALID_INPUT */,
1332
+ "Babylon BTC tip height cannot be 0"
1333
+ );
1334
+ }
1335
+ if (!inputUTXOs || inputUTXOs.length === 0) {
1336
+ throw new StakingError(
1337
+ "INVALID_INPUT" /* INVALID_INPUT */,
1338
+ "No input UTXOs provided"
1339
+ );
1340
+ }
1341
+ if (babylonAddress && !isValidBabylonAddress(babylonAddress)) {
1342
+ throw new StakingError(
1343
+ "INVALID_INPUT" /* INVALID_INPUT */,
1344
+ "Invalid Babylon address"
1345
+ );
1346
+ }
1347
+ if (stakingInput.stakingAmountSat !== previousStakingInput.stakingAmountSat) {
1348
+ throw new StakingError(
1349
+ "INVALID_INPUT" /* INVALID_INPUT */,
1350
+ "Staking expansion amount must equal the previous staking amount"
1351
+ );
1352
+ }
1353
+ const currentFPs = stakingInput.finalityProviderPksNoCoordHex;
1354
+ const previousFPs = previousStakingInput.finalityProviderPksNoCoordHex;
1355
+ const missingPreviousFPs = previousFPs.filter((prevFp) => !currentFPs.includes(prevFp));
1356
+ if (missingPreviousFPs.length > 0) {
1357
+ throw new StakingError(
1358
+ "INVALID_INPUT" /* INVALID_INPUT */,
1359
+ `Invalid staking expansion: all finality providers from the previous
1360
+ staking must be included. Missing: ${missingPreviousFPs.join(", ")}`
1361
+ );
1362
+ }
1363
+ };
1364
+ var validateStakingTxInputData = (stakingAmountSat, timelock, params, inputUTXOs, feeRate) => {
1365
+ if (stakingAmountSat < params.minStakingAmountSat || stakingAmountSat > params.maxStakingAmountSat) {
1366
+ throw new StakingError(
1367
+ "INVALID_INPUT" /* INVALID_INPUT */,
1368
+ "Invalid staking amount"
1369
+ );
1370
+ }
1371
+ if (timelock < params.minStakingTimeBlocks || timelock > params.maxStakingTimeBlocks) {
1372
+ throw new StakingError("INVALID_INPUT" /* INVALID_INPUT */, "Invalid timelock");
1373
+ }
1374
+ if (inputUTXOs.length == 0) {
1375
+ throw new StakingError(
1376
+ "INVALID_INPUT" /* INVALID_INPUT */,
1377
+ "No input UTXOs provided"
1378
+ );
1379
+ }
1380
+ if (feeRate <= 0) {
1381
+ throw new StakingError("INVALID_INPUT" /* INVALID_INPUT */, "Invalid fee rate");
1382
+ }
1383
+ };
1384
+ var validateParams = (params) => {
1385
+ if (params.covenantNoCoordPks.length == 0) {
1386
+ throw new StakingError(
1387
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1388
+ "Could not find any covenant public keys"
1389
+ );
1390
+ }
1391
+ if (params.covenantNoCoordPks.length < params.covenantQuorum) {
1392
+ throw new StakingError(
1393
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1394
+ "Covenant public keys must be greater than or equal to the quorum"
1395
+ );
1396
+ }
1397
+ params.covenantNoCoordPks.forEach((pk) => {
1398
+ if (!isValidNoCoordPublicKey(pk)) {
1399
+ throw new StakingError(
1400
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1401
+ "Covenant public key should contains no coordinate"
1402
+ );
1403
+ }
1404
+ });
1405
+ if (params.unbondingTime <= 0) {
1406
+ throw new StakingError(
1407
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1408
+ "Unbonding time must be greater than 0"
1409
+ );
1410
+ }
1411
+ if (params.unbondingFeeSat <= 0) {
1412
+ throw new StakingError(
1413
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1414
+ "Unbonding fee must be greater than 0"
1415
+ );
1416
+ }
1417
+ if (params.maxStakingAmountSat < params.minStakingAmountSat) {
1418
+ throw new StakingError(
1419
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1420
+ "Max staking amount must be greater or equal to min staking amount"
1421
+ );
1422
+ }
1423
+ if (params.minStakingAmountSat < params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE) {
1424
+ throw new StakingError(
1425
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1426
+ `Min staking amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`
1427
+ );
1428
+ }
1429
+ if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) {
1430
+ throw new StakingError(
1431
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1432
+ "Max staking time must be greater or equal to min staking time"
1433
+ );
1434
+ }
1435
+ if (params.minStakingTimeBlocks <= 0) {
1436
+ throw new StakingError(
1437
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1438
+ "Min staking time must be greater than 0"
1439
+ );
1440
+ }
1441
+ if (params.covenantQuorum <= 0) {
1442
+ throw new StakingError(
1443
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1444
+ "Covenant quorum must be greater than 0"
1445
+ );
1446
+ }
1447
+ if (params.slashing) {
1448
+ if (params.slashing.slashingRate <= 0) {
1449
+ throw new StakingError(
1450
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1451
+ "Slashing rate must be greater than 0"
1452
+ );
1453
+ }
1454
+ if (params.slashing.slashingRate > 1) {
1455
+ throw new StakingError(
1456
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1457
+ "Slashing rate must be less or equal to 1"
1458
+ );
1459
+ }
1460
+ if (params.slashing.slashingPkScriptHex.length == 0) {
1461
+ throw new StakingError(
1462
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1463
+ "Slashing public key script is missing"
1464
+ );
1465
+ }
1466
+ if (params.slashing.minSlashingTxFeeSat <= 0) {
1467
+ throw new StakingError(
1468
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
1469
+ "Minimum slashing transaction fee must be greater than 0"
1470
+ );
1471
+ }
1472
+ }
1206
1473
  };
1207
- var validateUnbondingOutput = (scripts, unbondingTx, network) => {
1208
- const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
1209
- if (unbondingOutputInfo.scriptPubKey.toString("hex") !== unbondingTx.outs[0].script.toString("hex")) {
1210
- throw new Error(
1211
- "Unbonding output script does not match the expected script while building psbt"
1474
+ var validateStakingTimelock = (stakingTimelock, params) => {
1475
+ if (stakingTimelock < params.minStakingTimeBlocks || stakingTimelock > params.maxStakingTimeBlocks) {
1476
+ throw new StakingError(
1477
+ "INVALID_INPUT" /* INVALID_INPUT */,
1478
+ "Staking transaction timelock is out of range"
1479
+ );
1480
+ }
1481
+ };
1482
+ var validateStakingExpansionCovenantQuorum = (paramsForPreviousStakingTx, paramsForCurrentStakingTx) => {
1483
+ const previousCovenantMembers = paramsForPreviousStakingTx.covenantNoCoordPks;
1484
+ const currentCovenantMembers = paramsForCurrentStakingTx.covenantNoCoordPks;
1485
+ const requiredQuorum = paramsForPreviousStakingTx.covenantQuorum;
1486
+ const activePreviousMembers = previousCovenantMembers.filter(
1487
+ (prevMember) => currentCovenantMembers.includes(prevMember)
1488
+ ).length;
1489
+ if (activePreviousMembers < requiredQuorum) {
1490
+ throw new StakingError(
1491
+ "INVALID_INPUT" /* INVALID_INPUT */,
1492
+ `Staking expansion failed: insufficient covenant quorum. Required: ${requiredQuorum}, Available: ${activePreviousMembers}. Too many covenant members have rotated out.`
1212
1493
  );
1213
1494
  }
1214
1495
  };
1215
1496
 
1216
1497
  // src/staking/index.ts
1217
- var Staking = class {
1218
- constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) {
1498
+ var Staking = class _Staking {
1499
+ constructor(network, stakerInfo, params, finalityProviderPksNoCoordHex, stakingTimelock) {
1219
1500
  if (!isValidBitcoinAddress(stakerInfo.address, network)) {
1220
1501
  throw new StakingError(
1221
1502
  "INVALID_INPUT" /* INVALID_INPUT */,
@@ -1228,10 +1509,10 @@ var Staking = class {
1228
1509
  "Invalid staker public key"
1229
1510
  );
1230
1511
  }
1231
- if (!isValidNoCoordPublicKey(finalityProviderPkNoCoordHex)) {
1512
+ if (finalityProviderPksNoCoordHex.length === 0 || !finalityProviderPksNoCoordHex.every(isValidNoCoordPublicKey)) {
1232
1513
  throw new StakingError(
1233
1514
  "INVALID_INPUT" /* INVALID_INPUT */,
1234
- "Invalid finality provider public key"
1515
+ "Invalid finality providers public keys"
1235
1516
  );
1236
1517
  }
1237
1518
  validateParams(params);
@@ -1239,14 +1520,14 @@ var Staking = class {
1239
1520
  this.network = network;
1240
1521
  this.stakerInfo = stakerInfo;
1241
1522
  this.params = params;
1242
- this.finalityProviderPkNoCoordHex = finalityProviderPkNoCoordHex;
1523
+ this.finalityProviderPksNoCoordHex = finalityProviderPksNoCoordHex;
1243
1524
  this.stakingTimelock = stakingTimelock;
1244
1525
  }
1245
1526
  /**
1246
1527
  * buildScripts builds the staking scripts for the staking transaction.
1247
1528
  * Note: different staking types may have different scripts.
1248
1529
  * e.g the observable staking script has a data embed script.
1249
- *
1530
+ *
1250
1531
  * @returns {StakingScripts} - The staking scripts.
1251
1532
  */
1252
1533
  buildScripts() {
@@ -1255,7 +1536,7 @@ var Staking = class {
1255
1536
  try {
1256
1537
  stakingScriptData = new StakingScriptData(
1257
1538
  Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
1258
- [Buffer.from(this.finalityProviderPkNoCoordHex, "hex")],
1539
+ this.finalityProviderPksNoCoordHex.map((pk) => Buffer.from(pk, "hex")),
1259
1540
  toBuffers(covenantNoCoordPks),
1260
1541
  covenantQuorum,
1261
1542
  this.stakingTimelock,
@@ -1282,9 +1563,9 @@ var Staking = class {
1282
1563
  }
1283
1564
  /**
1284
1565
  * Create a staking transaction for staking.
1285
- *
1566
+ *
1286
1567
  * @param {number} stakingAmountSat - The amount to stake in satoshis.
1287
- * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
1568
+ * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
1288
1569
  * transaction.
1289
1570
  * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
1290
1571
  * @returns {TransactionResult} - An object containing the unsigned
@@ -1322,10 +1603,85 @@ var Staking = class {
1322
1603
  }
1323
1604
  }
1324
1605
  /**
1325
- * Create a staking psbt based on the existing staking transaction.
1606
+ * Creates a staking expansion transaction that extends an existing BTC stake
1607
+ * to new finality providers or renews the timelock.
1326
1608
  *
1609
+ * This method implements RFC 037 BTC Stake Expansion,
1610
+ * allowing existing active BTC staking transactions
1611
+ * to extend their delegation to new finality providers without going through
1612
+ * the full unbonding process.
1613
+ *
1614
+ * The expansion transaction:
1615
+ * 1. Spends the previous staking transaction output as the first input
1616
+ * 2. Uses funding UTXO as additional input to cover transaction fees or
1617
+ * to increase the staking amount
1618
+ * 3. Creates a new staking output with expanded finality provider coverage or
1619
+ * renews the timelock
1620
+ * 4. Has an output returning the remaining funds as change (if any) to the
1621
+ * staker BTC address
1622
+ *
1623
+ * @param {number} stakingAmountSat - The total staking amount in satoshis
1624
+ * (The amount had to be equal to the previous staking amount for now, this
1625
+ * lib does not yet support increasing the staking amount at this stage)
1626
+ * @param {UTXO[]} inputUTXOs - Available UTXOs to use for funding the
1627
+ * expansion transaction fees. Only one will be selected for the expansion
1628
+ * @param {number} feeRate - Fee rate in satoshis per byte for the
1629
+ * expansion transaction
1630
+ * @param {StakingParams} paramsForPreviousStakingTx - Staking parameters
1631
+ * used in the previous staking transaction
1632
+ * @param {Object} previousStakingTxInfo - Necessary information to spend the
1633
+ * previous staking transaction.
1634
+ * @returns {TransactionResult & { fundingUTXO: UTXO }} - An object containing
1635
+ * the unsigned expansion transaction and calculated fee, and the funding UTXO
1636
+ * @throws {StakingError} - If the transaction cannot be built or validation
1637
+ * fails
1638
+ */
1639
+ createStakingExpansionTransaction(stakingAmountSat, inputUTXOs, feeRate, paramsForPreviousStakingTx, previousStakingTxInfo) {
1640
+ validateStakingTxInputData(
1641
+ stakingAmountSat,
1642
+ this.stakingTimelock,
1643
+ this.params,
1644
+ inputUTXOs,
1645
+ feeRate
1646
+ );
1647
+ validateStakingExpansionCovenantQuorum(
1648
+ paramsForPreviousStakingTx,
1649
+ this.params
1650
+ );
1651
+ const previousStaking = new _Staking(
1652
+ this.network,
1653
+ this.stakerInfo,
1654
+ paramsForPreviousStakingTx,
1655
+ previousStakingTxInfo.stakingInput.finalityProviderPksNoCoordHex,
1656
+ previousStakingTxInfo.stakingInput.stakingTimelock
1657
+ );
1658
+ const {
1659
+ transaction: stakingExpansionTx,
1660
+ fee: stakingExpansionTxFee,
1661
+ fundingUTXO
1662
+ } = stakingExpansionTransaction(
1663
+ this.network,
1664
+ this.buildScripts(),
1665
+ stakingAmountSat,
1666
+ this.stakerInfo.address,
1667
+ feeRate,
1668
+ inputUTXOs,
1669
+ {
1670
+ stakingTx: previousStakingTxInfo.stakingTx,
1671
+ scripts: previousStaking.buildScripts()
1672
+ }
1673
+ );
1674
+ return {
1675
+ transaction: stakingExpansionTx,
1676
+ fee: stakingExpansionTxFee,
1677
+ fundingUTXO
1678
+ };
1679
+ }
1680
+ /**
1681
+ * Create a staking psbt based on the existing staking transaction.
1682
+ *
1327
1683
  * @param {Transaction} stakingTx - The staking transaction.
1328
- * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
1684
+ * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
1329
1685
  * transaction. The UTXOs that were used to create the staking transaction should
1330
1686
  * be included in this array.
1331
1687
  * @returns {Psbt} - The psbt.
@@ -1342,15 +1698,54 @@ var Staking = class {
1342
1698
  stakingTx,
1343
1699
  this.network,
1344
1700
  inputUTXOs,
1345
- isTaproot(
1346
- this.stakerInfo.address,
1347
- this.network
1348
- ) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
1701
+ isTaproot(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
1702
+ );
1703
+ }
1704
+ /**
1705
+ * Convert a staking expansion transaction to a PSBT.
1706
+ *
1707
+ * @param {Transaction} stakingExpansionTx - The staking expansion
1708
+ * transaction to convert
1709
+ * @param {UTXO[]} inputUTXOs - Available UTXOs for the
1710
+ * funding input (second input)
1711
+ * @param {StakingParams} paramsForPreviousStakingTx - Staking parameters
1712
+ * used for the previous staking transaction
1713
+ * @param {Object} previousStakingTxInfo - Information about the previous
1714
+ * staking transaction
1715
+ * @returns {Psbt} The PSBT for the staking expansion transaction
1716
+ * @throws {Error} If the previous staking output cannot be found or
1717
+ * validation fails
1718
+ */
1719
+ toStakingExpansionPsbt(stakingExpansionTx, inputUTXOs, paramsForPreviousStakingTx, previousStakingTxInfo) {
1720
+ const previousStaking = new _Staking(
1721
+ this.network,
1722
+ this.stakerInfo,
1723
+ paramsForPreviousStakingTx,
1724
+ previousStakingTxInfo.stakingInput.finalityProviderPksNoCoordHex,
1725
+ previousStakingTxInfo.stakingInput.stakingTimelock
1726
+ );
1727
+ const previousScripts = previousStaking.buildScripts();
1728
+ const { outputAddress } = deriveStakingOutputInfo(previousScripts, this.network);
1729
+ const previousStakingOutputIndex = findMatchingTxOutputIndex(
1730
+ previousStakingTxInfo.stakingTx,
1731
+ outputAddress,
1732
+ this.network
1733
+ );
1734
+ return stakingExpansionPsbt(
1735
+ this.network,
1736
+ stakingExpansionTx,
1737
+ {
1738
+ stakingTx: previousStakingTxInfo.stakingTx,
1739
+ outputIndex: previousStakingOutputIndex
1740
+ },
1741
+ inputUTXOs,
1742
+ previousScripts,
1743
+ isTaproot(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
1349
1744
  );
1350
1745
  }
1351
1746
  /**
1352
1747
  * Create an unbonding transaction for staking.
1353
- *
1748
+ *
1354
1749
  * @param {Transaction} stakingTx - The staking transaction to unbond.
1355
1750
  * @returns {TransactionResult} - An object containing the unsigned
1356
1751
  * transaction, and fee
@@ -1387,10 +1782,10 @@ var Staking = class {
1387
1782
  /**
1388
1783
  * Create an unbonding psbt based on the existing unbonding transaction and
1389
1784
  * staking transaction.
1390
- *
1785
+ *
1391
1786
  * @param {Transaction} unbondingTx - The unbonding transaction.
1392
1787
  * @param {Transaction} stakingTx - The staking transaction.
1393
- *
1788
+ *
1394
1789
  * @returns {Psbt} - The psbt.
1395
1790
  */
1396
1791
  toUnbondingPsbt(unbondingTx, stakingTx) {
@@ -1405,7 +1800,7 @@ var Staking = class {
1405
1800
  * Creates a withdrawal transaction that spends from an unbonding or slashing
1406
1801
  * transaction. The timelock on the input transaction must have expired before
1407
1802
  * this withdrawal can be valid.
1408
- *
1803
+ *
1409
1804
  * @param {Transaction} earlyUnbondedTx - The unbonding or slashing
1410
1805
  * transaction to withdraw from
1411
1806
  * @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal
@@ -1433,9 +1828,9 @@ var Staking = class {
1433
1828
  }
1434
1829
  }
1435
1830
  /**
1436
- * Create a withdrawal psbt that spends a naturally expired staking
1831
+ * Create a withdrawal psbt that spends a naturally expired staking
1437
1832
  * transaction.
1438
- *
1833
+ *
1439
1834
  * @param {Transaction} stakingTx - The staking transaction to withdraw from.
1440
1835
  * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
1441
1836
  * @returns {PsbtResult} - An object containing the unsigned psbt and fee
@@ -1468,7 +1863,7 @@ var Staking = class {
1468
1863
  }
1469
1864
  /**
1470
1865
  * Create a slashing psbt spending from the staking output.
1471
- *
1866
+ *
1472
1867
  * @param {Transaction} stakingTx - The staking transaction to slash.
1473
1868
  * @returns {PsbtResult} - An object containing the unsigned psbt and fee
1474
1869
  * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
@@ -1481,6 +1876,12 @@ var Staking = class {
1481
1876
  );
1482
1877
  }
1483
1878
  const scripts = this.buildScripts();
1879
+ const { outputAddress } = deriveStakingOutputInfo(scripts, this.network);
1880
+ const stakingOutputIndex = findMatchingTxOutputIndex(
1881
+ stakingTx,
1882
+ outputAddress,
1883
+ this.network
1884
+ );
1484
1885
  try {
1485
1886
  const { psbt } = slashTimelockUnbondedTransaction(
1486
1887
  scripts,
@@ -1488,7 +1889,8 @@ var Staking = class {
1488
1889
  this.params.slashing.slashingPkScriptHex,
1489
1890
  this.params.slashing.slashingRate,
1490
1891
  this.params.slashing.minSlashingTxFeeSat,
1491
- this.network
1892
+ this.network,
1893
+ stakingOutputIndex
1492
1894
  );
1493
1895
  return {
1494
1896
  psbt,
@@ -1504,7 +1906,7 @@ var Staking = class {
1504
1906
  }
1505
1907
  /**
1506
1908
  * Create a slashing psbt for an unbonding output.
1507
- *
1909
+ *
1508
1910
  * @param {Transaction} unbondingTx - The unbonding transaction to slash.
1509
1911
  * @returns {PsbtResult} - An object containing the unsigned psbt and fee
1510
1912
  * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
@@ -1541,7 +1943,7 @@ var Staking = class {
1541
1943
  /**
1542
1944
  * Create a withdraw slashing psbt that spends a slashing transaction from the
1543
1945
  * staking output.
1544
- *
1946
+ *
1545
1947
  * @param {Transaction} slashingTx - The slashing transaction.
1546
1948
  * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
1547
1949
  * @returns {PsbtResult} - An object containing the unsigned psbt and fee
@@ -1561,267 +1963,37 @@ var Staking = class {
1561
1963
  slashingTx,
1562
1964
  this.stakerInfo.address,
1563
1965
  this.network,
1564
- feeRate,
1565
- slashingOutputIndex
1566
- );
1567
- } catch (error) {
1568
- throw StakingError.fromUnknown(
1569
- error,
1570
- "BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
1571
- "Cannot build withdraw slashing transaction"
1572
- );
1573
- }
1574
- }
1575
- };
1576
-
1577
- // src/staking/observable/observableStakingScript.ts
1578
- import { opcodes as opcodes3, script as script3 } from "bitcoinjs-lib";
1579
- var ObservableStakingScriptData = class extends StakingScriptData {
1580
- constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) {
1581
- super(
1582
- stakerKey,
1583
- finalityProviderKeys,
1584
- covenantKeys,
1585
- covenantThreshold,
1586
- stakingTimelock,
1587
- unbondingTimelock
1588
- );
1589
- if (!magicBytes) {
1590
- throw new Error("Missing required input values");
1591
- }
1592
- if (magicBytes.length != MAGIC_BYTES_LEN) {
1593
- throw new Error("Invalid script data provided");
1594
- }
1595
- this.magicBytes = magicBytes;
1596
- }
1597
- /**
1598
- * Builds a data embed script for staking in the form:
1599
- * OP_RETURN || <serializedStakingData>
1600
- * where serializedStakingData is the concatenation of:
1601
- * MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock
1602
- * Note: Only a single finality provider key is supported for now in phase 1
1603
- * @throws {Error} If the number of finality provider keys is not equal to 1.
1604
- * @returns {Buffer} The compiled data embed script.
1605
- */
1606
- buildDataEmbedScript() {
1607
- if (this.finalityProviderKeys.length != 1) {
1608
- throw new Error("Only a single finality provider key is supported");
1609
- }
1610
- const version = Buffer.alloc(1);
1611
- version.writeUInt8(0);
1612
- const stakingTimeLock = Buffer.alloc(2);
1613
- stakingTimeLock.writeUInt16BE(this.stakingTimeLock);
1614
- const serializedStakingData = Buffer.concat([
1615
- this.magicBytes,
1616
- version,
1617
- this.stakerKey,
1618
- this.finalityProviderKeys[0],
1619
- stakingTimeLock
1620
- ]);
1621
- return script3.compile([opcodes3.OP_RETURN, serializedStakingData]);
1622
- }
1623
- /**
1624
- * Builds the staking scripts.
1625
- * @returns {ObservableStakingScripts} The staking scripts that can be used to stake.
1626
- * contains the timelockScript, unbondingScript, slashingScript,
1627
- * unbondingTimelockScript, and dataEmbedScript.
1628
- * @throws {Error} If script data is invalid.
1629
- */
1630
- buildScripts() {
1631
- const scripts = super.buildScripts();
1632
- return {
1633
- ...scripts,
1634
- dataEmbedScript: this.buildDataEmbedScript()
1635
- };
1636
- }
1637
- };
1638
-
1639
- // src/staking/observable/index.ts
1640
- var ObservableStaking = class extends Staking {
1641
- constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) {
1642
- super(
1643
- network,
1644
- stakerInfo,
1645
- params,
1646
- finalityProviderPkNoCoordHex,
1647
- stakingTimelock
1648
- );
1649
- if (!params.tag) {
1650
- throw new StakingError(
1651
- "INVALID_INPUT" /* INVALID_INPUT */,
1652
- "Observable staking parameters must include tag"
1653
- );
1654
- }
1655
- if (!params.btcActivationHeight) {
1656
- throw new StakingError(
1657
- "INVALID_INPUT" /* INVALID_INPUT */,
1658
- "Observable staking parameters must include a positive activation height"
1659
- );
1660
- }
1661
- this.params = params;
1662
- }
1663
- /**
1664
- * Build the staking scripts for observable staking.
1665
- * This method overwrites the base method to include the OP_RETURN tag based
1666
- * on the tag provided in the parameters.
1667
- *
1668
- * @returns {ObservableStakingScripts} - The staking scripts for observable staking.
1669
- * @throws {StakingError} - If the scripts cannot be built.
1670
- */
1671
- buildScripts() {
1672
- const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params;
1673
- let stakingScriptData;
1674
- try {
1675
- stakingScriptData = new ObservableStakingScriptData(
1676
- Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
1677
- [Buffer.from(this.finalityProviderPkNoCoordHex, "hex")],
1678
- toBuffers(covenantNoCoordPks),
1679
- covenantQuorum,
1680
- this.stakingTimelock,
1681
- unbondingTime,
1682
- Buffer.from(tag, "hex")
1683
- );
1684
- } catch (error) {
1685
- throw StakingError.fromUnknown(
1686
- error,
1687
- "SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
1688
- "Cannot build staking script data"
1689
- );
1690
- }
1691
- let scripts;
1692
- try {
1693
- scripts = stakingScriptData.buildScripts();
1694
- } catch (error) {
1695
- throw StakingError.fromUnknown(
1696
- error,
1697
- "SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
1698
- "Cannot build staking scripts"
1699
- );
1700
- }
1701
- return scripts;
1702
- }
1703
- /**
1704
- * Create a staking transaction for observable staking.
1705
- * This overwrites the method from the Staking class with the addtion
1706
- * of the
1707
- * 1. OP_RETURN tag in the staking scripts
1708
- * 2. lockHeight parameter
1709
- *
1710
- * @param {number} stakingAmountSat - The amount to stake in satoshis.
1711
- * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
1712
- * transaction.
1713
- * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
1714
- * @returns {TransactionResult} - An object containing the unsigned transaction,
1715
- * and fee
1716
- */
1717
- createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
1718
- validateStakingTxInputData(
1719
- stakingAmountSat,
1720
- this.stakingTimelock,
1721
- this.params,
1722
- inputUTXOs,
1723
- feeRate
1724
- );
1725
- const scripts = this.buildScripts();
1726
- try {
1727
- const { transaction, fee } = stakingTransaction(
1728
- scripts,
1729
- stakingAmountSat,
1730
- this.stakerInfo.address,
1731
- inputUTXOs,
1732
- this.network,
1733
- feeRate,
1734
- // `lockHeight` is exclusive of the provided value.
1735
- // For example, if a Bitcoin height of X is provided,
1736
- // the transaction will be included starting from height X+1.
1737
- // https://learnmeabitcoin.com/technical/transaction/locktime/
1738
- this.params.btcActivationHeight - 1
1739
- );
1740
- return {
1741
- transaction,
1742
- fee
1743
- };
1966
+ feeRate,
1967
+ slashingOutputIndex
1968
+ );
1744
1969
  } catch (error) {
1745
1970
  throw StakingError.fromUnknown(
1746
1971
  error,
1747
1972
  "BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
1748
- "Cannot build unsigned staking transaction"
1973
+ "Cannot build withdraw slashing transaction"
1749
1974
  );
1750
1975
  }
1751
1976
  }
1752
- /**
1753
- * Create a staking psbt for observable staking.
1754
- *
1755
- * @param {Transaction} stakingTx - The staking transaction.
1756
- * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
1757
- * transaction.
1758
- * @returns {Psbt} - The psbt.
1759
- */
1760
- toStakingPsbt(stakingTx, inputUTXOs) {
1761
- return stakingPsbt(
1762
- stakingTx,
1763
- this.network,
1764
- inputUTXOs,
1765
- isTaproot(
1766
- this.stakerInfo.address,
1767
- this.network
1768
- ) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
1769
- );
1770
- }
1771
- };
1772
-
1773
- // src/utils/babylon.ts
1774
- import { fromBech32 } from "@cosmjs/encoding";
1775
- var isValidBabylonAddress = (address4) => {
1776
- try {
1777
- const { prefix } = fromBech32(address4);
1778
- return prefix === "bbn";
1779
- } catch (error) {
1780
- return false;
1781
- }
1782
- };
1783
-
1784
- // src/utils/staking/param.ts
1785
- var getBabylonParamByBtcHeight = (height, babylonParamsVersions) => {
1786
- const sortedParams = [...babylonParamsVersions].sort(
1787
- (a, b) => b.btcActivationHeight - a.btcActivationHeight
1788
- );
1789
- const params = sortedParams.find(
1790
- (p) => height >= p.btcActivationHeight
1791
- );
1792
- if (!params)
1793
- throw new Error(`Babylon params not found for height ${height}`);
1794
- return params;
1795
- };
1796
- var getBabylonParamByVersion = (version, babylonParams) => {
1797
- const params = babylonParams.find((p) => p.version === version);
1798
- if (!params)
1799
- throw new Error(`Babylon params not found for version ${version}`);
1800
- return params;
1801
1977
  };
1802
1978
 
1803
1979
  // src/staking/manager.ts
1804
- import { Psbt as Psbt3 } from "bitcoinjs-lib";
1805
- import { fromBech32 as fromBech322 } from "@cosmjs/encoding";
1806
- import {
1807
- btccheckpoint,
1808
- btcstaking,
1809
- btcstakingtx
1810
- } from "@babylonlabs-io/babylon-proto-ts";
1980
+ import { btccheckpoint, btcstaking, btcstakingtx } from "@babylonlabs-io/babylon-proto-ts";
1811
1981
  import {
1982
+ BIP322Sig,
1812
1983
  BTCSigType
1813
1984
  } from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop";
1985
+ import { Psbt as Psbt3, Transaction as Transaction4 } from "bitcoinjs-lib";
1814
1986
 
1815
1987
  // src/constants/registry.ts
1816
1988
  var BABYLON_REGISTRY_TYPE_URLS = {
1817
- MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation"
1989
+ MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation",
1990
+ MsgBtcStakeExpand: "/babylon.btcstaking.v1.MsgBtcStakeExpand"
1818
1991
  };
1819
1992
 
1820
1993
  // src/utils/index.ts
1821
1994
  var reverseBuffer = (buffer) => {
1822
1995
  const clonedBuffer = new Uint8Array(buffer);
1823
- if (clonedBuffer.length < 1)
1824
- return clonedBuffer;
1996
+ if (clonedBuffer.length < 1) return clonedBuffer;
1825
1997
  for (let i = 0, j = clonedBuffer.length - 1; i < clonedBuffer.length / 2; i++, j--) {
1826
1998
  let tmp = clonedBuffer[i];
1827
1999
  clonedBuffer[i] = clonedBuffer[j];
@@ -1829,32 +2001,57 @@ var reverseBuffer = (buffer) => {
1829
2001
  }
1830
2002
  return clonedBuffer;
1831
2003
  };
1832
- var uint8ArrayToHex = (uint8Array) => {
1833
- return Array.from(uint8Array).map((byte) => byte.toString(16).padStart(2, "0")).join("");
2004
+
2005
+ // src/utils/pop.ts
2006
+ import { sha256 } from "bitcoinjs-lib/src/crypto";
2007
+
2008
+ // src/constants/staking.ts
2009
+ var STAKING_MODULE_ADDRESS = "bbn13837feaxn8t0zvwcjwhw7lhpgdcx4s36eqteah";
2010
+
2011
+ // src/utils/pop.ts
2012
+ function createStakerPopContext(chainId, popContextVersion = 0) {
2013
+ const contextString = `btcstaking/${popContextVersion}/staker_pop/${chainId}/${STAKING_MODULE_ADDRESS}`;
2014
+ return sha256(Buffer.from(contextString, "utf8")).toString("hex");
2015
+ }
2016
+ function buildPopMessage(bech32Address, currentHeight, chainId, upgradeConfig) {
2017
+ if (chainId !== void 0 && upgradeConfig?.upgradeHeight !== void 0 && upgradeConfig.version !== void 0 && currentHeight !== void 0 && currentHeight >= upgradeConfig.upgradeHeight) {
2018
+ const contextHash = createStakerPopContext(chainId, upgradeConfig.version);
2019
+ return contextHash + bech32Address;
2020
+ }
2021
+ return bech32Address;
2022
+ }
2023
+
2024
+ // src/utils/staking/param.ts
2025
+ var getBabylonParamByBtcHeight = (height, babylonParamsVersions) => {
2026
+ const sortedParams = [...babylonParamsVersions].sort(
2027
+ (a, b) => b.btcActivationHeight - a.btcActivationHeight
2028
+ );
2029
+ const params = sortedParams.find(
2030
+ (p) => height >= p.btcActivationHeight
2031
+ );
2032
+ if (!params) throw new Error(`Babylon params not found for height ${height}`);
2033
+ return params;
2034
+ };
2035
+ var getBabylonParamByVersion = (version, babylonParams) => {
2036
+ const params = babylonParams.find((p) => p.version === version);
2037
+ if (!params) throw new Error(`Babylon params not found for version ${version}`);
2038
+ return params;
1834
2039
  };
1835
2040
 
1836
2041
  // src/staking/manager.ts
1837
- var SigningStep = /* @__PURE__ */ ((SigningStep2) => {
1838
- SigningStep2["STAKING_SLASHING"] = "staking-slashing";
1839
- SigningStep2["UNBONDING_SLASHING"] = "unbonding-slashing";
1840
- SigningStep2["PROOF_OF_POSSESSION"] = "proof-of-possession";
1841
- SigningStep2["CREATE_BTC_DELEGATION_MSG"] = "create-btc-delegation-msg";
1842
- SigningStep2["STAKING"] = "staking";
1843
- SigningStep2["UNBONDING"] = "unbonding";
1844
- SigningStep2["WITHDRAW_STAKING_EXPIRED"] = "withdraw-staking-expired";
1845
- SigningStep2["WITHDRAW_EARLY_UNBONDED"] = "withdraw-early-unbonded";
1846
- SigningStep2["WITHDRAW_SLASHING"] = "withdraw-slashing";
1847
- return SigningStep2;
1848
- })(SigningStep || {});
1849
2042
  var BabylonBtcStakingManager = class {
1850
- constructor(network, stakingParams, btcProvider, babylonProvider) {
2043
+ constructor(network, stakingParams, btcProvider, babylonProvider, ee, upgradeConfig) {
1851
2044
  this.network = network;
2045
+ this.stakingParams = stakingParams;
1852
2046
  this.btcProvider = btcProvider;
1853
2047
  this.babylonProvider = babylonProvider;
2048
+ this.ee = ee;
2049
+ this.network = network;
1854
2050
  if (stakingParams.length === 0) {
1855
2051
  throw new Error("No staking parameters provided");
1856
2052
  }
1857
2053
  this.stakingParams = stakingParams;
2054
+ this.upgradeConfig = upgradeConfig;
1858
2055
  }
1859
2056
  /**
1860
2057
  * Creates a signed Pre-Staking Registration transaction that is ready to be
@@ -1865,9 +2062,11 @@ var BabylonBtcStakingManager = class {
1865
2062
  * @param babylonBtcTipHeight - The Babylon BTC tip height.
1866
2063
  * @param inputUTXOs - The UTXOs that will be used to pay for the staking
1867
2064
  * transaction.
1868
- * @param feeRate - The fee rate in satoshis per byte.
2065
+ * @param feeRate - The fee rate in satoshis per byte. Typical value for the
2066
+ * fee rate is above 1. If the fee rate is too low, the transaction will not
2067
+ * be included in a block.
1869
2068
  * @param babylonAddress - The Babylon bech32 encoded address of the staker.
1870
- * @returns The signed babylon pre-staking registration transaction in base64
2069
+ * @returns The signed babylon pre-staking registration transaction in base64
1871
2070
  * format.
1872
2071
  */
1873
2072
  async preStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress) {
@@ -1880,23 +2079,17 @@ var BabylonBtcStakingManager = class {
1880
2079
  if (!isValidBabylonAddress(babylonAddress)) {
1881
2080
  throw new Error("Invalid Babylon address");
1882
2081
  }
1883
- const params = getBabylonParamByBtcHeight(
1884
- babylonBtcTipHeight,
1885
- this.stakingParams
1886
- );
2082
+ const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
1887
2083
  const staking = new Staking(
1888
2084
  this.network,
1889
2085
  stakerBtcInfo,
1890
2086
  params,
1891
- stakingInput.finalityProviderPkNoCoordHex,
2087
+ stakingInput.finalityProviderPksNoCoordHex,
1892
2088
  stakingInput.stakingTimelock
1893
2089
  );
1894
- const { transaction } = staking.createStakingTransaction(
1895
- stakingInput.stakingAmountSat,
1896
- inputUTXOs,
1897
- feeRate
1898
- );
2090
+ const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate);
1899
2091
  const msg = await this.createBtcDelegationMsg(
2092
+ "delegation:create",
1900
2093
  staking,
1901
2094
  stakingInput,
1902
2095
  transaction,
@@ -1904,26 +2097,131 @@ var BabylonBtcStakingManager = class {
1904
2097
  stakerBtcInfo,
1905
2098
  params
1906
2099
  );
2100
+ this.ee?.emit("delegation:create", {
2101
+ type: "create-btc-delegation-msg"
2102
+ });
1907
2103
  return {
1908
- signedBabylonTx: await this.babylonProvider.signTransaction(
1909
- "create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
1910
- msg
1911
- ),
2104
+ signedBabylonTx: await this.babylonProvider.signTransaction(msg),
1912
2105
  stakingTx: transaction
1913
2106
  };
1914
2107
  }
1915
2108
  /**
1916
- * Creates a signed post-staking registration transaction that is ready to be
1917
- * sent to the Babylon chain. This is used when a staking transaction is
1918
- * already created and included in a BTC block and we want to register it on
2109
+ * Create a signed staking expansion transaction that is ready to be sent to
2110
+ * the Babylon chain.
2111
+ */
2112
+ async stakingExpansionRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress, previousStakingTxInfo) {
2113
+ validateStakingExpansionInputs({
2114
+ babylonBtcTipHeight,
2115
+ inputUTXOs,
2116
+ stakingInput,
2117
+ previousStakingInput: previousStakingTxInfo.stakingInput,
2118
+ babylonAddress
2119
+ });
2120
+ const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
2121
+ const paramsForPreviousStakingTx = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams);
2122
+ const stakingInstance = new Staking(
2123
+ this.network,
2124
+ stakerBtcInfo,
2125
+ params,
2126
+ stakingInput.finalityProviderPksNoCoordHex,
2127
+ stakingInput.stakingTimelock
2128
+ );
2129
+ const { transaction: stakingExpansionTx, fundingUTXO } = stakingInstance.createStakingExpansionTransaction(
2130
+ stakingInput.stakingAmountSat,
2131
+ inputUTXOs,
2132
+ feeRate,
2133
+ paramsForPreviousStakingTx,
2134
+ previousStakingTxInfo
2135
+ );
2136
+ let fundingTx;
2137
+ try {
2138
+ fundingTx = await this.btcProvider.getTransactionHex(fundingUTXO.txid);
2139
+ } catch (error) {
2140
+ throw StakingError.fromUnknown(
2141
+ error,
2142
+ "INVALID_INPUT" /* INVALID_INPUT */,
2143
+ "Failed to retrieve funding transaction hex"
2144
+ );
2145
+ }
2146
+ const msg = await this.createBtcDelegationMsg(
2147
+ "delegation:expand",
2148
+ stakingInstance,
2149
+ stakingInput,
2150
+ stakingExpansionTx,
2151
+ babylonAddress,
2152
+ stakerBtcInfo,
2153
+ params,
2154
+ {
2155
+ delegationExpansionInfo: {
2156
+ previousStakingTx: previousStakingTxInfo.stakingTx,
2157
+ fundingTx: Transaction4.fromHex(fundingTx)
2158
+ }
2159
+ }
2160
+ );
2161
+ this.ee?.emit("delegation:expand", {
2162
+ type: "create-btc-delegation-msg"
2163
+ });
2164
+ return {
2165
+ signedBabylonTx: await this.babylonProvider.signTransaction(msg),
2166
+ stakingTx: stakingExpansionTx
2167
+ };
2168
+ }
2169
+ /**
2170
+ * Estimates the transaction fee for a BTC staking expansion transaction.
2171
+ *
2172
+ * @param {StakerInfo} stakerBtcInfo - The staker's Bitcoin information
2173
+ * including address and public key
2174
+ * @param {number} babylonBtcTipHeight - The current Babylon BTC tip height
2175
+ * used to determine staking parameters
2176
+ * @param {StakingInputs} stakingInput - The new staking input parameters for
2177
+ * the expansion
2178
+ * @param {UTXO[]} inputUTXOs - Available UTXOs that can be used for funding
2179
+ * the expansion transaction
2180
+ * @param {number} feeRate - Fee rate in satoshis per byte for the expansion
2181
+ * transaction
2182
+ * @param {Object} previousStakingTxInfo - Information about the previous
2183
+ * staking transaction being expanded
2184
+ * @returns {number} - The estimated transaction fee in satoshis
2185
+ * @throws {Error} - If validation fails or the fee cannot be calculated
2186
+ */
2187
+ estimateBtcStakingExpansionFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate, previousStakingTxInfo) {
2188
+ validateStakingExpansionInputs({
2189
+ babylonBtcTipHeight,
2190
+ inputUTXOs,
2191
+ stakingInput,
2192
+ previousStakingInput: previousStakingTxInfo.stakingInput
2193
+ });
2194
+ const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
2195
+ const paramsForPreviousStakingTx = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams);
2196
+ const stakingInstance = new Staking(
2197
+ this.network,
2198
+ stakerBtcInfo,
2199
+ params,
2200
+ stakingInput.finalityProviderPksNoCoordHex,
2201
+ stakingInput.stakingTimelock
2202
+ );
2203
+ const { fee } = stakingInstance.createStakingExpansionTransaction(
2204
+ stakingInput.stakingAmountSat,
2205
+ inputUTXOs,
2206
+ feeRate,
2207
+ paramsForPreviousStakingTx,
2208
+ previousStakingTxInfo
2209
+ );
2210
+ return fee;
2211
+ }
2212
+ /**
2213
+ * Creates a signed post-staking registration transaction that is ready to be
2214
+ * sent to the Babylon chain. This is used when a staking transaction is
2215
+ * already created and included in a BTC block and we want to register it on
1919
2216
  * the Babylon chain.
1920
2217
  * @param stakerBtcInfo - The staker BTC info which includes the BTC address
1921
2218
  * and the no-coord public key in hex format.
1922
2219
  * @param stakingTx - The staking transaction.
1923
- * @param stakingTxHeight - The BTC height in which the staking transaction
2220
+ * @param stakingTxHeight - The BTC height in which the staking transaction
1924
2221
  * is included.
1925
2222
  * @param stakingInput - The staking inputs.
1926
- * @param inclusionProof - The inclusion proof of the staking transaction.
2223
+ * @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction
2224
+ * inclusion in a Bitcoin block that is k-deep.
1927
2225
  * @param babylonAddress - The Babylon bech32 encoded address of the staker.
1928
2226
  * @returns The signed babylon transaction in base64 format.
1929
2227
  */
@@ -1936,30 +2234,29 @@ var BabylonBtcStakingManager = class {
1936
2234
  this.network,
1937
2235
  stakerBtcInfo,
1938
2236
  params,
1939
- stakingInput.finalityProviderPkNoCoordHex,
2237
+ stakingInput.finalityProviderPksNoCoordHex,
1940
2238
  stakingInput.stakingTimelock
1941
2239
  );
1942
2240
  const scripts = stakingInstance.buildScripts();
1943
2241
  const stakingOutputInfo = deriveStakingOutputInfo(scripts, this.network);
1944
- findMatchingTxOutputIndex(
1945
- stakingTx,
1946
- stakingOutputInfo.outputAddress,
1947
- this.network
1948
- );
2242
+ findMatchingTxOutputIndex(stakingTx, stakingOutputInfo.outputAddress, this.network);
1949
2243
  const delegationMsg = await this.createBtcDelegationMsg(
2244
+ "delegation:register",
1950
2245
  stakingInstance,
1951
2246
  stakingInput,
1952
2247
  stakingTx,
1953
2248
  babylonAddress,
1954
2249
  stakerBtcInfo,
1955
2250
  params,
1956
- this.getInclusionProof(inclusionProof)
2251
+ {
2252
+ inclusionProof: this.getInclusionProof(inclusionProof)
2253
+ }
1957
2254
  );
2255
+ this.ee?.emit("delegation:register", {
2256
+ type: "create-btc-delegation-msg"
2257
+ });
1958
2258
  return {
1959
- signedBabylonTx: await this.babylonProvider.signTransaction(
1960
- "create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
1961
- delegationMsg
1962
- )
2259
+ signedBabylonTx: await this.babylonProvider.signTransaction(delegationMsg)
1963
2260
  };
1964
2261
  }
1965
2262
  /**
@@ -1971,33 +2268,28 @@ var BabylonBtcStakingManager = class {
1971
2268
  * @param stakingInput - The staking inputs.
1972
2269
  * @param inputUTXOs - The UTXOs that will be used to pay for the staking
1973
2270
  * transaction.
1974
- * @param feeRate - The fee rate in satoshis per byte.
2271
+ * @param feeRate - The fee rate in satoshis per byte. Typical value for the
2272
+ * fee rate is above 1. If the fee rate is too low, the transaction will not
2273
+ * be included in a block.
1975
2274
  * @returns The estimated BTC fee in satoshis.
1976
2275
  */
1977
2276
  estimateBtcStakingFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate) {
1978
2277
  if (babylonBtcTipHeight === 0) {
1979
2278
  throw new Error("Babylon BTC tip height cannot be 0");
1980
2279
  }
1981
- const params = getBabylonParamByBtcHeight(
1982
- babylonBtcTipHeight,
1983
- this.stakingParams
1984
- );
2280
+ const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
1985
2281
  const staking = new Staking(
1986
2282
  this.network,
1987
2283
  stakerBtcInfo,
1988
2284
  params,
1989
- stakingInput.finalityProviderPkNoCoordHex,
2285
+ stakingInput.finalityProviderPksNoCoordHex,
1990
2286
  stakingInput.stakingTimelock
1991
2287
  );
1992
- const { fee: stakingFee } = staking.createStakingTransaction(
1993
- stakingInput.stakingAmountSat,
1994
- inputUTXOs,
1995
- feeRate
1996
- );
2288
+ const { fee: stakingFee } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate);
1997
2289
  return stakingFee;
1998
2290
  }
1999
2291
  /**
2000
- * Creates a signed staking transaction that is ready to be sent to the BTC
2292
+ * Creates a signed staking transaction that is ready to be sent to the BTC
2001
2293
  * network.
2002
2294
  * @param stakerBtcInfo - The staker BTC info which includes the BTC address
2003
2295
  * and the no-coord public key in hex format.
@@ -2018,18 +2310,129 @@ var BabylonBtcStakingManager = class {
2018
2310
  this.network,
2019
2311
  stakerBtcInfo,
2020
2312
  params,
2021
- stakingInput.finalityProviderPkNoCoordHex,
2313
+ stakingInput.finalityProviderPksNoCoordHex,
2022
2314
  stakingInput.stakingTimelock
2023
2315
  );
2024
- const stakingPsbt2 = staking.toStakingPsbt(
2025
- unsignedStakingTx,
2026
- inputUTXOs
2316
+ const stakingPsbt2 = staking.toStakingPsbt(unsignedStakingTx, inputUTXOs);
2317
+ const contracts = [
2318
+ {
2319
+ id: "babylon:staking" /* STAKING */,
2320
+ params: {
2321
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2322
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2323
+ covenantPks: params.covenantNoCoordPks,
2324
+ covenantThreshold: params.covenantQuorum,
2325
+ minUnbondingTime: params.unbondingTime,
2326
+ stakingDuration: stakingInput.stakingTimelock
2327
+ }
2328
+ }
2329
+ ];
2330
+ this.ee?.emit("delegation:stake", {
2331
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2332
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2333
+ covenantPks: params.covenantNoCoordPks,
2334
+ covenantThreshold: params.covenantQuorum,
2335
+ unbondingTimeBlocks: params.unbondingTime,
2336
+ stakingDuration: stakingInput.stakingTimelock,
2337
+ type: "staking"
2338
+ });
2339
+ const signedStakingPsbtHex = await this.btcProvider.signPsbt(stakingPsbt2.toHex(), {
2340
+ contracts,
2341
+ action: {
2342
+ name: "sign-btc-staking-transaction" /* SIGN_BTC_STAKING_TRANSACTION */
2343
+ }
2344
+ });
2345
+ return Psbt3.fromHex(signedStakingPsbtHex).extractTransaction();
2346
+ }
2347
+ /**
2348
+ * Creates a signed staking expansion transaction that is ready to be sent to
2349
+ * the BTC network.
2350
+ *
2351
+ * @param {StakerInfo} stakerBtcInfo - The staker's BTC information including
2352
+ * address and public key
2353
+ * @param {StakingInputs} stakingInput - The staking inputs for the expansion
2354
+ * @param {Transaction} unsignedStakingExpansionTx - The unsigned staking
2355
+ * expansion transaction
2356
+ * @param {UTXO[]} inputUTXOs - Available UTXOs for the funding input
2357
+ * @param {number} stakingParamsVersion - The version of staking parameters
2358
+ * that was used when registering the staking expansion delegation.
2359
+ * @param {Object} previousStakingTxInfo - Information about the previous
2360
+ * staking transaction
2361
+ * @param {Array} covenantStakingExpansionSignatures - Covenant committee
2362
+ * signatures for the expansion
2363
+ * @returns {Promise<Transaction>} The fully signed staking expansion
2364
+ * transaction
2365
+ * @throws {Error} If signing fails, validation fails, or required data is
2366
+ * missing
2367
+ */
2368
+ async createSignedBtcStakingExpansionTransaction(stakerBtcInfo, stakingInput, unsignedStakingExpansionTx, inputUTXOs, stakingParamsVersion, previousStakingTxInfo, covenantStakingExpansionSignatures) {
2369
+ validateStakingExpansionInputs({
2370
+ inputUTXOs,
2371
+ stakingInput,
2372
+ previousStakingInput: previousStakingTxInfo.stakingInput
2373
+ });
2374
+ const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
2375
+ if (inputUTXOs.length === 0) {
2376
+ throw new Error("No input UTXOs provided");
2377
+ }
2378
+ const staking = new Staking(
2379
+ this.network,
2380
+ stakerBtcInfo,
2381
+ params,
2382
+ stakingInput.finalityProviderPksNoCoordHex,
2383
+ stakingInput.stakingTimelock
2027
2384
  );
2028
- const signedStakingPsbtHex = await this.btcProvider.signPsbt(
2029
- "staking" /* STAKING */,
2030
- stakingPsbt2.toHex()
2385
+ const previousParams = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams);
2386
+ const stakingExpansionPsbt2 = staking.toStakingExpansionPsbt(
2387
+ unsignedStakingExpansionTx,
2388
+ inputUTXOs,
2389
+ previousParams,
2390
+ previousStakingTxInfo
2391
+ );
2392
+ const contracts = [
2393
+ {
2394
+ id: "babylon:staking" /* STAKING */,
2395
+ params: {
2396
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2397
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2398
+ covenantPks: params.covenantNoCoordPks,
2399
+ covenantThreshold: params.covenantQuorum,
2400
+ minUnbondingTime: params.unbondingTime,
2401
+ stakingDuration: stakingInput.stakingTimelock
2402
+ }
2403
+ }
2404
+ ];
2405
+ this.ee?.emit("delegation:stake", {
2406
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2407
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2408
+ covenantPks: params.covenantNoCoordPks,
2409
+ covenantThreshold: params.covenantQuorum,
2410
+ unbondingTimeBlocks: params.unbondingTime,
2411
+ stakingDuration: stakingInput.stakingTimelock,
2412
+ type: "staking"
2413
+ });
2414
+ const signedStakingPsbtHex = await this.btcProvider.signPsbt(stakingExpansionPsbt2.toHex(), {
2415
+ contracts,
2416
+ action: {
2417
+ name: "sign-btc-staking-transaction" /* SIGN_BTC_STAKING_TRANSACTION */
2418
+ }
2419
+ });
2420
+ const signedStakingExpansionTx = Psbt3.fromHex(signedStakingPsbtHex).extractTransaction();
2421
+ if (signedStakingExpansionTx.getId() !== unsignedStakingExpansionTx.getId()) {
2422
+ throw new Error("Staking expansion transaction hash does not match the computed hash");
2423
+ }
2424
+ const covenantBuffers = previousParams.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, "hex"));
2425
+ const witness = createCovenantWitness(
2426
+ // The first input of the staking expansion transaction is the previous
2427
+ // staking output. We will attach the covenant signatures to this input
2428
+ // to unbond the previousstaking output.
2429
+ signedStakingExpansionTx.ins[0].witness,
2430
+ covenantBuffers,
2431
+ covenantStakingExpansionSignatures,
2432
+ previousParams.covenantQuorum
2031
2433
  );
2032
- return Psbt3.fromHex(signedStakingPsbtHex).extractTransaction();
2434
+ signedStakingExpansionTx.ins[0].witness = witness;
2435
+ return signedStakingExpansionTx;
2033
2436
  }
2034
2437
  /**
2035
2438
  * Creates a partial signed unbonding transaction that is only signed by the
@@ -2046,36 +2449,64 @@ var BabylonBtcStakingManager = class {
2046
2449
  * @returns The partial signed unbonding transaction and its fee.
2047
2450
  */
2048
2451
  async createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx) {
2049
- const params = getBabylonParamByVersion(
2050
- stakingParamsVersion,
2051
- this.stakingParams
2052
- );
2452
+ const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
2053
2453
  const staking = new Staking(
2054
2454
  this.network,
2055
2455
  stakerBtcInfo,
2056
2456
  params,
2057
- stakingInput.finalityProviderPkNoCoordHex,
2457
+ stakingInput.finalityProviderPksNoCoordHex,
2058
2458
  stakingInput.stakingTimelock
2059
2459
  );
2060
- const {
2061
- transaction: unbondingTx,
2062
- fee
2063
- } = staking.createUnbondingTransaction(stakingTx);
2460
+ const { transaction: unbondingTx, fee } = staking.createUnbondingTransaction(stakingTx);
2064
2461
  const psbt = staking.toUnbondingPsbt(unbondingTx, stakingTx);
2065
- const signedUnbondingPsbtHex = await this.btcProvider.signPsbt(
2066
- "unbonding" /* UNBONDING */,
2067
- psbt.toHex()
2068
- );
2069
- const signedUnbondingTx = Psbt3.fromHex(
2070
- signedUnbondingPsbtHex
2071
- ).extractTransaction();
2462
+ const contracts = [
2463
+ {
2464
+ id: "babylon:staking" /* STAKING */,
2465
+ params: {
2466
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2467
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2468
+ covenantPks: params.covenantNoCoordPks,
2469
+ covenantThreshold: params.covenantQuorum,
2470
+ minUnbondingTime: params.unbondingTime,
2471
+ stakingDuration: stakingInput.stakingTimelock
2472
+ }
2473
+ },
2474
+ {
2475
+ id: "babylon:unbonding" /* UNBONDING */,
2476
+ params: {
2477
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2478
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2479
+ covenantPks: params.covenantNoCoordPks,
2480
+ covenantThreshold: params.covenantQuorum,
2481
+ unbondingTimeBlocks: params.unbondingTime,
2482
+ unbondingFeeSat: params.unbondingFeeSat
2483
+ }
2484
+ }
2485
+ ];
2486
+ this.ee?.emit("delegation:unbond", {
2487
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2488
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2489
+ covenantPks: params.covenantNoCoordPks,
2490
+ covenantThreshold: params.covenantQuorum,
2491
+ stakingDuration: stakingInput.stakingTimelock,
2492
+ unbondingTimeBlocks: params.unbondingTime,
2493
+ unbondingFeeSat: params.unbondingFeeSat,
2494
+ type: "unbonding"
2495
+ });
2496
+ const signedUnbondingPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), {
2497
+ contracts,
2498
+ action: {
2499
+ name: "sign-btc-unbonding-transaction" /* SIGN_BTC_UNBONDING_TRANSACTION */
2500
+ }
2501
+ });
2502
+ const signedUnbondingTx = Psbt3.fromHex(signedUnbondingPsbtHex).extractTransaction();
2072
2503
  return {
2073
2504
  transaction: signedUnbondingTx,
2074
2505
  fee
2075
2506
  };
2076
2507
  }
2077
2508
  /**
2078
- * Creates a signed unbonding transaction that is ready to be sent to the BTC
2509
+ * Creates a signed unbonding transaction that is ready to be sent to the BTC
2079
2510
  * network.
2080
2511
  * @param stakerBtcInfo - The staker BTC info which includes the BTC address
2081
2512
  * and the no-coord public key in hex format.
@@ -2089,27 +2520,17 @@ var BabylonBtcStakingManager = class {
2089
2520
  * @returns The signed unbonding transaction and its fee.
2090
2521
  */
2091
2522
  async createSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, unsignedUnbondingTx, covenantUnbondingSignatures) {
2092
- const params = getBabylonParamByVersion(
2093
- stakingParamsVersion,
2094
- this.stakingParams
2095
- );
2096
- const {
2097
- transaction: signedUnbondingTx,
2098
- fee
2099
- } = await this.createPartialSignedBtcUnbondingTransaction(
2523
+ const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
2524
+ const { transaction: signedUnbondingTx, fee } = await this.createPartialSignedBtcUnbondingTransaction(
2100
2525
  stakerBtcInfo,
2101
2526
  stakingInput,
2102
2527
  stakingParamsVersion,
2103
2528
  stakingTx
2104
2529
  );
2105
2530
  if (signedUnbondingTx.getId() !== unsignedUnbondingTx.getId()) {
2106
- throw new Error(
2107
- "Unbonding transaction hash does not match the computed hash"
2108
- );
2531
+ throw new Error("Unbonding transaction hash does not match the computed hash");
2109
2532
  }
2110
- const covenantBuffers = params.covenantNoCoordPks.map(
2111
- (covenant) => Buffer.from(covenant, "hex")
2112
- );
2533
+ const covenantBuffers = params.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, "hex"));
2113
2534
  const witness = createCovenantWitness(
2114
2535
  // Since unbonding transactions always have a single input and output,
2115
2536
  // we expect exactly one signature in TaprootScriptSpendSig when the
@@ -2126,42 +2547,54 @@ var BabylonBtcStakingManager = class {
2126
2547
  };
2127
2548
  }
2128
2549
  /**
2129
- * Creates a signed withdrawal transaction on the unbodning output expiry path
2550
+ * Creates a signed withdrawal transaction on the unbodning output expiry path
2130
2551
  * that is ready to be sent to the BTC network.
2131
2552
  * @param stakingInput - The staking inputs.
2132
2553
  * @param stakingParamsVersion - The params version that was used to create the
2133
2554
  * delegation in Babylon chain
2134
2555
  * @param earlyUnbondingTx - The early unbonding transaction.
2135
- * @param feeRate - The fee rate in satoshis per byte.
2556
+ * @param feeRate - The fee rate in satoshis per byte. Typical value for the
2557
+ * fee rate is above 1. If the fee rate is too low, the transaction will not
2558
+ * be included in a block.
2136
2559
  * @returns The signed withdrawal transaction and its fee.
2137
2560
  */
2138
2561
  async createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, earlyUnbondingTx, feeRate) {
2139
- const params = getBabylonParamByVersion(
2140
- stakingParamsVersion,
2141
- this.stakingParams
2142
- );
2562
+ const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
2143
2563
  const staking = new Staking(
2144
2564
  this.network,
2145
2565
  stakerBtcInfo,
2146
2566
  params,
2147
- stakingInput.finalityProviderPkNoCoordHex,
2567
+ stakingInput.finalityProviderPksNoCoordHex,
2148
2568
  stakingInput.stakingTimelock
2149
2569
  );
2150
- const { psbt: unbondingPsbt2, fee } = staking.createWithdrawEarlyUnbondedTransaction(
2151
- earlyUnbondingTx,
2152
- feeRate
2153
- );
2154
- const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(
2155
- "withdraw-early-unbonded" /* WITHDRAW_EARLY_UNBONDED */,
2156
- unbondingPsbt2.toHex()
2157
- );
2570
+ const { psbt: unbondingPsbt2, fee } = staking.createWithdrawEarlyUnbondedTransaction(earlyUnbondingTx, feeRate);
2571
+ const contracts = [
2572
+ {
2573
+ id: "babylon:withdraw" /* WITHDRAW */,
2574
+ params: {
2575
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2576
+ timelockBlocks: params.unbondingTime
2577
+ }
2578
+ }
2579
+ ];
2580
+ this.ee?.emit("delegation:withdraw", {
2581
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2582
+ timelockBlocks: params.unbondingTime,
2583
+ type: "early-unbonded"
2584
+ });
2585
+ const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(unbondingPsbt2.toHex(), {
2586
+ contracts,
2587
+ action: {
2588
+ name: "sign-btc-withdraw-transaction" /* SIGN_BTC_WITHDRAW_TRANSACTION */
2589
+ }
2590
+ });
2158
2591
  return {
2159
2592
  transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
2160
2593
  fee
2161
2594
  };
2162
2595
  }
2163
2596
  /**
2164
- * Creates a signed withdrawal transaction on the staking output expiry path
2597
+ * Creates a signed withdrawal transaction on the staking output expiry path
2165
2598
  * that is ready to be sent to the BTC network.
2166
2599
  * @param stakerBtcInfo - The staker BTC info which includes the BTC address
2167
2600
  * and the no-coord public key in hex format.
@@ -2169,36 +2602,48 @@ var BabylonBtcStakingManager = class {
2169
2602
  * @param stakingParamsVersion - The params version that was used to create the
2170
2603
  * delegation in Babylon chain
2171
2604
  * @param stakingTx - The staking transaction.
2172
- * @param feeRate - The fee rate in satoshis per byte.
2605
+ * @param feeRate - The fee rate in satoshis per byte. Typical value for the
2606
+ * fee rate is above 1. If the fee rate is too low, the transaction will not
2607
+ * be included in a block.
2173
2608
  * @returns The signed withdrawal transaction and its fee.
2174
2609
  */
2175
2610
  async createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, feeRate) {
2176
- const params = getBabylonParamByVersion(
2177
- stakingParamsVersion,
2178
- this.stakingParams
2179
- );
2611
+ const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
2180
2612
  const staking = new Staking(
2181
2613
  this.network,
2182
2614
  stakerBtcInfo,
2183
2615
  params,
2184
- stakingInput.finalityProviderPkNoCoordHex,
2616
+ stakingInput.finalityProviderPksNoCoordHex,
2185
2617
  stakingInput.stakingTimelock
2186
2618
  );
2187
- const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(
2188
- stakingTx,
2189
- feeRate
2190
- );
2191
- const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(
2192
- "withdraw-staking-expired" /* WITHDRAW_STAKING_EXPIRED */,
2193
- psbt.toHex()
2194
- );
2619
+ const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(stakingTx, feeRate);
2620
+ const contracts = [
2621
+ {
2622
+ id: "babylon:withdraw" /* WITHDRAW */,
2623
+ params: {
2624
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2625
+ timelockBlocks: stakingInput.stakingTimelock
2626
+ }
2627
+ }
2628
+ ];
2629
+ this.ee?.emit("delegation:withdraw", {
2630
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2631
+ timelockBlocks: stakingInput.stakingTimelock,
2632
+ type: "staking-expired"
2633
+ });
2634
+ const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), {
2635
+ contracts,
2636
+ action: {
2637
+ name: "sign-btc-withdraw-transaction" /* SIGN_BTC_WITHDRAW_TRANSACTION */
2638
+ }
2639
+ });
2195
2640
  return {
2196
2641
  transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
2197
2642
  fee
2198
2643
  };
2199
2644
  }
2200
2645
  /**
2201
- * Creates a signed withdrawal transaction for the expired slashing output that
2646
+ * Creates a signed withdrawal transaction for the expired slashing output that
2202
2647
  * is ready to be sent to the BTC network.
2203
2648
  * @param stakerBtcInfo - The staker BTC info which includes the BTC address
2204
2649
  * and the no-coord public key in hex format.
@@ -2206,31 +2651,43 @@ var BabylonBtcStakingManager = class {
2206
2651
  * @param stakingParamsVersion - The params version that was used to create the
2207
2652
  * delegation in Babylon chain
2208
2653
  * @param slashingTx - The slashing transaction.
2209
- * @param feeRate - The fee rate in satoshis per byte.
2654
+ * @param feeRate - The fee rate in satoshis per byte. Typical value for the
2655
+ * fee rate is above 1. If the fee rate is too low, the transaction will not
2656
+ * be included in a block.
2210
2657
  * @returns The signed withdrawal transaction and its fee.
2211
2658
  */
2212
2659
  async createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, slashingTx, feeRate) {
2213
- const params = getBabylonParamByVersion(
2214
- stakingParamsVersion,
2215
- this.stakingParams
2216
- );
2660
+ const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
2217
2661
  const staking = new Staking(
2218
2662
  this.network,
2219
2663
  stakerBtcInfo,
2220
2664
  params,
2221
- stakingInput.finalityProviderPkNoCoordHex,
2665
+ stakingInput.finalityProviderPksNoCoordHex,
2222
2666
  stakingInput.stakingTimelock
2223
2667
  );
2224
- const { psbt, fee } = staking.createWithdrawSlashingPsbt(
2225
- slashingTx,
2226
- feeRate
2227
- );
2228
- const signedSlashingPsbtHex = await this.btcProvider.signPsbt(
2229
- "withdraw-slashing" /* WITHDRAW_SLASHING */,
2230
- psbt.toHex()
2231
- );
2668
+ const { psbt, fee } = staking.createWithdrawSlashingPsbt(slashingTx, feeRate);
2669
+ const contracts = [
2670
+ {
2671
+ id: "babylon:withdraw" /* WITHDRAW */,
2672
+ params: {
2673
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2674
+ timelockBlocks: params.unbondingTime
2675
+ }
2676
+ }
2677
+ ];
2678
+ this.ee?.emit("delegation:withdraw", {
2679
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2680
+ timelockBlocks: params.unbondingTime,
2681
+ type: "slashing"
2682
+ });
2683
+ const signedWithrawSlashingPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), {
2684
+ contracts,
2685
+ action: {
2686
+ name: "sign-btc-withdraw-transaction" /* SIGN_BTC_WITHDRAW_TRANSACTION */
2687
+ }
2688
+ });
2232
2689
  return {
2233
- transaction: Psbt3.fromHex(signedSlashingPsbtHex).extractTransaction(),
2690
+ transaction: Psbt3.fromHex(signedWithrawSlashingPsbtHex).extractTransaction(),
2234
2691
  fee
2235
2692
  };
2236
2693
  }
@@ -2239,28 +2696,54 @@ var BabylonBtcStakingManager = class {
2239
2696
  * @param bech32Address - The staker's bech32 address.
2240
2697
  * @returns The proof of possession.
2241
2698
  */
2242
- async createProofOfPossession(bech32Address) {
2243
- if (!this.btcProvider.signMessage) {
2244
- throw new Error("Sign message function not found");
2245
- }
2246
- const bech32AddressHex = uint8ArrayToHex(fromBech322(bech32Address).data);
2247
- const signedBabylonAddress = await this.btcProvider.signMessage(
2248
- "proof-of-possession" /* PROOF_OF_POSSESSION */,
2249
- bech32AddressHex,
2250
- "ecdsa"
2699
+ async createProofOfPossession(channel, bech32Address, stakerBtcAddress) {
2700
+ let sigType = BTCSigType.ECDSA;
2701
+ if (isTaproot(stakerBtcAddress, this.network) || isNativeSegwit(stakerBtcAddress, this.network)) {
2702
+ sigType = BTCSigType.BIP322;
2703
+ }
2704
+ const [chainId, babyTipHeight] = await Promise.all([
2705
+ this.babylonProvider.getChainId?.(),
2706
+ this.babylonProvider.getCurrentHeight?.()
2707
+ ]);
2708
+ const upgradeConfig = this.upgradeConfig?.pop;
2709
+ const messageToSign = buildPopMessage(
2710
+ bech32Address,
2711
+ babyTipHeight,
2712
+ chainId,
2713
+ upgradeConfig && {
2714
+ upgradeHeight: upgradeConfig.upgradeHeight,
2715
+ version: upgradeConfig.version
2716
+ }
2251
2717
  );
2252
- const ecdsaSig = Uint8Array.from(Buffer.from(signedBabylonAddress, "base64"));
2718
+ this.ee?.emit(channel, {
2719
+ messageToSign,
2720
+ type: "proof-of-possession"
2721
+ });
2722
+ const signedBabylonAddress = await this.btcProvider.signMessage(
2723
+ messageToSign,
2724
+ sigType === BTCSigType.BIP322 ? "bip322-simple" : "ecdsa"
2725
+ );
2726
+ let btcSig;
2727
+ if (sigType === BTCSigType.BIP322) {
2728
+ const bip322Sig = BIP322Sig.fromPartial({
2729
+ address: stakerBtcAddress,
2730
+ sig: Buffer.from(signedBabylonAddress, "base64")
2731
+ });
2732
+ btcSig = BIP322Sig.encode(bip322Sig).finish();
2733
+ } else {
2734
+ btcSig = Buffer.from(signedBabylonAddress, "base64");
2735
+ }
2253
2736
  return {
2254
- btcSigType: BTCSigType.ECDSA,
2255
- btcSig: ecdsaSig
2737
+ btcSigType: sigType,
2738
+ btcSig
2256
2739
  };
2257
2740
  }
2258
2741
  /**
2259
- * Creates the unbonding, slashing, and unbonding slashing transactions and
2742
+ * Creates the unbonding, slashing, and unbonding slashing transactions and
2260
2743
  * PSBTs.
2261
2744
  * @param stakingInstance - The staking instance.
2262
2745
  * @param stakingTx - The staking transaction.
2263
- * @returns The unbonding, slashing, and unbonding slashing transactions and
2746
+ * @returns The unbonding, slashing, and unbonding slashing transactions and
2264
2747
  * PSBTs.
2265
2748
  */
2266
2749
  async createDelegationTransactionsAndPsbts(stakingInstance, stakingTx) {
@@ -2275,81 +2758,164 @@ var BabylonBtcStakingManager = class {
2275
2758
  }
2276
2759
  /**
2277
2760
  * Creates a protobuf message for the BTC delegation.
2761
+ * @param channel - The event channel to emit the message on.
2278
2762
  * @param stakingInstance - The staking instance.
2279
2763
  * @param stakingInput - The staking inputs.
2280
2764
  * @param stakingTx - The staking transaction.
2281
2765
  * @param bech32Address - The staker's babylon chain bech32 address
2282
- * @param stakerBtcInfo - The staker's BTC information such as address and
2766
+ * @param stakerBtcInfo - The staker's BTC information such as address and
2283
2767
  * public key
2284
2768
  * @param params - The staking parameters.
2285
- * @param inclusionProof - The inclusion proof of the staking transaction.
2769
+ * @param options - The options for the BTC delegation.
2770
+ * @param options.inclusionProof - The inclusion proof of the staking
2771
+ * transaction.
2772
+ * @param options.delegationExpansionInfo - The information for the BTC
2773
+ * delegation expansion.
2286
2774
  * @returns The protobuf message.
2287
2775
  */
2288
- async createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params, inclusionProof) {
2289
- const {
2290
- unbondingTx,
2291
- slashingPsbt,
2292
- unbondingSlashingPsbt
2293
- } = await this.createDelegationTransactionsAndPsbts(
2776
+ async createBtcDelegationMsg(channel, stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params, options) {
2777
+ if (!params.slashing) {
2778
+ throw new StakingError(
2779
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
2780
+ "Slashing parameters are required for creating delegation message"
2781
+ );
2782
+ }
2783
+ const { unbondingTx, slashingPsbt, unbondingSlashingPsbt } = await this.createDelegationTransactionsAndPsbts(
2294
2784
  stakingInstance,
2295
2785
  stakingTx
2296
2786
  );
2297
- const signedSlashingPsbtHex = await this.btcProvider.signPsbt(
2298
- "staking-slashing" /* STAKING_SLASHING */,
2299
- slashingPsbt.toHex()
2300
- );
2301
- const signedSlashingTx = Psbt3.fromHex(
2302
- signedSlashingPsbtHex
2303
- ).extractTransaction();
2304
- const slashingSig = extractFirstSchnorrSignatureFromTransaction(
2305
- signedSlashingTx
2306
- );
2787
+ const slashingContracts = [
2788
+ {
2789
+ id: "babylon:staking" /* STAKING */,
2790
+ params: {
2791
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2792
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2793
+ covenantPks: params.covenantNoCoordPks,
2794
+ covenantThreshold: params.covenantQuorum,
2795
+ minUnbondingTime: params.unbondingTime,
2796
+ stakingDuration: stakingInput.stakingTimelock
2797
+ }
2798
+ },
2799
+ {
2800
+ id: "babylon:slashing" /* SLASHING */,
2801
+ params: {
2802
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2803
+ unbondingTimeBlocks: params.unbondingTime,
2804
+ slashingFeeSat: params.slashing.minSlashingTxFeeSat
2805
+ }
2806
+ },
2807
+ {
2808
+ id: "babylon:slashing-burn" /* SLASHING_BURN */,
2809
+ params: {
2810
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2811
+ slashingPkScriptHex: params.slashing.slashingPkScriptHex
2812
+ }
2813
+ }
2814
+ ];
2815
+ this.ee?.emit(channel, {
2816
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2817
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2818
+ covenantPks: params.covenantNoCoordPks,
2819
+ covenantThreshold: params.covenantQuorum,
2820
+ unbondingTimeBlocks: params.unbondingTime,
2821
+ stakingDuration: stakingInput.stakingTimelock,
2822
+ slashingFeeSat: params.slashing.minSlashingTxFeeSat,
2823
+ slashingPkScriptHex: params.slashing.slashingPkScriptHex,
2824
+ type: "staking-slashing"
2825
+ });
2826
+ const signedSlashingPsbtHex = await this.btcProvider.signPsbt(slashingPsbt.toHex(), {
2827
+ contracts: slashingContracts,
2828
+ action: {
2829
+ name: "sign-btc-slashing-transaction" /* SIGN_BTC_SLASHING_TRANSACTION */
2830
+ }
2831
+ });
2832
+ const signedSlashingTx = Psbt3.fromHex(signedSlashingPsbtHex).extractTransaction();
2833
+ const slashingSig = extractFirstSchnorrSignatureFromTransaction(signedSlashingTx);
2307
2834
  if (!slashingSig) {
2308
2835
  throw new Error("No signature found in the staking output slashing PSBT");
2309
2836
  }
2310
- const signedUnbondingSlashingPsbtHex = await this.btcProvider.signPsbt(
2311
- "unbonding-slashing" /* UNBONDING_SLASHING */,
2312
- unbondingSlashingPsbt.toHex()
2313
- );
2314
- const signedUnbondingSlashingTx = Psbt3.fromHex(
2315
- signedUnbondingSlashingPsbtHex
2316
- ).extractTransaction();
2317
- const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(
2318
- signedUnbondingSlashingTx
2319
- );
2837
+ const unbondingSlashingContracts = [
2838
+ {
2839
+ id: "babylon:unbonding" /* UNBONDING */,
2840
+ params: {
2841
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2842
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2843
+ covenantPks: params.covenantNoCoordPks,
2844
+ covenantThreshold: params.covenantQuorum,
2845
+ unbondingTimeBlocks: params.unbondingTime,
2846
+ unbondingFeeSat: params.unbondingFeeSat
2847
+ }
2848
+ },
2849
+ {
2850
+ id: "babylon:slashing" /* SLASHING */,
2851
+ params: {
2852
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2853
+ unbondingTimeBlocks: params.unbondingTime,
2854
+ slashingFeeSat: params.slashing.minSlashingTxFeeSat
2855
+ }
2856
+ },
2857
+ {
2858
+ id: "babylon:slashing-burn" /* SLASHING_BURN */,
2859
+ params: {
2860
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2861
+ slashingPkScriptHex: params.slashing.slashingPkScriptHex
2862
+ }
2863
+ }
2864
+ ];
2865
+ this.ee?.emit(channel, {
2866
+ stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
2867
+ finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
2868
+ covenantPks: params.covenantNoCoordPks,
2869
+ covenantThreshold: params.covenantQuorum,
2870
+ unbondingTimeBlocks: params.unbondingTime,
2871
+ unbondingFeeSat: params.unbondingFeeSat,
2872
+ slashingFeeSat: params.slashing.minSlashingTxFeeSat,
2873
+ slashingPkScriptHex: params.slashing.slashingPkScriptHex,
2874
+ type: "unbonding-slashing"
2875
+ });
2876
+ const signedUnbondingSlashingPsbtHex = await this.btcProvider.signPsbt(unbondingSlashingPsbt.toHex(), {
2877
+ contracts: unbondingSlashingContracts,
2878
+ action: {
2879
+ name: "sign-btc-unbonding-slashing-transaction" /* SIGN_BTC_UNBONDING_SLASHING_TRANSACTION */
2880
+ }
2881
+ });
2882
+ const signedUnbondingSlashingTx = Psbt3.fromHex(signedUnbondingSlashingPsbtHex).extractTransaction();
2883
+ const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(signedUnbondingSlashingTx);
2320
2884
  if (!unbondingSignatures) {
2321
2885
  throw new Error("No signature found in the unbonding output slashing PSBT");
2322
2886
  }
2323
- const proofOfPossession = await this.createProofOfPossession(bech32Address);
2324
- const msg = btcstakingtx.MsgCreateBTCDelegation.fromPartial({
2887
+ const proofOfPossession = await this.createProofOfPossession(channel, bech32Address, stakerBtcInfo.address);
2888
+ const commonMsg = {
2325
2889
  stakerAddr: bech32Address,
2326
2890
  pop: proofOfPossession,
2327
- btcPk: Uint8Array.from(
2328
- Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex")
2329
- ),
2330
- fpBtcPkList: [
2331
- Uint8Array.from(
2332
- Buffer.from(stakingInput.finalityProviderPkNoCoordHex, "hex")
2333
- )
2334
- ],
2891
+ btcPk: Uint8Array.from(Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex")),
2892
+ fpBtcPkList: stakingInput.finalityProviderPksNoCoordHex.map((pk) => Uint8Array.from(Buffer.from(pk, "hex"))),
2335
2893
  stakingTime: stakingInput.stakingTimelock,
2336
2894
  stakingValue: stakingInput.stakingAmountSat,
2337
2895
  stakingTx: Uint8Array.from(stakingTx.toBuffer()),
2338
- slashingTx: Uint8Array.from(
2339
- Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")
2340
- ),
2896
+ slashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")),
2341
2897
  delegatorSlashingSig: Uint8Array.from(slashingSig),
2342
2898
  unbondingTime: params.unbondingTime,
2343
2899
  unbondingTx: Uint8Array.from(unbondingTx.toBuffer()),
2344
2900
  unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat,
2345
- unbondingSlashingTx: Uint8Array.from(
2346
- Buffer.from(
2347
- clearTxSignatures(signedUnbondingSlashingTx).toHex(),
2348
- "hex"
2349
- )
2350
- ),
2351
- delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures),
2352
- stakingTxInclusionProof: inclusionProof
2901
+ unbondingSlashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedUnbondingSlashingTx).toHex(), "hex")),
2902
+ delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures)
2903
+ };
2904
+ if (options?.delegationExpansionInfo) {
2905
+ const fundingTx = Uint8Array.from(options.delegationExpansionInfo.fundingTx.toBuffer());
2906
+ const msg2 = btcstakingtx.MsgBtcStakeExpand.fromPartial({
2907
+ ...commonMsg,
2908
+ previousStakingTxHash: options.delegationExpansionInfo.previousStakingTx.getId(),
2909
+ fundingTx
2910
+ });
2911
+ return {
2912
+ typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgBtcStakeExpand,
2913
+ value: msg2
2914
+ };
2915
+ }
2916
+ const msg = btcstakingtx.MsgCreateBTCDelegation.fromPartial({
2917
+ ...commonMsg,
2918
+ stakingTxInclusionProof: options?.inclusionProof
2353
2919
  });
2354
2920
  return {
2355
2921
  typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgCreateBTCDelegation,
@@ -2363,11 +2929,7 @@ var BabylonBtcStakingManager = class {
2363
2929
  * @returns The inclusion proof.
2364
2930
  */
2365
2931
  getInclusionProof(inclusionProof) {
2366
- const {
2367
- pos,
2368
- merkle,
2369
- blockHashHex
2370
- } = inclusionProof;
2932
+ const { pos, merkle, blockHashHex } = inclusionProof;
2371
2933
  const proofHex = deriveMerkleProof(merkle);
2372
2934
  const hash = reverseBuffer(Uint8Array.from(Buffer.from(blockHashHex, "hex")));
2373
2935
  const inclusionProofKey = btccheckpoint.TransactionKey.fromPartial({
@@ -2380,54 +2942,235 @@ var BabylonBtcStakingManager = class {
2380
2942
  });
2381
2943
  }
2382
2944
  };
2383
- var extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => {
2384
- for (const input of singedTransaction.ins) {
2385
- if (input.witness && input.witness.length > 0) {
2386
- const schnorrSignature = input.witness[0];
2387
- if (schnorrSignature.length === 64) {
2388
- return schnorrSignature;
2389
- }
2390
- }
2391
- }
2392
- return void 0;
2393
- };
2394
- var clearTxSignatures = (tx) => {
2395
- tx.ins.forEach((input) => {
2396
- input.script = Buffer.alloc(0);
2397
- input.witness = [];
2398
- });
2399
- return tx;
2400
- };
2401
- var deriveMerkleProof = (merkle) => {
2402
- const proofHex = merkle.reduce((acc, m) => {
2403
- return acc + Buffer.from(m, "hex").reverse().toString("hex");
2404
- }, "");
2405
- return proofHex;
2406
- };
2407
2945
  var getUnbondingTxStakerSignature = (unbondingTx) => {
2408
2946
  try {
2409
2947
  return unbondingTx.ins[0].witness[0].toString("hex");
2410
2948
  } catch (error) {
2411
- throw StakingError.fromUnknown(
2412
- error,
2413
- "INVALID_INPUT" /* INVALID_INPUT */,
2414
- "Failed to get staker signature"
2949
+ throw StakingError.fromUnknown(error, "INVALID_INPUT" /* INVALID_INPUT */, "Failed to get staker signature");
2950
+ }
2951
+ };
2952
+
2953
+ // src/staking/observable/observableStakingScript.ts
2954
+ import { opcodes as opcodes4, script as script3 } from "bitcoinjs-lib";
2955
+ var ObservableStakingScriptData = class extends StakingScriptData {
2956
+ constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) {
2957
+ super(
2958
+ stakerKey,
2959
+ finalityProviderKeys,
2960
+ covenantKeys,
2961
+ covenantThreshold,
2962
+ stakingTimelock,
2963
+ unbondingTimelock
2964
+ );
2965
+ if (!magicBytes) {
2966
+ throw new Error("Missing required input values");
2967
+ }
2968
+ if (magicBytes.length != MAGIC_BYTES_LEN) {
2969
+ throw new Error("Invalid script data provided");
2970
+ }
2971
+ this.magicBytes = magicBytes;
2972
+ }
2973
+ /**
2974
+ * Builds a data embed script for staking in the form:
2975
+ * OP_RETURN || <serializedStakingData>
2976
+ * where serializedStakingData is the concatenation of:
2977
+ * MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock
2978
+ * Note: Only a single finality provider key is supported for now in phase 1
2979
+ * @throws {Error} If the number of finality provider keys is not equal to 1.
2980
+ * @returns {Buffer} The compiled data embed script.
2981
+ */
2982
+ buildDataEmbedScript() {
2983
+ if (this.finalityProviderKeys.length != 1) {
2984
+ throw new Error("Only a single finality provider key is supported");
2985
+ }
2986
+ const version = Buffer.alloc(1);
2987
+ version.writeUInt8(0);
2988
+ const stakingTimeLock = Buffer.alloc(2);
2989
+ stakingTimeLock.writeUInt16BE(this.stakingTimeLock);
2990
+ const serializedStakingData = Buffer.concat([
2991
+ this.magicBytes,
2992
+ version,
2993
+ this.stakerKey,
2994
+ this.finalityProviderKeys[0],
2995
+ stakingTimeLock
2996
+ ]);
2997
+ return script3.compile([opcodes4.OP_RETURN, serializedStakingData]);
2998
+ }
2999
+ /**
3000
+ * Builds the staking scripts.
3001
+ * @returns {ObservableStakingScripts} The staking scripts that can be used to stake.
3002
+ * contains the timelockScript, unbondingScript, slashingScript,
3003
+ * unbondingTimelockScript, and dataEmbedScript.
3004
+ * @throws {Error} If script data is invalid.
3005
+ */
3006
+ buildScripts() {
3007
+ const scripts = super.buildScripts();
3008
+ return {
3009
+ ...scripts,
3010
+ dataEmbedScript: this.buildDataEmbedScript()
3011
+ };
3012
+ }
3013
+ };
3014
+
3015
+ // src/staking/observable/index.ts
3016
+ var ObservableStaking = class extends Staking {
3017
+ constructor(network, stakerInfo, params, finalityProviderPksNoCoordHex, stakingTimelock) {
3018
+ super(
3019
+ network,
3020
+ stakerInfo,
3021
+ params,
3022
+ finalityProviderPksNoCoordHex,
3023
+ stakingTimelock
3024
+ );
3025
+ if (!params.tag) {
3026
+ throw new StakingError(
3027
+ "INVALID_INPUT" /* INVALID_INPUT */,
3028
+ "Observable staking parameters must include tag"
3029
+ );
3030
+ }
3031
+ if (!params.btcActivationHeight) {
3032
+ throw new StakingError(
3033
+ "INVALID_INPUT" /* INVALID_INPUT */,
3034
+ "Observable staking parameters must include a positive activation height"
3035
+ );
3036
+ }
3037
+ if (finalityProviderPksNoCoordHex.length !== 1) {
3038
+ throw new StakingError(
3039
+ "INVALID_INPUT" /* INVALID_INPUT */,
3040
+ "Observable staking requires exactly one finality provider public key"
3041
+ );
3042
+ }
3043
+ this.params = params;
3044
+ }
3045
+ /**
3046
+ * Build the staking scripts for observable staking.
3047
+ * This method overwrites the base method to include the OP_RETURN tag based
3048
+ * on the tag provided in the parameters.
3049
+ *
3050
+ * @returns {ObservableStakingScripts} - The staking scripts for observable staking.
3051
+ * @throws {StakingError} - If the scripts cannot be built.
3052
+ */
3053
+ buildScripts() {
3054
+ const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params;
3055
+ let stakingScriptData;
3056
+ try {
3057
+ stakingScriptData = new ObservableStakingScriptData(
3058
+ Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
3059
+ this.finalityProviderPksNoCoordHex.map((pk) => Buffer.from(pk, "hex")),
3060
+ toBuffers(covenantNoCoordPks),
3061
+ covenantQuorum,
3062
+ this.stakingTimelock,
3063
+ unbondingTime,
3064
+ Buffer.from(tag, "hex")
3065
+ );
3066
+ } catch (error) {
3067
+ throw StakingError.fromUnknown(
3068
+ error,
3069
+ "SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
3070
+ "Cannot build staking script data"
3071
+ );
3072
+ }
3073
+ let scripts;
3074
+ try {
3075
+ scripts = stakingScriptData.buildScripts();
3076
+ } catch (error) {
3077
+ throw StakingError.fromUnknown(
3078
+ error,
3079
+ "SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
3080
+ "Cannot build staking scripts"
3081
+ );
3082
+ }
3083
+ return scripts;
3084
+ }
3085
+ /**
3086
+ * Create a staking transaction for observable staking.
3087
+ * This overwrites the method from the Staking class with the addtion
3088
+ * of the
3089
+ * 1. OP_RETURN tag in the staking scripts
3090
+ * 2. lockHeight parameter
3091
+ *
3092
+ * @param {number} stakingAmountSat - The amount to stake in satoshis.
3093
+ * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
3094
+ * transaction.
3095
+ * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
3096
+ * @returns {TransactionResult} - An object containing the unsigned transaction,
3097
+ * and fee
3098
+ */
3099
+ createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
3100
+ validateStakingTxInputData(
3101
+ stakingAmountSat,
3102
+ this.stakingTimelock,
3103
+ this.params,
3104
+ inputUTXOs,
3105
+ feeRate
3106
+ );
3107
+ const scripts = this.buildScripts();
3108
+ try {
3109
+ const { transaction, fee } = stakingTransaction(
3110
+ scripts,
3111
+ stakingAmountSat,
3112
+ this.stakerInfo.address,
3113
+ inputUTXOs,
3114
+ this.network,
3115
+ feeRate,
3116
+ // `lockHeight` is exclusive of the provided value.
3117
+ // For example, if a Bitcoin height of X is provided,
3118
+ // the transaction will be included starting from height X+1.
3119
+ // https://learnmeabitcoin.com/technical/transaction/locktime/
3120
+ this.params.btcActivationHeight - 1
3121
+ );
3122
+ return {
3123
+ transaction,
3124
+ fee
3125
+ };
3126
+ } catch (error) {
3127
+ throw StakingError.fromUnknown(
3128
+ error,
3129
+ "BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
3130
+ "Cannot build unsigned staking transaction"
3131
+ );
3132
+ }
3133
+ }
3134
+ /**
3135
+ * Create a staking psbt for observable staking.
3136
+ *
3137
+ * @param {Transaction} stakingTx - The staking transaction.
3138
+ * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
3139
+ * transaction.
3140
+ * @returns {Psbt} - The psbt.
3141
+ */
3142
+ toStakingPsbt(stakingTx, inputUTXOs) {
3143
+ return stakingPsbt(
3144
+ stakingTx,
3145
+ this.network,
3146
+ inputUTXOs,
3147
+ isTaproot(
3148
+ this.stakerInfo.address,
3149
+ this.network
3150
+ ) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
2415
3151
  );
2416
3152
  }
2417
3153
  };
3154
+
3155
+ // src/types/params.ts
3156
+ function hasSlashing(params) {
3157
+ return params.slashing !== void 0;
3158
+ }
2418
3159
  export {
2419
3160
  BabylonBtcStakingManager,
2420
3161
  BitcoinScriptType,
2421
3162
  ObservableStaking,
2422
3163
  ObservableStakingScriptData,
2423
- SigningStep,
2424
3164
  Staking,
2425
3165
  StakingScriptData,
2426
3166
  buildStakingTransactionOutputs,
3167
+ clearTxSignatures,
2427
3168
  createCovenantWitness,
3169
+ deriveMerkleProof,
2428
3170
  deriveSlashingOutput,
2429
3171
  deriveStakingOutputInfo,
2430
3172
  deriveUnbondingOutputInfo,
3173
+ extractFirstSchnorrSignatureFromTransaction,
2431
3174
  findInputUTXO,
2432
3175
  findMatchingTxOutputIndex,
2433
3176
  getBabylonParamByBtcHeight,
@@ -2436,21 +3179,22 @@ export {
2436
3179
  getPublicKeyNoCoord,
2437
3180
  getScriptType,
2438
3181
  getUnbondingTxStakerSignature,
3182
+ hasSlashing,
2439
3183
  initBTCCurve,
3184
+ isNativeSegwit,
2440
3185
  isTaproot,
2441
3186
  isValidBabylonAddress,
2442
3187
  isValidBitcoinAddress,
2443
3188
  isValidNoCoordPublicKey,
2444
3189
  slashEarlyUnbondedTransaction,
2445
3190
  slashTimelockUnbondedTransaction,
3191
+ stakingExpansionTransaction,
2446
3192
  stakingTransaction,
2447
3193
  toBuffers,
2448
3194
  transactionIdToHash,
2449
3195
  unbondingTransaction,
2450
- validateParams,
2451
- validateStakingTimelock,
2452
- validateStakingTxInputData,
2453
3196
  withdrawEarlyUnbondedTransaction,
2454
3197
  withdrawSlashingTransaction,
2455
3198
  withdrawTimelockUnbondedTransaction
2456
3199
  };
3200
+ //# sourceMappingURL=index.js.map