diff --git a/Cargo.lock b/Cargo.lock index 7a588cf13..b80e32a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2235,6 +2235,7 @@ dependencies = [ "lightning-transaction-sync", "ln-dlc-storage", "log", + "mempool", "p2pd-oracle-client", "parking_lot 0.12.1", "rand", @@ -2394,6 +2395,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mempool" +version = "0.1.0" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "migrations_internals" version = "2.0.0" diff --git a/crates/ln-dlc-node/Cargo.toml b/crates/ln-dlc-node/Cargo.toml index c6b632313..fa9ceb075 100644 --- a/crates/ln-dlc-node/Cargo.toml +++ b/crates/ln-dlc-node/Cargo.toml @@ -30,6 +30,7 @@ lightning-rapid-gossip-sync = { version = "0.0.117" } lightning-transaction-sync = { version = "0.0.117", features = ["esplora-blocking"] } ln-dlc-storage = { path = "../../crates/ln-dlc-storage" } log = "0.4.17" +mempool = { path = "../../crates/mempool" } p2pd-oracle-client = { version = "0.1.0" } parking_lot = { version = "0.12.1" } rand = "0.8.5" diff --git a/crates/ln-dlc-node/src/fee_rate_estimator.rs b/crates/ln-dlc-node/src/fee_rate_estimator.rs index 42ad0919f..43d2792ca 100644 --- a/crates/ln-dlc-node/src/fee_rate_estimator.rs +++ b/crates/ln-dlc-node/src/fee_rate_estimator.rs @@ -1,34 +1,25 @@ use anyhow::Result; use bdk::FeeRate; +use bitcoin::Network; use lightning::chain::chaininterface::ConfirmationTarget; use lightning::chain::chaininterface::FeeEstimator; use lightning::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; use parking_lot::RwLock; use std::collections::HashMap; -const CONFIRMATION_TARGETS: [(ConfirmationTarget, usize); 4] = [ - // We choose an extremely high background confirmation target to avoid force-closing channels - // unnecessarily. - (ConfirmationTarget::Background, 1008), - // We just want to end up in the mempool eventually. We just set the target to 1008 - // as that is esplora's highest block target available - (ConfirmationTarget::MempoolMinimum, 1008), - (ConfirmationTarget::Normal, 6), - (ConfirmationTarget::HighPriority, 3), -]; - /// Default values used when constructing the [`FeeRateEstimator`] if the fee rate sever cannot give /// us up-to-date values. /// /// In sats/kwu. -const FEE_RATE_DEFAULTS: [(ConfirmationTarget, u32); 3] = [ - (ConfirmationTarget::Background, FEERATE_FLOOR_SATS_PER_KW), - (ConfirmationTarget::Normal, 2000), - (ConfirmationTarget::HighPriority, 5000), +const FEE_RATE_DEFAULTS: [(ConfirmationTarget, u32); 4] = [ + (ConfirmationTarget::MempoolMinimum, 1000), + (ConfirmationTarget::Background, 2000), + (ConfirmationTarget::Normal, 3000), + (ConfirmationTarget::HighPriority, 4000), ]; pub struct FeeRateEstimator { - client: esplora_client::BlockingClient, + client: mempool::MempoolFeeRateEstimator, fee_rate_cache: RwLock>, } @@ -42,31 +33,27 @@ impl EstimateFeeRate for FeeRateEstimator { } } +fn to_mempool_network(value: Network) -> mempool::Network { + match value { + Network::Bitcoin => mempool::Network::Mainnet, + Network::Testnet => mempool::Network::Testnet, + Network::Signet => mempool::Network::Signet, + Network::Regtest => mempool::Network::Local, + } +} + impl FeeRateEstimator { /// Constructor for the [`FeeRateEstimator`]. - pub fn new(esplora_url: String) -> Self { - let client = esplora_client::BlockingClient::from_agent(esplora_url, ureq::agent()); - - let initial_fee_rates = match client.get_fee_estimates() { - Ok(estimates) => { - HashMap::from_iter(CONFIRMATION_TARGETS.into_iter().map(|(target, n_blocks)| { - let fee_rate = esplora_client::convert_fee_rate(n_blocks, estimates.clone()) - .expect("fee rates for our confirmation targets"); - let fee_rate = FeeRate::from_sat_per_vb(fee_rate); - - (target, fee_rate) - })) - } - Err(e) => { - tracing::warn!(defaults = ?FEE_RATE_DEFAULTS, "Initializing fee rate cache with default values: {e:#}"); - - HashMap::from_iter( - FEE_RATE_DEFAULTS.into_iter().map(|(target, fee_rate)| { - (target, FeeRate::from_sat_per_kwu(fee_rate as f32)) - }), - ) - } - }; + pub fn new(network: Network) -> Self { + let client = mempool::MempoolFeeRateEstimator::new(to_mempool_network(network)); + + tracing::warn!(defaults = ?FEE_RATE_DEFAULTS, "Initializing fee rate cache with default values."); + + let initial_fee_rates = HashMap::from_iter( + FEE_RATE_DEFAULTS + .into_iter() + .map(|(target, fee_rate)| (target, FeeRate::from_sat_per_kwu(fee_rate as f32))), + ); let fee_rate_cache = RwLock::new(initial_fee_rates); @@ -85,21 +72,29 @@ impl FeeRateEstimator { } pub(crate) async fn update(&self) -> Result<()> { - let estimates = self.client.get_fee_estimates()?; + let estimates = self.client.fetch_fee().await?; let mut locked_fee_rate_cache = self.fee_rate_cache.write(); - for (target, n_blocks) in CONFIRMATION_TARGETS { - let fee_rate = esplora_client::convert_fee_rate(n_blocks, estimates.clone())?; - let fee_rate = FeeRate::from_sat_per_vb(fee_rate); - - locked_fee_rate_cache.insert(target, fee_rate); - tracing::trace!( - n_blocks_confirmation = %n_blocks, - sats_per_kwu = %fee_rate.fee_wu(1000), - "Updated fee rate estimate", - ); - } + locked_fee_rate_cache.insert( + ConfirmationTarget::MempoolMinimum, + FeeRate::from_sat_per_vb(estimates.minimum_fee as f32), + ); + + locked_fee_rate_cache.insert( + ConfirmationTarget::Background, + FeeRate::from_sat_per_vb(estimates.economy_fee as f32), + ); + + locked_fee_rate_cache.insert( + ConfirmationTarget::Normal, + FeeRate::from_sat_per_vb(estimates.hour_fee as f32), + ); + + locked_fee_rate_cache.insert( + ConfirmationTarget::HighPriority, + FeeRate::from_sat_per_vb(estimates.fastest_fee as f32), + ); Ok(()) } diff --git a/crates/ln-dlc-node/src/node/mod.rs b/crates/ln-dlc-node/src/node/mod.rs index 50b951677..6ad816c22 100644 --- a/crates/ln-dlc-node/src/node/mod.rs +++ b/crates/ln-dlc-node/src/node/mod.rs @@ -305,7 +305,7 @@ impl Node Self { + FeeRate { + // we on purpose have different values to see an effect for clients asking for different + // priorities + fastest_fee: 5, + half_hour_fee: 4, + hour_fee: 3, + economy_fee: 2, + minimum_fee: 1, + } + } +} + +#[derive(PartialEq)] +pub enum Network { + Mainnet, + Signet, + Testnet, + /// We assume a local regtest setup and will not perform any request to mempool.space + Local, +} + +pub struct MempoolFeeRateEstimator { + url: String, + network: Network, +} + +impl MempoolFeeRateEstimator { + pub fn new(network: Network) -> Self { + let url = match network { + Network::Mainnet => MEMPOOL_FEE_RATE_URL_MAINNET, + Network::Signet => MEMPOOL_FEE_RATE_URL_SIGNET, + Network::Testnet => MEMPOOL_FEE_RATE_URL_TESTNET, + Network::Local => "http://thereisnosuchthingasabitcoinmempool.com", + } + .to_string(); + + Self { url, network } + } + + pub async fn fetch_fee(&self) -> Result { + if Network::Local == self.network { + return Ok(FeeRate::local_fee_rate()); + } + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/fees/recommended", self.url); + let response = client.get(url).send().await?; + let fee_rate = response.json().await?; + Ok(fee_rate) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // we keep this test running on CI even though it connects to the internet. This allows us to + // be notified if the API ever changes + #[tokio::test] + pub async fn test_fetching_fee_rate_from_mempool() { + let mempool = MempoolFeeRateEstimator::new(Network::Testnet); + let _testnet_fee_rate = mempool.fetch_fee().await.unwrap(); + let mempool = MempoolFeeRateEstimator::new(Network::Mainnet); + let _testnet_fee_rate = mempool.fetch_fee().await.unwrap(); + let mempool = MempoolFeeRateEstimator::new(Network::Signet); + let _testnet_fee_rate = mempool.fetch_fee().await.unwrap(); + let mempool = MempoolFeeRateEstimator::new(Network::Local); + let _testnet_fee_rate = mempool.fetch_fee().await.unwrap(); + } +}