fedimint_bitcoind/
feerate_source.rs1use std::str::FromStr;
2
3use anyhow::{Result, anyhow, bail};
4use fedimint_core::task::{MaybeSend, MaybeSync};
5use fedimint_core::util::SafeUrl;
6use fedimint_core::{Feerate, apply, async_trait_maybe_send};
7use fedimint_logging::LOG_MODULE_WALLET;
8use jaq_core::load::{Arena, File, Loader};
9use jaq_core::{Ctx, Native, RcIter};
10use jaq_json::Val;
11use tracing::{debug, trace};
12
13use crate::DynBitcoindRpc;
14
15const FEERATE_SOURCE_MAX_FEERATE_SATS_PER_VB: f64 = 10_000.0;
19
20const FEERATE_SOURCE_MIN_FEERATE_SATS_PER_VB: f64 = 1.0;
22
23#[apply(async_trait_maybe_send!)]
24pub trait FeeRateSource: MaybeSend + MaybeSync {
25 fn name(&self) -> String;
26 async fn fetch(&self, confirmation_target: u16) -> Result<Feerate>;
27}
28
29#[apply(async_trait_maybe_send!)]
30impl FeeRateSource for DynBitcoindRpc {
31 fn name(&self) -> String {
32 self.get_bitcoin_rpc_config().kind
33 }
34
35 async fn fetch(&self, confirmation_target: u16) -> Result<Feerate> {
36 self.get_fee_rate(confirmation_target)
37 .await?
38 .ok_or_else(|| anyhow!("bitcoind did not return any feerate"))
39 }
40}
41
42pub struct FetchJson {
43 filter: jaq_core::Filter<Native<Val>>,
44 source_url: SafeUrl,
45}
46
47impl FetchJson {
48 pub fn from_str(source_str: &str) -> Result<Self> {
49 let (source_url, code) = {
50 let (url, code) = match source_str.split_once('#') {
51 Some(val) => val,
52 None => (source_str, "."),
53 };
54
55 (SafeUrl::parse(url)?, code)
56 };
57
58 debug!(target: LOG_MODULE_WALLET, url = %source_url, code = %code, "Setting fee rate json source");
59 let program = File { code, path: () };
60
61 let loader = Loader::new([]);
62 let arena = Arena::default();
63 let modules = loader.load(&arena, program).map_err(|errs| {
64 anyhow!(
65 "Error parsing jq filter for {source_url}: {}",
66 errs.into_iter()
67 .map(|e| format!("{e:?}"))
68 .collect::<Vec<_>>()
69 .join("\n")
70 )
71 })?;
72
73 let filter = jaq_core::Compiler::<_, Native<_>>::default()
74 .compile(modules)
75 .map_err(|errs| anyhow!("Failed to compile program: {:?}", errs))?;
76
77 Ok(Self { filter, source_url })
78 }
79
80 fn apply_filter(&self, value: serde_json::Value) -> Result<Val> {
81 let inputs = RcIter::new(core::iter::empty());
82
83 let mut out = self.filter.run((Ctx::new([], &inputs), Val::from(value)));
84
85 out.next()
86 .ok_or_else(|| anyhow!("Missing value after applying filter"))?
87 .map_err(|e| anyhow!("Jaq err: {e}"))
88 }
89}
90
91#[apply(async_trait_maybe_send!)]
92impl FeeRateSource for FetchJson {
93 fn name(&self) -> String {
94 self.source_url
95 .host()
96 .map_or_else(|| "host-not-available".to_string(), |h| h.to_string())
97 }
98
99 async fn fetch(&self, _confirmation_target: u16) -> Result<Feerate> {
100 let json_resp: serde_json::Value = reqwest::get(self.source_url.clone().to_unsafe())
101 .await?
102 .json()
103 .await?;
104
105 trace!(target: LOG_MODULE_WALLET, name = %self.name(), resp = ?json_resp, "Got json response");
106
107 let val = self.apply_filter(json_resp)?;
108
109 let rate = match val {
110 Val::Float(rate) => rate,
111 #[allow(clippy::cast_precision_loss)]
112 Val::Int(rate) => rate as f64,
113 Val::Num(rate) => FromStr::from_str(&rate)?,
114 _ => {
115 bail!("Value returned by feerate source has invalid type: {val:?}");
116 }
117 };
118 debug!(target: LOG_MODULE_WALLET, name = %self.name(), rate_sats_vb = %rate, "Got fee rate");
119
120 if rate < FEERATE_SOURCE_MIN_FEERATE_SATS_PER_VB {
121 bail!("Fee rate returned by source not positive: {rate}")
122 }
123
124 if FEERATE_SOURCE_MAX_FEERATE_SATS_PER_VB <= rate {
125 bail!("Fee rate returned by source too large: {rate}")
126 }
127
128 Ok(Feerate {
129 #[allow(clippy::cast_sign_loss)]
131 sats_per_kvb: (rate * 1000.0).floor() as u64,
132 })
133 }
134}
135
136#[cfg(test)]
137mod test;