PHP WebShell
Текущая директория: /opt/BitGoJS/modules/babylonlabs-io-btc-staking-ts/dist
Просмотр файла: index.js
// src/staking/stakingScript.ts
import { opcodes, script } from "bitcoinjs-lib";
// src/constants/keys.ts
var NO_COORD_PK_BYTE_LENGTH = 32;
// src/staking/stakingScript.ts
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 script.compile([
this.stakerKey,
opcodes.OP_CHECKSIGVERIFY,
script.number.encode(timelock),
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 script.compile([
pk,
withVerify ? opcodes.OP_CHECKSIGVERIFY : 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], opcodes.OP_CHECKSIG];
for (let i = 1; i < sortedPks.length; i++) {
scriptElements.push(sortedPks[i]);
scriptElements.push(opcodes.OP_CHECKSIGADD);
}
scriptElements.push(script.number.encode(threshold));
if (withVerify) {
scriptElements.push(opcodes.OP_NUMEQUALVERIFY);
} else {
scriptElements.push(opcodes.OP_NUMEQUAL);
}
return script.compile(scriptElements);
}
};
// 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/staking/transactions.ts
import { Psbt, Transaction as Transaction2, payments as payments3, script as script2, address as address3, opcodes as opcodes3 } from "bitcoinjs-lib";
// src/constants/dustSat.ts
var BTC_DUST_SAT = 546;
// src/constants/internalPubkey.ts
var key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
var internalPubkey = Buffer.from(key, "hex").subarray(1, 33);
// src/utils/btc.ts
import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
import { initEccLib, address, networks } from "bitcoinjs-lib";
var initBTCCurve = () => {
initEccLib(ecc);
};
var isValidBitcoinAddress = (btcAddress, network) => {
try {
return !!address.toOutputScript(btcAddress, network);
} catch (error) {
return false;
}
};
var isTaproot = (taprootAddress, network) => {
try {
const decoded = address.fromBech32(taprootAddress);
if (decoded.version !== 1) {
return false;
}
if (network.bech32 === networks.bitcoin.bech32) {
return taprootAddress.startsWith("bc1p");
} else if (network.bech32 === networks.testnet.bech32) {
return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p");
}
return false;
} catch (error) {
return false;
}
};
var isNativeSegwit = (segwitAddress, network) => {
try {
const decoded = address.fromBech32(segwitAddress);
if (decoded.version !== 0) {
return false;
}
if (network.bech32 === networks.bitcoin.bech32) {
return segwitAddress.startsWith("bc1q");
} else if (network.bech32 === 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/fee/index.ts
import { script as bitcoinScript2 } from "bitcoinjs-lib";
// src/constants/fee.ts
var DEFAULT_INPUT_SIZE = 180;
var P2WPKH_INPUT_SIZE = 68;
var P2TR_INPUT_SIZE = 58;
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
import { script as bitcoinScript, opcodes as opcodes2, payments } from "bitcoinjs-lib";
var isOP_RETURN = (script4) => {
const decompiled = bitcoinScript.decompile(script4);
return !!decompiled && decompiled[0] === opcodes2.OP_RETURN;
};
var getInputSizeByScript = (script4) => {
try {
const { address: p2wpkhAddress } = payments.p2wpkh({
output: script4
});
if (p2wpkhAddress) {
return P2WPKH_INPUT_SIZE;
}
} catch (error) {
}
try {
const { address: p2trAddress } = 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 !!bitcoinScript2.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 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 = bitcoinScript2.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/utils/staking/index.ts
import { address as address2, payments as payments2 } from "bitcoinjs-lib";
// src/constants/unbonding.ts
var MIN_UNBONDING_OUTPUT_VALUE = 1e3;
// 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 = payments2.p2tr({
internalPubkey,
scriptTree,
network
});
if (!stakingOutput.address) {
throw new StakingError(
"INVALID_OUTPUT" /* INVALID_OUTPUT */,
"Failed to build staking output"
);
}
return {
outputAddress: stakingOutput.address,
scriptPubKey: address2.toOutputScript(stakingOutput.address, network)
};
};
var deriveUnbondingOutputInfo = (scripts, network) => {
const outputScriptTree = [
{
output: scripts.slashingScript
},
{ output: scripts.unbondingTimelockScript }
];
const unbondingOutput = payments2.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: address2.toOutputScript(unbondingOutput.address, network)
};
};
var deriveSlashingOutput = (scripts, network) => {
const slashingOutput = payments2.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: address2.toOutputScript(slashingOutputAddress, network)
};
};
var findMatchingTxOutputIndex = (tx, outputAddress, network) => {
const index = tx.outs.findIndex((output) => {
return address2.fromOutputScript(output.script, network) === outputAddress;
});
if (index === -1) {
throw new StakingError(
"INVALID_OUTPUT" /* INVALID_OUTPUT */,
`Matching output not found for address: ${outputAddress}`
);
}
return index;
};
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 amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`
);
}
if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Max staking time must be greater or equal to min staking time"
);
}
if (params.minStakingTimeBlocks <= 0) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Min staking time must be greater than 0"
);
}
if (params.covenantQuorum <= 0) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Covenant quorum must be greater than 0"
);
}
if (params.slashing) {
if (params.slashing.slashingRate <= 0) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Slashing rate must be greater than 0"
);
}
if (params.slashing.slashingRate > 1) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Slashing rate must be less or equal to 1"
);
}
if (params.slashing.slashingPkScriptHex.length == 0) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Slashing public key script is missing"
);
}
if (params.slashing.minSlashingTxFeeSat <= 0) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Minimum slashing transaction fee must be greater than 0"
);
}
}
};
var validateStakingTimelock = (stakingTimelock, params) => {
if (stakingTimelock < params.minStakingTimeBlocks || stakingTimelock > params.maxStakingTimeBlocks) {
throw new StakingError(
"INVALID_INPUT" /* INVALID_INPUT */,
"Staking transaction timelock is out of range"
);
}
};
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"
);
}
};
// src/constants/psbt.ts
var NON_RBF_SEQUENCE = 4294967295;
var TRANSACTION_VERSION = 2;
// src/constants/transaction.ts
var REDEEM_VERSION = 192;
// 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 Transaction2();
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(
address3.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 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 = script2.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 = script2.number.decode(timeBuffer);
} else {
const wrap = decompiled[timePosition] % 16;
timelock = wrap === 0 ? 16 : wrap;
}
const redeem = {
output: scripts.timelockScript,
redeemVersion: REDEEM_VERSION
};
const p2tr = payments3.p2tr({
internalPubkey,
scriptTree,
redeem,
network
});
const tapLeafScript = {
leafVersion: redeem.redeemVersion,
script: redeem.output,
controlBlock: p2tr.witness[p2tr.witness.length - 1]
};
const psbt = new 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 = payments3.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 (opcodes3.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 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 = payments3.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 Transaction2();
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}`
);
}
for (const sig of covenantSigs) {
const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex");
if (!paramsCovenants.some((covenant) => covenant.equals(btcPkHexBuf))) {
throw new Error(
`Covenant signature public key ${sig.btcPkHex} not found in params covenants`
);
}
}
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/staking/psbt.ts
import { Psbt as Psbt2, payments as payments5 } from "bitcoinjs-lib";
// 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
import { payments as payments4 } from "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 {
payments4.p2pkh({ output: script4 });
return "pubkeyhash" /* P2PKH */;
} catch {
}
try {
payments4.p2sh({ output: script4 });
return "scripthash" /* P2SH */;
} catch {
}
try {
payments4.p2wpkh({ output: script4 });
return "witnesspubkeyhash" /* P2WPKH */;
} catch {
}
try {
payments4.p2wsh({ output: script4 });
return "witnessscripthash" /* P2WSH */;
} catch {
}
try {
payments4.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 Psbt2({ 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 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 Psbt2({ 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 = payments5.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/index.ts
var Staking = class {
constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) {
if (!isValidBitcoinAddress(stakerInfo.address, network)) {
throw new StakingError(
"INVALID_INPUT" /* INVALID_INPUT */,
"Invalid staker bitcoin address"
);
}
if (!isValidNoCoordPublicKey(stakerInfo.publicKeyNoCoordHex)) {
throw new StakingError(
"INVALID_INPUT" /* INVALID_INPUT */,
"Invalid staker public key"
);
}
if (!isValidNoCoordPublicKey(finalityProviderPkNoCoordHex)) {
throw new StakingError(
"INVALID_INPUT" /* INVALID_INPUT */,
"Invalid finality provider public key"
);
}
validateParams(params);
validateStakingTimelock(stakingTimelock, params);
this.network = network;
this.stakerInfo = stakerInfo;
this.params = params;
this.finalityProviderPkNoCoordHex = finalityProviderPkNoCoordHex;
this.stakingTimelock = stakingTimelock;
}
/**
* buildScripts builds the staking scripts for the staking transaction.
* Note: different staking types may have different scripts.
* e.g the observable staking script has a data embed script.
*
* @returns {StakingScripts} - The staking scripts.
*/
buildScripts() {
const { covenantQuorum, covenantNoCoordPks, unbondingTime } = this.params;
let stakingScriptData;
try {
stakingScriptData = new StakingScriptData(
Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
[Buffer.from(this.finalityProviderPkNoCoordHex, "hex")],
toBuffers(covenantNoCoordPks),
covenantQuorum,
this.stakingTimelock,
unbondingTime
);
} catch (error) {
throw StakingError.fromUnknown(
error,
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
"Cannot build staking script data"
);
}
let scripts;
try {
scripts = stakingScriptData.buildScripts();
} catch (error) {
throw StakingError.fromUnknown(
error,
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
"Cannot build staking scripts"
);
}
return scripts;
}
/**
* Create a staking transaction for staking.
*
* @param {number} stakingAmountSat - The amount to stake in satoshis.
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
* transaction.
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
* @returns {TransactionResult} - An object containing the unsigned
* transaction, and fee
* @throws {StakingError} - If the transaction cannot be built
*/
createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
validateStakingTxInputData(
stakingAmountSat,
this.stakingTimelock,
this.params,
inputUTXOs,
feeRate
);
const scripts = this.buildScripts();
try {
const { transaction, fee } = stakingTransaction(
scripts,
stakingAmountSat,
this.stakerInfo.address,
inputUTXOs,
this.network,
feeRate
);
return {
transaction,
fee
};
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build unsigned staking transaction"
);
}
}
/**
* Create a staking psbt based on the existing staking transaction.
*
* @param {Transaction} stakingTx - The staking transaction.
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
* transaction. The UTXOs that were used to create the staking transaction should
* be included in this array.
* @returns {Psbt} - The psbt.
*/
toStakingPsbt(stakingTx, inputUTXOs) {
const scripts = this.buildScripts();
const stakingOutputInfo = deriveStakingOutputInfo(scripts, this.network);
findMatchingTxOutputIndex(
stakingTx,
stakingOutputInfo.outputAddress,
this.network
);
return stakingPsbt(
stakingTx,
this.network,
inputUTXOs,
isTaproot(
this.stakerInfo.address,
this.network
) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
);
}
/**
* Create an unbonding transaction for staking.
*
* @param {Transaction} stakingTx - The staking transaction to unbond.
* @returns {TransactionResult} - An object containing the unsigned
* transaction, and fee
* @throws {StakingError} - If the transaction cannot be built
*/
createUnbondingTransaction(stakingTx) {
const scripts = this.buildScripts();
const { outputAddress } = deriveStakingOutputInfo(scripts, this.network);
const stakingOutputIndex = findMatchingTxOutputIndex(
stakingTx,
outputAddress,
this.network
);
try {
const { transaction } = unbondingTransaction(
scripts,
stakingTx,
this.params.unbondingFeeSat,
this.network,
stakingOutputIndex
);
return {
transaction,
fee: this.params.unbondingFeeSat
};
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build the unbonding transaction"
);
}
}
/**
* Create an unbonding psbt based on the existing unbonding transaction and
* staking transaction.
*
* @param {Transaction} unbondingTx - The unbonding transaction.
* @param {Transaction} stakingTx - The staking transaction.
*
* @returns {Psbt} - The psbt.
*/
toUnbondingPsbt(unbondingTx, stakingTx) {
return unbondingPsbt(
this.buildScripts(),
unbondingTx,
stakingTx,
this.network
);
}
/**
* Creates a withdrawal transaction that spends from an unbonding or slashing
* transaction. The timelock on the input transaction must have expired before
* this withdrawal can be valid.
*
* @param {Transaction} earlyUnbondedTx - The unbonding or slashing
* transaction to withdraw from
* @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal
* transaction
* @returns {PsbtResult} - Contains the unsigned PSBT and fee amount
* @throws {StakingError} - If the input transaction is invalid or withdrawal
* transaction cannot be built
*/
createWithdrawEarlyUnbondedTransaction(earlyUnbondedTx, feeRate) {
const scripts = this.buildScripts();
try {
return withdrawEarlyUnbondedTransaction(
scripts,
earlyUnbondedTx,
this.stakerInfo.address,
this.network,
feeRate
);
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build unsigned withdraw early unbonded transaction"
);
}
}
/**
* Create a withdrawal psbt that spends a naturally expired staking
* transaction.
*
* @param {Transaction} stakingTx - The staking transaction to withdraw from.
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
createWithdrawStakingExpiredPsbt(stakingTx, feeRate) {
const scripts = this.buildScripts();
const { outputAddress } = deriveStakingOutputInfo(scripts, this.network);
const stakingOutputIndex = findMatchingTxOutputIndex(
stakingTx,
outputAddress,
this.network
);
try {
return withdrawTimelockUnbondedTransaction(
scripts,
stakingTx,
this.stakerInfo.address,
this.network,
feeRate,
stakingOutputIndex
);
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build unsigned timelock unbonded transaction"
);
}
}
/**
* Create a slashing psbt spending from the staking output.
*
* @param {Transaction} stakingTx - The staking transaction to slash.
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
createStakingOutputSlashingPsbt(stakingTx) {
if (!this.params.slashing) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Slashing parameters are missing"
);
}
const scripts = this.buildScripts();
try {
const { psbt } = slashTimelockUnbondedTransaction(
scripts,
stakingTx,
this.params.slashing.slashingPkScriptHex,
this.params.slashing.slashingRate,
this.params.slashing.minSlashingTxFeeSat,
this.network
);
return {
psbt,
fee: this.params.slashing.minSlashingTxFeeSat
};
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build the slash timelock unbonded transaction"
);
}
}
/**
* Create a slashing psbt for an unbonding output.
*
* @param {Transaction} unbondingTx - The unbonding transaction to slash.
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
createUnbondingOutputSlashingPsbt(unbondingTx) {
if (!this.params.slashing) {
throw new StakingError(
"INVALID_PARAMS" /* INVALID_PARAMS */,
"Slashing parameters are missing"
);
}
const scripts = this.buildScripts();
try {
const { psbt } = slashEarlyUnbondedTransaction(
scripts,
unbondingTx,
this.params.slashing.slashingPkScriptHex,
this.params.slashing.slashingRate,
this.params.slashing.minSlashingTxFeeSat,
this.network
);
return {
psbt,
fee: this.params.slashing.minSlashingTxFeeSat
};
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build the slash early unbonded transaction"
);
}
}
/**
* Create a withdraw slashing psbt that spends a slashing transaction from the
* staking output.
*
* @param {Transaction} slashingTx - The slashing transaction.
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
createWithdrawSlashingPsbt(slashingTx, feeRate) {
const scripts = this.buildScripts();
const slashingOutputInfo = deriveSlashingOutput(scripts, this.network);
const slashingOutputIndex = findMatchingTxOutputIndex(
slashingTx,
slashingOutputInfo.outputAddress,
this.network
);
try {
return withdrawSlashingTransaction(
scripts,
slashingTx,
this.stakerInfo.address,
this.network,
feeRate,
slashingOutputIndex
);
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build withdraw slashing transaction"
);
}
}
};
// src/staking/observable/observableStakingScript.ts
import { opcodes as opcodes4, script as script3 } from "bitcoinjs-lib";
var ObservableStakingScriptData = class extends StakingScriptData {
constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) {
super(
stakerKey,
finalityProviderKeys,
covenantKeys,
covenantThreshold,
stakingTimelock,
unbondingTimelock
);
if (!magicBytes) {
throw new Error("Missing required input values");
}
if (magicBytes.length != MAGIC_BYTES_LEN) {
throw new Error("Invalid script data provided");
}
this.magicBytes = magicBytes;
}
/**
* Builds a data embed script for staking in the form:
* OP_RETURN || <serializedStakingData>
* where serializedStakingData is the concatenation of:
* MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock
* Note: Only a single finality provider key is supported for now in phase 1
* @throws {Error} If the number of finality provider keys is not equal to 1.
* @returns {Buffer} The compiled data embed script.
*/
buildDataEmbedScript() {
if (this.finalityProviderKeys.length != 1) {
throw new Error("Only a single finality provider key is supported");
}
const version = Buffer.alloc(1);
version.writeUInt8(0);
const stakingTimeLock = Buffer.alloc(2);
stakingTimeLock.writeUInt16BE(this.stakingTimeLock);
const serializedStakingData = Buffer.concat([
this.magicBytes,
version,
this.stakerKey,
this.finalityProviderKeys[0],
stakingTimeLock
]);
return script3.compile([opcodes4.OP_RETURN, serializedStakingData]);
}
/**
* Builds the staking scripts.
* @returns {ObservableStakingScripts} The staking scripts that can be used to stake.
* contains the timelockScript, unbondingScript, slashingScript,
* unbondingTimelockScript, and dataEmbedScript.
* @throws {Error} If script data is invalid.
*/
buildScripts() {
const scripts = super.buildScripts();
return {
...scripts,
dataEmbedScript: this.buildDataEmbedScript()
};
}
};
// src/staking/observable/index.ts
var ObservableStaking = class extends Staking {
constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) {
super(
network,
stakerInfo,
params,
finalityProviderPkNoCoordHex,
stakingTimelock
);
if (!params.tag) {
throw new StakingError(
"INVALID_INPUT" /* INVALID_INPUT */,
"Observable staking parameters must include tag"
);
}
if (!params.btcActivationHeight) {
throw new StakingError(
"INVALID_INPUT" /* INVALID_INPUT */,
"Observable staking parameters must include a positive activation height"
);
}
this.params = params;
}
/**
* Build the staking scripts for observable staking.
* This method overwrites the base method to include the OP_RETURN tag based
* on the tag provided in the parameters.
*
* @returns {ObservableStakingScripts} - The staking scripts for observable staking.
* @throws {StakingError} - If the scripts cannot be built.
*/
buildScripts() {
const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params;
let stakingScriptData;
try {
stakingScriptData = new ObservableStakingScriptData(
Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
[Buffer.from(this.finalityProviderPkNoCoordHex, "hex")],
toBuffers(covenantNoCoordPks),
covenantQuorum,
this.stakingTimelock,
unbondingTime,
Buffer.from(tag, "hex")
);
} catch (error) {
throw StakingError.fromUnknown(
error,
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
"Cannot build staking script data"
);
}
let scripts;
try {
scripts = stakingScriptData.buildScripts();
} catch (error) {
throw StakingError.fromUnknown(
error,
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
"Cannot build staking scripts"
);
}
return scripts;
}
/**
* Create a staking transaction for observable staking.
* This overwrites the method from the Staking class with the addtion
* of the
* 1. OP_RETURN tag in the staking scripts
* 2. lockHeight parameter
*
* @param {number} stakingAmountSat - The amount to stake in satoshis.
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
* transaction.
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
* @returns {TransactionResult} - An object containing the unsigned transaction,
* and fee
*/
createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
validateStakingTxInputData(
stakingAmountSat,
this.stakingTimelock,
this.params,
inputUTXOs,
feeRate
);
const scripts = this.buildScripts();
try {
const { transaction, fee } = stakingTransaction(
scripts,
stakingAmountSat,
this.stakerInfo.address,
inputUTXOs,
this.network,
feeRate,
// `lockHeight` is exclusive of the provided value.
// For example, if a Bitcoin height of X is provided,
// the transaction will be included starting from height X+1.
// https://learnmeabitcoin.com/technical/transaction/locktime/
this.params.btcActivationHeight - 1
);
return {
transaction,
fee
};
} catch (error) {
throw StakingError.fromUnknown(
error,
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
"Cannot build unsigned staking transaction"
);
}
}
/**
* Create a staking psbt for observable staking.
*
* @param {Transaction} stakingTx - The staking transaction.
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
* transaction.
* @returns {Psbt} - The psbt.
*/
toStakingPsbt(stakingTx, inputUTXOs) {
return stakingPsbt(
stakingTx,
this.network,
inputUTXOs,
isTaproot(
this.stakerInfo.address,
this.network
) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
);
}
};
// src/utils/babylon.ts
import { fromBech32 } from "@cosmjs/encoding";
var isValidBabylonAddress = (address4) => {
try {
const { prefix } = fromBech32(address4);
return prefix === "bbn";
} catch (error) {
return false;
}
};
// src/utils/staking/param.ts
var getBabylonParamByBtcHeight = (height, babylonParamsVersions) => {
const sortedParams = [...babylonParamsVersions].sort(
(a, b) => b.btcActivationHeight - a.btcActivationHeight
);
const params = sortedParams.find(
(p) => height >= p.btcActivationHeight
);
if (!params)
throw new Error(`Babylon params not found for height ${height}`);
return params;
};
var getBabylonParamByVersion = (version, babylonParams) => {
const params = babylonParams.find((p) => p.version === version);
if (!params)
throw new Error(`Babylon params not found for version ${version}`);
return params;
};
// src/staking/manager.ts
import { Psbt as Psbt3 } from "bitcoinjs-lib";
import {
btccheckpoint,
btcstaking,
btcstakingtx
} from "@babylonlabs-io/babylon-proto-ts";
import {
BIP322Sig,
BTCSigType
} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop";
// src/constants/registry.ts
var BABYLON_REGISTRY_TYPE_URLS = {
MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation"
};
// src/utils/index.ts
var reverseBuffer = (buffer) => {
const clonedBuffer = new Uint8Array(buffer);
if (clonedBuffer.length < 1)
return clonedBuffer;
for (let i = 0, j = clonedBuffer.length - 1; i < clonedBuffer.length / 2; i++, j--) {
let tmp = clonedBuffer[i];
clonedBuffer[i] = clonedBuffer[j];
clonedBuffer[j] = tmp;
}
return clonedBuffer;
};
// src/staking/manager.ts
var SigningStep = /* @__PURE__ */ ((SigningStep2) => {
SigningStep2["STAKING_SLASHING"] = "staking-slashing";
SigningStep2["UNBONDING_SLASHING"] = "unbonding-slashing";
SigningStep2["PROOF_OF_POSSESSION"] = "proof-of-possession";
SigningStep2["CREATE_BTC_DELEGATION_MSG"] = "create-btc-delegation-msg";
SigningStep2["STAKING"] = "staking";
SigningStep2["UNBONDING"] = "unbonding";
SigningStep2["WITHDRAW_STAKING_EXPIRED"] = "withdraw-staking-expired";
SigningStep2["WITHDRAW_EARLY_UNBONDED"] = "withdraw-early-unbonded";
SigningStep2["WITHDRAW_SLASHING"] = "withdraw-slashing";
return SigningStep2;
})(SigningStep || {});
var BabylonBtcStakingManager = class {
constructor(network, stakingParams, btcProvider, babylonProvider) {
this.network = network;
this.btcProvider = btcProvider;
this.babylonProvider = babylonProvider;
if (stakingParams.length === 0) {
throw new Error("No staking parameters provided");
}
this.stakingParams = stakingParams;
}
/**
* Creates a signed Pre-Staking Registration transaction that is ready to be
* sent to the Babylon chain.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param stakingInput - The staking inputs.
* @param babylonBtcTipHeight - The Babylon BTC tip height.
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
* transaction.
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
* fee rate is above 1. If the fee rate is too low, the transaction will not
* be included in a block.
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
* @returns The signed babylon pre-staking registration transaction in base64
* format.
*/
async preStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress) {
if (babylonBtcTipHeight === 0) {
throw new Error("Babylon BTC tip height cannot be 0");
}
if (inputUTXOs.length === 0) {
throw new Error("No input UTXOs provided");
}
if (!isValidBabylonAddress(babylonAddress)) {
throw new Error("Invalid Babylon address");
}
const params = getBabylonParamByBtcHeight(
babylonBtcTipHeight,
this.stakingParams
);
const staking = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const { transaction } = staking.createStakingTransaction(
stakingInput.stakingAmountSat,
inputUTXOs,
feeRate
);
const msg = await this.createBtcDelegationMsg(
staking,
stakingInput,
transaction,
babylonAddress,
stakerBtcInfo,
params
);
return {
signedBabylonTx: await this.babylonProvider.signTransaction(
"create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
msg
),
stakingTx: transaction
};
}
/**
* Creates a signed post-staking registration transaction that is ready to be
* sent to the Babylon chain. This is used when a staking transaction is
* already created and included in a BTC block and we want to register it on
* the Babylon chain.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param stakingTx - The staking transaction.
* @param stakingTxHeight - The BTC height in which the staking transaction
* is included.
* @param stakingInput - The staking inputs.
* @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction
* inclusion in a Bitcoin block that is k-deep.
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
* @returns The signed babylon transaction in base64 format.
*/
async postStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingTx, stakingTxHeight, stakingInput, inclusionProof, babylonAddress) {
const params = getBabylonParamByBtcHeight(stakingTxHeight, this.stakingParams);
if (!isValidBabylonAddress(babylonAddress)) {
throw new Error("Invalid Babylon address");
}
const stakingInstance = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const scripts = stakingInstance.buildScripts();
const stakingOutputInfo = deriveStakingOutputInfo(scripts, this.network);
findMatchingTxOutputIndex(
stakingTx,
stakingOutputInfo.outputAddress,
this.network
);
const delegationMsg = await this.createBtcDelegationMsg(
stakingInstance,
stakingInput,
stakingTx,
babylonAddress,
stakerBtcInfo,
params,
this.getInclusionProof(inclusionProof)
);
return {
signedBabylonTx: await this.babylonProvider.signTransaction(
"create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
delegationMsg
)
};
}
/**
* Estimates the BTC fee required for staking.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param babylonBtcTipHeight - The BTC tip height recorded on the Babylon
* chain.
* @param stakingInput - The staking inputs.
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
* transaction.
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
* fee rate is above 1. If the fee rate is too low, the transaction will not
* be included in a block.
* @returns The estimated BTC fee in satoshis.
*/
estimateBtcStakingFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate) {
if (babylonBtcTipHeight === 0) {
throw new Error("Babylon BTC tip height cannot be 0");
}
const params = getBabylonParamByBtcHeight(
babylonBtcTipHeight,
this.stakingParams
);
const staking = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const { fee: stakingFee } = staking.createStakingTransaction(
stakingInput.stakingAmountSat,
inputUTXOs,
feeRate
);
return stakingFee;
}
/**
* Creates a signed staking transaction that is ready to be sent to the BTC
* network.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param stakingInput - The staking inputs.
* @param unsignedStakingTx - The unsigned staking transaction.
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
* transaction.
* @param stakingParamsVersion - The params version that was used to create the
* delegation in Babylon chain
* @returns The signed staking transaction.
*/
async createSignedBtcStakingTransaction(stakerBtcInfo, stakingInput, unsignedStakingTx, inputUTXOs, stakingParamsVersion) {
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
if (inputUTXOs.length === 0) {
throw new Error("No input UTXOs provided");
}
const staking = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const stakingPsbt2 = staking.toStakingPsbt(
unsignedStakingTx,
inputUTXOs
);
const signedStakingPsbtHex = await this.btcProvider.signPsbt(
"staking" /* STAKING */,
stakingPsbt2.toHex()
);
return Psbt3.fromHex(signedStakingPsbtHex).extractTransaction();
}
/**
* Creates a partial signed unbonding transaction that is only signed by the
* staker. In order to complete the unbonding transaction, the covenant
* unbonding signatures need to be added to the transaction before sending it
* to the BTC network.
* NOTE: This method should only be used for Babylon phase-1 unbonding.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param stakingInput - The staking inputs.
* @param stakingParamsVersion - The params version that was used to create the
* delegation in Babylon chain
* @param stakingTx - The staking transaction.
* @returns The partial signed unbonding transaction and its fee.
*/
async createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx) {
const params = getBabylonParamByVersion(
stakingParamsVersion,
this.stakingParams
);
const staking = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const {
transaction: unbondingTx,
fee
} = staking.createUnbondingTransaction(stakingTx);
const psbt = staking.toUnbondingPsbt(unbondingTx, stakingTx);
const signedUnbondingPsbtHex = await this.btcProvider.signPsbt(
"unbonding" /* UNBONDING */,
psbt.toHex()
);
const signedUnbondingTx = Psbt3.fromHex(
signedUnbondingPsbtHex
).extractTransaction();
return {
transaction: signedUnbondingTx,
fee
};
}
/**
* Creates a signed unbonding transaction that is ready to be sent to the BTC
* network.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param stakingInput - The staking inputs.
* @param stakingParamsVersion - The params version that was used to create the
* delegation in Babylon chain
* @param stakingTx - The staking transaction.
* @param unsignedUnbondingTx - The unsigned unbonding transaction.
* @param covenantUnbondingSignatures - The covenant unbonding signatures.
* It can be retrieved from the Babylon chain or API.
* @returns The signed unbonding transaction and its fee.
*/
async createSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, unsignedUnbondingTx, covenantUnbondingSignatures) {
const params = getBabylonParamByVersion(
stakingParamsVersion,
this.stakingParams
);
const {
transaction: signedUnbondingTx,
fee
} = await this.createPartialSignedBtcUnbondingTransaction(
stakerBtcInfo,
stakingInput,
stakingParamsVersion,
stakingTx
);
if (signedUnbondingTx.getId() !== unsignedUnbondingTx.getId()) {
throw new Error(
"Unbonding transaction hash does not match the computed hash"
);
}
const covenantBuffers = params.covenantNoCoordPks.map(
(covenant) => Buffer.from(covenant, "hex")
);
const witness = createCovenantWitness(
// Since unbonding transactions always have a single input and output,
// we expect exactly one signature in TaprootScriptSpendSig when the
// signing is successful
signedUnbondingTx.ins[0].witness,
covenantBuffers,
covenantUnbondingSignatures,
params.covenantQuorum
);
signedUnbondingTx.ins[0].witness = witness;
return {
transaction: signedUnbondingTx,
fee
};
}
/**
* Creates a signed withdrawal transaction on the unbodning output expiry path
* that is ready to be sent to the BTC network.
* @param stakingInput - The staking inputs.
* @param stakingParamsVersion - The params version that was used to create the
* delegation in Babylon chain
* @param earlyUnbondingTx - The early unbonding transaction.
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
* fee rate is above 1. If the fee rate is too low, the transaction will not
* be included in a block.
* @returns The signed withdrawal transaction and its fee.
*/
async createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, earlyUnbondingTx, feeRate) {
const params = getBabylonParamByVersion(
stakingParamsVersion,
this.stakingParams
);
const staking = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const { psbt: unbondingPsbt2, fee } = staking.createWithdrawEarlyUnbondedTransaction(
earlyUnbondingTx,
feeRate
);
const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(
"withdraw-early-unbonded" /* WITHDRAW_EARLY_UNBONDED */,
unbondingPsbt2.toHex()
);
return {
transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
fee
};
}
/**
* Creates a signed withdrawal transaction on the staking output expiry path
* that is ready to be sent to the BTC network.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param stakingInput - The staking inputs.
* @param stakingParamsVersion - The params version that was used to create the
* delegation in Babylon chain
* @param stakingTx - The staking transaction.
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
* fee rate is above 1. If the fee rate is too low, the transaction will not
* be included in a block.
* @returns The signed withdrawal transaction and its fee.
*/
async createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, feeRate) {
const params = getBabylonParamByVersion(
stakingParamsVersion,
this.stakingParams
);
const staking = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(
stakingTx,
feeRate
);
const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(
"withdraw-staking-expired" /* WITHDRAW_STAKING_EXPIRED */,
psbt.toHex()
);
return {
transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
fee
};
}
/**
* Creates a signed withdrawal transaction for the expired slashing output that
* is ready to be sent to the BTC network.
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
* and the no-coord public key in hex format.
* @param stakingInput - The staking inputs.
* @param stakingParamsVersion - The params version that was used to create the
* delegation in Babylon chain
* @param slashingTx - The slashing transaction.
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
* fee rate is above 1. If the fee rate is too low, the transaction will not
* be included in a block.
* @returns The signed withdrawal transaction and its fee.
*/
async createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, slashingTx, feeRate) {
const params = getBabylonParamByVersion(
stakingParamsVersion,
this.stakingParams
);
const staking = new Staking(
this.network,
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
const { psbt, fee } = staking.createWithdrawSlashingPsbt(
slashingTx,
feeRate
);
const signedSlashingPsbtHex = await this.btcProvider.signPsbt(
"withdraw-slashing" /* WITHDRAW_SLASHING */,
psbt.toHex()
);
return {
transaction: Psbt3.fromHex(signedSlashingPsbtHex).extractTransaction(),
fee
};
}
/**
* Creates a proof of possession for the staker based on ECDSA signature.
* @param bech32Address - The staker's bech32 address on the babylon chain
* @param stakerBtcAddress - The staker's BTC address.
* @returns The proof of possession.
*/
async createProofOfPossession(bech32Address, stakerBtcAddress) {
let sigType = BTCSigType.ECDSA;
if (isTaproot(stakerBtcAddress, this.network) || isNativeSegwit(stakerBtcAddress, this.network)) {
sigType = BTCSigType.BIP322;
}
const signedBabylonAddress = await this.btcProvider.signMessage(
"proof-of-possession" /* PROOF_OF_POSSESSION */,
bech32Address,
sigType === BTCSigType.BIP322 ? "bip322-simple" : "ecdsa"
);
let btcSig;
if (sigType === BTCSigType.BIP322) {
const bip322Sig = BIP322Sig.fromPartial({
address: stakerBtcAddress,
sig: Buffer.from(signedBabylonAddress, "base64")
});
btcSig = BIP322Sig.encode(bip322Sig).finish();
} else {
btcSig = Buffer.from(signedBabylonAddress, "base64");
}
return {
btcSigType: sigType,
btcSig
};
}
/**
* Creates the unbonding, slashing, and unbonding slashing transactions and
* PSBTs.
* @param stakingInstance - The staking instance.
* @param stakingTx - The staking transaction.
* @returns The unbonding, slashing, and unbonding slashing transactions and
* PSBTs.
*/
async createDelegationTransactionsAndPsbts(stakingInstance, stakingTx) {
const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction(stakingTx);
const { psbt: slashingPsbt } = stakingInstance.createStakingOutputSlashingPsbt(stakingTx);
const { psbt: unbondingSlashingPsbt } = stakingInstance.createUnbondingOutputSlashingPsbt(unbondingTx);
return {
unbondingTx,
slashingPsbt,
unbondingSlashingPsbt
};
}
/**
* Creates a protobuf message for the BTC delegation.
* @param stakingInstance - The staking instance.
* @param stakingInput - The staking inputs.
* @param stakingTx - The staking transaction.
* @param bech32Address - The staker's babylon chain bech32 address
* @param stakerBtcInfo - The staker's BTC information such as address and
* public key
* @param params - The staking parameters.
* @param inclusionProof - The inclusion proof of the staking transaction.
* @returns The protobuf message.
*/
async createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params, inclusionProof) {
const {
unbondingTx,
slashingPsbt,
unbondingSlashingPsbt
} = await this.createDelegationTransactionsAndPsbts(
stakingInstance,
stakingTx
);
const signedSlashingPsbtHex = await this.btcProvider.signPsbt(
"staking-slashing" /* STAKING_SLASHING */,
slashingPsbt.toHex()
);
const signedSlashingTx = Psbt3.fromHex(
signedSlashingPsbtHex
).extractTransaction();
const slashingSig = extractFirstSchnorrSignatureFromTransaction(
signedSlashingTx
);
if (!slashingSig) {
throw new Error("No signature found in the staking output slashing PSBT");
}
const signedUnbondingSlashingPsbtHex = await this.btcProvider.signPsbt(
"unbonding-slashing" /* UNBONDING_SLASHING */,
unbondingSlashingPsbt.toHex()
);
const signedUnbondingSlashingTx = Psbt3.fromHex(
signedUnbondingSlashingPsbtHex
).extractTransaction();
const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(
signedUnbondingSlashingTx
);
if (!unbondingSignatures) {
throw new Error("No signature found in the unbonding output slashing PSBT");
}
const proofOfPossession = await this.createProofOfPossession(
bech32Address,
stakerBtcInfo.address
);
const msg = btcstakingtx.MsgCreateBTCDelegation.fromPartial({
stakerAddr: bech32Address,
pop: proofOfPossession,
btcPk: Uint8Array.from(
Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex")
),
fpBtcPkList: [
Uint8Array.from(
Buffer.from(stakingInput.finalityProviderPkNoCoordHex, "hex")
)
],
stakingTime: stakingInput.stakingTimelock,
stakingValue: stakingInput.stakingAmountSat,
stakingTx: Uint8Array.from(stakingTx.toBuffer()),
slashingTx: Uint8Array.from(
Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")
),
delegatorSlashingSig: Uint8Array.from(slashingSig),
unbondingTime: params.unbondingTime,
unbondingTx: Uint8Array.from(unbondingTx.toBuffer()),
unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat,
unbondingSlashingTx: Uint8Array.from(
Buffer.from(
clearTxSignatures(signedUnbondingSlashingTx).toHex(),
"hex"
)
),
delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures),
stakingTxInclusionProof: inclusionProof
});
return {
typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgCreateBTCDelegation,
value: msg
};
}
/**
* Gets the inclusion proof for the staking transaction.
* See the type `InclusionProof` for more information
* @param inclusionProof - The inclusion proof.
* @returns The inclusion proof.
*/
getInclusionProof(inclusionProof) {
const {
pos,
merkle,
blockHashHex
} = inclusionProof;
const proofHex = deriveMerkleProof(merkle);
const hash = reverseBuffer(Uint8Array.from(Buffer.from(blockHashHex, "hex")));
const inclusionProofKey = btccheckpoint.TransactionKey.fromPartial({
index: pos,
hash
});
return btcstaking.InclusionProof.fromPartial({
key: inclusionProofKey,
proof: Uint8Array.from(Buffer.from(proofHex, "hex"))
});
}
};
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;
};
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 getUnbondingTxStakerSignature = (unbondingTx) => {
try {
return unbondingTx.ins[0].witness[0].toString("hex");
} catch (error) {
throw StakingError.fromUnknown(
error,
"INVALID_INPUT" /* INVALID_INPUT */,
"Failed to get staker signature"
);
}
};
export {
BabylonBtcStakingManager,
BitcoinScriptType,
ObservableStaking,
ObservableStakingScriptData,
SigningStep,
Staking,
StakingScriptData,
buildStakingTransactionOutputs,
createCovenantWitness,
deriveSlashingOutput,
deriveStakingOutputInfo,
deriveUnbondingOutputInfo,
findInputUTXO,
findMatchingTxOutputIndex,
getBabylonParamByBtcHeight,
getBabylonParamByVersion,
getPsbtInputFields,
getPublicKeyNoCoord,
getScriptType,
getUnbondingTxStakerSignature,
initBTCCurve,
isNativeSegwit,
isTaproot,
isValidBabylonAddress,
isValidBitcoinAddress,
isValidNoCoordPublicKey,
slashEarlyUnbondedTransaction,
slashTimelockUnbondedTransaction,
stakingTransaction,
toBuffers,
transactionIdToHash,
unbondingTransaction,
validateParams,
validateStakingTimelock,
validateStakingTxInputData,
withdrawEarlyUnbondedTransaction,
withdrawSlashingTransaction,
withdrawTimelockUnbondedTransaction
};
Выполнить команду
Для локальной разработки. Не используйте в интернете!