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::env;
10use std::fmt::Debug;
11use std::sync::Arc;
12
13use anyhow::{Result, format_err};
14use bitcoin::{ScriptBuf, Transaction, Txid};
15use esplora_client::{AsyncClient, Builder};
16use fedimint_core::envs::FM_FORCE_BITCOIN_RPC_URL_ENV;
17use fedimint_core::txoproof::TxOutProof;
18use fedimint_core::util::SafeUrl;
19use fedimint_core::{apply, async_trait_maybe_send};
20
21#[cfg(feature = "bitcoincore")]
22pub mod bitcoincore;
23
24pub fn create_esplora_rpc(url: &SafeUrl) -> Result<DynBitcoindRpc> {
25 let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
26 .ok()
27 .map(|s| SafeUrl::parse(&s))
28 .transpose()?
29 .unwrap_or_else(|| url.clone());
30
31 Ok(EsploraClient::new(&url)?.into_dyn())
32}
33
34pub type DynBitcoindRpc = Arc<dyn IBitcoindRpc + Send + Sync>;
35
36#[apply(async_trait_maybe_send!)]
40pub trait IBitcoindRpc: Debug + Send + Sync + 'static {
41 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
43
44 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
46
47 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
49
50 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
52
53 fn into_dyn(self) -> DynBitcoindRpc
54 where
55 Self: Sized,
56 {
57 Arc::new(self)
58 }
59}
60
61#[derive(Debug)]
62pub struct EsploraClient {
63 client: AsyncClient,
64}
65
66impl EsploraClient {
67 pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
68 Ok(Self {
69 client: Builder::new(url.as_str().trim_end_matches('/')).build_async()?,
71 })
72 }
73}
74
75#[apply(async_trait_maybe_send!)]
76impl IBitcoindRpc for EsploraClient {
77 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
78 Ok(self
79 .client
80 .get_tx_status(txid)
81 .await?
82 .block_height
83 .map(u64::from))
84 }
85
86 async fn watch_script_history(&self, _: &ScriptBuf) -> anyhow::Result<()> {
87 Ok(())
89 }
90
91 async fn get_script_history(
92 &self,
93 script: &ScriptBuf,
94 ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
95 const MAX_TX_HISTORY: usize = 1000;
96
97 let mut transactions = Vec::new();
98 let mut last_seen: Option<Txid> = None;
99
100 loop {
101 let page = self.client.scripthash_txs(script, last_seen).await?;
102
103 if page.is_empty() {
104 break;
105 }
106
107 for tx in &page {
108 transactions.push(tx.to_tx());
109 }
110
111 if transactions.len() >= MAX_TX_HISTORY {
112 return Err(format_err!(
113 "Script history exceeds maximum limit of {}",
114 MAX_TX_HISTORY
115 ));
116 }
117
118 last_seen = Some(page.last().expect("page not empty").txid);
119 }
120
121 Ok(transactions)
122 }
123
124 async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
125 let proof = self
126 .client
127 .get_merkle_block(&txid)
128 .await?
129 .ok_or(format_err!("No merkle proof found"))?;
130
131 Ok(TxOutProof {
132 block_header: proof.header,
133 merkle_proof: proof.txn,
134 })
135 }
136}