fedimint_server_core/
bitcoin_rpc.rs1use std::fmt::Debug;
2use std::sync::{Arc, OnceLock};
3use std::time::Duration;
4
5use anyhow::{Context, Result, ensure};
6use fedimint_core::bitcoin::{Block, BlockHash, Network, Transaction};
7use fedimint_core::envs::BitcoinRpcConfig;
8use fedimint_core::task::TaskGroup;
9use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl};
10use fedimint_core::{ChainId, Feerate};
11use fedimint_logging::LOG_SERVER;
12use tokio::sync::watch;
13use tracing::{debug, warn};
14
15use crate::dashboard_ui::ServerBitcoinRpcStatus;
16
17const MAINNET_CHAIN_ID_STR: &str =
20 "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048";
21const TESTNET_CHAIN_ID_STR: &str =
23 "00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206";
24const SIGNET_4_CHAIN_ID_STR: &str =
26 "00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53";
27const MUTINYNET_CHAIN_ID_STR: &str =
29 "000002855893a0a9b24eaffc5efc770558a326fee4fc10c9da22fc19cd2954f9";
30
31pub fn network_from_chain_id(chain_id: ChainId) -> Network {
36 match chain_id.to_string().as_str() {
37 MAINNET_CHAIN_ID_STR => Network::Bitcoin,
38 TESTNET_CHAIN_ID_STR => Network::Testnet,
39 SIGNET_4_CHAIN_ID_STR => Network::Signet,
40 MUTINYNET_CHAIN_ID_STR => Network::Signet,
41 _ => {
42 Network::Regtest
44 }
45 }
46}
47
48#[derive(Debug)]
49pub struct ServerBitcoinRpcMonitor {
50 rpc: DynServerBitcoinRpc,
51 status_receiver: watch::Receiver<Option<ServerBitcoinRpcStatus>>,
52 chain_id: OnceLock<ChainId>,
55}
56
57impl ServerBitcoinRpcMonitor {
58 pub fn new(
59 rpc: DynServerBitcoinRpc,
60 update_interval: Duration,
61 task_group: &TaskGroup,
62 ) -> Self {
63 let (status_sender, status_receiver) = watch::channel(None);
64
65 let rpc_clone = rpc.clone();
66 debug!(
67 target: LOG_SERVER,
68 interval_ms = %update_interval.as_millis(),
69 "Starting bitcoin rpc monitor"
70 );
71
72 task_group.spawn_cancellable("bitcoin-status-update", async move {
73 let mut interval = tokio::time::interval(update_interval);
74 loop {
75 interval.tick().await;
76 match Self::fetch_status(&rpc_clone).await {
77 Ok(new_status) => {
78 status_sender.send_replace(Some(new_status));
79 }
80 Err(err) => {
81 warn!(
82 target: LOG_SERVER,
83 err = %err.fmt_compact_anyhow(),
84 "Bitcoin status update failed"
85 );
86 status_sender.send_replace(None);
87 }
88 }
89 }
90 });
91
92 Self {
93 rpc,
94 status_receiver,
95 chain_id: OnceLock::new(),
96 }
97 }
98
99 async fn fetch_status(rpc: &DynServerBitcoinRpc) -> Result<ServerBitcoinRpcStatus> {
100 let chain_id = rpc.get_chain_id().await?;
101 let network = network_from_chain_id(chain_id);
102 let block_count = rpc.get_block_count().await?;
103 let sync_progress = rpc.get_sync_progress().await?;
104
105 let fee_rate = if network == Network::Regtest {
106 Feerate { sats_per_kvb: 1000 }
107 } else {
108 rpc.get_feerate().await?.context("Feerate not available")?
109 };
110
111 Ok(ServerBitcoinRpcStatus {
112 network,
113 block_count,
114 fee_rate,
115 sync_progress,
116 })
117 }
118
119 pub fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
120 self.rpc.get_bitcoin_rpc_config()
121 }
122
123 pub fn url(&self) -> SafeUrl {
124 self.rpc.get_url()
125 }
126
127 pub fn status(&self) -> Option<ServerBitcoinRpcStatus> {
128 self.status_receiver.borrow().clone()
129 }
130
131 pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
132 ensure!(
133 self.status_receiver.borrow().is_some(),
134 "Not connected to bitcoin backend"
135 );
136
137 self.rpc.get_block(hash).await
138 }
139
140 pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
141 ensure!(
142 self.status_receiver.borrow().is_some(),
143 "Not connected to bitcoin backend"
144 );
145
146 self.rpc.get_block_hash(height).await
147 }
148
149 pub async fn submit_transaction(&self, tx: Transaction) {
150 if self.status_receiver.borrow().is_some() {
151 self.rpc.submit_transaction(tx).await;
152 }
153 }
154
155 pub async fn get_chain_id(&self) -> Result<ChainId> {
158 if let Some(chain_id) = self.chain_id.get() {
160 return Ok(*chain_id);
161 }
162
163 ensure!(
164 self.status_receiver.borrow().is_some(),
165 "Not connected to bitcoin backend"
166 );
167
168 let chain_id = self.rpc.get_chain_id().await?;
170 let _ = self.chain_id.set(chain_id);
172
173 Ok(chain_id)
174 }
175}
176
177impl Clone for ServerBitcoinRpcMonitor {
178 fn clone(&self) -> Self {
179 Self {
180 rpc: self.rpc.clone(),
181 status_receiver: self.status_receiver.clone(),
182 chain_id: self
183 .chain_id
184 .get()
185 .copied()
186 .map(|h| {
187 let lock = OnceLock::new();
188 let _ = lock.set(h);
189 lock
190 })
191 .unwrap_or_default(),
192 }
193 }
194}
195
196pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
197
198#[async_trait::async_trait]
199pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
200 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
202
203 fn get_url(&self) -> SafeUrl;
205
206 async fn get_block_count(&self) -> Result<u64>;
208
209 async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
221
222 async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
223
224 async fn get_feerate(&self) -> Result<Option<Feerate>>;
229
230 async fn submit_transaction(&self, transaction: Transaction);
245
246 async fn get_sync_progress(&self) -> Result<Option<f64>>;
249
250 async fn get_chain_id(&self) -> Result<ChainId>;
256
257 fn into_dyn(self) -> DynServerBitcoinRpc
258 where
259 Self: Sized,
260 {
261 Arc::new(self)
262 }
263}