@bitgo/babylonlabs-io-btc-staking-ts
Version:
Library exposing methods for the creation and consumption of Bitcoin transactions pertaining to Babylon's Bitcoin Staking protocol.
4 lines • 236 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/index.ts", "../src/error/index.ts", "../src/utils/btc.ts", "../src/constants/keys.ts", "../src/utils/staking/index.ts", "../src/constants/internalPubkey.ts", "../src/staking/psbt.ts", "../src/constants/transaction.ts", "../src/utils/utxo/findInputUTXO.ts", "../src/utils/utxo/getScriptType.ts", "../src/utils/utxo/getPsbtInputFields.ts", "../src/staking/stakingScript.ts", "../src/staking/transactions.ts", "../src/constants/dustSat.ts", "../src/utils/fee/index.ts", "../src/constants/fee.ts", "../src/utils/fee/utils.ts", "../src/constants/psbt.ts", "../src/constants/unbonding.ts", "../src/utils/babylon.ts", "../src/utils/staking/validation.ts", "../src/staking/index.ts", "../src/staking/manager.ts", "../src/constants/registry.ts", "../src/utils/index.ts", "../src/utils/pop.ts", "../src/constants/staking.ts", "../src/utils/staking/param.ts", "../src/staking/observable/observableStakingScript.ts", "../src/staking/observable/index.ts", "../src/types/params.ts"],
"sourcesContent": ["export { Staking, StakingScriptData } from \"./staking\";\nexport type { StakingScripts } from \"./staking\";\nexport * from \"./staking/manager\";\nexport {\n ObservableStaking,\n ObservableStakingScriptData,\n} from \"./staking/observable\";\nexport * from \"./staking/transactions\";\nexport * from \"./types\";\nexport * from \"./utils/btc\";\nexport {\n getBabylonParamByBtcHeight,\n getBabylonParamByVersion,\n} from \"./utils/staking/param\";\nexport * from \"./utils/utxo/findInputUTXO\";\nexport * from \"./utils/utxo/getPsbtInputFields\";\nexport * from \"./utils/utxo/getScriptType\";\n\n// BitGo-specific exports\nexport * from \"./utils/babylon\";\nexport * from \"./utils/staking\";\n", "export enum StakingErrorCode {\n UNKNOWN_ERROR = \"UNKNOWN_ERROR\",\n INVALID_INPUT = \"INVALID_INPUT\",\n INVALID_OUTPUT = \"INVALID_OUTPUT\",\n SCRIPT_FAILURE = \"SCRIPT_FAILURE\",\n BUILD_TRANSACTION_FAILURE = \"BUILD_TRANSACTION_FAILURE\",\n INVALID_PARAMS = \"INVALID_PARAMS\",\n}\n\nexport class StakingError extends Error {\n public code: StakingErrorCode;\n constructor(code: StakingErrorCode, message?: string) {\n super(message);\n this.code = code;\n }\n\n // Static method to safely handle unknown errors\n static fromUnknown(\n error: unknown, code: StakingErrorCode, fallbackMsg?: string\n ): StakingError {\n if (error instanceof StakingError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new StakingError(code, error.message);\n }\n return new StakingError(code, fallbackMsg);\n }\n}", "import * as ecc from \"@bitcoin-js/tiny-secp256k1-asmjs\";\nimport { address, initEccLib, networks } from \"bitcoinjs-lib\";\nimport { NO_COORD_PK_BYTE_LENGTH } from \"../constants/keys\";\n\n// Initialize elliptic curve library\nexport const initBTCCurve = () => {\n initEccLib(ecc);\n};\n\n/**\n * Check whether the given address is a valid Bitcoin address.\n *\n * @param {string} btcAddress - The Bitcoin address to check.\n * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).\n * @returns {boolean} - True if the address is valid, otherwise false.\n */\nexport const isValidBitcoinAddress = (\n btcAddress: string,\n network: networks.Network,\n): boolean => {\n try {\n return !!address.toOutputScript(btcAddress, network);\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Check whether the given address is a Taproot address.\n *\n * @param {string} taprootAddress - The Bitcoin bech32 encoded address to check.\n * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).\n * @returns {boolean} - True if the address is a Taproot address, otherwise false.\n */\nexport const isTaproot = (\n taprootAddress: string,\n network: networks.Network,\n): boolean => {\n try {\n const decoded = address.fromBech32(taprootAddress);\n if (decoded.version !== 1) {\n return false;\n }\n\n // Compare network properties instead of object reference\n // The bech32 is hardcoded in the bitcoinjs-lib library.\n // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36\n if (network.bech32 === networks.bitcoin.bech32) {\n // Check if address starts with \"bc1p\"\n return taprootAddress.startsWith(\"bc1p\");\n } else if (network.bech32 === networks.testnet.bech32) {\n // signet, regtest and testnet taproot addresses start with \"tb1p\" or \"sb1p\"\n return (\n taprootAddress.startsWith(\"tb1p\") || taprootAddress.startsWith(\"sb1p\")\n );\n }\n return false;\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Check whether the given address is a Native SegWit address.\n *\n * @param {string} segwitAddress - The Bitcoin bech32 encoded address to check.\n * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).\n * @returns {boolean} - True if the address is a Native SegWit address, otherwise false.\n */\nexport const isNativeSegwit = (\n segwitAddress: string,\n network: networks.Network,\n): boolean => {\n try {\n const decoded = address.fromBech32(segwitAddress);\n if (decoded.version !== 0) {\n return false;\n }\n\n // Compare network properties instead of object reference\n // The bech32 is hardcoded in the bitcoinjs-lib library.\n // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36\n if (network.bech32 === networks.bitcoin.bech32) {\n // Check if address starts with \"bc1q\"\n return segwitAddress.startsWith(\"bc1q\");\n } else if (network.bech32 === networks.testnet.bech32) {\n // testnet native segwit addresses start with \"tb1q\"\n return segwitAddress.startsWith(\"tb1q\");\n }\n return false;\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Check whether the given public key is a valid public key without a coordinate.\n *\n * @param {string} pkWithNoCoord - public key without the coordinate.\n * @returns {boolean} - True if the public key without the coordinate is valid, otherwise false.\n */\nexport const isValidNoCoordPublicKey = (pkWithNoCoord: string): boolean => {\n try {\n const keyBuffer = Buffer.from(pkWithNoCoord, \"hex\");\n return validateNoCoordPublicKeyBuffer(keyBuffer);\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Get the public key without the coordinate.\n *\n * @param {string} pkHex - The public key in hex, with or without the coordinate.\n * @returns {string} - The public key without the coordinate in hex.\n * @throws {Error} - If the public key is invalid.\n */\nexport const getPublicKeyNoCoord = (pkHex: string): string => {\n const publicKey = Buffer.from(pkHex, \"hex\");\n\n const publicKeyNoCoordBuffer =\n publicKey.length === NO_COORD_PK_BYTE_LENGTH\n ? publicKey\n : publicKey.subarray(1, 33);\n\n // Validate the public key without coordinate\n if (!validateNoCoordPublicKeyBuffer(publicKeyNoCoordBuffer)) {\n throw new Error(\"Invalid public key without coordinate\");\n }\n\n return publicKeyNoCoordBuffer.toString(\"hex\");\n};\n\nconst validateNoCoordPublicKeyBuffer = (pkBuffer: Buffer): boolean => {\n if (pkBuffer.length !== NO_COORD_PK_BYTE_LENGTH) {\n return false;\n }\n\n // Try both compressed forms: y-coordinate even (0x02) and y-coordinate odd (0x03)\n const compressedKeyEven = Buffer.concat([Buffer.from([0x02]), pkBuffer]);\n const compressedKeyOdd = Buffer.concat([Buffer.from([0x03]), pkBuffer]);\n\n return ecc.isPoint(compressedKeyEven) || ecc.isPoint(compressedKeyOdd);\n};\n\n/**\n * Convert a transaction id to a hash. in buffer format.\n *\n * @param {string} txId - The transaction id.\n * @returns {Buffer} - The transaction hash.\n */\nexport const transactionIdToHash = (txId: string): Buffer => {\n if (txId === \"\") {\n throw new Error(\"Transaction id cannot be empty\");\n }\n return Buffer.from(txId, \"hex\").reverse();\n};\n", "// NO_COORD_PK_BYTE_LENGTH is the length of a BTC public key without the coordinate in bytes.\nexport const NO_COORD_PK_BYTE_LENGTH = 32;", "import { address, networks, payments, Transaction } from \"bitcoinjs-lib\";\nimport { Taptree } from \"bitcoinjs-lib/src/types\";\nimport { internalPubkey } from \"../../constants/internalPubkey\";\nimport { StakingError, StakingErrorCode } from \"../../error\";\nimport { TransactionOutput } from \"../../types/psbtOutputs\";\nexport interface OutputInfo {\n scriptPubKey: Buffer;\n outputAddress: string;\n}\n\n/**\n * Build the staking output for the transaction which contains p2tr output\n * with staking scripts.\n *\n * @param {StakingScripts} scripts - The staking scripts.\n * @param {networks.Network} network - The Bitcoin network.\n * @param {number} amount - The amount to stake.\n * @returns {TransactionOutput[]} - The staking transaction outputs.\n * @throws {Error} - If the staking output cannot be built.\n */\nexport const buildStakingTransactionOutputs = (\n scripts: {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n dataEmbedScript?: Buffer;\n },\n network: networks.Network,\n amount: number,\n): TransactionOutput[] => {\n const stakingOutputInfo = deriveStakingOutputInfo(scripts, network);\n const transactionOutputs: { scriptPubKey: Buffer; value: number }[] = [\n {\n scriptPubKey: stakingOutputInfo.scriptPubKey,\n value: amount,\n },\n ];\n if (scripts.dataEmbedScript) {\n // Add the data embed output to the transaction\n transactionOutputs.push({\n scriptPubKey: scripts.dataEmbedScript,\n value: 0,\n });\n }\n return transactionOutputs;\n};\n\n/**\n * Derive the staking output address from the staking scripts.\n *\n * @param {StakingScripts} scripts - The staking scripts.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {StakingOutput} - The staking output address and scriptPubKey.\n * @throws {StakingError} - If the staking output address cannot be derived.\n */\nexport const deriveStakingOutputInfo = (\n scripts: {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n },\n network: networks.Network,\n) => {\n // Build outputs\n const scriptTree: Taptree = [\n {\n output: scripts.slashingScript,\n },\n [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }],\n ];\n\n // Create an pay-2-taproot (p2tr) output using the staking script\n const stakingOutput = payments.p2tr({\n internalPubkey,\n scriptTree,\n network,\n });\n\n if (!stakingOutput.address) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n \"Failed to build staking output\",\n );\n }\n\n return {\n outputAddress: stakingOutput.address,\n scriptPubKey: address.toOutputScript(stakingOutput.address, network),\n };\n};\n\n/**\n * Derive the unbonding output address and scriptPubKey from the staking scripts.\n *\n * @param {StakingScripts} scripts - The staking scripts.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {OutputInfo} - The unbonding output address and scriptPubKey.\n * @throws {StakingError} - If the unbonding output address cannot be derived.\n */\nexport const deriveUnbondingOutputInfo = (\n scripts: {\n unbondingTimelockScript: Buffer;\n slashingScript: Buffer;\n },\n network: networks.Network,\n) => {\n const outputScriptTree: Taptree = [\n {\n output: scripts.slashingScript,\n },\n { output: scripts.unbondingTimelockScript },\n ];\n\n const unbondingOutput = payments.p2tr({\n internalPubkey,\n scriptTree: outputScriptTree,\n network,\n });\n\n if (!unbondingOutput.address) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n \"Failed to build unbonding output\",\n );\n }\n\n return {\n outputAddress: unbondingOutput.address,\n scriptPubKey: address.toOutputScript(unbondingOutput.address, network),\n };\n};\n\n/**\n * Derive the slashing output address and scriptPubKey from the staking scripts.\n *\n * @param {StakingScripts} scripts - The unbonding timelock scripts, we use the\n * unbonding timelock script as the timelock of the slashing transaction.\n * This is due to slashing tx timelock is the same as the unbonding timelock.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {OutputInfo} - The slashing output address and scriptPubKey.\n * @throws {StakingError} - If the slashing output address cannot be derived.\n */\nexport const deriveSlashingOutput = (\n scripts: {\n unbondingTimelockScript: Buffer;\n },\n network: networks.Network,\n) => {\n const slashingOutput = payments.p2tr({\n internalPubkey,\n scriptTree: { output: scripts.unbondingTimelockScript },\n network,\n });\n const slashingOutputAddress = slashingOutput.address;\n\n if (!slashingOutputAddress) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n \"Failed to build slashing output address\",\n );\n }\n\n return {\n outputAddress: slashingOutputAddress,\n scriptPubKey: address.toOutputScript(slashingOutputAddress, network),\n };\n};\n\n/**\n * Find the matching output index for the given transaction.\n *\n * @param {Transaction} tx - The transaction.\n * @param {string} outputAddress - The output address.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {number} - The output index.\n * @throws {Error} - If the matching output is not found.\n */\nexport const findMatchingTxOutputIndex = (\n tx: Transaction,\n outputAddress: string,\n network: networks.Network,\n) => {\n const index = tx.outs.findIndex((output) => {\n try {\n return address.fromOutputScript(output.script, network) === outputAddress;\n } catch (error) {\n return false;\n }\n });\n\n if (index === -1) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n `Matching output not found for address: ${outputAddress}`,\n );\n }\n\n return index;\n};\n\n/**\n * toBuffers converts an array of strings to an array of buffers.\n *\n * @param {string[]} inputs - The input strings.\n * @returns {Buffer[]} - The buffers.\n * @throws {StakingError} - If the values cannot be converted to buffers.\n */\nexport const toBuffers = (inputs: string[]): Buffer[] => {\n try {\n return inputs.map((i) => Buffer.from(i, \"hex\"));\n } catch (error) {\n throw StakingError.fromUnknown(\n error,\n StakingErrorCode.INVALID_INPUT,\n \"Cannot convert values to buffers\",\n );\n }\n};\n\n\n/**\n * Strips all signatures from a transaction by clearing both the script and\n * witness data. This is due to the fact that we only need the raw unsigned\n * transaction structure. The signatures are sent in a separate protobuf field\n * when creating the delegation message in the Babylon.\n * @param tx - The transaction to strip signatures from\n * @returns A copy of the transaction with all signatures removed\n */\nexport const clearTxSignatures = (tx: Transaction): Transaction => {\n tx.ins.forEach((input) => {\n input.script = Buffer.alloc(0);\n input.witness = [];\n });\n return tx;\n};\n\n/**\n * Derives the merkle proof from the list of hex strings. Note the\n * sibling hashes are reversed from hex before concatenation.\n * @param merkle - The merkle proof hex strings.\n * @returns The merkle proof in hex string format.\n */\nexport const deriveMerkleProof = (merkle: string[]) => {\n const proofHex = merkle.reduce((acc: string, m: string) => {\n return acc + Buffer.from(m, \"hex\").reverse().toString(\"hex\");\n }, \"\");\n return proofHex;\n};\n\n/**\n * Extracts the first valid Schnorr signature from a signed transaction.\n *\n * Since we only handle transactions with a single input and request a signature\n * for one public key, there can be at most one signature from the Bitcoin node.\n * A valid Schnorr signature is exactly 64 bytes in length.\n *\n * @param singedTransaction - The signed Bitcoin transaction to extract the signature from\n * @returns The first valid 64-byte Schnorr signature found in the transaction witness data,\n * or undefined if no valid signature exists\n */\nexport const extractFirstSchnorrSignatureFromTransaction = (\n singedTransaction: Transaction,\n): Buffer | undefined => {\n // Loop through each input to extract the witness signature\n for (const input of singedTransaction.ins) {\n if (input.witness && input.witness.length > 0) {\n const schnorrSignature = input.witness[0];\n\n // Check that it's a 64-byte Schnorr signature\n if (schnorrSignature.length === 64) {\n return schnorrSignature; // Return the first valid signature found\n }\n }\n }\n return undefined;\n};\n", "// internalPubkey denotes an unspendable internal public key to be used for the taproot output\nconst key =\n \"0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0\";\nexport const internalPubkey = Buffer.from(key, \"hex\").subarray(1, 33); // Do a subarray(1, 33) to get the public coordinate\n", "import { Psbt, Transaction, networks, payments } from \"bitcoinjs-lib\";\nimport { Taptree } from \"bitcoinjs-lib/src/types\";\nimport { internalPubkey } from \"../constants/internalPubkey\";\nimport { NO_COORD_PK_BYTE_LENGTH } from \"../constants/keys\";\nimport { REDEEM_VERSION } from \"../constants/transaction\";\nimport { UTXO } from \"../types/UTXO\";\nimport { deriveUnbondingOutputInfo } from \"../utils/staking\";\nimport { findInputUTXO } from \"../utils/utxo/findInputUTXO\";\nimport { getPsbtInputFields } from \"../utils/utxo/getPsbtInputFields\";\nimport { BitcoinScriptType, getScriptType } from \"../utils/utxo/getScriptType\";\nimport { StakingScripts } from \"./stakingScript\";\n\n/**\n * Convert a staking transaction to a PSBT.\n *\n * @param {Transaction} stakingTx - The staking transaction to convert to PSBT.\n * @param {networks.Network} network - The network to use for the PSBT.\n * @param {UTXO[]} inputUTXOs - The UTXOs to be used as inputs for the staking\n * transaction.\n * @param {Buffer} [publicKeyNoCoord] - The public key of staker (optional)\n * @returns {Psbt} - The PSBT for the staking transaction.\n * @throws {Error} If unable to create PSBT from transaction\n */\nexport const stakingPsbt = (\n stakingTx: Transaction,\n network: networks.Network,\n inputUTXOs: UTXO[],\n publicKeyNoCoord?: Buffer,\n): Psbt => {\n if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {\n throw new Error(\"Invalid public key\");\n }\n\n const psbt = new Psbt({ network });\n\n if (stakingTx.version !== undefined) psbt.setVersion(stakingTx.version);\n if (stakingTx.locktime !== undefined) psbt.setLocktime(stakingTx.locktime);\n\n stakingTx.ins.forEach((input) => {\n const inputUTXO = findInputUTXO(inputUTXOs, input);\n const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);\n\n psbt.addInput({\n hash: input.hash,\n index: input.index,\n sequence: input.sequence,\n ...psbtInputData,\n });\n });\n\n stakingTx.outs.forEach((o) => {\n psbt.addOutput({ script: o.script, value: o.value });\n });\n\n return psbt;\n};\n\n/**\n * Convert a staking expansion transaction to a PSBT.\n *\n * @param {networks.Network} network - The Bitcoin network to use for the PSBT\n * @param {Transaction} stakingTx - The staking expansion transaction to convert\n * @param {Object} previousStakingTxInfo - Information about the previous staking transaction\n * @param {Transaction} previousStakingTxInfo.stakingTx - The previous staking transaction\n * @param {number} previousStakingTxInfo.outputIndex - The index of the staking output in the previous transaction\n * @param {UTXO[]} inputUTXOs - Available UTXOs for the funding input\n * @param {Buffer} [publicKeyNoCoord] - The staker's public key without coordinate (for Taproot)\n * @returns {Psbt} The PSBT for the staking expansion transaction\n * @throws {Error} If validation fails or required data is missing\n */\nexport const stakingExpansionPsbt = (\n network: networks.Network,\n stakingTx: Transaction,\n previousStakingTxInfo: {\n stakingTx: Transaction,\n outputIndex: number,\n },\n inputUTXOs: UTXO[],\n previousScripts: StakingScripts,\n publicKeyNoCoord?: Buffer,\n): Psbt => {\n // Initialize PSBT with the specified network\n const psbt = new Psbt({ network });\n \n // Set transaction version and locktime if provided\n if (stakingTx.version !== undefined) psbt.setVersion(stakingTx.version);\n if (stakingTx.locktime !== undefined) psbt.setLocktime(stakingTx.locktime);\n\n // Validate the public key format if provided\n if (\n publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH\n ) {\n throw new Error(\"Invalid public key\");\n }\n\n // Extract the previous staking output from the previous staking transaction\n const previousStakingOutput = previousStakingTxInfo.stakingTx.outs[\n previousStakingTxInfo.outputIndex\n ];\n if (!previousStakingOutput) {\n throw new Error(\"Previous staking output not found\");\n };\n \n // Validate that the previous staking output is a Taproot (P2TR) script\n if (\n getScriptType(previousStakingOutput.script) !== BitcoinScriptType.P2TR\n ) {\n throw new Error(\"Previous staking output script type is not P2TR\");\n }\n\n // Validate that the staking expansion transaction has exactly 2 inputs\n // Input 0: Previous staking output (existing stake)\n // Input 1: Funding UTXO (additional funds for fees or staking amount)\n if (stakingTx.ins.length !== 2) {\n throw new Error(\n \"Staking expansion transaction must have exactly 2 inputs\",\n );\n }\n\n // Validate the first input matches the previous staking transaction\n const txInputs = stakingTx.ins;\n \n // Check that the first input references the correct previous staking\n // transaction\n if (\n Buffer.from(txInputs[0].hash).reverse().toString(\"hex\") !== previousStakingTxInfo.stakingTx.getId()\n ) {\n throw new Error(\"Previous staking input hash does not match\");\n } \n // Check that the first input references the correct output index\n else if (txInputs[0].index !== previousStakingTxInfo.outputIndex) {\n throw new Error(\"Previous staking input index does not match\");\n }\n\n // Build input tapleaf script that spends the previous staking output\n const inputScriptTree: Taptree = [\n { output: previousScripts.slashingScript },\n [{ output: previousScripts.unbondingScript }, { output: previousScripts.timelockScript }],\n ];\n const inputRedeem = {\n output: previousScripts.unbondingScript,\n redeemVersion: REDEEM_VERSION,\n };\n const p2tr = payments.p2tr({\n internalPubkey,\n scriptTree: inputScriptTree,\n redeem: inputRedeem,\n network,\n });\n\n if (!p2tr.witness || p2tr.witness.length === 0) {\n throw new Error(\n \"Failed to create P2TR witness for expansion transaction input\"\n );\n }\n\n const inputTapLeafScript = {\n leafVersion: inputRedeem.redeemVersion,\n script: inputRedeem.output,\n controlBlock: p2tr.witness[p2tr.witness.length - 1],\n };\n\n // Add the previous staking input to the PSBT\n // This input spends the existing staking output\n psbt.addInput({\n hash: txInputs[0].hash,\n index: txInputs[0].index,\n sequence: txInputs[0].sequence,\n witnessUtxo: {\n script: previousStakingOutput.script,\n value: previousStakingOutput.value,\n },\n tapInternalKey: internalPubkey,\n tapLeafScript: [inputTapLeafScript],\n });\n\n // Add the second input (funding UTXO) to the PSBT\n // This input provides additional funds for fees or staking amount\n const inputUTXO = findInputUTXO(inputUTXOs, txInputs[1]);\n const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);\n\n psbt.addInput({\n hash: txInputs[1].hash,\n index: txInputs[1].index,\n sequence: txInputs[1].sequence,\n ...psbtInputData,\n });\n\n // Add all outputs from the staking expansion transaction to the PSBT\n stakingTx.outs.forEach((o) => {\n psbt.addOutput({ script: o.script, value: o.value });\n });\n\n return psbt;\n};\n\nexport const unbondingPsbt = (\n scripts: {\n unbondingScript: Buffer;\n timelockScript: Buffer;\n slashingScript: Buffer;\n unbondingTimelockScript: Buffer;\n },\n unbondingTx: Transaction,\n stakingTx: Transaction,\n network: networks.Network,\n): Psbt => {\n if (unbondingTx.outs.length !== 1) {\n throw new Error(\"Unbonding transaction must have exactly one output\");\n }\n if (unbondingTx.ins.length !== 1) {\n throw new Error(\"Unbonding transaction must have exactly one input\");\n }\n\n validateUnbondingOutput(scripts, unbondingTx, network);\n\n const psbt = new Psbt({ network });\n\n if (unbondingTx.version !== undefined) {\n psbt.setVersion(unbondingTx.version);\n }\n if (unbondingTx.locktime !== undefined) {\n psbt.setLocktime(unbondingTx.locktime);\n }\n\n const input = unbondingTx.ins[0];\n const outputIndex = input.index;\n\n // Build input tapleaf script\n const inputScriptTree: Taptree = [\n { output: scripts.slashingScript },\n [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }],\n ];\n\n // This is the tapleaf we are actually spending\n const inputRedeem = {\n output: scripts.unbondingScript,\n redeemVersion: REDEEM_VERSION,\n };\n\n // Create a P2TR payment that includes scriptTree + redeem\n const p2tr = payments.p2tr({\n internalPubkey,\n scriptTree: inputScriptTree,\n redeem: inputRedeem,\n network,\n });\n\n const inputTapLeafScript = {\n leafVersion: inputRedeem.redeemVersion,\n script: inputRedeem.output,\n controlBlock: p2tr.witness![p2tr.witness!.length - 1],\n };\n\n psbt.addInput({\n hash: input.hash,\n index: input.index,\n sequence: input.sequence,\n tapInternalKey: internalPubkey,\n witnessUtxo: {\n value: stakingTx.outs[outputIndex].value,\n script: stakingTx.outs[outputIndex].script,\n },\n tapLeafScript: [inputTapLeafScript],\n });\n\n psbt.addOutput({\n script: unbondingTx.outs[0].script,\n value: unbondingTx.outs[0].value,\n });\n\n return psbt;\n};\n\n/**\n * Validate the unbonding output for a given unbonding transaction.\n *\n * @param {Object} scripts - The scripts to use for the unbonding output.\n * @param {Transaction} unbondingTx - The unbonding transaction.\n * @param {networks.Network} network - The network to use for the unbonding output.\n */\nconst validateUnbondingOutput = (\n scripts: {\n slashingScript: Buffer;\n unbondingTimelockScript: Buffer;\n },\n unbondingTx: Transaction,\n network: networks.Network,\n) => {\n const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);\n if (\n unbondingOutputInfo.scriptPubKey.toString(\"hex\") !==\n unbondingTx.outs[0].script.toString(\"hex\")\n ) {\n throw new Error(\n \"Unbonding output script does not match the expected\" +\n \" script while building psbt\",\n );\n }\n};\n", "export const REDEEM_VERSION = 192;", "import { Input } from \"bitcoinjs-lib/src/transaction\";\n\nimport { UTXO } from \"../../types/UTXO\";\nimport { transactionIdToHash } from \"../btc\";\n\nexport const findInputUTXO = (inputUTXOs: UTXO[], input: Input): UTXO => {\n const inputUTXO = inputUTXOs.find(\n (u) =>\n transactionIdToHash(u.txid).toString(\"hex\") ===\n input.hash.toString(\"hex\") && u.vout === input.index,\n );\n if (!inputUTXO) {\n throw new Error(\n `Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString(\"hex\")} ` +\n `and vout: ${input.index}`,\n );\n }\n return inputUTXO;\n};\n", "import { payments } from \"bitcoinjs-lib\";\n\n/**\n * Supported Bitcoin script types\n */\nexport enum BitcoinScriptType {\n // Pay to Public Key Hash\n P2PKH = \"pubkeyhash\",\n // Pay to Script Hash\n P2SH = \"scripthash\",\n // Pay to Witness Public Key Hash\n P2WPKH = \"witnesspubkeyhash\",\n // Pay to Witness Script Hash\n P2WSH = \"witnessscripthash\",\n // Pay to Taproot\n P2TR = \"taproot\",\n}\n\n/**\n * Determines the type of Bitcoin script.\n *\n * This function tries to parse the script using different Bitcoin payment types and returns\n * a string identifier for the script type.\n *\n * @param script - The raw script as a Buffer\n * @returns {BitcoinScriptType} The identified script type\n * @throws {Error} If the script cannot be identified as any known type\n */\n\nexport const getScriptType = (script: Buffer): BitcoinScriptType => {\n try {\n payments.p2pkh({ output: script });\n return BitcoinScriptType.P2PKH;\n } catch {}\n try {\n payments.p2sh({ output: script });\n return BitcoinScriptType.P2SH;\n } catch {}\n try {\n payments.p2wpkh({ output: script });\n return BitcoinScriptType.P2WPKH;\n } catch {}\n try {\n payments.p2wsh({ output: script });\n return BitcoinScriptType.P2WSH;\n } catch {}\n try {\n payments.p2tr({ output: script });\n return BitcoinScriptType.P2TR;\n } catch {}\n\n throw new Error(\"Unknown script type\");\n};\n", "import { PsbtInputExtended } from \"bip174/src/lib/interfaces\";\n\nimport { UTXO } from \"../../types\";\nimport { BitcoinScriptType, getScriptType } from \"./getScriptType\";\n\n/**\n * Determines and constructs the correct PSBT input fields for a given UTXO based on its script type.\n * This function handles different Bitcoin script types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR) and returns\n * the appropriate PSBT input fields required for that UTXO.\n *\n * @param {UTXO} utxo - The unspent transaction output to process\n * @param {Buffer} [publicKeyNoCoord] - The public of the staker (optional).\n * @returns {object} PSBT input fields object containing the necessary data\n * @throws {Error} If required input data is missing or if an unsupported script type is provided\n */\n\nexport const getPsbtInputFields = (\n utxo: UTXO,\n publicKeyNoCoord?: Buffer,\n): PsbtInputExtended => {\n const scriptPubKey = Buffer.from(utxo.scriptPubKey, \"hex\");\n const type = getScriptType(scriptPubKey);\n\n switch (type) {\n case BitcoinScriptType.P2PKH: {\n if (!utxo.rawTxHex) {\n throw new Error(\"Missing rawTxHex for legacy P2PKH input\");\n }\n return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, \"hex\") };\n }\n case BitcoinScriptType.P2SH: {\n if (!utxo.rawTxHex) {\n throw new Error(\"Missing rawTxHex for P2SH input\");\n }\n if (!utxo.redeemScript) {\n throw new Error(\"Missing redeemScript for P2SH input\");\n }\n return {\n nonWitnessUtxo: Buffer.from(utxo.rawTxHex, \"hex\"),\n redeemScript: Buffer.from(utxo.redeemScript, \"hex\"),\n };\n }\n case BitcoinScriptType.P2WPKH: {\n return {\n witnessUtxo: {\n script: scriptPubKey,\n value: utxo.value,\n },\n };\n }\n case BitcoinScriptType.P2WSH: {\n if (!utxo.witnessScript) {\n throw new Error(\"Missing witnessScript for P2WSH input\");\n }\n return {\n witnessUtxo: {\n script: scriptPubKey,\n value: utxo.value,\n },\n witnessScript: Buffer.from(utxo.witnessScript, \"hex\"),\n };\n }\n case BitcoinScriptType.P2TR: {\n return {\n witnessUtxo: {\n script: scriptPubKey,\n value: utxo.value,\n },\n // this is needed only if the wallet is in taproot mode\n ...(publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord }),\n };\n }\n default:\n throw new Error(`Unsupported script type: ${type}`);\n }\n};\n", "import { opcodes, script } from \"bitcoinjs-lib\";\nimport { NO_COORD_PK_BYTE_LENGTH } from \"../constants/keys\";\n\nexport const MAGIC_BYTES_LEN = 4;\n\n// Represents the staking scripts used in BTC staking.\nexport interface StakingScripts {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n unbondingTimelockScript: Buffer;\n}\n\n// StakingScriptData is a class that holds the data required for the BTC Staking Script\n// and exposes methods for converting it into useful formats\nexport class StakingScriptData {\n stakerKey: Buffer;\n finalityProviderKeys: Buffer[];\n covenantKeys: Buffer[];\n covenantThreshold: number;\n stakingTimeLock: number;\n unbondingTimeLock: number;\n\n constructor(\n // The `stakerKey` is the public key of the staker without the coordinate bytes.\n stakerKey: Buffer,\n // A list of public keys without the coordinate bytes corresponding to the finality providers\n // the stake will be delegated to.\n // Currently, Babylon does not support restaking, so this should contain only a single item.\n finalityProviderKeys: Buffer[],\n // A list of the public keys without the coordinate bytes corresponding to\n // the covenant emulators.\n // This is a parameter of the Babylon system and should be retrieved from there.\n covenantKeys: Buffer[],\n // The number of covenant emulator signatures required for a transaction\n // to be valid.\n // This is a parameter of the Babylon system and should be retrieved from there.\n covenantThreshold: number,\n // The staking period denoted as a number of BTC blocks.\n stakingTimelock: number,\n // The unbonding period denoted as a number of BTC blocks.\n // This value should be more than equal than the minimum unbonding time of the\n // Babylon system.\n unbondingTimelock: number,\n ) {\n if (\n !stakerKey ||\n !finalityProviderKeys ||\n !covenantKeys ||\n !covenantThreshold ||\n !stakingTimelock ||\n !unbondingTimelock\n ) {\n throw new Error(\"Missing required input values\");\n }\n this.stakerKey = stakerKey;\n this.finalityProviderKeys = finalityProviderKeys;\n this.covenantKeys = covenantKeys;\n this.covenantThreshold = covenantThreshold;\n this.stakingTimeLock = stakingTimelock;\n this.unbondingTimeLock = unbondingTimelock;\n\n // Run the validate method to check if the provided script data is valid\n if (!this.validate()) {\n throw new Error(\"Invalid script data provided\");\n }\n }\n\n /**\n * Validates the staking script.\n * @returns {boolean} Returns true if the staking script is valid, otherwise false.\n */\n validate(): boolean {\n // check that staker key is the correct length\n if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {\n return false;\n }\n // check that finalityProvider keys are the correct length\n if (\n this.finalityProviderKeys.some(\n (finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH,\n )\n ) {\n return false;\n }\n // check that covenant keys are the correct length\n if (\n this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)\n ) {\n return false;\n }\n\n // Check whether we have any duplicate keys\n const allPks = [\n this.stakerKey,\n ...this.finalityProviderKeys,\n ...this.covenantKeys,\n ];\n const allPksSet = new Set(allPks);\n if (allPks.length !== allPksSet.size) {\n return false;\n }\n\n // check that the threshold is above 0 and less than or equal to\n // the size of the covenant emulators set\n if (\n this.covenantThreshold <= 0 ||\n this.covenantThreshold > this.covenantKeys.length\n ) {\n return false;\n }\n\n // check that maximum value for staking time is not greater than uint16 and above 0\n if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) {\n return false;\n }\n\n // check that maximum value for unbonding time is not greater than uint16 and above 0\n if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) {\n return false;\n }\n\n return true;\n }\n\n // The staking script allows for multiple finality provider public keys\n // to support (re)stake to multiple finality providers\n // Covenant members are going to have multiple keys\n\n /**\n * Builds a timelock script.\n * @param timelock - The timelock value to encode in the script.\n * @returns {Buffer} containing the compiled timelock script.\n */\n buildTimelockScript(timelock: number): Buffer {\n return script.compile([\n this.stakerKey,\n opcodes.OP_CHECKSIGVERIFY,\n script.number.encode(timelock),\n opcodes.OP_CHECKSEQUENCEVERIFY,\n ]);\n }\n\n /**\n * Builds the staking timelock script.\n * Only holder of private key for given pubKey can spend after relative lock time\n * Creates the timelock script in the form:\n * <stakerPubKey>\n * OP_CHECKSIGVERIFY\n * <stakingTimeBlocks>\n * OP_CHECKSEQUENCEVERIFY\n * @returns {Buffer} The staking timelock script.\n */\n buildStakingTimelockScript(): Buffer {\n return this.buildTimelockScript(this.stakingTimeLock);\n }\n\n /**\n * Builds the unbonding timelock script.\n * Creates the unbonding timelock script in the form:\n * <stakerPubKey>\n * OP_CHECKSIGVERIFY\n * <unbondingTimeBlocks>\n * OP_CHECKSEQUENCEVERIFY\n * @returns {Buffer} The unbonding timelock script.\n */\n buildUnbondingTimelockScript(): Buffer {\n return this.buildTimelockScript(this.unbondingTimeLock);\n }\n\n /**\n * Builds the unbonding script in the form:\n * buildSingleKeyScript(stakerPk, true) ||\n * buildMultiKeyScript(covenantPks, covenantThreshold, false)\n * || means combining the scripts\n * @returns {Buffer} The unbonding script.\n */\n buildUnbondingScript(): Buffer {\n return Buffer.concat([\n this.buildSingleKeyScript(this.stakerKey, true),\n this.buildMultiKeyScript(\n this.covenantKeys,\n this.covenantThreshold,\n false,\n ),\n ]);\n }\n\n /**\n * Builds the slashing script for staking in the form:\n * buildSingleKeyScript(stakerPk, true) ||\n * buildMultiKeyScript(finalityProviderPKs, 1, true) ||\n * buildMultiKeyScript(covenantPks, covenantThreshold, false)\n * || means combining the scripts\n * The slashing script is a combination of single-key and multi-key scripts.\n * The single-key script is used for staker key verification.\n * The multi-key script is used for finality provider key verification and covenant key verification.\n * @returns {Buffer} The slashing script as a Buffer.\n */\n buildSlashingScript(): Buffer {\n return Buffer.concat([\n this.buildSingleKeyScript(this.stakerKey, true),\n this.buildMultiKeyScript(\n this.finalityProviderKeys,\n // The threshold is always 1 as we only need one\n // finalityProvider signature to perform slashing\n // (only one finalityProvider performs an offence)\n 1,\n // OP_VERIFY/OP_CHECKSIGVERIFY is added at the end\n true,\n ),\n this.buildMultiKeyScript(\n this.covenantKeys,\n this.covenantThreshold,\n // No need to add verify since covenants are at the end of the script\n false,\n ),\n ]);\n }\n /**\n * Builds the staking scripts.\n * @returns {StakingScripts} The staking scripts.\n */\n buildScripts(): StakingScripts {\n return {\n timelockScript: this.buildStakingTimelockScript(),\n unbondingScript: this.buildUnbondingScript(),\n slashingScript: this.buildSlashingScript(),\n unbondingTimelockScript: this.buildUnbondingTimelockScript(),\n };\n }\n\n // buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality\n // for creating Bitcoin scripts for the unbonding script and the slashing script\n\n /**\n * Builds a single key script in the form:\n * buildSingleKeyScript creates a single key script\n * <pk> OP_CHECKSIGVERIFY (if withVerify is true)\n * <pk> OP_CHECKSIG (if withVerify is false)\n * @param pk - The public key buffer.\n * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode.\n * @returns The compiled script buffer.\n */\n buildSingleKeyScript(pk: Buffer, withVerify: boolean): Buffer {\n // Check public key length\n if (pk.length != NO_COORD_PK_BYTE_LENGTH) {\n throw new Error(\"Invalid key length\");\n }\n return script.compile([\n pk,\n withVerify ? opcodes.OP_CHECKSIGVERIFY : opcodes.OP_CHECKSIG,\n ]);\n }\n\n /**\n * Builds a multi-key script in the form:\n * <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL\n * <withVerify -> OP_NUMEQUALVERIFY>\n * It validates whether provided keys are unique and the threshold is not greater than number of keys\n * If there is only one key provided it will return single key sig script\n * @param pks - An array of public keys.\n * @param threshold - The required number of valid signers.\n * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode.\n * @returns The compiled multi-key script as a Buffer.\n * @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.\n */\n buildMultiKeyScript(\n pks: Buffer[],\n threshold: number,\n withVerify: boolean,\n ): Buffer {\n // Verify that pks is not empty\n if (!pks || pks.length === 0) {\n throw new Error(\"No keys provided\");\n }\n // Check buffer object have expected lengths like checking pks.length\n if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) {\n throw new Error(\"Invalid key length\");\n }\n // Verify that threshold <= len(pks)\n if (threshold > pks.length) {\n throw new Error(\n \"Required number of valid signers is greater than number of provided keys\",\n );\n }\n if (pks.length === 1) {\n return this.buildSingleKeyScript(pks[0], withVerify);\n }\n // keys must be sorted\n const sortedPks = [...pks].sort(Buffer.compare);\n // verify there are no duplicates\n for (let i = 0; i < sortedPks.length - 1; ++i) {\n if (sortedPks[i].equals(sortedPks[i + 1])) {\n throw new Error(\"Duplicate keys provided\");\n }\n }\n const scriptElements = [sortedPks[0], opcodes.OP_CHECKSIG];\n for (let i = 1; i < sortedPks.length; i++) {\n scriptElements.push(sortedPks[i]);\n scriptElements.push(opcodes.OP_CHECKSIGADD);\n }\n scriptElements.push(script.number.encode(threshold));\n if (withVerify) {\n scriptElements.push(opcodes.OP_NUMEQUALVERIFY);\n } else {\n scriptElements.push(opcodes.OP_NUMEQUAL);\n }\n return script.compile(scriptElements);\n }\n}\n", "import {\n Psbt, Transaction, networks, payments, script, address, opcodes\n} from \"bitcoinjs-lib\";\nimport { Taptree } from \"bitcoinjs-lib/src/types\";\n\nimport { BTC_DUST_SAT } from \"../constants/dustSat\";\nimport { internalPubkey } from \"../constants/internalPubkey\";\nimport { UTXO } from \"../types/UTXO\";\nimport { PsbtResult, TransactionResult } from \"../types/transaction\";\nimport { isValidBitcoinAddress, transactionIdToHash } from \"../utils/btc\";\nimport {\n getStakingExpansionTxFundingUTXOAndFees,\n getStakingTxInputUTXOsAndFees,\n getWithdrawTxFee,\n} from \"../utils/fee\";\nimport { inputValueSum } from \"../utils/fee/utils\";\nimport {\n buildStakingTransactionOutputs,\n deriveStakingOutputInfo,\n deriveUnbondingOutputInfo,\n findMatchingTxOutputIndex,\n} from \"../utils/staking\";\nimport { NON_RBF_SEQUENCE, TRANSACTION_VERSION } from \"../constants/psbt\";\nimport { CovenantSignature } from \"../types/covenantSignatures\";\nimport { REDEEM_VERSION } from \"../constants/transaction\";\n\n// https://bips.xyz/370\nconst BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 500000000;\nconst BTC_SLASHING_FRACTION_DIGITS = 4;\n\n/**\n * Constructs an unsigned BTC Staking transaction in psbt format.\n *\n * Outputs:\n * - psbt:\n * - The first output corresponds to the staking script with the specified amount.\n * - The second output corresponds to the change from spending the amount and the transaction fee.\n * - If a data embed script is provided, it will be added as the second output, and the fee will be the third output.\n * - fee: The total fee amount for the transaction.\n *\n * Inputs:\n * - scripts:\n * - timelockScript, unbondingScript, slashingScript: Scripts for different transaction types.\n * - dataEmbedScript: Optional data embed script.\n * - amount: Amount to stake.\n * - changeAddress: Address to send the change to.\n * - inputUTXOs: All available UTXOs from the wallet.\n * - network: Bitcoin network.\n * - feeRate: Fee rate in satoshis per byte.\n * - publicKeyNoCoord: Public key if the wallet is in taproot mode.\n * - lockHeight: Optional block height locktime to set for the transaction (i.e., not mined until the block height).\n *\n * @param {Object} scripts - Scripts used to construct the taproot output.\n * such as timelockScript, unbondingScript, slashingScript, and dataEmbedScript.\n * @param {number} amount - The amount to stake.\n * @param {string} changeAddress - The address to send the change to.\n * @param {UTXO[]} inputUTXOs - All available UTXOs from the wallet.\n * @param {networks.Network} network - The Bitcoin network.\n * @param {number} feeRate - The fee rate in satoshis per byte.\n * @param {number} [lockHeight] - The optional block height locktime.\n * @returns {TransactionResult} - An object containing the unsigned transaction and fee\n * @throws Will throw an error if the amount or fee rate is less than or equal\n * to 0, if the change address is invalid, or if the public key is invalid.\n */\nexport function stakingTransaction(\n scripts: {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n dataEmbedScript?: Buffer;\n },\n amount: number,\n changeAddress: string,\n inputUTXOs: UTXO[],\n network: networks.Network,\n feeRate: number,\n lockHeight?: number,\n): TransactionResult {\n // Check that amount and fee are bigger than 0\n if (amount <= 0 || feeRate <= 0) {\n throw new Error(\"Amount and fee rate must be bigger than 0\");\n }\n\n // Check whether the change address is a valid Bitcoin address.\n if (!isValidBitcoinAddress(changeAddress, network)) {\n throw new Error(\"Invalid change address\");\n }\n\n // Build outputs and estimate the fee\n const stakingOutputs = buildStakingTransactionOutputs(scripts, network, amount);\n const { selectedUTXOs, fee } = getStakingTxInputUTXOsAndFees(\n inputUTXOs,\n amount,\n feeRate,\n stakingOutputs,\n );\n\n const tx = new Transaction();\n tx.version = TRANSACTION_VERSION;\n \n for (let i = 0; i < selectedUTXOs.length; ++i) {\n const input = selectedUTXOs[i];\n tx.addInput(\n transactionIdToHash(input.txid),\n input.vout,\n NON_RBF_SEQUENCE,\n );\n }\n\n stakingOutputs.forEach((o) => {\n tx.addOutput(o.scriptPubKey, o.value);\n });\n\n // Add a change output only if there's any amount leftover from the inputs\n const inputsSum = inputValueSum(selectedUTXOs);\n // Check if the change amount is above the dust limit, and if so, add it as a change output\n if (inputsSum - (amount + fee) > BTC_DUST_SAT) {\n tx.addOutput(\n address.toOutputScript(changeAddress, network),\n inputsSum - (amount + fee),\n );\n }\n\n // Set the locktime field if provided. If not provided, the locktime will be set to 0 by default\n // Only height based locktime is supported\n if (lockHeight) {\n if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) {\n throw new Error(\"Invalid lock height\");\n }\n tx.locktime = lockHeight;\n }\n\n return {\n transaction: tx,\n fee,\n };\n}\n\n/**\n * Expand an existing staking transaction with additional finality providers\n * or renew timelock.\n * \n * This function builds a Bitcoin transaction that:\n * 1. Spends the previous staking transaction output as the first input\n * 2. Uses a funding UTXO as the second input to cover transaction fees\n * 3. Creates new staking outputs where the timelock is renewed or FPs added\n * 4. Returns any remaining funds as change\n * \n * @param network - Bitcoin network (mainnet, testnet, etc.)\n * @param scripts - Scripts for the new staking outputs\n * @param amount - Total staking amount (must equal previous staking amount,\n * we only support equal amounts for now)\n * @param changeAddress - Bitcoin address to receive change from funding UTXO\n * @param feeRate - Fee rate in satoshis per byte\n * @param inputUTXOs - Available UTXOs to use for funding the expansion\n * @param previousStakingTxInfo - Details of the previous staking transaction\n * being expanded\n * @returns {TransactionResult & { fundingUTXO: UTXO }} containing the built\n * transaction and calculated fee, and the funding UTXO\n */\nexport function stakingExpansionTransaction(\n network: networks.Network,\n scripts: {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n },\n amount: number,\n changeAddress: string,\n feeRate: number,\n inputUTXOs: UTXO[],\n previousStakingTxInfo: {\n stakingTx: Transaction,\n scripts: {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n },\n },\n): TransactionResult & {\n fundingUTXO: UTXO;\n} {\n // Validate input parameters\n if (amount <= 0 || feeRate <= 0) {\n throw new Error(\"Amount and fee rate must be bigger than 0\");\n } else if (!isValidBitcoinAddress(changeAddress, network