fedimint_server_core/
bitcoin_rpc.rs1use std::fmt::Debug;
2use std::sync::{Arc, OnceLock};
3use std::time::Duration;
4
5use anyhow::{Result, ensure};
6use fedimint_core::bitcoin::{Block, BlockHash, Network, Transaction, Txid};
7use fedimint_core::envs::BitcoinRpcConfig;
8use fedimint_core::task::TaskGroup;
9use fedimint_core::util::{FmtCompact as _, SafeUrl};
10use fedimint_core::{ChainId, Feerate};
11use fedimint_logging::LOG_SERVER;
12use tokio::sync::watch;
13use tracing::{debug, warn};
14
15use crate::dashboard_ui::ServerBitcoinRpcStatus;
16
17const MAINNET_CHAIN_ID_STR: &str =
20 "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048";
21const TESTNET_CHAIN_ID_STR: &str =
23 "00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206";
24const SIGNET_4_CHAIN_ID_STR: &str =
26 "00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53";
27const MUTINYNET_CHAIN_ID_STR: &str =
29 "000002855893a0a9b24eaffc5efc770558a326fee4fc10c9da22fc19cd2954f9";
30
31pub fn network_from_chain_id(chain_id: ChainId) -> Network {
36 match chain_id.to_string().as_str() {
37 MAINNET_CHAIN_ID_STR => Network::Bitcoin,
38 TESTNET_CHAIN_ID_STR => Network::Testnet,
39 SIGNET_4_CHAIN_ID_STR => Network::Signet,
40 MUTINYNET_CHAIN_ID_STR => Network::Signet,
41 _ => {
42 Network::Regtest
44 }
45 }
46}
47
48#[derive(Debug)]
49pub struct ServerBitcoinRpcMonitor {
50 rpc: DynServerBitcoinRpc,
51 status_receiver: watch::Receiver<Option<ServerBitcoinRpcStatus>>,
52 chain_id: OnceLock<ChainId>,
55}
56
57impl ServerBitcoinRpcMonitor {
58 pub fn new(
59 rpc: DynServerBitcoinRpc,
60 update_interval: Duration,
61 task_group: &TaskGroup,
62 ) -> Self {
63 let (status_sender, status_receiver) = watch::channel(None);
64
65 let rpc_clone = rpc.clone();
66 debug!(
67 target: LOG_SERVER,
68 interval_ms = %update_interval.as_millis(),
69 "Starting bitcoin rpc monitor"
70 );
71
72 task_group.spawn_cancellable("bitcoin-status-update", async move {
73 let mut interval = tokio::time::interval(update_interval);
74 loop {
75 interval.tick().await;
76 match Self::fetch_status(&rpc_clone).await {
77 Ok(new_status) => {
78 status_sender.send_replace(Some(new_status));
79 }
80 Err(err) => {
81 warn!(
82 target: LOG_SERVER,
83 err = %err.fmt_compact(),
84 "Bitcoin status update failed"
85 );
86 status_sender.send_replace(None);
87 }
88 }
89 }
90 });
91
92 Self {
93 rpc,
94 status_receiver,
95 chain_id: OnceLock::new(),
96 }
97 }
98
99 async fn fetch_status(rpc: &DynServerBitcoinRpc) -> Result<ServerBitcoinRpcStatus> {
100 let chain_id = rpc.get_chain_id().await?;
101 let network = network_from_chain_id(chain_id);
102 let block_count = rpc.get_block_count().await?;
103 let sync_progress = rpc.get_sync_progress().await?;
104
105 let fee_rate = if network == Network::Regtest {
106 Some(Feerate { sats_per_kvb: 1000 })
107 } else {
108 rpc.get_feerate().await?
109 };
110
111 Ok(ServerBitcoinRpcStatus {
112 network,
113 block_count,
114 fee_rate,
115 sync_progress,
116 })
117 }
118
119 pub fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
120 self.rpc.get_bitcoin_rpc_config()
121 }
122
123 pub fn url(&self) -> SafeUrl {
124 self.rpc.get_url()
125 }
126
127 pub fn status(&self) -> Option<ServerBitcoinRpcStatus> {
128 self.status_receiver.borrow().clone()
129 }
130
131 pub fn subscribe_status(&self) -> watch::Receiver<Option<ServerBitcoinRpcStatus>> {
142 self.status_receiver.clone()
143 }
144
145 pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
146 ensure!(
147 self.status_receiver.borrow().is_some(),
148 "Not connected to bitcoin backend"
149 );
150
151 self.rpc.get_block(hash).await
152 }
153
154 pub async fn get_mempool_txids(&self) -> Result<Option<Vec<Txid>>> {
157 ensure!(
158 self.status_receiver.borrow().is_some(),
159 "Not connected to bitcoin backend"
160 );
161
162 self.rpc.get_mempool_txids().await
163 }
164
165 pub async fn get_mempool_tx(&self, txid: &Txid) -> Result<Option<Transaction>> {
167 ensure!(
168 self.status_receiver.borrow().is_some(),
169 "Not connected to bitcoin backend"
170 );
171
172 self.rpc.get_mempool_tx(txid).await
173 }
174
175 pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
176 ensure!(
177 self.status_receiver.borrow().is_some(),
178 "Not connected to bitcoin backend"
179 );
180
181 self.rpc.get_block_hash(height).await
182 }
183
184 pub async fn submit_transaction(&self, tx: Transaction) -> Result<()> {
185 self.rpc.submit_transaction(tx).await
186 }
187
188 pub async fn get_chain_id(&self) -> Result<ChainId> {
191 if let Some(chain_id) = self.chain_id.get() {
193 return Ok(*chain_id);
194 }
195
196 ensure!(
197 self.status_receiver.borrow().is_some(),
198 "Not connected to bitcoin backend"
199 );
200
201 let chain_id = self.rpc.get_chain_id().await?;
203 let _ = self.chain_id.set(chain_id);
205
206 Ok(chain_id)
207 }
208}
209
210impl Clone for ServerBitcoinRpcMonitor {
211 fn clone(&self) -> Self {
212 Self {
213 rpc: self.rpc.clone(),
214 status_receiver: self.status_receiver.clone(),
215 chain_id: self
216 .chain_id
217 .get()
218 .copied()
219 .map(|h| {
220 let lock = OnceLock::new();
221 let _ = lock.set(h);
222 lock
223 })
224 .unwrap_or_default(),
225 }
226 }
227}
228
229pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
230
231#[async_trait::async_trait]
232pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
233 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
235
236 fn get_url(&self) -> SafeUrl;
238
239 async fn get_block_count(&self) -> Result<u64>;
241
242 async fn get_block_count_and_initial_block_download(&self) -> Result<(u64, bool)> {
248 Ok((self.get_block_count().await?, false))
249 }
250
251 async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
263
264 async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
265
266 async fn get_feerate(&self) -> Result<Option<Feerate>>;
271
272 async fn submit_transaction(&self, transaction: Transaction) -> Result<()>;
285
286 async fn get_sync_progress(&self) -> Result<Option<f64>>;
289
290 async fn get_mempool_txids(&self) -> Result<Option<Vec<Txid>>> {
301 Ok(None)
302 }
303
304 async fn get_mempool_tx(&self, txid: &Txid) -> Result<Option<Transaction>> {
310 let _ = txid;
311
312 Ok(None)
313 }
314
315 async fn get_chain_id(&self) -> Result<ChainId>;
321
322 fn into_dyn(self) -> DynServerBitcoinRpc
323 where
324 Self: Sized,
325 {
326 Arc::new(self)
327 }
328}