fedimint_server_core/
bitcoin_rpc.rs1use std::fmt::Debug;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Context, Result, ensure};
6use fedimint_core::Feerate;
7use fedimint_core::bitcoin::{Block, BlockHash, Network, Transaction};
8use fedimint_core::envs::BitcoinRpcConfig;
9use fedimint_core::task::TaskGroup;
10use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl};
11use fedimint_logging::LOG_SERVER;
12use tokio::sync::watch;
13use tracing::{debug, warn};
14
15use crate::dashboard_ui::ServerBitcoinRpcStatus;
16
17#[derive(Debug, Clone)]
18pub struct ServerBitcoinRpcMonitor {
19 rpc: DynServerBitcoinRpc,
20 status_receiver: watch::Receiver<Option<ServerBitcoinRpcStatus>>,
21}
22
23impl ServerBitcoinRpcMonitor {
24 pub fn new(
25 rpc: DynServerBitcoinRpc,
26 update_interval: Duration,
27 task_group: &TaskGroup,
28 ) -> Self {
29 let (status_sender, status_receiver) = watch::channel(None);
30
31 let rpc_clone = rpc.clone();
32 debug!(
33 target: LOG_SERVER,
34 interval_ms = %update_interval.as_millis(),
35 "Starting bitcoin rpc monitor"
36 );
37
38 task_group.spawn_cancellable("bitcoin-status-update", async move {
39 let mut interval = tokio::time::interval(update_interval);
40 loop {
41 interval.tick().await;
42 match Self::fetch_status(&rpc_clone).await {
43 Ok(new_status) => {
44 status_sender.send_replace(Some(new_status));
45 }
46 Err(err) => {
47 warn!(
48 target: LOG_SERVER,
49 err = %err.fmt_compact_anyhow(),
50 "Bitcoin status update failed"
51 );
52 status_sender.send_replace(None);
53 }
54 }
55 }
56 });
57
58 Self {
59 rpc,
60 status_receiver,
61 }
62 }
63
64 async fn fetch_status(rpc: &DynServerBitcoinRpc) -> Result<ServerBitcoinRpcStatus> {
65 let network = rpc.get_network().await?;
66 let block_count = rpc.get_block_count().await?;
67 let sync_progress = rpc.get_sync_progress().await?;
68
69 let fee_rate = if network == Network::Regtest {
70 Feerate { sats_per_kvb: 1000 }
71 } else {
72 rpc.get_feerate().await?.context("Feerate not available")?
73 };
74
75 Ok(ServerBitcoinRpcStatus {
76 network,
77 block_count,
78 fee_rate,
79 sync_progress,
80 })
81 }
82
83 pub fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
84 self.rpc.get_bitcoin_rpc_config()
85 }
86
87 pub fn url(&self) -> SafeUrl {
88 self.rpc.get_url()
89 }
90
91 pub fn status(&self) -> Option<ServerBitcoinRpcStatus> {
92 self.status_receiver.borrow().clone()
93 }
94
95 pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
96 ensure!(
97 self.status_receiver.borrow().is_some(),
98 "Not connected to bitcoin backend"
99 );
100
101 self.rpc.get_block(hash).await
102 }
103
104 pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
105 ensure!(
106 self.status_receiver.borrow().is_some(),
107 "Not connected to bitcoin backend"
108 );
109
110 self.rpc.get_block_hash(height).await
111 }
112
113 pub async fn submit_transaction(&self, tx: Transaction) {
114 if self.status_receiver.borrow().is_some() {
115 self.rpc.submit_transaction(tx).await;
116 }
117 }
118}
119
120pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
121
122#[async_trait::async_trait]
123pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
124 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
126
127 fn get_url(&self) -> SafeUrl;
129
130 async fn get_network(&self) -> Result<Network>;
132
133 async fn get_block_count(&self) -> Result<u64>;
135
136 async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
148
149 async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
150
151 async fn get_feerate(&self) -> Result<Option<Feerate>>;
156
157 async fn submit_transaction(&self, transaction: Transaction);
172
173 async fn get_sync_progress(&self) -> Result<Option<f64>>;
176
177 fn into_dyn(self) -> DynServerBitcoinRpc
178 where
179 Self: Sized,
180 {
181 Arc::new(self)
182 }
183}