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
24#[derive(Debug, Clone)]
25pub struct BlockchainInfo {
26 pub block_height: u64,
27 pub synced: bool,
28}
29
30pub fn create_esplora_rpc(url: &SafeUrl) -> Result<DynBitcoindRpc> {
31 let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
32 .ok()
33 .map(|s| SafeUrl::parse(&s))
34 .transpose()?
35 .unwrap_or_else(|| url.clone());
36
37 Ok(EsploraClient::new(&url)?.into_dyn())
38}
39
40pub type DynBitcoindRpc = Arc<dyn IBitcoindRpc + Send + Sync>;
41
42#[apply(async_trait_maybe_send!)]
46pub trait IBitcoindRpc: Debug + Send + Sync + 'static {
47 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
49
50 async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
52
53 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
55
56 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
58
59 async fn get_info(&self) -> Result<BlockchainInfo>;
62
63 fn into_dyn(self) -> DynBitcoindRpc
64 where
65 Self: Sized,
66 {
67 Arc::new(self)
68 }
69}
70
71#[derive(Debug)]
72pub struct EsploraClient {
73 client: AsyncClient,
74}
75
76impl EsploraClient {
77 pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
78 Ok(Self {
79 client: Builder::new(url.as_str().trim_end_matches('/')).build_async()?,
81 })
82 }
83}
84
85#[apply(async_trait_maybe_send!)]
86impl IBitcoindRpc for EsploraClient {
87 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
88 Ok(self
89 .client
90 .get_tx_status(txid)
91 .await?
92 .block_height
93 .map(u64::from))
94 }
95
96 async fn watch_script_history(&self, _: &ScriptBuf) -> anyhow::Result<()> {
97 Ok(())
99 }
100
101 async fn get_script_history(
102 &self,
103 script: &ScriptBuf,
104 ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
105 const MAX_TX_HISTORY: usize = 1000;
106
107 let mut transactions = Vec::new();
108 let mut last_seen: Option<Txid> = None;
109
110 loop {
111 let page = self.client.scripthash_txs(script, last_seen).await?;
112
113 if page.is_empty() {
114 break;
115 }
116
117 for tx in &page {
118 transactions.push(tx.to_tx());
119 }
120
121 if transactions.len() >= MAX_TX_HISTORY {
122 return Err(format_err!(
123 "Script history exceeds maximum limit of {}",
124 MAX_TX_HISTORY
125 ));
126 }
127
128 last_seen = Some(page.last().expect("page not empty").txid);
129 }
130
131 Ok(transactions)
132 }
133
134 async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
135 let proof = self
136 .client
137 .get_merkle_block(&txid)
138 .await?
139 .ok_or(format_err!("No merkle proof found"))?;
140
141 Ok(TxOutProof {
142 block_header: proof.header,
143 merkle_proof: proof.txn,
144 })
145 }
146
147 async fn get_info(&self) -> anyhow::Result<BlockchainInfo> {
148 let height = self.client.get_height().await?;
149 Ok(BlockchainInfo {
150 block_height: u64::from(height),
151 synced: true,
152 })
153 }
154}