fedimint_server_bitcoin_rpc/
lib.rs

1pub mod bitcoind;
2pub mod esplora;
3
4use anyhow::Result;
5use bitcoin::{BlockHash, Network, Transaction};
6use fedimint_core::Feerate;
7use fedimint_core::envs::BitcoinRpcConfig;
8use fedimint_core::util::{FmtCompactAnyhow, SafeUrl};
9use fedimint_logging::LOG_SERVER;
10use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
11use tracing::warn;
12
13use crate::bitcoind::BitcoindClient;
14use crate::esplora::EsploraClient;
15
16#[derive(Debug)]
17pub struct BitcoindClientWithFallback {
18    bitcoind_client: BitcoindClient,
19    esplora_client: EsploraClient,
20}
21
22impl BitcoindClientWithFallback {
23    pub fn new(
24        username: String,
25        password: String,
26        bitcoind_url: &SafeUrl,
27        esplora_url: &SafeUrl,
28    ) -> Result<Self> {
29        warn!(
30            target: LOG_SERVER,
31            %bitcoind_url,
32            %esplora_url,
33            "Initiallizing bitcoin bitcoind backend with esplora fallback"
34        );
35        let bitcoind_client = BitcoindClient::new(username, password, bitcoind_url)?;
36        let esplora_client = EsploraClient::new(esplora_url)?;
37
38        Ok(Self {
39            bitcoind_client,
40            esplora_client,
41        })
42    }
43}
44
45#[async_trait::async_trait]
46impl IServerBitcoinRpc for BitcoindClientWithFallback {
47    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
48        self.bitcoind_client.get_bitcoin_rpc_config()
49    }
50
51    fn get_url(&self) -> SafeUrl {
52        self.bitcoind_client.get_url()
53    }
54
55    async fn get_network(&self) -> Result<Network> {
56        match self.bitcoind_client.get_network().await {
57            Ok(bitcoind_network) => {
58                // Assert that bitcoind network matches esplora network, if available
59                //
60                // This is OK to do every time, as first success is cached internally.
61                if let Ok(esplora_network) = self.esplora_client.get_network().await {
62                    assert_eq!(
63                        bitcoind_network, esplora_network,
64                        "Network mismatch: bitcoind reported {:?} but esplora reported {:?}",
65                        bitcoind_network, esplora_network
66                    );
67                }
68                Ok(bitcoind_network)
69            }
70            Err(e) => {
71                warn!(
72                    target: LOG_SERVER,
73                    error = %e.fmt_compact_anyhow(),
74                    "BitcoindClient failed for get_network, falling back to EsploraClient"
75                );
76
77                self.esplora_client.get_network().await
78            }
79        }
80    }
81
82    async fn get_block_count(&self) -> Result<u64> {
83        match self.bitcoind_client.get_block_count().await {
84            Ok(count) => Ok(count),
85            Err(e) => {
86                warn!(
87                    target: LOG_SERVER,
88                    error = %e.fmt_compact_anyhow(),
89                    "BitcoindClient failed for get_block_count, falling back to EsploraClient"
90                );
91                self.esplora_client.get_block_count().await
92            }
93        }
94    }
95
96    async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
97        match self.bitcoind_client.get_block_hash(height).await {
98            Ok(hash) => Ok(hash),
99            Err(e) => {
100                warn!(
101                    target: LOG_SERVER,
102                    error = %e.fmt_compact_anyhow(),
103                    height = height,
104                    "BitcoindClient failed for get_block_hash, falling back to EsploraClient"
105                );
106                self.esplora_client.get_block_hash(height).await
107            }
108        }
109    }
110
111    async fn get_block(&self, block_hash: &BlockHash) -> Result<bitcoin::Block> {
112        match self.bitcoind_client.get_block(block_hash).await {
113            Ok(block) => Ok(block),
114            Err(e) => {
115                warn!(
116                    target: LOG_SERVER,
117                    error = %e.fmt_compact_anyhow(),
118                    block_hash = %block_hash,
119                    "BitcoindClient failed for get_block, falling back to EsploraClient"
120                );
121                self.esplora_client.get_block(block_hash).await
122            }
123        }
124    }
125
126    async fn get_feerate(&self) -> Result<Option<Feerate>> {
127        match self.bitcoind_client.get_feerate().await {
128            Ok(feerate) => Ok(feerate),
129            Err(e) => {
130                warn!(
131                    target: LOG_SERVER,
132                    error = %e.fmt_compact_anyhow(),
133                    "BitcoindClient failed for get_feerate, falling back to EsploraClient"
134                );
135                self.esplora_client.get_feerate().await
136            }
137        }
138    }
139
140    async fn submit_transaction(&self, transaction: Transaction) {
141        // Since this endpoint does not return an error, we can just always broadcast to
142        // both places
143        self.bitcoind_client
144            .submit_transaction(transaction.clone())
145            .await;
146        self.esplora_client.submit_transaction(transaction).await;
147    }
148
149    async fn get_sync_percentage(&self) -> Result<Option<f64>> {
150        // We're always in sync, just like esplora
151        self.esplora_client.get_sync_percentage().await
152    }
153}