PHP WebShell

Текущая директория: /opt/BitGoJS/modules/babylonlabs-io-btc-staking-ts/build/src/staking

Просмотр файла: transactions.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCovenantWitness = void 0;
exports.stakingTransaction = stakingTransaction;
exports.withdrawEarlyUnbondedTransaction = withdrawEarlyUnbondedTransaction;
exports.withdrawTimelockUnbondedTransaction = withdrawTimelockUnbondedTransaction;
exports.withdrawSlashingTransaction = withdrawSlashingTransaction;
exports.slashTimelockUnbondedTransaction = slashTimelockUnbondedTransaction;
exports.slashEarlyUnbondedTransaction = slashEarlyUnbondedTransaction;
exports.unbondingTransaction = unbondingTransaction;
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
const dustSat_1 = require("../constants/dustSat");
const internalPubkey_1 = require("../constants/internalPubkey");
const btc_1 = require("../utils/btc");
const fee_1 = require("../utils/fee");
const utils_1 = require("../utils/fee/utils");
const staking_1 = require("../utils/staking");
const psbt_1 = require("../constants/psbt");
const transaction_1 = require("../constants/transaction");
// https://bips.xyz/370
const BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 500000000;
const BTC_SLASHING_FRACTION_DIGITS = 4;
/**
 * Constructs an unsigned BTC Staking transaction in psbt format.
 *
 * Outputs:
 * - psbt:
 *   - The first output corresponds to the staking script with the specified amount.
 *   - The second output corresponds to the change from spending the amount and the transaction fee.
 *   - If a data embed script is provided, it will be added as the second output, and the fee will be the third output.
 * - fee: The total fee amount for the transaction.
 *
 * Inputs:
 * - scripts:
 *   - timelockScript, unbondingScript, slashingScript: Scripts for different transaction types.
 *   - dataEmbedScript: Optional data embed script.
 * - amount: Amount to stake.
 * - changeAddress: Address to send the change to.
 * - inputUTXOs: All available UTXOs from the wallet.
 * - network: Bitcoin network.
 * - feeRate: Fee rate in satoshis per byte.
 * - publicKeyNoCoord: Public key if the wallet is in taproot mode.
 * - lockHeight: Optional block height locktime to set for the transaction (i.e., not mined until the block height).
 *
 * @param {Object} scripts - Scripts used to construct the taproot output.
 * such as timelockScript, unbondingScript, slashingScript, and dataEmbedScript.
 * @param {number} amount - The amount to stake.
 * @param {string} changeAddress - The address to send the change to.
 * @param {UTXO[]} inputUTXOs - All available UTXOs from the wallet.
 * @param {networks.Network} network - The Bitcoin network.
 * @param {number} feeRate - The fee rate in satoshis per byte.
 * @param {number} [lockHeight] - The optional block height locktime.
 * @returns {TransactionResult} - An object containing the unsigned transaction and fee
 * @throws Will throw an error if the amount or fee rate is less than or equal
 * to 0, if the change address is invalid, or if the public key is invalid.
 */
function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) {
    // Check that amount and fee are bigger than 0
    if (amount <= 0 || feeRate <= 0) {
        throw new Error("Amount and fee rate must be bigger than 0");
    }
    // Check whether the change address is a valid Bitcoin address.
    if (!(0, btc_1.isValidBitcoinAddress)(changeAddress, network)) {
        throw new Error("Invalid change address");
    }
    // Build outputs and estimate the fee
    const stakingOutputs = (0, staking_1.buildStakingTransactionOutputs)(scripts, network, amount);
    const { selectedUTXOs, fee } = (0, fee_1.getStakingTxInputUTXOsAndFees)(inputUTXOs, amount, feeRate, stakingOutputs);
    const tx = new bitcoinjs_lib_1.Transaction();
    tx.version = psbt_1.TRANSACTION_VERSION;
    for (let i = 0; i < selectedUTXOs.length; ++i) {
        const input = selectedUTXOs[i];
        tx.addInput((0, btc_1.transactionIdToHash)(input.txid), input.vout, psbt_1.NON_RBF_SEQUENCE);
    }
    stakingOutputs.forEach((o) => {
        tx.addOutput(o.scriptPubKey, o.value);
    });
    // Add a change output only if there's any amount leftover from the inputs
    const inputsSum = (0, utils_1.inputValueSum)(selectedUTXOs);
    // Check if the change amount is above the dust limit, and if so, add it as a change output
    if (inputsSum - (amount + fee) > dustSat_1.BTC_DUST_SAT) {
        tx.addOutput(bitcoinjs_lib_1.address.toOutputScript(changeAddress, network), inputsSum - (amount + fee));
    }
    // Set the locktime field if provided. If not provided, the locktime will be set to 0 by default
    // Only height based locktime is supported
    if (lockHeight) {
        if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) {
            throw new Error("Invalid lock height");
        }
        tx.locktime = lockHeight;
    }
    return {
        transaction: tx,
        fee,
    };
}
/**
 * Constructs a withdrawal transaction for manually unbonded delegation.
 *
 * This transaction spends the unbonded output from the staking transaction.
 *
 * Inputs:
 * - scripts: Scripts used to construct the taproot output.
 *   - unbondingTimelockScript: Script for the unbonding timelock condition.
 *   - slashingScript: Script for the slashing condition.
 * - unbondingTx: The unbonding transaction.
 * - withdrawalAddress: The address to send the withdrawn funds to.
 * - network: The Bitcoin network.
 * - feeRate: The fee rate for the transaction in satoshis per byte.
 *
 * Returns:
 * - psbt: The partially signed transaction (PSBT).
 *
 * @param {Object} scripts - The scripts used in the transaction.
 * @param {Transaction} unbondingTx - The unbonding transaction.
 * @param {string} withdrawalAddress - The address to send the withdrawn funds to.
 * @param {networks.Network} network - The Bitcoin network.
 * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
 * @returns {PsbtResult} An object containing the partially signed transaction (PSBT).
 */
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);
}
/**
 * Constructs a withdrawal transaction for naturally unbonded delegation.
 *
 * This transaction spends the unbonded output from the staking transaction when the timelock has expired.
 *
 * Inputs:
 * - scripts: Scripts used to construct the taproot output.
 *   - timelockScript: Script for the timelock condition.
 *   - slashingScript: Script for the slashing condition.
 *   - unbondingScript: Script for the unbonding condition.
 * - tx: The original staking transaction.
 * - withdrawalAddress: The address to send the withdrawn funds to.
 * - network: The Bitcoin network.
 * - feeRate: The fee rate for the transaction in satoshis per byte.
 * - outputIndex: The index of the output to be spent in the original transaction (default is 0).
 *
 * Returns:
 * - psbt: The partially signed transaction (PSBT).
 *
 * @param {Object} scripts - The scripts used in the transaction.
 * @param {Transaction} tx - The original staking transaction.
 * @param {string} withdrawalAddress - The address to send the withdrawn funds to.
 * @param {networks.Network} network - The Bitcoin network.
 * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
 * @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction.
 * @returns {PsbtResult} An object containing the partially signed transaction (PSBT).
 */
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);
}
/**
 * Constructs a withdrawal transaction for a slashing transaction.
 *
 * This transaction spends the output from the slashing transaction.
 *
 * @param {Object} scripts - The unbondingTimelockScript
 * We use the unbonding timelock script as the timelock of the slashing transaction.
 * This is due to slashing tx timelock is the same as the unbonding timelock.
 * @param {Transaction} slashingTx - The slashing transaction.
 * @param {string} withdrawalAddress - The address to send the withdrawn funds to.
 * @param {networks.Network} network - The Bitcoin network.
 * @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
 * @param {number} outputIndex - The index of the output to be spent in the original transaction.
 * @returns {PsbtResult} An object containing the partially signed transaction (PSBT).
 */
function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) {
    const scriptTree = { output: scripts.unbondingTimelockScript };
    return withdrawalTransaction({
        timelockScript: scripts.unbondingTimelockScript,
    }, scriptTree, slashingTx, withdrawalAddress, network, feeRate, outputIndex);
}
// withdrawalTransaction generates a transaction that
// spends the staking output of the staking transaction
function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
    // Check that withdrawal feeRate is bigger than 0
    if (feeRate <= 0) {
        throw new Error("Withdrawal feeRate must be bigger than 0");
    }
    // Check that outputIndex is bigger or equal to 0
    if (outputIndex < 0) {
        throw new Error("Output index must be bigger or equal to 0");
    }
    // position of time in the timelock script
    const timePosition = 2;
    const decompiled = bitcoinjs_lib_1.script.decompile(scripts.timelockScript);
    if (!decompiled) {
        throw new Error("Timelock script is not valid");
    }
    let timelock = 0;
    // if the timelock is a buffer, it means it's a number bigger than 16 blocks
    if (typeof decompiled[timePosition] !== "number") {
        const timeBuffer = decompiled[timePosition];
        timelock = bitcoinjs_lib_1.script.number.decode(timeBuffer);
    }
    else {
        // in case timelock is <= 16 it will be a number, not a buffer
        const wrap = decompiled[timePosition] % 16;
        timelock = wrap === 0 ? 16 : wrap;
    }
    const redeem = {
        output: scripts.timelockScript,
        redeemVersion: transaction_1.REDEEM_VERSION,
    };
    const p2tr = bitcoinjs_lib_1.payments.p2tr({
        internalPubkey: internalPubkey_1.internalPubkey,
        scriptTree,
        redeem,
        network,
    });
    const tapLeafScript = {
        leafVersion: redeem.redeemVersion,
        script: redeem.output,
        controlBlock: p2tr.witness[p2tr.witness.length - 1],
    };
    const psbt = new bitcoinjs_lib_1.Psbt({ network });
    // only transactions with version 2 can trigger OP_CHECKSEQUENCEVERIFY
    // https://github.com/btcsuite/btcd/blob/master/txscript/opcode.go#L1174
    psbt.setVersion(psbt_1.TRANSACTION_VERSION);
    psbt.addInput({
        hash: tx.getHash(),
        index: outputIndex,
        tapInternalKey: internalPubkey_1.internalPubkey,
        witnessUtxo: {
            value: tx.outs[outputIndex].value,
            script: tx.outs[outputIndex].script,
        },
        tapLeafScript: [tapLeafScript],
        sequence: timelock,
    });
    const estimatedFee = (0, fee_1.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 < dustSat_1.BTC_DUST_SAT) {
        throw new Error("Output value is less than dust limit");
    }
    psbt.addOutput({
        address: withdrawalAddress,
        value: outputValue,
    });
    // Withdraw transaction has no time-based restrictions and can be included 
    // in the next block immediately.
    psbt.setLocktime(0);
    return {
        psbt,
        fee: estimatedFee,
    };
}
/**
 * Constructs a slashing transaction for a staking output without prior unbonding.
 *
 * This transaction spends the staking output of the staking transaction and distributes the funds
 * according to the specified slashing rate.
 *
 * Outputs:
 * - The first output sends `input * slashing_rate` funds to the slashing address.
 * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address.
 *
 * Inputs:
 * - scripts: Scripts used to construct the taproot output.
 *   - slashingScript: Script for the slashing condition.
 *   - timelockScript: Script for the timelock condition.
 *   - unbondingScript: Script for the unbonding condition.
 *   - unbondingTimelockScript: Script for the unbonding timelock condition.
 * - transaction: The original staking transaction.
 * - slashingAddress: The address to send the slashed funds to.
 * - slashingRate: The rate at which the funds are slashed (0 < slashingRate < 1).
 * - minimumFee: The minimum fee for the transaction in satoshis.
 * - network: The Bitcoin network.
 * - outputIndex: The index of the output to be spent in the original transaction (default is 0).
 *
 * @param {Object} scripts - The scripts used in the transaction.
 * @param {Transaction} stakingTransaction - The original staking transaction.
 * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to.
 * @param {number} slashingRate - The rate at which the funds are slashed.
 * @param {number} minimumFee - The minimum fee for the transaction in satoshis.
 * @param {networks.Network} network - The Bitcoin network.
 * @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction.
 * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT).
 */
function slashTimelockUnbondedTransaction(scripts, stakingTransaction, 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, stakingTransaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex);
}
/**
 * Constructs a slashing transaction for an early unbonded transaction.
 *
 * This transaction spends the staking output of the staking transaction and distributes the funds
 * according to the specified slashing rate.
 *
 * Transaction outputs:
 * - The first output sends `input * slashing_rate` funds to the slashing address.
 * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address.
 *
 * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript
 * @param {Transaction} unbondingTx - The unbonding transaction.
 * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to.
 * @param {number} slashingRate - The rate at which the funds are slashed.
 * @param {number} minimumSlashingFee - The minimum fee for the transaction in satoshis.
 * @param {networks.Network} network - The Bitcoin network.
 * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT).
 */
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);
}
/**
 * Constructs a slashing transaction for an on-demand unbonding.
 *
 * This transaction spends the staking output of the staking transaction and distributes the funds
 * according to the specified slashing rate.
 *
 * Transaction outputs:
 * - The first output sends `input * slashing_rate` funds to the slashing address.
 * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address.
 *
 * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript
 * @param {Transaction} transaction - The original staking/unbonding transaction.
 * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to.
 * @param {number} slashingRate - The rate at which the funds are slashed. Two decimal places, otherwise it will be rounded down.
 * @param {number} minimumFee - The minimum fee for the transaction in satoshis.
 * @param {networks.Network} network - The Bitcoin network.
 * @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction.
 * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT).
 */
function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
    // Check that slashing rate and minimum fee are bigger than 0
    if (slashingRate <= 0 || slashingRate >= 1) {
        throw new Error("Slashing rate must be between 0 and 1");
    }
    // Round the slashing rate to two decimal places
    slashingRate = parseFloat(slashingRate.toFixed(BTC_SLASHING_FRACTION_DIGITS));
    // Minimum fee must be a postive integer
    if (minimumFee <= 0 || !Number.isInteger(minimumFee)) {
        throw new Error("Minimum fee must be a positve integer");
    }
    // Check that outputIndex is bigger or equal to 0
    if (outputIndex < 0 || !Number.isInteger(outputIndex)) {
        throw new Error("Output index must be an integer bigger or equal to 0");
    }
    // Check that outputIndex is within the bounds of the transaction
    if (!transaction.outs[outputIndex]) {
        throw new Error("Output index is out of range");
    }
    const redeem = {
        output: scripts.slashingScript,
        redeemVersion: transaction_1.REDEEM_VERSION,
    };
    const p2tr = bitcoinjs_lib_1.payments.p2tr({
        internalPubkey: internalPubkey_1.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;
    // Slashing rate is a percentage of the staking amount, rounded down to
    // the nearest integer to avoid sending decimal satoshis
    const slashingAmount = Math.round(stakingAmount * slashingRate);
    // Compute the slashing output
    const slashingOutput = Buffer.from(slashingPkScriptHex, "hex");
    // If OP_RETURN is not included, the slashing amount must be greater than the
    // dust limit.
    if (bitcoinjs_lib_1.opcodes.OP_RETURN != slashingOutput[0]) {
        if (slashingAmount <= dustSat_1.BTC_DUST_SAT) {
            throw new Error("Slashing amount is less than dust limit");
        }
    }
    const userFunds = stakingAmount - slashingAmount - minimumFee;
    if (userFunds <= dustSat_1.BTC_DUST_SAT) {
        throw new Error("User funds are less than dust limit");
    }
    const psbt = new bitcoinjs_lib_1.Psbt({ network });
    psbt.setVersion(psbt_1.TRANSACTION_VERSION);
    psbt.addInput({
        hash: transaction.getHash(),
        index: outputIndex,
        tapInternalKey: internalPubkey_1.internalPubkey,
        witnessUtxo: {
            value: stakingAmount,
            script: transaction.outs[outputIndex].script,
        },
        tapLeafScript: [tapLeafScript],
        // not RBF-able
        sequence: psbt_1.NON_RBF_SEQUENCE,
    });
    // Add the slashing output
    psbt.addOutput({
        script: slashingOutput,
        value: slashingAmount,
    });
    // Change output contains unbonding timelock script
    const changeOutput = bitcoinjs_lib_1.payments.p2tr({
        internalPubkey: internalPubkey_1.internalPubkey,
        scriptTree: { output: scripts.unbondingTimelockScript },
        network,
    });
    // Add the change output
    psbt.addOutput({
        address: changeOutput.address,
        value: userFunds,
    });
    // Slashing transaction has no time-based restrictions and can be included 
    // in the next block immediately.
    psbt.setLocktime(0);
    return { psbt };
}
function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) {
    // Check that transaction fee is bigger than 0
    if (unbondingFee <= 0) {
        throw new Error("Unbonding fee must be bigger than 0");
    }
    // Check that outputIndex is bigger or equal to 0
    if (outputIndex < 0) {
        throw new Error("Output index must be bigger or equal to 0");
    }
    const tx = new bitcoinjs_lib_1.Transaction();
    tx.version = psbt_1.TRANSACTION_VERSION;
    tx.addInput(stakingTx.getHash(), outputIndex, psbt_1.NON_RBF_SEQUENCE);
    const unbondingOutputInfo = (0, staking_1.deriveUnbondingOutputInfo)(scripts, network);
    const outputValue = stakingTx.outs[outputIndex].value - unbondingFee;
    if (outputValue < dustSat_1.BTC_DUST_SAT) {
        throw new Error("Output value is less than dust limit for unbonding transaction");
    }
    // Add the unbonding output
    if (!unbondingOutputInfo.outputAddress) {
        throw new Error("Unbonding output address is not defined");
    }
    tx.addOutput(unbondingOutputInfo.scriptPubKey, outputValue);
    // Unbonding transaction has no time-based restrictions and can be included 
    // in the next block immediately.
    tx.locktime = 0;
    return {
        transaction: tx,
        fee: unbondingFee,
    };
}
// This function attaches covenant signatures as the transaction's witness
// Note that the witness script expects exactly covenantQuorum number of signatures
// to match the covenant parameters.
const createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => {
    if (covenantSigs.length < covenantQuorum) {
        throw new Error(`Not enough covenant signatures. Required: ${covenantQuorum}, `
            + `got: ${covenantSigs.length}`);
    }
    // Verify all btcPkHex from covenantSigs exist in paramsCovenants
    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`);
        }
    }
    // We only take exactly covenantQuorum number of signatures, even if more are provided.
    // Including extra signatures will cause the unbonding transaction to fail validation.
    // This is because the witness script expects exactly covenantQuorum number of signatures
    // to match the covenant parameters.
    const covenantSigsBuffers = covenantSigs
        .slice(0, covenantQuorum)
        .map((sig) => ({
        btcPkHex: Buffer.from(sig.btcPkHex, "hex"),
        sigHex: Buffer.from(sig.sigHex, "hex"),
    }));
    // we need covenant from params to be sorted in reverse order
    const paramsCovenantsSorted = [...paramsCovenants]
        .sort(Buffer.compare)
        .reverse();
    const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => {
        // in case there's covenant with this btc_pk_hex we return the sig
        // otherwise we return empty Buffer
        const covenantSig = covenantSigsBuffers.find((sig) => sig.btcPkHex.compare(covenant) === 0);
        return (covenantSig === null || covenantSig === void 0 ? void 0 : covenantSig.sigHex) || Buffer.alloc(0);
    });
    return [...composedCovenantSigs, ...originalWitness];
};
exports.createCovenantWitness = createCovenantWitness;

Выполнить команду


Для локальной разработки. Не используйте в интернете!