UNPKG

@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.

1,493 lines (1,467 loc) 117 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key2 of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key2) && key2 !== except) __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { BabylonBtcStakingManager: () => BabylonBtcStakingManager, BitcoinScriptType: () => BitcoinScriptType, ObservableStaking: () => ObservableStaking, ObservableStakingScriptData: () => ObservableStakingScriptData, Staking: () => Staking, StakingScriptData: () => StakingScriptData, buildStakingTransactionOutputs: () => buildStakingTransactionOutputs, clearTxSignatures: () => clearTxSignatures, createCovenantWitness: () => createCovenantWitness, deriveMerkleProof: () => deriveMerkleProof, deriveSlashingOutput: () => deriveSlashingOutput, deriveStakingOutputInfo: () => deriveStakingOutputInfo, deriveUnbondingOutputInfo: () => deriveUnbondingOutputInfo, extractFirstSchnorrSignatureFromTransaction: () => extractFirstSchnorrSignatureFromTransaction, findInputUTXO: () => findInputUTXO, findMatchingTxOutputIndex: () => findMatchingTxOutputIndex, getBabylonParamByBtcHeight: () => getBabylonParamByBtcHeight, getBabylonParamByVersion: () => getBabylonParamByVersion, getPsbtInputFields: () => getPsbtInputFields, getPublicKeyNoCoord: () => getPublicKeyNoCoord, getScriptType: () => getScriptType, getUnbondingTxStakerSignature: () => getUnbondingTxStakerSignature, hasSlashing: () => hasSlashing, initBTCCurve: () => initBTCCurve, isNativeSegwit: () => isNativeSegwit, isTaproot: () => isTaproot, isValidBabylonAddress: () => isValidBabylonAddress, isValidBitcoinAddress: () => isValidBitcoinAddress, isValidNoCoordPublicKey: () => isValidNoCoordPublicKey, slashEarlyUnbondedTransaction: () => slashEarlyUnbondedTransaction, slashTimelockUnbondedTransaction: () => slashTimelockUnbondedTransaction, stakingExpansionTransaction: () => stakingExpansionTransaction, stakingTransaction: () => stakingTransaction, toBuffers: () => toBuffers, transactionIdToHash: () => transactionIdToHash, unbondingTransaction: () => unbondingTransaction, withdrawEarlyUnbondedTransaction: () => withdrawEarlyUnbondedTransaction, withdrawSlashingTransaction: () => withdrawSlashingTransaction, withdrawTimelockUnbondedTransaction: () => withdrawTimelockUnbondedTransaction }); module.exports = __toCommonJS(index_exports); // src/error/index.ts var StakingError = class _StakingError extends Error { constructor(code, message) { super(message); this.code = code; } // Static method to safely handle unknown errors static fromUnknown(error, code, fallbackMsg) { if (error instanceof _StakingError) { return error; } if (error instanceof Error) { return new _StakingError(code, error.message); } return new _StakingError(code, fallbackMsg); } }; // src/utils/btc.ts var ecc = __toESM(require("@bitcoin-js/tiny-secp256k1-asmjs"), 1); var import_bitcoinjs_lib = require("bitcoinjs-lib"); // src/constants/keys.ts var NO_COORD_PK_BYTE_LENGTH = 32; // src/utils/btc.ts var initBTCCurve = () => { (0, import_bitcoinjs_lib.initEccLib)(ecc); }; var isValidBitcoinAddress = (btcAddress, network) => { try { return !!import_bitcoinjs_lib.address.toOutputScript(btcAddress, network); } catch (error) { return false; } }; var isTaproot = (taprootAddress, network) => { try { const decoded = import_bitcoinjs_lib.address.fromBech32(taprootAddress); if (decoded.version !== 1) { return false; } if (network.bech32 === import_bitcoinjs_lib.networks.bitcoin.bech32) { return taprootAddress.startsWith("bc1p"); } else if (network.bech32 === import_bitcoinjs_lib.networks.testnet.bech32) { return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p"); } return false; } catch (error) { return false; } }; var isNativeSegwit = (segwitAddress, network) => { try { const decoded = import_bitcoinjs_lib.address.fromBech32(segwitAddress); if (decoded.version !== 0) { return false; } if (network.bech32 === import_bitcoinjs_lib.networks.bitcoin.bech32) { return segwitAddress.startsWith("bc1q"); } else if (network.bech32 === import_bitcoinjs_lib.networks.testnet.bech32) { return segwitAddress.startsWith("tb1q"); } return false; } catch (error) { return false; } }; var isValidNoCoordPublicKey = (pkWithNoCoord) => { try { const keyBuffer = Buffer.from(pkWithNoCoord, "hex"); return validateNoCoordPublicKeyBuffer(keyBuffer); } catch (error) { return false; } }; var getPublicKeyNoCoord = (pkHex) => { const publicKey = Buffer.from(pkHex, "hex"); const publicKeyNoCoordBuffer = publicKey.length === NO_COORD_PK_BYTE_LENGTH ? publicKey : publicKey.subarray(1, 33); if (!validateNoCoordPublicKeyBuffer(publicKeyNoCoordBuffer)) { throw new Error("Invalid public key without coordinate"); } return publicKeyNoCoordBuffer.toString("hex"); }; var validateNoCoordPublicKeyBuffer = (pkBuffer) => { if (pkBuffer.length !== NO_COORD_PK_BYTE_LENGTH) { return false; } const compressedKeyEven = Buffer.concat([Buffer.from([2]), pkBuffer]); const compressedKeyOdd = Buffer.concat([Buffer.from([3]), pkBuffer]); return ecc.isPoint(compressedKeyEven) || ecc.isPoint(compressedKeyOdd); }; var transactionIdToHash = (txId) => { if (txId === "") { throw new Error("Transaction id cannot be empty"); } return Buffer.from(txId, "hex").reverse(); }; // src/utils/staking/index.ts var import_bitcoinjs_lib2 = require("bitcoinjs-lib"); // src/constants/internalPubkey.ts var key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; var internalPubkey = Buffer.from(key, "hex").subarray(1, 33); // src/utils/staking/index.ts var buildStakingTransactionOutputs = (scripts, network, amount) => { const stakingOutputInfo = deriveStakingOutputInfo(scripts, network); const transactionOutputs = [ { scriptPubKey: stakingOutputInfo.scriptPubKey, value: amount } ]; if (scripts.dataEmbedScript) { transactionOutputs.push({ scriptPubKey: scripts.dataEmbedScript, value: 0 }); } return transactionOutputs; }; var deriveStakingOutputInfo = (scripts, network) => { const scriptTree = [ { output: scripts.slashingScript }, [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }] ]; const stakingOutput = import_bitcoinjs_lib2.payments.p2tr({ internalPubkey, scriptTree, network }); if (!stakingOutput.address) { throw new StakingError( "INVALID_OUTPUT" /* INVALID_OUTPUT */, "Failed to build staking output" ); } return { outputAddress: stakingOutput.address, scriptPubKey: import_bitcoinjs_lib2.address.toOutputScript(stakingOutput.address, network) }; }; var deriveUnbondingOutputInfo = (scripts, network) => { const outputScriptTree = [ { output: scripts.slashingScript }, { output: scripts.unbondingTimelockScript } ]; const unbondingOutput = import_bitcoinjs_lib2.payments.p2tr({ internalPubkey, scriptTree: outputScriptTree, network }); if (!unbondingOutput.address) { throw new StakingError( "INVALID_OUTPUT" /* INVALID_OUTPUT */, "Failed to build unbonding output" ); } return { outputAddress: unbondingOutput.address, scriptPubKey: import_bitcoinjs_lib2.address.toOutputScript(unbondingOutput.address, network) }; }; var deriveSlashingOutput = (scripts, network) => { const slashingOutput = import_bitcoinjs_lib2.payments.p2tr({ internalPubkey, scriptTree: { output: scripts.unbondingTimelockScript }, network }); const slashingOutputAddress = slashingOutput.address; if (!slashingOutputAddress) { throw new StakingError( "INVALID_OUTPUT" /* INVALID_OUTPUT */, "Failed to build slashing output address" ); } return { outputAddress: slashingOutputAddress, scriptPubKey: import_bitcoinjs_lib2.address.toOutputScript(slashingOutputAddress, network) }; }; var findMatchingTxOutputIndex = (tx, outputAddress, network) => { const index = tx.outs.findIndex((output) => { try { return import_bitcoinjs_lib2.address.fromOutputScript(output.script, network) === outputAddress; } catch (error) { return false; } }); if (index === -1) { throw new StakingError( "INVALID_OUTPUT" /* INVALID_OUTPUT */, `Matching output not found for address: ${outputAddress}` ); } return index; }; var toBuffers = (inputs) => { try { return inputs.map((i) => Buffer.from(i, "hex")); } catch (error) { throw StakingError.fromUnknown( error, "INVALID_INPUT" /* INVALID_INPUT */, "Cannot convert values to buffers" ); } }; var clearTxSignatures = (tx) => { tx.ins.forEach((input) => { input.script = Buffer.alloc(0); input.witness = []; }); return tx; }; var deriveMerkleProof = (merkle) => { const proofHex = merkle.reduce((acc, m) => { return acc + Buffer.from(m, "hex").reverse().toString("hex"); }, ""); return proofHex; }; var extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => { for (const input of singedTransaction.ins) { if (input.witness && input.witness.length > 0) { const schnorrSignature = input.witness[0]; if (schnorrSignature.length === 64) { return schnorrSignature; } } } return void 0; }; // src/staking/psbt.ts var import_bitcoinjs_lib4 = require("bitcoinjs-lib"); // src/constants/transaction.ts var REDEEM_VERSION = 192; // src/utils/utxo/findInputUTXO.ts var findInputUTXO = (inputUTXOs, input) => { const inputUTXO = inputUTXOs.find( (u) => transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") && u.vout === input.index ); if (!inputUTXO) { throw new Error( `Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString("hex")} and vout: ${input.index}` ); } return inputUTXO; }; // src/utils/utxo/getScriptType.ts var import_bitcoinjs_lib3 = require("bitcoinjs-lib"); var BitcoinScriptType = /* @__PURE__ */ ((BitcoinScriptType2) => { BitcoinScriptType2["P2PKH"] = "pubkeyhash"; BitcoinScriptType2["P2SH"] = "scripthash"; BitcoinScriptType2["P2WPKH"] = "witnesspubkeyhash"; BitcoinScriptType2["P2WSH"] = "witnessscripthash"; BitcoinScriptType2["P2TR"] = "taproot"; return BitcoinScriptType2; })(BitcoinScriptType || {}); var getScriptType = (script4) => { try { import_bitcoinjs_lib3.payments.p2pkh({ output: script4 }); return "pubkeyhash" /* P2PKH */; } catch { } try { import_bitcoinjs_lib3.payments.p2sh({ output: script4 }); return "scripthash" /* P2SH */; } catch { } try { import_bitcoinjs_lib3.payments.p2wpkh({ output: script4 }); return "witnesspubkeyhash" /* P2WPKH */; } catch { } try { import_bitcoinjs_lib3.payments.p2wsh({ output: script4 }); return "witnessscripthash" /* P2WSH */; } catch { } try { import_bitcoinjs_lib3.payments.p2tr({ output: script4 }); return "taproot" /* P2TR */; } catch { } throw new Error("Unknown script type"); }; // src/utils/utxo/getPsbtInputFields.ts var getPsbtInputFields = (utxo, publicKeyNoCoord) => { const scriptPubKey = Buffer.from(utxo.scriptPubKey, "hex"); const type = getScriptType(scriptPubKey); switch (type) { case "pubkeyhash" /* P2PKH */: { if (!utxo.rawTxHex) { throw new Error("Missing rawTxHex for legacy P2PKH input"); } return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex") }; } case "scripthash" /* P2SH */: { if (!utxo.rawTxHex) { throw new Error("Missing rawTxHex for P2SH input"); } if (!utxo.redeemScript) { throw new Error("Missing redeemScript for P2SH input"); } return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex"), redeemScript: Buffer.from(utxo.redeemScript, "hex") }; } case "witnesspubkeyhash" /* P2WPKH */: { return { witnessUtxo: { script: scriptPubKey, value: utxo.value } }; } case "witnessscripthash" /* P2WSH */: { if (!utxo.witnessScript) { throw new Error("Missing witnessScript for P2WSH input"); } return { witnessUtxo: { script: scriptPubKey, value: utxo.value }, witnessScript: Buffer.from(utxo.witnessScript, "hex") }; } case "taproot" /* P2TR */: { return { witnessUtxo: { script: scriptPubKey, value: utxo.value }, // this is needed only if the wallet is in taproot mode ...publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord } }; } default: throw new Error(`Unsupported script type: ${type}`); } }; // src/staking/psbt.ts var stakingPsbt = (stakingTx, network, inputUTXOs, publicKeyNoCoord) => { if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) { throw new Error("Invalid public key"); } const psbt = new import_bitcoinjs_lib4.Psbt({ network }); if (stakingTx.version !== void 0) psbt.setVersion(stakingTx.version); if (stakingTx.locktime !== void 0) psbt.setLocktime(stakingTx.locktime); stakingTx.ins.forEach((input) => { const inputUTXO = findInputUTXO(inputUTXOs, input); const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord); psbt.addInput({ hash: input.hash, index: input.index, sequence: input.sequence, ...psbtInputData }); }); stakingTx.outs.forEach((o) => { psbt.addOutput({ script: o.script, value: o.value }); }); return psbt; }; var stakingExpansionPsbt = (network, stakingTx, previousStakingTxInfo, inputUTXOs, previousScripts, publicKeyNoCoord) => { const psbt = new import_bitcoinjs_lib4.Psbt({ network }); if (stakingTx.version !== void 0) psbt.setVersion(stakingTx.version); if (stakingTx.locktime !== void 0) psbt.setLocktime(stakingTx.locktime); if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) { throw new Error("Invalid public key"); } const previousStakingOutput = previousStakingTxInfo.stakingTx.outs[previousStakingTxInfo.outputIndex]; if (!previousStakingOutput) { throw new Error("Previous staking output not found"); } ; if (getScriptType(previousStakingOutput.script) !== "taproot" /* P2TR */) { throw new Error("Previous staking output script type is not P2TR"); } if (stakingTx.ins.length !== 2) { throw new Error( "Staking expansion transaction must have exactly 2 inputs" ); } const txInputs = stakingTx.ins; if (Buffer.from(txInputs[0].hash).reverse().toString("hex") !== previousStakingTxInfo.stakingTx.getId()) { throw new Error("Previous staking input hash does not match"); } else if (txInputs[0].index !== previousStakingTxInfo.outputIndex) { throw new Error("Previous staking input index does not match"); } const inputScriptTree = [ { output: previousScripts.slashingScript }, [{ output: previousScripts.unbondingScript }, { output: previousScripts.timelockScript }] ]; const inputRedeem = { output: previousScripts.unbondingScript, redeemVersion: REDEEM_VERSION }; const p2tr = import_bitcoinjs_lib4.payments.p2tr({ internalPubkey, scriptTree: inputScriptTree, redeem: inputRedeem, network }); if (!p2tr.witness || p2tr.witness.length === 0) { throw new Error( "Failed to create P2TR witness for expansion transaction input" ); } const inputTapLeafScript = { leafVersion: inputRedeem.redeemVersion, script: inputRedeem.output, controlBlock: p2tr.witness[p2tr.witness.length - 1] }; psbt.addInput({ hash: txInputs[0].hash, index: txInputs[0].index, sequence: txInputs[0].sequence, witnessUtxo: { script: previousStakingOutput.script, value: previousStakingOutput.value }, tapInternalKey: internalPubkey, tapLeafScript: [inputTapLeafScript] }); const inputUTXO = findInputUTXO(inputUTXOs, txInputs[1]); const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord); psbt.addInput({ hash: txInputs[1].hash, index: txInputs[1].index, sequence: txInputs[1].sequence, ...psbtInputData }); stakingTx.outs.forEach((o) => { psbt.addOutput({ script: o.script, value: o.value }); }); return psbt; }; var unbondingPsbt = (scripts, unbondingTx, stakingTx, network) => { if (unbondingTx.outs.length !== 1) { throw new Error("Unbonding transaction must have exactly one output"); } if (unbondingTx.ins.length !== 1) { throw new Error("Unbonding transaction must have exactly one input"); } validateUnbondingOutput(scripts, unbondingTx, network); const psbt = new import_bitcoinjs_lib4.Psbt({ network }); if (unbondingTx.version !== void 0) { psbt.setVersion(unbondingTx.version); } if (unbondingTx.locktime !== void 0) { psbt.setLocktime(unbondingTx.locktime); } const input = unbondingTx.ins[0]; const outputIndex = input.index; const inputScriptTree = [ { output: scripts.slashingScript }, [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }] ]; const inputRedeem = { output: scripts.unbondingScript, redeemVersion: REDEEM_VERSION }; const p2tr = import_bitcoinjs_lib4.payments.p2tr({ internalPubkey, scriptTree: inputScriptTree, redeem: inputRedeem, network }); const inputTapLeafScript = { leafVersion: inputRedeem.redeemVersion, script: inputRedeem.output, controlBlock: p2tr.witness[p2tr.witness.length - 1] }; psbt.addInput({ hash: input.hash, index: input.index, sequence: input.sequence, tapInternalKey: internalPubkey, witnessUtxo: { value: stakingTx.outs[outputIndex].value, script: stakingTx.outs[outputIndex].script }, tapLeafScript: [inputTapLeafScript] }); psbt.addOutput({ script: unbondingTx.outs[0].script, value: unbondingTx.outs[0].value }); return psbt; }; var validateUnbondingOutput = (scripts, unbondingTx, network) => { const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network); if (unbondingOutputInfo.scriptPubKey.toString("hex") !== unbondingTx.outs[0].script.toString("hex")) { throw new Error( "Unbonding output script does not match the expected script while building psbt" ); } }; // src/staking/stakingScript.ts var import_bitcoinjs_lib5 = require("bitcoinjs-lib"); var MAGIC_BYTES_LEN = 4; var StakingScriptData = class { constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock) { if (!stakerKey || !finalityProviderKeys || !covenantKeys || !covenantThreshold || !stakingTimelock || !unbondingTimelock) { throw new Error("Missing required input values"); } this.stakerKey = stakerKey; this.finalityProviderKeys = finalityProviderKeys; this.covenantKeys = covenantKeys; this.covenantThreshold = covenantThreshold; this.stakingTimeLock = stakingTimelock; this.unbondingTimeLock = unbondingTimelock; if (!this.validate()) { throw new Error("Invalid script data provided"); } } /** * Validates the staking script. * @returns {boolean} Returns true if the staking script is valid, otherwise false. */ validate() { if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) { return false; } if (this.finalityProviderKeys.some( (finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH )) { return false; } if (this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)) { return false; } const allPks = [ this.stakerKey, ...this.finalityProviderKeys, ...this.covenantKeys ]; const allPksSet = new Set(allPks); if (allPks.length !== allPksSet.size) { return false; } if (this.covenantThreshold <= 0 || this.covenantThreshold > this.covenantKeys.length) { return false; } if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) { return false; } if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) { return false; } return true; } // The staking script allows for multiple finality provider public keys // to support (re)stake to multiple finality providers // Covenant members are going to have multiple keys /** * Builds a timelock script. * @param timelock - The timelock value to encode in the script. * @returns {Buffer} containing the compiled timelock script. */ buildTimelockScript(timelock) { return import_bitcoinjs_lib5.script.compile([ this.stakerKey, import_bitcoinjs_lib5.opcodes.OP_CHECKSIGVERIFY, import_bitcoinjs_lib5.script.number.encode(timelock), import_bitcoinjs_lib5.opcodes.OP_CHECKSEQUENCEVERIFY ]); } /** * Builds the staking timelock script. * Only holder of private key for given pubKey can spend after relative lock time * Creates the timelock script in the form: * <stakerPubKey> * OP_CHECKSIGVERIFY * <stakingTimeBlocks> * OP_CHECKSEQUENCEVERIFY * @returns {Buffer} The staking timelock script. */ buildStakingTimelockScript() { return this.buildTimelockScript(this.stakingTimeLock); } /** * Builds the unbonding timelock script. * Creates the unbonding timelock script in the form: * <stakerPubKey> * OP_CHECKSIGVERIFY * <unbondingTimeBlocks> * OP_CHECKSEQUENCEVERIFY * @returns {Buffer} The unbonding timelock script. */ buildUnbondingTimelockScript() { return this.buildTimelockScript(this.unbondingTimeLock); } /** * Builds the unbonding script in the form: * buildSingleKeyScript(stakerPk, true) || * buildMultiKeyScript(covenantPks, covenantThreshold, false) * || means combining the scripts * @returns {Buffer} The unbonding script. */ buildUnbondingScript() { return Buffer.concat([ this.buildSingleKeyScript(this.stakerKey, true), this.buildMultiKeyScript( this.covenantKeys, this.covenantThreshold, false ) ]); } /** * Builds the slashing script for staking in the form: * buildSingleKeyScript(stakerPk, true) || * buildMultiKeyScript(finalityProviderPKs, 1, true) || * buildMultiKeyScript(covenantPks, covenantThreshold, false) * || means combining the scripts * The slashing script is a combination of single-key and multi-key scripts. * The single-key script is used for staker key verification. * The multi-key script is used for finality provider key verification and covenant key verification. * @returns {Buffer} The slashing script as a Buffer. */ buildSlashingScript() { return Buffer.concat([ this.buildSingleKeyScript(this.stakerKey, true), this.buildMultiKeyScript( this.finalityProviderKeys, // The threshold is always 1 as we only need one // finalityProvider signature to perform slashing // (only one finalityProvider performs an offence) 1, // OP_VERIFY/OP_CHECKSIGVERIFY is added at the end true ), this.buildMultiKeyScript( this.covenantKeys, this.covenantThreshold, // No need to add verify since covenants are at the end of the script false ) ]); } /** * Builds the staking scripts. * @returns {StakingScripts} The staking scripts. */ buildScripts() { return { timelockScript: this.buildStakingTimelockScript(), unbondingScript: this.buildUnbondingScript(), slashingScript: this.buildSlashingScript(), unbondingTimelockScript: this.buildUnbondingTimelockScript() }; } // buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality // for creating Bitcoin scripts for the unbonding script and the slashing script /** * Builds a single key script in the form: * buildSingleKeyScript creates a single key script * <pk> OP_CHECKSIGVERIFY (if withVerify is true) * <pk> OP_CHECKSIG (if withVerify is false) * @param pk - The public key buffer. * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode. * @returns The compiled script buffer. */ buildSingleKeyScript(pk, withVerify) { if (pk.length != NO_COORD_PK_BYTE_LENGTH) { throw new Error("Invalid key length"); } return import_bitcoinjs_lib5.script.compile([ pk, withVerify ? import_bitcoinjs_lib5.opcodes.OP_CHECKSIGVERIFY : import_bitcoinjs_lib5.opcodes.OP_CHECKSIG ]); } /** * Builds a multi-key script in the form: * <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL * <withVerify -> OP_NUMEQUALVERIFY> * It validates whether provided keys are unique and the threshold is not greater than number of keys * If there is only one key provided it will return single key sig script * @param pks - An array of public keys. * @param threshold - The required number of valid signers. * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode. * @returns The compiled multi-key script as a Buffer. * @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. */ buildMultiKeyScript(pks, threshold, withVerify) { if (!pks || pks.length === 0) { throw new Error("No keys provided"); } if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) { throw new Error("Invalid key length"); } if (threshold > pks.length) { throw new Error( "Required number of valid signers is greater than number of provided keys" ); } if (pks.length === 1) { return this.buildSingleKeyScript(pks[0], withVerify); } const sortedPks = [...pks].sort(Buffer.compare); for (let i = 0; i < sortedPks.length - 1; ++i) { if (sortedPks[i].equals(sortedPks[i + 1])) { throw new Error("Duplicate keys provided"); } } const scriptElements = [sortedPks[0], import_bitcoinjs_lib5.opcodes.OP_CHECKSIG]; for (let i = 1; i < sortedPks.length; i++) { scriptElements.push(sortedPks[i]); scriptElements.push(import_bitcoinjs_lib5.opcodes.OP_CHECKSIGADD); } scriptElements.push(import_bitcoinjs_lib5.script.number.encode(threshold)); if (withVerify) { scriptElements.push(import_bitcoinjs_lib5.opcodes.OP_NUMEQUALVERIFY); } else { scriptElements.push(import_bitcoinjs_lib5.opcodes.OP_NUMEQUAL); } return import_bitcoinjs_lib5.script.compile(scriptElements); } }; // src/staking/transactions.ts var import_bitcoinjs_lib8 = require("bitcoinjs-lib"); // src/constants/dustSat.ts var BTC_DUST_SAT = 546; // src/utils/fee/index.ts var import_bitcoinjs_lib7 = require("bitcoinjs-lib"); // src/constants/fee.ts var DEFAULT_INPUT_SIZE = 180; var P2WPKH_INPUT_SIZE = 68; var P2TR_INPUT_SIZE = 58; var P2TR_STAKING_EXPANSION_INPUT_SIZE = 268; var TX_BUFFER_SIZE_OVERHEAD = 11; var LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30; var MAX_NON_LEGACY_OUTPUT_SIZE = 43; var WITHDRAW_TX_BUFFER_SIZE = 17; var WALLET_RELAY_FEE_RATE_THRESHOLD = 2; var OP_RETURN_OUTPUT_VALUE_SIZE = 8; var OP_RETURN_VALUE_SERIALIZE_SIZE = 1; // src/utils/fee/utils.ts var import_bitcoinjs_lib6 = require("bitcoinjs-lib"); var isOP_RETURN = (script4) => { const decompiled = import_bitcoinjs_lib6.script.decompile(script4); return !!decompiled && decompiled[0] === import_bitcoinjs_lib6.opcodes.OP_RETURN; }; var getInputSizeByScript = (script4) => { try { const { address: p2wpkhAddress } = import_bitcoinjs_lib6.payments.p2wpkh({ output: script4 }); if (p2wpkhAddress) { return P2WPKH_INPUT_SIZE; } } catch (error) { } try { const { address: p2trAddress } = import_bitcoinjs_lib6.payments.p2tr({ output: script4 }); if (p2trAddress) { return P2TR_INPUT_SIZE; } } catch (error) { } return DEFAULT_INPUT_SIZE; }; var getEstimatedChangeOutputSize = () => { return MAX_NON_LEGACY_OUTPUT_SIZE; }; var inputValueSum = (inputUTXOs) => { return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0); }; // src/utils/fee/index.ts var getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => { if (availableUTXOs.length === 0) { throw new Error("Insufficient funds"); } const validUTXOs = availableUTXOs.filter((utxo) => { const script4 = Buffer.from(utxo.scriptPubKey, "hex"); return !!import_bitcoinjs_lib7.script.decompile(script4); }); if (validUTXOs.length === 0) { throw new Error("Insufficient funds: no valid UTXOs available for staking"); } const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value); const selectedUTXOs = []; let accumulatedValue = 0; let estimatedFee = 0; for (const utxo of sortedUTXOs) { selectedUTXOs.push(utxo); accumulatedValue += utxo.value; const estimatedSize = getEstimatedSize(selectedUTXOs, outputs); estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate); if (accumulatedValue - (stakingAmount + estimatedFee) > BTC_DUST_SAT) { estimatedFee += getEstimatedChangeOutputSize() * feeRate; } if (accumulatedValue >= stakingAmount + estimatedFee) { break; } } if (accumulatedValue < stakingAmount + estimatedFee) { throw new Error( "Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees" ); } return { selectedUTXOs, fee: estimatedFee }; }; var getStakingExpansionTxFundingUTXOAndFees = (availableUTXOs, feeRate, outputs) => { if (availableUTXOs.length === 0) { throw new Error("Insufficient funds"); } const validUTXOs = availableUTXOs.filter((utxo) => { const script4 = Buffer.from(utxo.scriptPubKey, "hex"); const decompiledScript = import_bitcoinjs_lib7.script.decompile(script4); return decompiledScript && decompiledScript.length > 0; }); if (validUTXOs.length === 0) { throw new Error("Insufficient funds: no valid UTXOs available for staking"); } const sortedUTXOs = validUTXOs.sort((a, b) => a.value - b.value); for (const utxo of sortedUTXOs) { const estimatedSize = getEstimatedSize( [utxo], outputs ) + P2TR_STAKING_EXPANSION_INPUT_SIZE; let estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate); if (utxo.value >= estimatedFee) { if (utxo.value - estimatedFee > BTC_DUST_SAT) { estimatedFee += getEstimatedChangeOutputSize() * feeRate; } if (utxo.value >= estimatedFee) { return { selectedUTXO: utxo, fee: estimatedFee }; } } } throw new Error( "Insufficient funds: unable to find a UTXO to cover the fees for the staking expansion transaction." ); }; var getWithdrawTxFee = (feeRate) => { const inputSize = P2TR_INPUT_SIZE; const outputSize = getEstimatedChangeOutputSize(); return feeRate * (inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD + WITHDRAW_TX_BUFFER_SIZE) + rateBasedTxBufferFee(feeRate); }; var getEstimatedSize = (inputUtxos, outputs) => { const inputSize = inputUtxos.reduce((acc, u) => { const script4 = Buffer.from(u.scriptPubKey, "hex"); const decompiledScript = import_bitcoinjs_lib7.script.decompile(script4); if (!decompiledScript) { return acc; } return acc + getInputSizeByScript(script4); }, 0); const outputSize = outputs.reduce((acc, output) => { if (isOP_RETURN(output.scriptPubKey)) { return acc + output.scriptPubKey.length + OP_RETURN_OUTPUT_VALUE_SIZE + OP_RETURN_VALUE_SERIALIZE_SIZE; } return acc + MAX_NON_LEGACY_OUTPUT_SIZE; }, 0); return inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD; }; var rateBasedTxBufferFee = (feeRate) => { return feeRate <= WALLET_RELAY_FEE_RATE_THRESHOLD ? LOW_RATE_ESTIMATION_ACCURACY_BUFFER : 0; }; // src/constants/psbt.ts var NON_RBF_SEQUENCE = 4294967295; var TRANSACTION_VERSION = 2; // src/staking/transactions.ts var BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 5e8; var BTC_SLASHING_FRACTION_DIGITS = 4; function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) { if (amount <= 0 || feeRate <= 0) { throw new Error("Amount and fee rate must be bigger than 0"); } if (!isValidBitcoinAddress(changeAddress, network)) { throw new Error("Invalid change address"); } const stakingOutputs = buildStakingTransactionOutputs(scripts, network, amount); const { selectedUTXOs, fee } = getStakingTxInputUTXOsAndFees( inputUTXOs, amount, feeRate, stakingOutputs ); const tx = new import_bitcoinjs_lib8.Transaction(); tx.version = TRANSACTION_VERSION; for (let i = 0; i < selectedUTXOs.length; ++i) { const input = selectedUTXOs[i]; tx.addInput( transactionIdToHash(input.txid), input.vout, NON_RBF_SEQUENCE ); } stakingOutputs.forEach((o) => { tx.addOutput(o.scriptPubKey, o.value); }); const inputsSum = inputValueSum(selectedUTXOs); if (inputsSum - (amount + fee) > BTC_DUST_SAT) { tx.addOutput( import_bitcoinjs_lib8.address.toOutputScript(changeAddress, network), inputsSum - (amount + fee) ); } if (lockHeight) { if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) { throw new Error("Invalid lock height"); } tx.locktime = lockHeight; } return { transaction: tx, fee }; } function stakingExpansionTransaction(network, scripts, amount, changeAddress, feeRate, inputUTXOs, previousStakingTxInfo) { if (amount <= 0 || feeRate <= 0) { throw new Error("Amount and fee rate must be bigger than 0"); } else if (!isValidBitcoinAddress(changeAddress, network)) { throw new Error("Invalid BTC change address"); } const previousStakingOutputInfo = deriveStakingOutputInfo( previousStakingTxInfo.scripts, network ); const previousStakingOutputIndex = findMatchingTxOutputIndex( previousStakingTxInfo.stakingTx, previousStakingOutputInfo.outputAddress, network ); const previousStakingAmount = previousStakingTxInfo.stakingTx.outs[previousStakingOutputIndex].value; if (amount !== previousStakingAmount) { throw new Error( "Expansion staking transaction amount must be equal to the previous staking amount. Increase of the staking amount is not supported yet." ); } const stakingOutputs = buildStakingTransactionOutputs( scripts, network, amount ); const { selectedUTXO, fee } = getStakingExpansionTxFundingUTXOAndFees( inputUTXOs, feeRate, stakingOutputs ); const tx = new import_bitcoinjs_lib8.Transaction(); tx.version = TRANSACTION_VERSION; tx.addInput( previousStakingTxInfo.stakingTx.getHash(), previousStakingOutputIndex, NON_RBF_SEQUENCE ); tx.addInput( transactionIdToHash(selectedUTXO.txid), selectedUTXO.vout, NON_RBF_SEQUENCE ); stakingOutputs.forEach((o) => { tx.addOutput(o.scriptPubKey, o.value); }); if (selectedUTXO.value - fee > BTC_DUST_SAT) { tx.addOutput( import_bitcoinjs_lib8.address.toOutputScript(changeAddress, network), selectedUTXO.value - fee ); } return { transaction: tx, fee, fundingUTXO: selectedUTXO }; } function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) { const scriptTree = [ { output: scripts.slashingScript }, { output: scripts.unbondingTimelockScript } ]; return withdrawalTransaction( { timelockScript: scripts.unbondingTimelockScript }, scriptTree, unbondingTx, withdrawalAddress, network, feeRate, 0 // unbonding always has a single output ); } function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) { const scriptTree = [ { output: scripts.slashingScript }, [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }] ]; return withdrawalTransaction( scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex ); } function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) { const scriptTree = { output: scripts.unbondingTimelockScript }; return withdrawalTransaction( { timelockScript: scripts.unbondingTimelockScript }, scriptTree, slashingTx, withdrawalAddress, network, feeRate, outputIndex ); } function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) { if (feeRate <= 0) { throw new Error("Withdrawal feeRate must be bigger than 0"); } if (outputIndex < 0) { throw new Error("Output index must be bigger or equal to 0"); } const timePosition = 2; const decompiled = import_bitcoinjs_lib8.script.decompile(scripts.timelockScript); if (!decompiled) { throw new Error("Timelock script is not valid"); } let timelock = 0; if (typeof decompiled[timePosition] !== "number") { const timeBuffer = decompiled[timePosition]; timelock = import_bitcoinjs_lib8.script.number.decode(timeBuffer); } else { const wrap = decompiled[timePosition] % 16; timelock = wrap === 0 ? 16 : wrap; } const redeem = { output: scripts.timelockScript, redeemVersion: REDEEM_VERSION }; const p2tr = import_bitcoinjs_lib8.payments.p2tr({ internalPubkey, scriptTree, redeem, network }); const tapLeafScript = { leafVersion: redeem.redeemVersion, script: redeem.output, controlBlock: p2tr.witness[p2tr.witness.length - 1] }; const psbt = new import_bitcoinjs_lib8.Psbt({ network }); psbt.setVersion(TRANSACTION_VERSION); psbt.addInput({ hash: tx.getHash(), index: outputIndex, tapInternalKey: internalPubkey, witnessUtxo: { value: tx.outs[outputIndex].value, script: tx.outs[outputIndex].script }, tapLeafScript: [tapLeafScript], sequence: timelock }); const estimatedFee = getWithdrawTxFee(feeRate); const outputValue = tx.outs[outputIndex].value - estimatedFee; if (outputValue < 0) { throw new Error( "Not enough funds to cover the fee for withdrawal transaction" ); } if (outputValue < BTC_DUST_SAT) { throw new Error("Output value is less than dust limit"); } psbt.addOutput({ address: withdrawalAddress, value: outputValue }); psbt.setLocktime(0); return { psbt, fee: estimatedFee }; } function slashTimelockUnbondedTransaction(scripts, stakingTransaction2, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) { const slashingScriptTree = [ { output: scripts.slashingScript }, [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }] ]; return slashingTransaction( { unbondingTimelockScript: scripts.unbondingTimelockScript, slashingScript: scripts.slashingScript }, slashingScriptTree, stakingTransaction2, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex ); } function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) { const unbondingScriptTree = [ { output: scripts.slashingScript }, { output: scripts.unbondingTimelockScript } ]; return slashingTransaction( { unbondingTimelockScript: scripts.unbondingTimelockScript, slashingScript: scripts.slashingScript }, unbondingScriptTree, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network, 0 // unbonding always has a single output ); } function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) { if (slashingRate <= 0 || slashingRate >= 1) { throw new Error("Slashing rate must be between 0 and 1"); } slashingRate = parseFloat(slashingRate.toFixed(BTC_SLASHING_FRACTION_DIGITS)); if (minimumFee <= 0 || !Number.isInteger(minimumFee)) { throw new Error("Minimum fee must be a positve integer"); } if (outputIndex < 0 || !Number.isInteger(outputIndex)) { throw new Error("Output index must be an integer bigger or equal to 0"); } if (!transaction.outs[outputIndex]) { throw new Error("Output index is out of range"); } const redeem = { output: scripts.slashingScript, redeemVersion: REDEEM_VERSION }; const p2tr = import_bitcoinjs_lib8.payments.p2tr({ internalPubkey, scriptTree, redeem, network }); const tapLeafScript = { leafVersion: redeem.redeemVersion, script: redeem.output, controlBlock: p2tr.witness[p2tr.witness.length - 1] }; const stakingAmount = transaction.outs[outputIndex].value; const slashingAmount = Math.round(stakingAmount * slashingRate); const slashingOutput = Buffer.from(slashingPkScriptHex, "hex"); if (import_bitcoinjs_lib8.opcodes.OP_RETURN != slashingOutput[0]) { if (slashingAmount <= BTC_DUST_SAT) { throw new Error("Slashing amount is less than dust limit"); } } const userFunds = stakingAmount - slashingAmount - minimumFee; if (userFunds <= BTC_DUST_SAT) { throw new Error("User funds are less than dust limit"); } const psbt = new import_bitcoinjs_lib8.Psbt({ network }); psbt.setVersion(TRANSACTION_VERSION); psbt.addInput({ hash: transaction.getHash(), index: outputIndex, tapInternalKey: internalPubkey, witnessUtxo: { value: stakingAmount, script: transaction.outs[outputIndex].script }, tapLeafScript: [tapLeafScript], // not RBF-able sequence: NON_RBF_SEQUENCE }); psbt.addOutput({ script: slashingOutput, value: slashingAmount }); const changeOutput = import_bitcoinjs_lib8.payments.p2tr({ internalPubkey, scriptTree: { output: scripts.unbondingTimelockScript }, network }); psbt.addOutput({ address: changeOutput.address, value: userFunds }); psbt.setLocktime(0); return { psbt }; } function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) { if (unbondingFee <= 0) { throw new Error("Unbonding fee must be bigger than 0"); } if (outputIndex < 0) { throw new Error("Output index must be bigger or equal to 0"); } const tx = new import_bitcoinjs_lib8.Transaction(); tx.version = TRANSACTION_VERSION; tx.addInput( stakingTx.getHash(), outputIndex, NON_RBF_SEQUENCE // not RBF-able ); const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network); const outputValue = stakingTx.outs[outputIndex].value - unbondingFee; if (outputValue < BTC_DUST_SAT) { throw new Error("Output value is less than dust limit for unbonding transaction"); } if (!unbondingOutputInfo.outputAddress) { throw new Error("Unbonding output address is not defined"); } tx.addOutput( unbondingOutputInfo.scriptPubKey, outputValue ); tx.locktime = 0; return { transaction: tx, fee: unbondingFee }; } var createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => { if (covenantSigs.length < covenantQuorum) { throw new Error( `Not enough covenant signatures. Required: ${covenantQuorum}, got: ${covenantSigs.length}` ); } const filteredCovenantSigs = covenantSigs.filter((sig) => { const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex"); return paramsCovenants.some((covenant) => covenant.equals(btcPkHexBuf)); }); if (filteredCovenantSigs.length < covenantQuorum) { throw new Error( `Not enough valid covenant signatures. Required: ${covenantQuorum}, got: ${filteredCovenantSigs.length}` ); } const covenantSigsBuffers = covenantSigs.slice(0, covenantQuorum).map((sig) => ({ btcPkHex: Buffer.from(sig.btcPkHex, "hex"), sigHex: Buffer.from(sig.sigHex, "hex") })); const paramsCovenantsSorted = [...paramsCovenants].sort(Buffer.compare).reverse(); const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => { const covenantSig = covenantSigsBuffers.find( (sig) => sig.btcPkHex.compare(covenant) === 0 ); return covenantSig?.sigHex || Buffer.alloc(0); }); return [...composedCovenantSigs, ...originalWitness]; }; // src/constants/unbonding.ts var MIN_UNBONDING_OUTPUT_VALUE = 1e3; // src/utils/babylon.ts var import_encoding = require("@cosmjs/encoding"); var isValidBabylonAddress = (address4) => { try { const { prefix } = (0, import_encoding.fromBech32)(address4); return prefix === "bbn"; } catch (error) { return false; } }; // src/utils/staking/validation.ts var validateStakingExpansionInputs = ({ babylonBtcTipHeight, inputUTXOs, stakingInput, previousStakingInput, babylonAddress }) => { if (babylonBtcTipHeight === 0) { throw new StakingError( "INVALID_INPUT" /* INVALID_INPUT */, "Babylon BTC tip height cannot be 0" ); } if (!inputUTXOs || inputUTXOs.length === 0) { throw new StakingError( "INVALID_INPUT" /* INVALID_INPUT */, "No input UTXOs provided" ); } if (babylonAddress && !isValidBabylonAddress(babylonAddress)) { throw new StakingError( "INVALID_INPUT" /* INVALID_INPUT */, "Invalid Babylon address" ); } if (stakingInput.stakingAmountSat !== previousStakingInput.stakingAmountSat) { throw new StakingError( "INVALID_INPUT" /* INVALID_INPUT */, "Staking expansion amount must equal the previous staking amount" ); } const currentFPs = stakingInput.finalityProviderPksNoCoordHex; const previousFPs = previousStakingInput.finalityProviderPksNoCoordHex; const missingPreviousFPs = previousFPs.filter((prevFp) => !currentFPs.includes(prevFp)); if (missingPreviousFPs.length > 0) { throw new StakingError( "INVALID_INPUT" /* INVALID_INPUT */, `Invalid staking expansion: all finality providers from the previous staking must be included. Missing: ${missingPreviousFPs.join(", ")}` ); } }; var validateStakingTxInputData = (stakingAmountSat, timelock, params, inputUTXOs, feeRate) => { if (stakingAmountSat < params.minStakingAmountSat || stakingAmountSat > params.maxStakingAmountSat) { throw new StakingError( "INVALID_INPUT" /* INVALID_INPUT */, "Invalid staking amount" ); } if (timelock < params.minStakingTimeBlocks || timelock > params.maxStakingTimeBlocks) { throw new StakingError("INVALID_INPUT" /* INVALID_INPUT */, "Invalid timelock"); } if (inputUTXOs.length == 0) { throw new StakingError( "INVALID_INPUT" /* INVALID_INPUT */, "No input UTXOs provided" ); } if (feeRate <= 0) { throw new StakingError("INVALID_INPUT" /* INVALID_INPUT */, "Invalid fee rate"); } }; var validateParams = (params) => { if (params.covenantNoCoordPks.length == 0) { throw new StakingError( "INVALID_PARAMS" /* INVALID_PARAMS */, "Could not find any covenant public keys" ); } if (params.covenantNoCoordPks.length < params.covenantQuorum) { throw new StakingError( "INVALID_PARAMS" /* INVALID_PARAMS */, "Covenant public keys must be greater than or equal to the quorum" ); } params.covenantNoCoordPks.forEach((pk) => { if (!isValidNoCoordPublicKey(pk)) { throw new StakingError( "INVALID_PARAMS" /* INVALID_PARAMS */, "Covenant public key should contains no coordinate" ); } }); if (params.unbondingTime <= 0) { throw new StakingError( "INVALID_PARAMS" /* INVALID_PARAMS */, "Unbonding time must be greater than 0" ); } if (params.unbondingFeeSat <= 0) { throw new StakingError( "INVALID_PARAMS" /* INVALID_PARAMS */, "Unbonding fee must be greater than 0" ); } if (params.maxStakingAmountSat < params.minStakingAmountSat) { throw new StakingError( "INVALID_PARAMS" /* INVALID_PARAMS */, "Max staking amount must be greater or equal to min staking amount" ); } if (params.minStakingAmountSat < params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE) { throw new StakingError( "INVALID_PARAMS" /* INVALID_PARAMS */, `Min staking a