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::cmp::min;
10use std::collections::BTreeMap;
11use std::env;
12use std::fmt::Debug;
13use std::future::Future;
14use std::sync::{Arc, LazyLock, Mutex};
15use std::time::Duration;
16
17use anyhow::Context;
18pub use anyhow::Result;
19use bitcoin::{BlockHash, Network, ScriptBuf, Transaction, Txid};
20use fedimint_core::envs::{
21 BitcoinRpcConfig, FM_FORCE_BITCOIN_RPC_KIND_ENV, FM_FORCE_BITCOIN_RPC_URL_ENV,
22};
23use fedimint_core::fmt_utils::OptStacktrace;
24use fedimint_core::runtime::sleep;
25use fedimint_core::task::TaskHandle;
26use fedimint_core::txoproof::TxOutProof;
27use fedimint_core::util::SafeUrl;
28use fedimint_core::{apply, async_trait_maybe_send, dyn_newtype_define, Feerate};
29use fedimint_logging::{LOG_BLOCKCHAIN, LOG_CORE};
30use tracing::{debug, info};
31
32#[cfg(feature = "bitcoincore-rpc")]
33pub mod bitcoincore;
34#[cfg(feature = "electrum-client")]
35mod electrum;
36#[cfg(feature = "esplora-client")]
37mod esplora;
38
39const MAINNET_GENESIS_BLOCK_HASH: &str =
41 "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
42const TESTNET_GENESIS_BLOCK_HASH: &str =
44 "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
45const SIGNET_GENESIS_BLOCK_HASH: &str =
47 "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6";
48const REGTEST_GENESIS_BLOCK_HASH: &str =
51 "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206";
52
53static BITCOIN_RPC_REGISTRY: LazyLock<Mutex<BTreeMap<String, DynBitcoindRpcFactory>>> =
55 LazyLock::new(|| {
56 Mutex::new(BTreeMap::from([
57 #[cfg(feature = "esplora-client")]
58 ("esplora".to_string(), esplora::EsploraFactory.into()),
59 #[cfg(feature = "electrum-client")]
60 ("electrum".to_string(), electrum::ElectrumFactory.into()),
61 #[cfg(feature = "bitcoincore-rpc")]
62 ("bitcoind".to_string(), bitcoincore::BitcoindFactory.into()),
63 ]))
64 });
65
66pub fn create_bitcoind(config: &BitcoinRpcConfig, handle: TaskHandle) -> Result<DynBitcoindRpc> {
68 let registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
69
70 let kind = env::var(FM_FORCE_BITCOIN_RPC_KIND_ENV)
71 .ok()
72 .unwrap_or_else(|| config.kind.clone());
73 let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
74 .ok()
75 .map(|s| SafeUrl::parse(&s))
76 .transpose()?
77 .unwrap_or_else(|| config.url.clone());
78 debug!(target: LOG_CORE, %kind, %url, "Starting bitcoin rpc");
79 let maybe_factory = registry.get(&kind);
80 let factory = maybe_factory.with_context(|| {
81 anyhow::anyhow!(
82 "{} rpc not registered, available options: {:?}",
83 config.kind,
84 registry.keys()
85 )
86 })?;
87 factory.create_connection(&url, handle)
88}
89
90pub fn register_bitcoind(kind: String, factory: DynBitcoindRpcFactory) {
92 let mut registry = BITCOIN_RPC_REGISTRY.lock().expect("lock poisoned");
93 registry.insert(kind, factory);
94}
95
96pub trait IBitcoindRpcFactory: Debug + Send + Sync {
98 fn create_connection(&self, url: &SafeUrl, handle: TaskHandle) -> Result<DynBitcoindRpc>;
100}
101
102dyn_newtype_define! {
103 #[derive(Clone)]
104 pub DynBitcoindRpcFactory(Arc<IBitcoindRpcFactory>)
105}
106
107#[apply(async_trait_maybe_send!)]
111pub trait IBitcoindRpc: Debug {
112 async fn get_network(&self) -> Result<bitcoin::Network>;
114
115 async fn get_block_count(&self) -> Result<u64>;
117
118 async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
130
131 async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>>;
136
137 async fn submit_transaction(&self, transaction: Transaction);
152
153 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
157
158 async fn is_tx_in_block(
160 &self,
161 txid: &Txid,
162 block_hash: &BlockHash,
163 block_height: u64,
164 ) -> Result<bool>;
165
166 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
173
174 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
179
180 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
182
183 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
185}
186
187dyn_newtype_define! {
188 #[derive(Clone)]
189 pub DynBitcoindRpc(Arc<IBitcoindRpc>)
190}
191
192const RETRY_SLEEP_MIN_MS: Duration = Duration::from_millis(10);
193const RETRY_SLEEP_MAX_MS: Duration = Duration::from_millis(5000);
194
195#[derive(Debug)]
197pub struct RetryClient<C> {
198 inner: C,
199 task_handle: TaskHandle,
200}
201
202impl<C> RetryClient<C> {
203 pub fn new(inner: C, task_handle: TaskHandle) -> Self {
204 Self { inner, task_handle }
205 }
206
207 async fn retry_call<T, F, R>(&self, call_fn: F) -> Result<T>
210 where
211 F: Fn() -> R,
212 R: Future<Output = Result<T>>,
213 {
214 let mut retry_time = RETRY_SLEEP_MIN_MS;
215 let ret = loop {
216 match call_fn().await {
217 Ok(ret) => {
218 break ret;
219 }
220 Err(e) => {
221 if self.task_handle.is_shutting_down() {
222 return Err(e);
223 }
224
225 info!(target: LOG_BLOCKCHAIN, "Bitcoind error {}, retrying", OptStacktrace(e));
226 sleep(retry_time).await;
227 retry_time = min(RETRY_SLEEP_MAX_MS, retry_time * 2);
228 }
229 }
230 };
231 Ok(ret)
232 }
233}
234
235#[apply(async_trait_maybe_send!)]
236impl<C> IBitcoindRpc for RetryClient<C>
237where
238 C: IBitcoindRpc + Sync + Send,
239{
240 async fn get_network(&self) -> Result<Network> {
241 self.retry_call(|| async { self.inner.get_network().await })
242 .await
243 }
244
245 async fn get_block_count(&self) -> Result<u64> {
246 self.retry_call(|| async { self.inner.get_block_count().await })
247 .await
248 }
249
250 async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
251 self.retry_call(|| async { self.inner.get_block_hash(height).await })
252 .await
253 }
254
255 async fn get_fee_rate(&self, confirmation_target: u16) -> Result<Option<Feerate>> {
256 self.retry_call(|| async { self.inner.get_fee_rate(confirmation_target).await })
257 .await
258 }
259
260 async fn submit_transaction(&self, transaction: Transaction) {
261 self.inner.submit_transaction(transaction.clone()).await;
262 }
263
264 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>> {
265 self.retry_call(|| async { self.inner.get_tx_block_height(txid).await })
266 .await
267 }
268
269 async fn is_tx_in_block(
270 &self,
271 txid: &Txid,
272 block_hash: &BlockHash,
273 block_height: u64,
274 ) -> Result<bool> {
275 self.retry_call(|| async {
276 self.inner
277 .is_tx_in_block(txid, block_hash, block_height)
278 .await
279 })
280 .await
281 }
282
283 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()> {
284 self.retry_call(|| async { self.inner.watch_script_history(script).await })
285 .await
286 }
287
288 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>> {
289 self.retry_call(|| async { self.inner.get_script_history(script).await })
290 .await
291 }
292
293 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof> {
294 self.retry_call(|| async { self.inner.get_txout_proof(txid).await })
295 .await
296 }
297
298 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
299 self.inner.get_bitcoin_rpc_config()
300 }
301}