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