@bitgo-beta/babylonlabs-io-btc-staking-ts 0.4.0-beta.73 → 0.4.0-beta.731

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