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 tokio::sync::watch;
1213use crate::dashboard_ui::ServerBitcoinRpcStatus;
1415#[derive(Debug, Clone)]
16pub struct ServerBitcoinRpcMonitor {
17 rpc: DynServerBitcoinRpc,
18 status_receiver: watch::Receiver<Option<ServerBitcoinRpcStatus>>,
19}
2021impl ServerBitcoinRpcMonitor {
22pub fn new(
23 rpc: DynServerBitcoinRpc,
24 update_interval: Duration,
25 task_group: &TaskGroup,
26 ) -> Self {
27let (status_sender, status_receiver) = watch::channel(None);
2829let rpc_clone = rpc.clone();
3031 task_group.spawn_cancellable("bitcoin-status-update", async move {
32loop {
33match Self::fetch_status(&rpc_clone).await {
34Ok(new_status) => {
35 status_sender.send_replace(Some(new_status));
36 }
37Err(..) => {
38 status_sender.send_replace(None);
39 }
40 }
4142 fedimint_core::task::sleep(update_interval).await;
43 }
44 });
4546Self {
47 rpc,
48 status_receiver,
49 }
50 }
5152async fn fetch_status(rpc: &DynServerBitcoinRpc) -> Result<ServerBitcoinRpcStatus> {
53let network = rpc.get_network().await?;
54let block_count = rpc.get_block_count().await?;
55let sync_percentage = rpc.get_sync_percentage().await?;
5657let fee_rate = if network == Network::Regtest {
58 Feerate { sats_per_kvb: 1000 }
59 } else {
60 rpc.get_feerate().await?.context("Feerate not available")?
61};
6263Ok(ServerBitcoinRpcStatus {
64 network,
65 block_count,
66 fee_rate,
67 sync_percentage,
68 })
69 }
7071pub fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
72self.rpc.get_bitcoin_rpc_config()
73 }
7475pub fn url(&self) -> SafeUrl {
76self.rpc.get_url()
77 }
7879pub fn status(&self) -> Option<ServerBitcoinRpcStatus> {
80self.status_receiver.borrow().clone()
81 }
8283pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
84ensure!(
85self.status_receiver.borrow().is_some(),
86"Not connected to bitcoin backend"
87);
8889self.rpc.get_block(hash).await
90}
9192pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
93ensure!(
94self.status_receiver.borrow().is_some(),
95"Not connected to bitcoin backend"
96);
9798self.rpc.get_block_hash(height).await
99}
100101pub async fn submit_transaction(&self, tx: Transaction) {
102if self.status_receiver.borrow().is_some() {
103self.rpc.submit_transaction(tx).await;
104 }
105 }
106}
107108pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
109110#[async_trait::async_trait]
111pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
112/// Returns the Bitcoin RPC config
113fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
114115/// Returns the Bitcoin RPC url
116fn get_url(&self) -> SafeUrl;
117118/// Returns the Bitcoin network the node is connected to
119async fn get_network(&self) -> Result<Network>;
120121/// Returns the current block count
122async fn get_block_count(&self) -> Result<u64>;
123124/// 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.
135async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
136137async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
138139/// 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`.
143async fn get_feerate(&self) -> Result<Option<Feerate>>;
144145/// 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.
159async fn submit_transaction(&self, transaction: Transaction);
160161/// 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.
163async fn get_sync_percentage(&self) -> Result<Option<f64>>;
164165fn into_dyn(self) -> DynServerBitcoinRpc
166where
167Self: Sized,
168 {
169 Arc::new(self)
170 }
171}