Skip to main content

fedimint_server_core/
bitcoin_rpc.rs

1use 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    /// Returns the Bitcoin RPC config
125    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
126
127    /// Returns the Bitcoin RPC url
128    fn get_url(&self) -> SafeUrl;
129
130    /// Returns the Bitcoin network the node is connected to
131    async fn get_network(&self) -> Result<Network>;
132
133    /// Returns the current block count
134    async fn get_block_count(&self) -> Result<u64>;
135
136    /// Returns the block hash at a given height
137    ///
138    /// # Panics
139    /// If the node does not know a block for that height. Make sure to only
140    /// query blocks of a height less to the one returned by
141    /// `Self::get_block_count`.
142    ///
143    /// While there is a corner case that the blockchain shrinks between these
144    /// two calls (through on average heavier blocks on a fork) this is
145    /// prevented by only querying hashes for blocks tailing the chain tip
146    /// by a certain number of blocks.
147    async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
148
149    async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
150
151    /// Estimates the fee rate for a given confirmation target. Make sure that
152    /// all federation members use the same algorithm to avoid widely
153    /// diverging results. If the node is not ready yet to return a fee rate
154    /// estimation this function returns `None`.
155    async fn get_feerate(&self) -> Result<Option<Feerate>>;
156
157    /// Submits a transaction to the Bitcoin network
158    ///
159    /// This operation does not return anything as it never OK to consider its
160    /// success as final anyway. The caller should be retrying
161    /// broadcast periodically until it confirms the transaction was actually
162    /// via other means or decides that is no longer relevant.
163    ///
164    /// Also - most backends considers brodcasting a tx that is already included
165    /// in the blockchain as an error, which breaks idempotency and requires
166    /// brittle workarounds just to reliably ignore... just to retry on the
167    /// higher level anyway.
168    ///
169    /// Implementations of this error should log errors for debugging purposes
170    /// when it makes sense.
171    async fn submit_transaction(&self, transaction: Transaction);
172
173    /// Returns the node's estimated chain sync percentage as a float between
174    /// 0.0 and 1.0, or `None` if the node doesn't support this feature.
175    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}