diff --git a/src/functions/bitcoin/bitcoin-functions.ts b/src/functions/bitcoin/bitcoin-functions.ts index 24beecb..616c76a 100644 --- a/src/functions/bitcoin/bitcoin-functions.ts +++ b/src/functions/bitcoin/bitcoin-functions.ts @@ -21,7 +21,9 @@ import { BitcoinInputSigningConfig, BitcoinTransaction, BitcoinTransactionVectorOutput, + BlockData, FeeRates, + HistoricalFeeRate, PaymentTypes, UTXO, } from '../../models/bitcoin-models.js'; @@ -143,44 +145,94 @@ export function createTaprootMultisigPayment( } /** - * Evaluates the fee rate from the bitcoin blockchain API. + * Fetches the last two blocks' fee rates from the bitcoin blockchain API. * - * @returns The fee rate. + * @returns A promise that resolves to the last two blocks' median fee rates. */ -function checkFeeRate(feeRate: number | undefined): number { - if (!feeRate || feeRate < 2) { - return 2; +export async function getLastTwoBlocksFeeRate( + bitcoinBlockchainAPIFeeURL: string +): Promise { + const dayFeeRateAPI = `${bitcoinBlockchainAPIFeeURL}/api/v1/mining/blocks/fee-rates/24h`; + + const response = await fetch(dayFeeRateAPI); + + if (!response.ok) { + throw new Error(`Bitcoin Blockchain Fee Rate Response was not OK: ${response.statusText}`); } - return feeRate; + + const historicalFeeRates: HistoricalFeeRate[] = await response.json(); + + return historicalFeeRates.slice(historicalFeeRates.length - 2).map(rate => rate.avgFee_50); } /** - * Fetches the fee rate from the bitcoin blockchain API. + * Fetches the current mempool block median fee rate from the bitcoin blockchain API. * - * @returns A promise that resolves to the hour fee rate. + * @param bitcoinBlockchainAPIFeeURL + * @returns */ -export async function getFeeRate( - bitcoinBlockchainAPIFeeURL: string, - feeRateMultiplier?: number +export async function getCurrentMempoolBlockFeeRate( + bitcoinBlockchainAPIFeeURL: string ): Promise { - const response = await fetch(bitcoinBlockchainAPIFeeURL); + const mempoolBlocksAPI = `${bitcoinBlockchainAPIFeeURL}/api/v1/fees/mempool-blocks`; + + const response = await fetch(mempoolBlocksAPI); if (!response.ok) { throw new Error(`Bitcoin Blockchain Fee Rate Response was not OK: ${response.statusText}`); } - let feeRates: FeeRates; + const currentBlockFeeRate: BlockData[] = await response.json(); - try { - feeRates = await response.json(); - } catch (error) { - throw new Error(`Error parsing Bitcoin Blockchain Fee Rate Response JSON: ${error}`); + return currentBlockFeeRate[0].medianFee; +} + +/** + * Fetches the estimated fee rate from the bitcoin blockchain API. + * + * @returns A promise that resolves to the fastest fee rate. + */ +export async function getEstimatedFeeRate(bitcoinBlockchainAPIFeeURL: string): Promise { + const estimatedFeeAPI = `${bitcoinBlockchainAPIFeeURL}/api/v1/fees/recommended`; + + const response = await fetch(estimatedFeeAPI); + + if (!response.ok) { + throw new Error(`Bitcoin Blockchain Fee Rate Response was not OK: ${response.statusText}`); } - const feeRate = checkFeeRate(feeRates.fastestFee); - const multipliedFeeRate = feeRate * (feeRateMultiplier ?? 1); + const feeRates: FeeRates = await response.json(); + + return feeRates.fastestFee; +} + +/** + * Return the fee rate for the transaction. + * + * @returns A promise that resolves to the fee rate. + */ +export async function getFeeRate( + bitcoinBlockchainAPIFeeURL: string, + feeRateMultiplier?: number, + isRegtest = false +): Promise { + if (isRegtest) return 2; + + const multiplier = feeRateMultiplier ?? 1; + + const currentBlockFeeRate = await getCurrentMempoolBlockFeeRate(bitcoinBlockchainAPIFeeURL); + const lastTwoBlocksFeeRate = await getLastTwoBlocksFeeRate(bitcoinBlockchainAPIFeeURL); + + const feeRates = lastTwoBlocksFeeRate.concat(currentBlockFeeRate); + const feeRateAverage = feeRates.reduce((a, b) => a + b) / feeRates.length; + const feeRateAverageMultiplied = Math.ceil(feeRateAverage) * multiplier; + console.log('Fee Rate Average Multiplied:', feeRateAverageMultiplied); + + const estimatedFeeRate = await getEstimatedFeeRate(bitcoinBlockchainAPIFeeURL); + const estimatedFeeRateMultiplied = estimatedFeeRate * multiplier; + console.log('Estimated Fee Rate Multiplied:', estimatedFeeRateMultiplied); - return multipliedFeeRate; + return Math.max(feeRateAverageMultiplied, estimatedFeeRateMultiplied); } /** diff --git a/src/models/bitcoin-models.ts b/src/models/bitcoin-models.ts index fc23293..b4984c6 100644 --- a/src/models/bitcoin-models.ts +++ b/src/models/bitcoin-models.ts @@ -21,6 +21,27 @@ export interface FeeRates { minimumFee: number; } +export interface BlockData { + blockSize: number; + blockVSize: number; + nTx: number; + totalFees: number; + medianFee: number; + feeRange: number[]; +} + +export interface HistoricalFeeRate { + avgHeight: number; + timestamp: number; + avgFee_0: number; + avgFee_10: number; + avgFee_25: number; + avgFee_50: number; + avgFee_75: number; + avgFee_90: number; + avgFee_100: number; +} + export interface PaymentInformation { fundingPayment: P2Ret | P2TROut; multisigPayment: P2TROut; diff --git a/tests/unit/bitcoin-functions.test.ts b/tests/unit/bitcoin-functions.test.ts index 606ec28..10a3375 100644 --- a/tests/unit/bitcoin-functions.test.ts +++ b/tests/unit/bitcoin-functions.test.ts @@ -8,6 +8,7 @@ import { ecdsaPublicKeyToSchnorr, finalizeUserInputs, getFeeAmount, + getFeeRate, getFeeRecipientAddress, getInputIndicesByScript, getScriptMatchingOutputFromTransaction, @@ -39,6 +40,12 @@ import { import { TEST_VAULT_UUID_1 } from '../mocks/ethereum.test.constants'; describe('Bitcoin Functions', () => { + describe('getFeeRate', () => { + it('should return the fee rate in satoshis per byte', async () => { + const feeRate = await getFeeRate('https://mempool.space'); + console.log(feeRate); + }, 30000); + }); describe('getInputIndicesByScript', () => { it('correctly retrieves one input index by script', () => { const transaction = Transaction.fromPSBT(