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

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