use crate::config::FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS;
use crate::logger::{log_error, log_trace, Logger};
use crate::{Config, Error};
use lightning::chain::chaininterface::{
ConfirmationTarget, FeeEstimator, FEERATE_FLOOR_SATS_PER_KW,
};
use bdk::FeeRate;
use esplora_client::AsyncClient as EsploraClient;
use bitcoin::blockdata::weight::Weight;
use bitcoin::Network;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::{Arc, RwLock};
use std::time::Duration;
pub(crate) struct OnchainFeeEstimator<L: Deref>
where
L::Target: Logger,
{
fee_rate_cache: RwLock<HashMap<ConfirmationTarget, FeeRate>>,
esplora_client: EsploraClient,
config: Arc<Config>,
logger: L,
}
impl<L: Deref> OnchainFeeEstimator<L>
where
L::Target: Logger,
{
pub(crate) fn new(esplora_client: EsploraClient, config: Arc<Config>, logger: L) -> Self {
let fee_rate_cache = RwLock::new(HashMap::new());
Self { fee_rate_cache, esplora_client, config, logger }
}
pub(crate) async fn update_fee_estimates(&self) -> Result<(), Error> {
let confirmation_targets = vec![
ConfirmationTarget::OnChainSweep,
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee,
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee,
ConfirmationTarget::AnchorChannelFee,
ConfirmationTarget::NonAnchorChannelFee,
ConfirmationTarget::ChannelCloseMinimum,
ConfirmationTarget::OutputSpendingFee,
];
for target in confirmation_targets {
let num_blocks = match target {
ConfirmationTarget::OnChainSweep => 6,
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => 1008,
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => 144,
ConfirmationTarget::AnchorChannelFee => 1008,
ConfirmationTarget::NonAnchorChannelFee => 12,
ConfirmationTarget::ChannelCloseMinimum => 144,
ConfirmationTarget::OutputSpendingFee => 12,
};
let estimates = tokio::time::timeout(
Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS),
self.esplora_client.get_fee_estimates(),
)
.await
.map_err(|e| {
log_error!(
self.logger,
"Updating fee rate estimates for {:?} timed out: {}",
target,
e
);
Error::FeerateEstimationUpdateTimeout
})?
.map_err(|e| {
log_error!(
self.logger,
"Failed to retrieve fee rate estimates for {:?}: {}",
target,
e
);
Error::FeerateEstimationUpdateFailed
})?;
if estimates.is_empty() && self.config.network == Network::Bitcoin {
log_error!(
self.logger,
"Failed to retrieve fee rate estimates for {:?}: empty fee estimates are dissallowed on Mainnet.",
target,
);
return Err(Error::FeerateEstimationUpdateFailed);
}
let converted_estimates = esplora_client::convert_fee_rate(num_blocks, estimates)
.map_err(|e| {
log_error!(
self.logger,
"Failed to convert fee rate estimates for {:?}: {}",
target,
e
);
Error::FeerateEstimationUpdateFailed
})?;
let fee_rate = FeeRate::from_sat_per_vb(converted_estimates);
let adjusted_fee_rate = match target {
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => {
let slightly_less_than_background =
fee_rate.fee_wu(Weight::from_wu(1000)) - 250;
FeeRate::from_sat_per_kwu(slightly_less_than_background as f32)
},
_ => fee_rate,
};
let mut locked_fee_rate_cache = self.fee_rate_cache.write().unwrap();
locked_fee_rate_cache.insert(target, adjusted_fee_rate);
log_trace!(
self.logger,
"Fee rate estimation updated for {:?}: {} sats/kwu",
target,
adjusted_fee_rate.fee_wu(Weight::from_wu(1000))
);
}
Ok(())
}
pub(crate) fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();
let fallback_sats_kwu = match confirmation_target {
ConfirmationTarget::OnChainSweep => 5000,
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW,
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW,
ConfirmationTarget::AnchorChannelFee => 500,
ConfirmationTarget::NonAnchorChannelFee => 1000,
ConfirmationTarget::ChannelCloseMinimum => 500,
ConfirmationTarget::OutputSpendingFee => 1000,
};
let fallback_rate = FeeRate::from_sat_per_kwu(fallback_sats_kwu as f32);
*locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate)
}
}
impl<L: Deref> FeeEstimator for OnchainFeeEstimator<L>
where
L::Target: Logger,
{
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
(self.estimate_fee_rate(confirmation_target).fee_wu(Weight::from_wu(1000)) as u32)
.max(FEERATE_FLOOR_SATS_PER_KW)
}
}