@@ -13,13 +13,11 @@ import {
1313 deriveStakingOutputInfo ,
1414 findMatchingTxOutputIndex ,
1515 toBuffers ,
16- validateParams ,
17- validateStakingTimelock ,
18- validateStakingTxInputData ,
1916} from "../utils/staking" ;
20- import { stakingPsbt , unbondingPsbt } from "./psbt" ;
17+ import { stakingExpansionPsbt , stakingPsbt , unbondingPsbt } from "./psbt" ;
2118import { StakingScriptData , StakingScripts } from "./stakingScript" ;
2219import {
20+ stakingExpansionTransaction ,
2321 slashEarlyUnbondedTransaction ,
2422 slashTimelockUnbondedTransaction ,
2523 stakingTransaction ,
@@ -28,6 +26,7 @@ import {
2826 withdrawSlashingTransaction ,
2927 withdrawTimelockUnbondedTransaction ,
3028} from "./transactions" ;
29+ import { validateParams , validateStakingExpansionCovenantQuorum , validateStakingTimelock , validateStakingTxInputData } from "../utils/staking/validation" ;
3130export * from "./stakingScript" ;
3231
3332export interface StakerInfo {
@@ -171,6 +170,106 @@ export class Staking {
171170 }
172171 }
173172
173+ /**
174+ * Creates a staking expansion transaction that extends an existing BTC stake
175+ * to new finality providers or renews the timelock.
176+ *
177+ * This method implements RFC 037 BTC Stake Expansion,
178+ * allowing existing active BTC staking transactions
179+ * to extend their delegation to new finality providers without going through
180+ * the full unbonding process.
181+ *
182+ * The expansion transaction:
183+ * 1. Spends the previous staking transaction output as the first input
184+ * 2. Uses funding UTXO as additional input to cover transaction fees or
185+ * to increase the staking amount
186+ * 3. Creates a new staking output with expanded finality provider coverage or
187+ * renews the timelock
188+ * 4. Has an output returning the remaining funds as change (if any) to the
189+ * staker BTC address
190+ *
191+ * @param {number } stakingAmountSat - The total staking amount in satoshis
192+ * (The amount had to be equal to the previous staking amount for now, this
193+ * lib does not yet support increasing the staking amount at this stage)
194+ * @param {UTXO[] } inputUTXOs - Available UTXOs to use for funding the
195+ * expansion transaction fees. Only one will be selected for the expansion
196+ * @param {number } feeRate - Fee rate in satoshis per byte for the
197+ * expansion transaction
198+ * @param {StakingParams } paramsForPreviousStakingTx - Staking parameters
199+ * used in the previous staking transaction
200+ * @param {Object } previousStakingTxInfo - Necessary information to spend the
201+ * previous staking transaction.
202+ * @returns {TransactionResult & { fundingUTXO: UTXO } } - An object containing
203+ * the unsigned expansion transaction and calculated fee, and the funding UTXO
204+ * @throws {StakingError } - If the transaction cannot be built or validation
205+ * fails
206+ */
207+ public createStakingExpansionTransaction (
208+ stakingAmountSat : number ,
209+ inputUTXOs : UTXO [ ] ,
210+ feeRate : number ,
211+ paramsForPreviousStakingTx : StakingParams ,
212+ previousStakingTxInfo : {
213+ stakingTx : Transaction ,
214+ stakingInput : {
215+ finalityProviderPksNoCoordHex : string [ ] ,
216+ stakingTimelock : number ,
217+ } ,
218+ } ,
219+ ) : TransactionResult & {
220+ fundingUTXO : UTXO ;
221+ } {
222+ validateStakingTxInputData (
223+ stakingAmountSat ,
224+ this . stakingTimelock ,
225+ this . params ,
226+ inputUTXOs ,
227+ feeRate ,
228+ ) ;
229+ validateStakingExpansionCovenantQuorum (
230+ paramsForPreviousStakingTx ,
231+ this . params ,
232+ ) ;
233+
234+ // Create a Staking instance for the previous staking transaction
235+ // This allows us to build the scripts needed to spend the previous
236+ // staking output
237+ const previousStaking = new Staking (
238+ this . network ,
239+ this . stakerInfo ,
240+ paramsForPreviousStakingTx ,
241+ previousStakingTxInfo . stakingInput . finalityProviderPksNoCoordHex ,
242+ previousStakingTxInfo . stakingInput . stakingTimelock ,
243+ ) ;
244+
245+ // Build the expansion transaction using the stakingExpansionTransaction
246+ // utility function.
247+ // This creates a transaction that spends the previous staking output and
248+ // creates new staking outputs
249+ const {
250+ transaction : stakingExpansionTx ,
251+ fee : stakingExpansionTxFee ,
252+ fundingUTXO,
253+ } = stakingExpansionTransaction (
254+ this . network ,
255+ this . buildScripts ( ) ,
256+ stakingAmountSat ,
257+ this . stakerInfo . address ,
258+ feeRate ,
259+ inputUTXOs ,
260+ {
261+ stakingTx : previousStakingTxInfo . stakingTx ,
262+ scripts : previousStaking . buildScripts ( ) ,
263+ } ,
264+ )
265+
266+ return {
267+ transaction : stakingExpansionTx ,
268+ fee : stakingExpansionTxFee ,
269+ fundingUTXO,
270+ } ;
271+ }
272+
174273 /**
175274 * Create a staking psbt based on the existing staking transaction.
176275 *
@@ -200,6 +299,76 @@ export class Staking {
200299 ) ;
201300 }
202301
302+ /**
303+ * Convert a staking expansion transaction to a PSBT.
304+ *
305+ * @param {Transaction } stakingExpansionTx - The staking expansion
306+ * transaction to convert
307+ * @param {UTXO[] } inputUTXOs - Available UTXOs for the
308+ * funding input (second input)
309+ * @param {StakingParams } paramsForPreviousStakingTx - Staking parameters
310+ * used for the previous staking transaction
311+ * @param {Object } previousStakingTxInfo - Information about the previous
312+ * staking transaction
313+ * @returns {Psbt } The PSBT for the staking expansion transaction
314+ * @throws {Error } If the previous staking output cannot be found or
315+ * validation fails
316+ */
317+ public toStakingExpansionPsbt (
318+ stakingExpansionTx : Transaction ,
319+ inputUTXOs : UTXO [ ] ,
320+ paramsForPreviousStakingTx : StakingParams ,
321+ previousStakingTxInfo : {
322+ stakingTx : Transaction ,
323+ stakingInput : {
324+ finalityProviderPksNoCoordHex : string [ ] ,
325+ stakingTimelock : number ,
326+ } ,
327+ } ,
328+ ) : Psbt {
329+ // Reconstruct the previous staking instance to access its scripts and
330+ // parameters. This is necessary because we need to identify which output
331+ // in the previous staking transaction is the staking output (it could be
332+ // at any output index)
333+ const previousStaking = new Staking (
334+ this . network ,
335+ this . stakerInfo ,
336+ paramsForPreviousStakingTx ,
337+ previousStakingTxInfo . stakingInput . finalityProviderPksNoCoordHex ,
338+ previousStakingTxInfo . stakingInput . stakingTimelock ,
339+ ) ;
340+
341+ // Find the staking output address in the previous staking transaction
342+ const previousScripts = previousStaking . buildScripts ( ) ;
343+ const { outputAddress } = deriveStakingOutputInfo ( previousScripts , this . network ) ;
344+
345+ // Find the output index in the previous staking transaction that matches
346+ // the staking output address.
347+ const previousStakingOutputIndex = findMatchingTxOutputIndex (
348+ previousStakingTxInfo . stakingTx ,
349+ outputAddress ,
350+ this . network ,
351+ ) ;
352+
353+ // Create and return the PSBT for the staking expansion transaction
354+ // The PSBT will have two inputs:
355+ // 1. The previous staking output
356+ // 2. A funding UTXO from inputUTXOs (for additional funds)
357+ return stakingExpansionPsbt (
358+ this . network ,
359+ stakingExpansionTx ,
360+ {
361+ stakingTx : previousStakingTxInfo . stakingTx ,
362+ outputIndex : previousStakingOutputIndex ,
363+ } ,
364+ inputUTXOs ,
365+ previousScripts ,
366+ isTaproot ( this . stakerInfo . address , this . network )
367+ ? Buffer . from ( this . stakerInfo . publicKeyNoCoordHex , "hex" )
368+ : undefined ,
369+ ) ;
370+ }
371+
203372 /**
204373 * Create an unbonding transaction for staking.
205374 *
0 commit comments