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