@bitgo-beta/babylonlabs-io-btc-staking-ts 0.4.0-beta.62 → 0.4.0-beta.620

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