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
9pub mod metrics;
10
11use std::env;
12use std::fmt::Debug;
13use std::sync::Arc;
14
15use anyhow::{Result, format_err};
16use bitcoin::{ScriptBuf, Transaction, Txid};
17use esplora_client::{AsyncClient, Builder};
18use fedimint_core::envs::FM_FORCE_BITCOIN_RPC_URL_ENV;
19use fedimint_core::time::now;
20use fedimint_core::txoproof::TxOutProof;
21use fedimint_core::util::{FmtCompactResultAnyhow as _, SafeUrl};
22use fedimint_core::{apply, async_trait_maybe_send};
23use fedimint_logging::LOG_BITCOIND;
24use fedimint_metrics::HistogramExt as _;
25use tracing::trace;
26
27use crate::metrics::{BITCOIND_RPC_DURATION_SECONDS, BITCOIND_RPC_REQUESTS_TOTAL};
28
29#[cfg(feature = "bitcoincore")]
30pub mod bitcoincore;
31
32#[derive(Debug, Clone)]
33pub struct BlockchainInfo {
34 pub block_height: u64,
35 pub synced: bool,
36}
37
38pub fn create_esplora_rpc(url: &SafeUrl) -> Result<DynBitcoindRpc> {
39 let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
40 .ok()
41 .map(|s| SafeUrl::parse(&s))
42 .transpose()?
43 .unwrap_or_else(|| url.clone());
44
45 Ok(EsploraClient::new(&url)?.into_dyn())
46}
47
48pub type DynBitcoindRpc = Arc<dyn IBitcoindRpc + Send + Sync>;
49
50#[apply(async_trait_maybe_send!)]
54pub trait IBitcoindRpc: Debug + Send + Sync + 'static {
55 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
57
58 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
60
61 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
63
64 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
66
67 async fn get_info(&self) -> Result<BlockchainInfo>;
70
71 fn into_dyn(self) -> DynBitcoindRpc
72 where
73 Self: Sized,
74 {
75 Arc::new(self)
76 }
77}
78
79pub struct BitcoindTracked {
85 inner: DynBitcoindRpc,
86 name: &'static str,
87}
88
89impl std::fmt::Debug for BitcoindTracked {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("BitcoindTracked")
92 .field("name", &self.name)
93 .field("inner", &self.inner)
94 .finish()
95 }
96}
97
98impl BitcoindTracked {
99 pub fn new(inner: DynBitcoindRpc, name: &'static str) -> Self {
104 Self { inner, name }
105 }
106
107 fn record_call<T>(&self, method: &str, result: &Result<T>) {
108 let result_label = if result.is_ok() { "success" } else { "error" };
109 BITCOIND_RPC_REQUESTS_TOTAL
110 .with_label_values(&[method, self.name, result_label])
111 .inc();
112 }
113}
114
115macro_rules! tracked_call {
116 ($self:ident, $method:expr, $call:expr) => {{
117 trace!(
118 target: LOG_BITCOIND,
119 method = $method,
120 name = $self.name,
121 "starting bitcoind rpc"
122 );
123 let start = now();
124 let timer = BITCOIND_RPC_DURATION_SECONDS
125 .with_label_values(&[$method, $self.name])
126 .start_timer_ext();
127 let result = $call;
128 timer.observe_duration();
129 $self.record_call($method, &result);
130 let duration_ms = now()
131 .duration_since(start)
132 .unwrap_or_default()
133 .as_secs_f64()
134 * 1000.0;
135 trace!(
136 target: LOG_BITCOIND,
137 method = $method,
138 name = $self.name,
139 duration_ms,
140 error = %result.fmt_compact_result_anyhow(),
141 "completed bitcoind rpc"
142 );
143 result
144 }};
145}
146
147#[apply(async_trait_maybe_send!)]
148impl IBitcoindRpc for BitcoindTracked {
149 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>> {
150 tracked_call!(
151 self,
152 "get_tx_block_height",
153 self.inner.get_tx_block_height(txid).await
154 )
155 }
156
157 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()> {
158 tracked_call!(
159 self,
160 "watch_script_history",
161 self.inner.watch_script_history(script).await
162 )
163 }
164
165 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>> {
166 tracked_call!(
167 self,
168 "get_script_history",
169 self.inner.get_script_history(script).await
170 )
171 }
172
173 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof> {
174 tracked_call!(
175 self,
176 "get_txout_proof",
177 self.inner.get_txout_proof(txid).await
178 )
179 }
180
181 async fn get_info(&self) -> Result<BlockchainInfo> {
182 tracked_call!(self, "get_info", self.inner.get_info().await)
183 }
184}
185
186#[derive(Debug)]
187pub struct EsploraClient {
188 client: AsyncClient,
189}
190
191impl EsploraClient {
192 pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
193 Ok(Self {
194 client: Builder::new(url.as_str().trim_end_matches('/')).build_async()?,
196 })
197 }
198}
199
200#[apply(async_trait_maybe_send!)]
201impl IBitcoindRpc for EsploraClient {
202 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
203 Ok(self
204 .client
205 .get_tx_status(txid)
206 .await?
207 .block_height
208 .map(u64::from))
209 }
210
211 async fn watch_script_history(&self, _: &ScriptBuf) -> anyhow::Result<()> {
212 Ok(())
214 }
215
216 async fn get_script_history(
217 &self,
218 script: &ScriptBuf,
219 ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
220 const MAX_TX_HISTORY: usize = 1000;
221
222 let mut transactions = Vec::new();
223 let mut last_seen: Option<Txid> = None;
224
225 loop {
226 let page = self.client.scripthash_txs(script, last_seen).await?;
227
228 if page.is_empty() {
229 break;
230 }
231
232 for tx in &page {
233 transactions.push(tx.to_tx());
234 }
235
236 if transactions.len() >= MAX_TX_HISTORY {
237 return Err(format_err!(
238 "Script history exceeds maximum limit of {}",
239 MAX_TX_HISTORY
240 ));
241 }
242
243 last_seen = Some(page.last().expect("page not empty").txid);
244 }
245
246 Ok(transactions)
247 }
248
249 async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
250 let proof = self
251 .client
252 .get_merkle_block(&txid)
253 .await?
254 .ok_or(format_err!("No merkle proof found"))?;
255
256 Ok(TxOutProof {
257 block_header: proof.header,
258 merkle_proof: proof.txn,
259 })
260 }
261
262 async fn get_info(&self) -> anyhow::Result<BlockchainInfo> {
263 let height = self.client.get_height().await?;
264 Ok(BlockchainInfo {
265 block_height: u64::from(height),
266 synced: true,
267 })
268 }
269}