1use std::fmt::Debug;
2use std::sync::Arc;
3use std::time::Duration;
45use 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::SafeUrl;
11use fedimint_logging::LOG_SERVER;
12use tokio::sync::watch;
13use tracing::debug;
1415use crate::dashboard_ui::ServerBitcoinRpcStatus;
1617#[derive(Debug, Clone)]
18pub struct ServerBitcoinRpcMonitor {
19 rpc: DynServerBitcoinRpc,
20 status_receiver: watch::Receiver<Option<ServerBitcoinRpcStatus>>,
21}
2223impl ServerBitcoinRpcMonitor {
24pub fn new(
25 rpc: DynServerBitcoinRpc,
26 update_interval: Duration,
27 task_group: &TaskGroup,
28 ) -> Self {
29let (status_sender, status_receiver) = watch::channel(None);
3031let rpc_clone = rpc.clone();
32debug!(
33 target: LOG_SERVER,
34 interval_ms = %update_interval.as_millis(),
35"Starting bitcoin rpc monitor"
36);
3738 task_group.spawn_cancellable("bitcoin-status-update", async move {
39let mut interval = tokio::time::interval(update_interval);
40loop {
41 interval.tick().await;
42match Self::fetch_status(&rpc_clone).await {
43Ok(new_status) => {
44 status_sender.send_replace(Some(new_status));
45 }
46Err(..) => {
47 status_sender.send_replace(None);
48 }
49 }
50 }
51 });
5253Self {
54 rpc,
55 status_receiver,
56 }
57 }
5859async fn fetch_status(rpc: &DynServerBitcoinRpc) -> Result<ServerBitcoinRpcStatus> {
60let network = rpc.get_network().await?;
61let block_count = rpc.get_block_count().await?;
62let sync_percentage = rpc.get_sync_percentage().await?;
6364let fee_rate = if network == Network::Regtest {
65 Feerate { sats_per_kvb: 1000 }
66 } else {
67 rpc.get_feerate().await?.context("Feerate not available")?
68};
6970Ok(ServerBitcoinRpcStatus {
71 network,
72 block_count,
73 fee_rate,
74 sync_percentage,
75 })
76 }
7778pub fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
79self.rpc.get_bitcoin_rpc_config()
80 }
8182pub fn url(&self) -> SafeUrl {
83self.rpc.get_url()
84 }
8586pub fn status(&self) -> Option<ServerBitcoinRpcStatus> {
87self.status_receiver.borrow().clone()
88 }
8990pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
91ensure!(
92self.status_receiver.borrow().is_some(),
93"Not connected to bitcoin backend"
94);
9596self.rpc.get_block(hash).await
97}
9899pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
100ensure!(
101self.status_receiver.borrow().is_some(),
102"Not connected to bitcoin backend"
103);
104105self.rpc.get_block_hash(height).await
106}
107108pub async fn submit_transaction(&self, tx: Transaction) {
109if self.status_receiver.borrow().is_some() {
110self.rpc.submit_transaction(tx).await;
111 }
112 }
113}
114115pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
116117#[async_trait::async_trait]
118pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
119/// Returns the Bitcoin RPC config
120fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
121122/// Returns the Bitcoin RPC url
123fn get_url(&self) -> SafeUrl;
124125/// Returns the Bitcoin network the node is connected to
126async fn get_network(&self) -> Result<Network>;
127128/// Returns the current block count
129async fn get_block_count(&self) -> Result<u64>;
130131/// Returns the block hash at a given height
132 ///
133 /// # Panics
134 /// If the node does not know a block for that height. Make sure to only
135 /// query blocks of a height less to the one returned by
136 /// `Self::get_block_count`.
137 ///
138 /// While there is a corner case that the blockchain shrinks between these
139 /// two calls (through on average heavier blocks on a fork) this is
140 /// prevented by only querying hashes for blocks tailing the chain tip
141 /// by a certain number of blocks.
142async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
143144async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
145146/// Estimates the fee rate for a given confirmation target. Make sure that
147 /// all federation members use the same algorithm to avoid widely
148 /// diverging results. If the node is not ready yet to return a fee rate
149 /// estimation this function returns `None`.
150async fn get_feerate(&self) -> Result<Option<Feerate>>;
151152/// Submits a transaction to the Bitcoin network
153 ///
154 /// This operation does not return anything as it never OK to consider its
155 /// success as final anyway. The caller should be retrying
156 /// broadcast periodically until it confirms the transaction was actually
157 /// via other means or decides that is no longer relevant.
158 ///
159 /// Also - most backends considers brodcasting a tx that is already included
160 /// in the blockchain as an error, which breaks idempotency and requires
161 /// brittle workarounds just to reliably ignore... just to retry on the
162 /// higher level anyway.
163 ///
164 /// Implementations of this error should log errors for debugging purposes
165 /// when it makes sense.
166async fn submit_transaction(&self, transaction: Transaction);
167168/// Returns the node's estimated chain sync percentage as a float between
169 /// 0.0 and 1.0, or `None` if the node doesn't support this feature.
170async fn get_sync_percentage(&self) -> Result<Option<f64>>;
171172fn into_dyn(self) -> DynServerBitcoinRpc
173where
174Self: Sized,
175 {
176 Arc::new(self)
177 }
178}