fedimint_server_core/
bitcoin_rpc.rs1use std::fmt::Debug;
2use std::sync::{Arc, OnceLock};
3use std::time::Duration;
4
5use anyhow::{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 Some(Feerate { sats_per_kvb: 1000 })
107 } else {
108 rpc.get_feerate().await?
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) -> Result<()> {
150 self.rpc.submit_transaction(tx).await
151 }
152
153 pub async fn get_chain_id(&self) -> Result<ChainId> {
156 if let Some(chain_id) = self.chain_id.get() {
158 return Ok(*chain_id);
159 }
160
161 ensure!(
162 self.status_receiver.borrow().is_some(),
163 "Not connected to bitcoin backend"
164 );
165
166 let chain_id = self.rpc.get_chain_id().await?;
168 let _ = self.chain_id.set(chain_id);
170
171 Ok(chain_id)
172 }
173}
174
175impl Clone for ServerBitcoinRpcMonitor {
176 fn clone(&self) -> Self {
177 Self {
178 rpc: self.rpc.clone(),
179 status_receiver: self.status_receiver.clone(),
180 chain_id: self
181 .chain_id
182 .get()
183 .copied()
184 .map(|h| {
185 let lock = OnceLock::new();
186 let _ = lock.set(h);
187 lock
188 })
189 .unwrap_or_default(),
190 }
191 }
192}
193
194pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
195
196#[async_trait::async_trait]
197pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
198 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
200
201 fn get_url(&self) -> SafeUrl;
203
204 async fn get_block_count(&self) -> Result<u64>;
206
207 async fn get_block_count_and_initial_block_download(&self) -> Result<(u64, bool)> {
213 Ok((self.get_block_count().await?, false))
214 }
215
216 async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
228
229 async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
230
231 async fn get_feerate(&self) -> Result<Option<Feerate>>;
236
237 async fn submit_transaction(&self, transaction: Transaction) -> Result<()>;
250
251 async fn get_sync_progress(&self) -> Result<Option<f64>>;
254
255 async fn get_chain_id(&self) -> Result<ChainId>;
261
262 fn into_dyn(self) -> DynServerBitcoinRpc
263 where
264 Self: Sized,
265 {
266 Arc::new(self)
267 }
268}