feerate_source.rs
1 use std::str::FromStr; 2 3 use anyhow::{anyhow, bail, Result}; 4 use fedimint_bitcoind::DynBitcoindRpc; 5 use fedimint_core::util::SafeUrl; 6 use fedimint_core::{apply, async_trait_maybe_send, Feerate}; 7 use fedimint_logging::LOG_MODULE_WALLET; 8 use fedimint_wallet_common::CONFIRMATION_TARGET; 9 use jaq_core::load::{Arena, File, Loader}; 10 use jaq_core::{Ctx, Native, RcIter}; 11 use jaq_json::Val; 12 use tracing::{debug, trace}; 13 14 /// A feerate that we don't expect to ever happen in practice, that we are 15 /// going to reject from a source to help catching mistakes and 16 /// misconfigurations. 17 const FEERATE_SOURCE_MAX_FEERATE_SATS_PER_VB: f64 = 10_000.0; 18 19 /// Like [`FEERATE_SOURCE_MAX_FEERATE_SATS_PER_VB`], but minimum one we accept 20 const FEERATE_SOURCE_MIN_FEERATE_SATS_PER_VB: f64 = 1.0; 21 22 #[apply(async_trait_maybe_send!)] 23 pub trait FeeRateSource: Send + Sync { 24 fn name(&self) -> String; 25 async fn fetch(&self) -> Result<Feerate>; 26 } 27 28 #[apply(async_trait_maybe_send!)] 29 impl FeeRateSource for DynBitcoindRpc { 30 fn name(&self) -> String { 31 self.get_bitcoin_rpc_config().kind 32 } 33 34 async fn fetch(&self) -> Result<Feerate> { 35 self.get_fee_rate(CONFIRMATION_TARGET) 36 .await? 37 .ok_or_else(|| anyhow!("bitcoind did not return any feerate")) 38 } 39 } 40 41 pub struct FetchJson { 42 filter: jaq_core::Filter<Native<Val>>, 43 source_url: SafeUrl, 44 } 45 46 impl FetchJson { 47 pub fn from_str(source_str: &str) -> Result<Self> { 48 let (source_url, code) = { 49 let (url, code) = match source_str.split_once('#') { 50 Some(val) => val, 51 None => (source_str, "."), 52 }; 53 54 (SafeUrl::parse(url)?, code) 55 }; 56 57 debug!(target: LOG_MODULE_WALLET, url = %source_url, code = %code, "Setting fee rate json source"); 58 let program = File { code, path: () }; 59 60 let loader = Loader::new([]); 61 let arena = Arena::default(); 62 let modules = loader.load(&arena, program).map_err(|errs| { 63 anyhow!( 64 "Error parsing jq filter for {source_url}: {}", 65 errs.into_iter() 66 .map(|e| format!("{e:?}")) 67 .collect::<Vec<_>>() 68 .join("\n") 69 ) 70 })?; 71 72 let filter = jaq_core::Compiler::<_, Native<_>>::default() 73 .compile(modules) 74 .map_err(|errs| anyhow!("Failed to compile program: {:?}", errs))?; 75 76 Ok(Self { filter, source_url }) 77 } 78 79 fn apply_filter(&self, value: serde_json::Value) -> Result<Val> { 80 let inputs = RcIter::new(core::iter::empty()); 81 82 let mut out = self.filter.run((Ctx::new([], &inputs), Val::from(value))); 83 84 out.next() 85 .ok_or_else(|| anyhow!("Missing value after applying filter"))? 86 .map_err(|e| anyhow!("Jaq err: {e}")) 87 } 88 } 89 90 #[apply(async_trait_maybe_send!)] 91 impl FeeRateSource for FetchJson { 92 fn name(&self) -> String { 93 self.source_url 94 .host() 95 .map_or_else(|| "host-not-available".to_string(), |h| h.to_string()) 96 } 97 98 async fn fetch(&self) -> Result<Feerate> { 99 let json_resp: serde_json::Value = reqwest::get(self.source_url.clone().to_unsafe()) 100 .await? 101 .json() 102 .await?; 103 104 trace!(target: LOG_MODULE_WALLET, name = %self.name(), resp = ?json_resp, "Got json response"); 105 106 let val = self.apply_filter(json_resp)?; 107 108 let rate = match val { 109 Val::Float(rate) => rate, 110 #[allow(clippy::cast_precision_loss)] 111 Val::Int(rate) => rate as f64, 112 Val::Num(rate) => FromStr::from_str(&rate)?, 113 _ => { 114 bail!("Value returned by feerate source has invalid type: {val:?}"); 115 } 116 }; 117 debug!(target: LOG_MODULE_WALLET, name = %self.name(), rate_sats_vb = %rate, "Got fee rate"); 118 119 if rate < FEERATE_SOURCE_MIN_FEERATE_SATS_PER_VB { 120 bail!("Fee rate returned by source not positive: {rate}") 121 } 122 123 if FEERATE_SOURCE_MAX_FEERATE_SATS_PER_VB <= rate { 124 bail!("Fee rate returned by source too large: {rate}") 125 } 126 127 Ok(Feerate { 128 // just checked that it's not negative 129 #[allow(clippy::cast_sign_loss)] 130 sats_per_kvb: (rate * 1000.0).floor() as u64, 131 }) 132 } 133 } 134 135 #[cfg(test)] 136 mod test { 137 use std::rc::Rc; 138 139 use jaq_json::Val; 140 141 use crate::feerate_source::FetchJson; 142 143 fn val_str(s: &str) -> Val { 144 Val::Str(Rc::new(s.to_owned())) 145 } 146 147 #[test] 148 fn test_filter() { 149 let source_id = FetchJson::from_str("https://example.com#.").expect("Failed to parse url"); 150 assert_eq!( 151 source_id 152 .apply_filter(serde_json::json!("foo")) 153 .expect("Failed to apply filter"), 154 val_str("foo") 155 ); 156 157 let source_access_member = 158 FetchJson::from_str("https://example.com#.[0].foo").expect("Failed to parse url"); 159 assert_eq!( 160 source_access_member 161 .apply_filter(serde_json::json!([{"foo": "bar"}, 1, 2, 3])) 162 .expect("Failed to apply filter"), 163 val_str("bar") 164 ); 165 } 166 }