fedimint_bitcoind/
lib.rs

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/// Trait that allows interacting with the Bitcoin blockchain
43///
44/// Functions may panic if the bitcoind node is not reachable.
45#[apply(async_trait_maybe_send!)]
46pub trait IBitcoindRpc: Debug + Send + Sync + 'static {
47    /// If a transaction is included in a block, returns the block height.
48    async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
49
50    /// Watches for a script and returns any transaction associated with it
51    async fn watch_script_history(&self, script: &ScriptBuf) -> Result<()>;
52
53    /// Get script transaction history
54    async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
55
56    /// Returns a proof that a tx is included in the bitcoin blockchain
57    async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
58
59    /// Returns `BlockchainInfo` which contains a subset of info about the chain
60    /// data source.
61    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            // URL needs to have any trailing path including '/' removed
80            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        // no watching needed, has all the history already
98        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}