fedimint_server_bitcoin_rpc/
lib.rs

1pub mod bitcoind;
2pub mod esplora;
3
4use anyhow::Result;
5use bitcoin::{BlockHash, Transaction};
6use fedimint_core::envs::BitcoinRpcConfig;
7use fedimint_core::util::{FmtCompactAnyhow, SafeUrl};
8use fedimint_core::{ChainId, Feerate};
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            "Initializing 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_block_count(&self) -> Result<u64> {
56        match self.bitcoind_client.get_block_count().await {
57            Ok(count) => Ok(count),
58            Err(e) => {
59                warn!(
60                    target: LOG_SERVER,
61                    error = %e.fmt_compact_anyhow(),
62                    "BitcoindClient failed for get_block_count, falling back to EsploraClient"
63                );
64                self.esplora_client.get_block_count().await
65            }
66        }
67    }
68
69    async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
70        match self.bitcoind_client.get_block_hash(height).await {
71            Ok(hash) => Ok(hash),
72            Err(e) => {
73                warn!(
74                    target: LOG_SERVER,
75                    error = %e.fmt_compact_anyhow(),
76                    height = height,
77                    "BitcoindClient failed for get_block_hash, falling back to EsploraClient"
78                );
79                self.esplora_client.get_block_hash(height).await
80            }
81        }
82    }
83
84    async fn get_block(&self, block_hash: &BlockHash) -> Result<bitcoin::Block> {
85        match self.bitcoind_client.get_block(block_hash).await {
86            Ok(block) => Ok(block),
87            Err(e) => {
88                warn!(
89                    target: LOG_SERVER,
90                    error = %e.fmt_compact_anyhow(),
91                    block_hash = %block_hash,
92                    "BitcoindClient failed for get_block, falling back to EsploraClient"
93                );
94                self.esplora_client.get_block(block_hash).await
95            }
96        }
97    }
98
99    async fn get_feerate(&self) -> Result<Option<Feerate>> {
100        match self.bitcoind_client.get_feerate().await {
101            Ok(feerate) => Ok(feerate),
102            Err(e) => {
103                warn!(
104                    target: LOG_SERVER,
105                    error = %e.fmt_compact_anyhow(),
106                    "BitcoindClient failed for get_feerate, falling back to EsploraClient"
107                );
108                self.esplora_client.get_feerate().await
109            }
110        }
111    }
112
113    async fn submit_transaction(&self, transaction: Transaction) {
114        // Since this endpoint does not return an error, we can just always broadcast to
115        // both places
116        self.bitcoind_client
117            .submit_transaction(transaction.clone())
118            .await;
119        self.esplora_client.submit_transaction(transaction).await;
120    }
121
122    async fn get_sync_progress(&self) -> Result<Option<f64>> {
123        // We're always in sync, just like esplora
124        self.esplora_client.get_sync_progress().await
125    }
126
127    async fn get_chain_id(&self) -> Result<ChainId> {
128        match self.bitcoind_client.get_chain_id().await {
129            Ok(chain_id) => Ok(chain_id),
130            Err(e) => {
131                warn!(
132                    target: LOG_SERVER,
133                    error = %e.fmt_compact_anyhow(),
134                    "BitcoindClient failed for get_chain_id, falling back to EsploraClient"
135                );
136                self.esplora_client.get_chain_id().await
137            }
138        }
139    }
140}