1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_sign_loss)]
4#![allow(clippy::missing_errors_doc)]
5#![allow(clippy::missing_panics_doc)]
6#![allow(clippy::module_name_repetitions)]
7#![allow(clippy::similar_names)]
8
9use std::collections::BTreeMap;
10use std::fmt::Debug;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, LazyLock, Mutex};
13use std::time::Duration;
14use std::{env, iter};
15
16use anyhow::{Context, Result};
17use bitcoin::{Block, BlockHash, Network, ScriptBuf, Transaction, Txid};
18use fedimint_core::envs::{
19 BitcoinRpcConfig, FM_BITCOIN_POLLING_INTERVAL_SECS_ENV, FM_FORCE_BITCOIN_RPC_KIND_ENV,
20 FM_FORCE_BITCOIN_RPC_URL_ENV, FM_WALLET_FEERATE_SOURCES_ENV, is_running_in_test_env,
21};
22use fedimint_core::task::TaskGroup;
23use fedimint_core::time::now;
24use fedimint_core::txoproof::TxOutProof;
25use fedimint_core::util::{FmtCompact as _, FmtCompactAnyhow, SafeUrl, get_median};
26use fedimint_core::{Feerate, apply, async_trait_maybe_send, dyn_newtype_define};
27use fedimint_logging::{LOG_BITCOIND, LOG_CORE};
28use feerate_source::{FeeRateSource, FetchJson};
29use tokio::time::Interval;
30use tracing::{debug, trace, warn};
31
32#[cfg(feature = "bitcoincore-rpc")]
33pub mod bitcoincore;
34#[cfg(feature = "esplora-client")]
35mod esplora;
36mod feerate_source;
37
38pub mod shared;
39
40const MAINNET_GENESIS_BLOCK_HASH: &str =
42 "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
43const TESTNET_GENESIS_BLOCK_HASH: &str =
45 "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
46const SIGNET_GENESIS_BLOCK_HASH: &str =
48 "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6";
49const REGTEST_GENESIS_BLOCK_HASH: &str =
52 "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206";
53
54static BITCOIN_RPC_REGISTRY: LazyLock<Mutex<BTreeMap<String, DynBitcoindRpcFactory>>> =
56 LazyLock::new(|| {
57 Mutex::new(BTreeMap::from([
58 #[cfg(feature = "esplora-client")]
59 ("esplora".to_string(), esplora::EsploraFactory.into()),
60 #[cfg(feature = "bitcoincore-rpc")]
61 ("bitcoind".to_string(), bitcoincore::BitcoindFactory.into()),
62 ]))
63 });
64
65pub fn create_bitcoind(config: &BitcoinRpcConfig) -> Result<DynBitcoindRpc> {
67 let registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
68
69 let kind = env::var(FM_FORCE_BITCOIN_RPC_KIND_ENV)
70 .ok()
71 .unwrap_or_else(|| config.kind.clone());
72 let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
73 .ok()
74 .map(|s| SafeUrl::parse(&s))
75 .transpose()?
76 .unwrap_or_else(|| config.url.clone());
77 debug!(target: LOG_CORE, %kind, %url, "Starting bitcoin rpc");
78 let maybe_factory = registry.get(&kind);
79 let factory = maybe_factory.with_context(|| {
80 anyhow::anyhow!(
81 "{} rpc not registered, available options: {:?}",
82 config.kind,
83 registry.keys()
84 )
85 })?;
86 factory.create_connection(&url)
87}
88
89pub fn register_bitcoind(kind: String, factory: DynBitcoindRpcFactory) {
91 let mut registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
92 registry.insert(kind, factory);
93}
94
95pub trait IBitcoindRpcFactory: Debug + Send + Sync {
97 fn create_connection(&self, url: &SafeUrl) -> Result<DynBitcoindRpc>;
99}
100
101dyn_newtype_define! {
102 #[derive(Clone)]
103 pub DynBitcoindRpcFactory(Arc<IBitcoindRpcFactory>)
104}
105
106#[apply(async_trait_maybe_send!)]
110pub trait IBitcoindRpc: Debug {
111 async fn get_network(&self) -> Result<bitcoin::Network>;
113
114 async fn get_block_count(&self) -> Result<u64>;
116
117 async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
129
130 async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
131
132 async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>>;
137
138 async fn submit_transaction(&self, transaction: Transaction);
153
154 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
158
159 async fn is_tx_in_block(
161 &self,
162 txid: &Txid,
163 block_hash: &BlockHash,
164 block_height: u64,
165 ) -> Result<bool>;
166
167 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
174
175 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
180
181 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
183
184 async fn get_sync_percentage(&self) -> Result<Option<f64>>;
187
188 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
190}
191
192dyn_newtype_define! {
193 #[derive(Clone)]
194 pub DynBitcoindRpc(Arc<IBitcoindRpc>)
195}
196
197impl DynBitcoindRpc {
198 pub fn spawn_block_count_update_task(
201 self,
202 task_group: &TaskGroup,
203 on_update: impl Fn(u64) + Send + Sync + 'static,
204 ) {
205 let mut desired_interval = get_bitcoin_polling_interval();
206
207 let last_block_count = AtomicU64::new(0);
209
210 task_group.spawn_cancellable("block count background task", {
211 async move {
212 trace!(target: LOG_BITCOIND, "Fetching block count from bitcoind");
213
214 let update_block_count = || async {
215 let res = self
216 .get_block_count()
217 .await;
218
219 match res {
220 Ok(block_count) => {
221 if last_block_count.load(Ordering::SeqCst) != block_count {
222 on_update(block_count);
223 last_block_count.store(block_count, Ordering::SeqCst);
224 }
225 },
226 Err(err) => {
227 warn!(target: LOG_BITCOIND, err = %err.fmt_compact_anyhow(), "Unable to get block count from the node");
228 }
229 }
230 };
231
232 loop {
233 let start = now();
234 update_block_count().await;
235 let duration = now().duration_since(start).unwrap_or_default();
236 if Duration::from_secs(10) < duration {
237 warn!(target: LOG_BITCOIND, duration_secs=duration.as_secs(), "Updating block count from bitcoind slow");
238 }
239 desired_interval.tick().await;
240 }
241 }
242 });
243 }
244
245 pub fn spawn_fee_rate_update_task(
248 self,
249 task_group: &TaskGroup,
250 network: Network,
251 confirmation_target: u16,
252 on_update: impl Fn(Feerate) + Send + Sync + 'static,
253 ) -> anyhow::Result<()> {
254 let sources = std::env::var(FM_WALLET_FEERATE_SOURCES_ENV)
255 .unwrap_or_else(|_| match network {
256 Network::Bitcoin => "https://mempool.space/api/v1/fees/recommended#.hourFee;https://blockstream.info/api/fee-estimates#.\"1\"".to_owned(),
257 _ => String::new(),
258 })
259 .split(';')
260 .filter(|s| !s.is_empty())
261 .map(|s| Ok(Box::new(FetchJson::from_str(s)?) as Box<dyn FeeRateSource>))
262 .chain(iter::once(Ok(
263 Box::new(self.clone()) as Box<dyn FeeRateSource>
264 )))
265 .collect::<anyhow::Result<Vec<Box<dyn FeeRateSource>>>>()?;
266 let feerates = Arc::new(std::sync::Mutex::new(vec![None; sources.len()]));
267
268 let mut desired_interval = get_bitcoin_polling_interval();
269
270 task_group.spawn_cancellable("feerate background task", async move {
271 trace!(target: LOG_BITCOIND, "Fetching feerate from sources");
272
273 let last_feerate = AtomicU64::new(0);
275
276 let update_fee_rate = || async {
277 trace!(target: LOG_BITCOIND, "Updating bitcoin fee rate");
278
279 let feerates_new = futures::future::join_all(sources.iter().map(|s| async { (s.name(), s.fetch(confirmation_target).await) } )).await;
280
281 let mut feerates = feerates.lock().expect("lock poisoned");
282 for (i, (name, res)) in feerates_new.into_iter().enumerate() {
283 match res {
284 Ok(ok) => feerates[i] = Some(ok),
285 Err(err) => {
286 if !is_running_in_test_env() {
288 warn!(target: LOG_BITCOIND, err = %err.fmt_compact_anyhow(), %name, "Error getting feerate from source");
289 }
290 },
291 }
292 }
293
294 let mut available_feerates : Vec<_> = feerates.iter().filter_map(Clone::clone).map(|r| r.sats_per_kvb).collect();
295
296 available_feerates.sort_unstable();
297
298 if let Some(feerate) = get_median(&available_feerates) {
299 if feerate != last_feerate.load(Ordering::SeqCst) {
300 on_update(Feerate { sats_per_kvb: feerate });
301 last_feerate.store(feerate, Ordering::SeqCst);
302 }
303 } else {
304 if !is_running_in_test_env() {
306 warn!(target: LOG_BITCOIND, "Unable to calculate any fee rate");
307 }
308 }
309 };
310
311 loop {
312 let start = now();
313 update_fee_rate().await;
314 let duration = now().duration_since(start).unwrap_or_default();
315 if Duration::from_secs(10) < duration {
316 warn!(target: LOG_BITCOIND, duration_secs=duration.as_secs(), "Updating feerate from bitcoind slow");
317 }
318 desired_interval.tick().await;
319 }
320 });
321
322 Ok(())
323 }
324}
325
326fn get_bitcoin_polling_interval() -> Interval {
327 fn get_bitcoin_polling_period() -> Duration {
328 if let Ok(s) = env::var(FM_BITCOIN_POLLING_INTERVAL_SECS_ENV) {
329 use std::str::FromStr;
330 match u64::from_str(&s) {
331 Ok(secs) => return Duration::from_secs(secs),
332 Err(err) => {
333 warn!(
334 target: LOG_BITCOIND,
335 err = %err.fmt_compact(),
336 env = FM_BITCOIN_POLLING_INTERVAL_SECS_ENV,
337 "Could not parse env variable"
338 );
339 }
340 }
341 };
342 if is_running_in_test_env() {
343 debug!(target: LOG_BITCOIND, "Running in devimint, using fast node polling");
346 Duration::from_millis(100)
347 } else {
348 Duration::from_secs(60)
349 }
350 }
351 tokio::time::interval(get_bitcoin_polling_period())
352}