import { AddressTxsUtxo, DEFAULT_ARCH_P2TR_INPUT, DEFAULT_P2TR_OUTPUT, getInputTypeFromAddress, getTotalSizeAndFeesFromAncestors, MempoolEntriesMap, TransactionSizeCalculator, } from '@saturnbtcio/psbt'; import { hex } from '@scure/base'; import { NETWORK, Transaction } from '@scure/btc-signer'; import { PoolErrorException, PoolErrorType } from '../error/pool.error'; import { IBitcoinProvider, MempoolInfoMap, } from '../providers/bitcoin.provider'; import { CollectionUtxo, Wallet } from '../wallet/wallet.dto'; import { max } from './bigint'; import { getTxIdsFromUtxos } from './mempool'; import { Runestone } from '@saturnbtcio/ordinals-lib'; export const getUtxosFromTxInputs = ( tx: Transaction, walletUtxos: Array, ): Array => { let txStatuses: Array = []; for (let i = 0; i < tx.inputsLength; i++) { const input = tx.getInput(i); if (!input.txid) { continue; } const txid = hex.encode(input.txid); const utxo = walletUtxos.find( (u) => `${u.txid}:${u.vout}` === `${txid}:${input.index}`, ); if (!utxo) { throw new PoolErrorException({ type: PoolErrorType.InvalidUtxo, message: `Invalid utxo. Utxo is not in wallet`, utxos: [`${txid}:${input.index}`], }); } txStatuses.push(utxo); } return txStatuses; }; export const getTotalFeeAndSizeFromAncestorsInputs = ( txStatuses: Array, mempoolTxStatuses: MempoolInfoMap, ) => { const { totalAncestorsSize, totalAncestorsFee } = getTotalSizeAndFeesFromAncestors( txStatuses, mempoolTxStatuses as MempoolEntriesMap, ); return { totalSize: totalAncestorsSize, totalFee: totalAncestorsFee, }; }; export const getFeeForOpenOrIncreaseLiquidityStateChange = ( hasRuneUtxo: boolean, hasBtcUtxo: boolean, ) => { const txSizeCalculator = new TransactionSizeCalculator(); // 2 input account utxos (LP shard and position) + 2 input utxos (btc and rune) + (hasRuneUtxo ? 1 : 0) + (hasBtcUtxo ? 1 : 0) + 1 fee payer let inputAmount = 2 + 2 + (hasRuneUtxo ? 1 : 0) + (hasBtcUtxo ? 1 : 0) + 1; for (let i = 0; i < inputAmount; i++) { txSizeCalculator.addInput(DEFAULT_ARCH_P2TR_INPUT); } // 2 output account utxos (LP shard and position) + 2 output utxos (btc and rune) + 1 fee payer for (let i = 0; i < 4 + 1; i++) { txSizeCalculator.addOutput(DEFAULT_P2TR_OUTPUT); } const runestone = Runestone.default(); runestone.pointer = 2; const enciphered = runestone.encipher(); txSizeCalculator.addOutput({ script: enciphered, amount: 0n, }); return txSizeCalculator; }; export const calculateFeeForSendFundsTxAfterMerge = ( wallet: Wallet, secondaryWallet: Wallet | undefined, includeAccount: boolean, feeRate: bigint, network: typeof NETWORK, ) => { const txCalculator = new TransactionSizeCalculator(); // Bitcoin addr txCalculator.addInput( getInputTypeFromAddress(secondaryWallet?.address ?? wallet.address), ); // Ordinals addr txCalculator.addInput(getInputTypeFromAddress(wallet.address)); // Outputs to program address. txCalculator.addOutput(DEFAULT_P2TR_OUTPUT); txCalculator.addOutput(DEFAULT_P2TR_OUTPUT); if (includeAccount) { txCalculator.addOutput(DEFAULT_P2TR_OUTPUT); } return txCalculator.calculateFee(feeRate, { network: network }); }; export const checkFee = async ( psbt: Transaction, collectionUtxos: Array, bitcoinProvider: IBitcoinProvider, mempoolInfo?: MempoolInfoMap, ) => { const totalAmountInBtc = collectionUtxos.reduce( (acc, utxo) => acc + utxo.value, 0n, ); let mempoolTxStatuses: MempoolInfoMap; if (!mempoolInfo) { mempoolTxStatuses = await bitcoinProvider.getMempoolInfo( getTxIdsFromUtxos(collectionUtxos), ); } else { mempoolTxStatuses = mempoolInfo; } let { totalAncestorsFee, totalAncestorsSize, ancestorsCount } = getTotalSizeAndFeesFromAncestors(collectionUtxos, mempoolTxStatuses); let totalAmountOutBtc = 0n; for (let i = 0; i < psbt.outputsLength; i++) { const output = psbt.getOutput(i); totalAmountOutBtc += output.amount ?? 0n; } const fee = totalAmountInBtc - totalAmountOutBtc + totalAncestorsFee; const size = psbt.vsize + totalAncestorsSize; const feeRate = Number(fee) / Number(size); await checkFeeRate(Number(feeRate), bitcoinProvider); return { totalAncestorsFee, totalAncestorsSize, ancestorsCount, fee, size, feeRate, }; }; export const checkFeeRate = async ( feeRate: number, bitcoinProvider: IBitcoinProvider, ) => { const recommendedFees = await bitcoinProvider.getRecommendedFees(); const minFee = Math.floor(recommendedFees.fastestFee); if (feeRate < minFee) { throw new PoolErrorException({ message: `Fee rate is too low. It should be higher or equal than ${minFee} sat/vB`, type: PoolErrorType.InvalidFeeRate, minFeeRate: minFee, }); } }; export const getFeeForCreateAccountsStateChange = (shardsLength: number) => { const txSizeCalculator = new TransactionSizeCalculator(); // shards length + 1 pool config + 1 fee + 1 fee payer for (let i = 0; i < shardsLength + 1 + 1 + 1; i++) { txSizeCalculator.addInput(DEFAULT_ARCH_P2TR_INPUT); } // shards length + 1 pool config + 1 fee payer for (let i = 0; i < shardsLength + 1 + 1; i++) { txSizeCalculator.addOutput(DEFAULT_P2TR_OUTPUT); } return txSizeCalculator; }; export const calculateFeeRuneToBtcExactInSwap = ( ancestorsSize: number, ancestorsFeePaid: bigint, txSize: number, feeRate: bigint, ) => { const feeWithoutAncestors = BigInt(Math.ceil(txSize * Number(feeRate))); const feeWithAncestors = BigInt( Math.ceil((txSize + ancestorsSize) * Number(feeRate)), ); const feeToPay = max( feeWithoutAncestors, feeWithAncestors - ancestorsFeePaid, ); return feeToPay; }; export const getFeeForSwapBtcToRune = ( txSizeCalculator: TransactionSizeCalculator, shards: number, ) => { // 1 fee payer txSizeCalculator.addInput(DEFAULT_ARCH_P2TR_INPUT); // for each shard, one account and one rune input for (let i = 0; i < shards * 2; i++) { txSizeCalculator.addInput(DEFAULT_ARCH_P2TR_INPUT); } return txSizeCalculator; };