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 {bitcoind_network:?} but esplora reported {esplora_network:?}",
65                    );
66                }
67                Ok(bitcoind_network)
68            }
69            Err(e) => {
70                warn!(
71                    target: LOG_SERVER,
72                    error = %e.fmt_compact_anyhow(),
73                    "BitcoindClient failed for get_network, falling back to EsploraClient"
74                );
75
76                self.esplora_client.get_network().await
77            }
78        }
79    }
80
81    async fn get_block_count(&self) -> Result<u64> {
82        match self.bitcoind_client.get_block_count().await {
83            Ok(count) => Ok(count),
84            Err(e) => {
85                warn!(
86                    target: LOG_SERVER,
87                    error = %e.fmt_compact_anyhow(),
88                    "BitcoindClient failed for get_block_count, falling back to EsploraClient"
89                );
90                self.esplora_client.get_block_count().await
91            }
92        }
93    }
94
95    async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
96        match self.bitcoind_client.get_block_hash(height).await {
97            Ok(hash) => Ok(hash),
98            Err(e) => {
99                warn!(
100                    target: LOG_SERVER,
101                    error = %e.fmt_compact_anyhow(),
102                    height = height,
103                    "BitcoindClient failed for get_block_hash, falling back to EsploraClient"
104                );
105                self.esplora_client.get_block_hash(height).await
106            }
107        }
108    }
109
110    async fn get_block(&self, block_hash: &BlockHash) -> Result<bitcoin::Block> {
111        match self.bitcoind_client.get_block(block_hash).await {
112            Ok(block) => Ok(block),
113            Err(e) => {
114                warn!(
115                    target: LOG_SERVER,
116                    error = %e.fmt_compact_anyhow(),
117                    block_hash = %block_hash,
118                    "BitcoindClient failed for get_block, falling back to EsploraClient"
119                );
120                self.esplora_client.get_block(block_hash).await
121            }
122        }
123    }
124
125    async fn get_feerate(&self) -> Result<Option<Feerate>> {
126        match self.bitcoind_client.get_feerate().await {
127            Ok(feerate) => Ok(feerate),
128            Err(e) => {
129                warn!(
130                    target: LOG_SERVER,
131                    error = %e.fmt_compact_anyhow(),
132                    "BitcoindClient failed for get_feerate, falling back to EsploraClient"
133                );
134                self.esplora_client.get_feerate().await
135            }
136        }
137    }
138
139    async fn submit_transaction(&self, transaction: Transaction) {
140        // Since this endpoint does not return an error, we can just always broadcast to
141        // both places
142        self.bitcoind_client
143            .submit_transaction(transaction.clone())
144            .await;
145        self.esplora_client.submit_transaction(transaction).await;
146    }
147
148    async fn get_sync_progress(&self) -> Result<Option<f64>> {
149        // We're always in sync, just like esplora
150        self.esplora_client.get_sync_progress().await
151    }
152}