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::SafeUrl;
11use tokio::sync::watch;
12
13use crate::dashboard_ui::ServerBitcoinRpcStatus;
14
15#[derive(Debug, Clone)]
16pub struct ServerBitcoinRpcMonitor {
17    rpc: DynServerBitcoinRpc,
18    status_receiver: watch::Receiver<Option<ServerBitcoinRpcStatus>>,
19}
20
21impl ServerBitcoinRpcMonitor {
22    pub fn new(
23        rpc: DynServerBitcoinRpc,
24        update_interval: Duration,
25        task_group: &TaskGroup,
26    ) -> Self {
27        let (status_sender, status_receiver) = watch::channel(None);
28
29        let rpc_clone = rpc.clone();
30
31        task_group.spawn_cancellable("bitcoin-status-update", async move {
32            loop {
33                match Self::fetch_status(&rpc_clone).await {
34                    Ok(new_status) => {
35                        status_sender.send_replace(Some(new_status));
36                    }
37                    Err(..) => {
38                        status_sender.send_replace(None);
39                    }
40                }
41
42                fedimint_core::task::sleep(update_interval).await;
43            }
44        });
45
46        Self {
47            rpc,
48            status_receiver,
49        }
50    }
51
52    async fn fetch_status(rpc: &DynServerBitcoinRpc) -> Result<ServerBitcoinRpcStatus> {
53        let network = rpc.get_network().await?;
54        let block_count = rpc.get_block_count().await?;
55        let sync_percentage = rpc.get_sync_percentage().await?;
56
57        let fee_rate = if network == Network::Regtest {
58            Feerate { sats_per_kvb: 1000 }
59        } else {
60            rpc.get_feerate().await?.context("Feerate not available")?
61        };
62
63        Ok(ServerBitcoinRpcStatus {
64            network,
65            block_count,
66            fee_rate,
67            sync_percentage,
68        })
69    }
70
71    pub fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
72        self.rpc.get_bitcoin_rpc_config()
73    }
74
75    pub fn url(&self) -> SafeUrl {
76        self.rpc.get_url()
77    }
78
79    pub fn status(&self) -> Option<ServerBitcoinRpcStatus> {
80        self.status_receiver.borrow().clone()
81    }
82
83    pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
84        ensure!(
85            self.status_receiver.borrow().is_some(),
86            "Not connected to bitcoin backend"
87        );
88
89        self.rpc.get_block(hash).await
90    }
91
92    pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
93        ensure!(
94            self.status_receiver.borrow().is_some(),
95            "Not connected to bitcoin backend"
96        );
97
98        self.rpc.get_block_hash(height).await
99    }
100
101    pub async fn submit_transaction(&self, tx: Transaction) {
102        if self.status_receiver.borrow().is_some() {
103            self.rpc.submit_transaction(tx).await;
104        }
105    }
106}
107
108pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
109
110#[async_trait::async_trait]
111pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
112    /// Returns the Bitcoin RPC config
113    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
114
115    /// Returns the Bitcoin RPC url
116    fn get_url(&self) -> SafeUrl;
117
118    /// Returns the Bitcoin network the node is connected to
119    async fn get_network(&self) -> Result<Network>;
120
121    /// Returns the current block count
122    async fn get_block_count(&self) -> Result<u64>;
123
124    /// Returns the block hash at a given height
125    ///
126    /// # Panics
127    /// If the node does not know a block for that height. Make sure to only
128    /// query blocks of a height less to the one returned by
129    /// `Self::get_block_count`.
130    ///
131    /// While there is a corner case that the blockchain shrinks between these
132    /// two calls (through on average heavier blocks on a fork) this is
133    /// prevented by only querying hashes for blocks tailing the chain tip
134    /// by a certain number of blocks.
135    async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
136
137    async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
138
139    /// Estimates the fee rate for a given confirmation target. Make sure that
140    /// all federation members use the same algorithm to avoid widely
141    /// diverging results. If the node is not ready yet to return a fee rate
142    /// estimation this function returns `None`.
143    async fn get_feerate(&self) -> Result<Option<Feerate>>;
144
145    /// Submits a transaction to the Bitcoin network
146    ///
147    /// This operation does not return anything as it never OK to consider its
148    /// success as final anyway. The caller should be retrying
149    /// broadcast periodically until it confirms the transaction was actually
150    /// via other means or decides that is no longer relevant.
151    ///
152    /// Also - most backends considers brodcasting a tx that is already included
153    /// in the blockchain as an error, which breaks idempotency and requires
154    /// brittle workarounds just to reliably ignore... just to retry on the
155    /// higher level anyway.
156    ///
157    /// Implementations of this error should log errors for debugging purposes
158    /// when it makes sense.
159    async fn submit_transaction(&self, transaction: Transaction);
160
161    /// Returns the node's estimated chain sync percentage as a float between
162    /// 0.0 and 1.0, or `None` if the node doesn't support this feature.
163    async fn get_sync_percentage(&self) -> Result<Option<f64>>;
164
165    fn into_dyn(self) -> DynServerBitcoinRpc
166    where
167        Self: Sized,
168    {
169        Arc::new(self)
170    }
171}