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