use std::fmt::Debug;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use anyhow::{Context, Result, ensure};
use fedimint_core::bitcoin::{Block, BlockHash, Network, Transaction};
use fedimint_core::envs::BitcoinRpcConfig;
use fedimint_core::task::TaskGroup;
use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl};
use fedimint_core::{ChainId, Feerate};
use fedimint_logging::LOG_SERVER;
use tokio::sync::watch;
use tracing::{debug, warn};
use crate::dashboard_ui::ServerBitcoinRpcStatus;
const MAINNET_CHAIN_ID_STR: &str =
"00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048";
const TESTNET_CHAIN_ID_STR: &str =
"00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206";
const SIGNET_4_CHAIN_ID_STR: &str =
"00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53";
const MUTINYNET_CHAIN_ID_STR: &str =
"000002855893a0a9b24eaffc5efc770558a326fee4fc10c9da22fc19cd2954f9";
pub fn network_from_chain_id(chain_id: ChainId) -> Network {
match chain_id.to_string().as_str() {
MAINNET_CHAIN_ID_STR => Network::Bitcoin,
TESTNET_CHAIN_ID_STR => Network::Testnet,
SIGNET_4_CHAIN_ID_STR => Network::Signet,
MUTINYNET_CHAIN_ID_STR => Network::Signet,
_ => {
Network::Regtest
}
}
}
#[derive(Debug)]
pub struct ServerBitcoinRpcMonitor {
rpc: DynServerBitcoinRpc,
status_receiver: watch::Receiver<Option<ServerBitcoinRpcStatus>>,
chain_id: OnceLock<ChainId>,
}
impl ServerBitcoinRpcMonitor {
pub fn new(
rpc: DynServerBitcoinRpc,
update_interval: Duration,
task_group: &TaskGroup,
) -> Self {
let (status_sender, status_receiver) = watch::channel(None);
let rpc_clone = rpc.clone();
debug!(
target: LOG_SERVER,
interval_ms = %update_interval.as_millis(),
"Starting bitcoin rpc monitor"
);
task_group.spawn_cancellable("bitcoin-status-update", async move {
let mut interval = tokio::time::interval(update_interval);
loop {
interval.tick().await;
match Self::fetch_status(&rpc_clone).await {
Ok(new_status) => {
status_sender.send_replace(Some(new_status));
}
Err(err) => {
warn!(
target: LOG_SERVER,
err = %err.fmt_compact_anyhow(),
"Bitcoin status update failed"
);
status_sender.send_replace(None);
}
}
}
});
Self {
rpc,
status_receiver,
chain_id: OnceLock::new(),
}
}
async fn fetch_status(rpc: &DynServerBitcoinRpc) -> Result<ServerBitcoinRpcStatus> {
let chain_id = rpc.get_chain_id().await?;
let network = network_from_chain_id(chain_id);
let block_count = rpc.get_block_count().await?;
let sync_progress = rpc.get_sync_progress().await?;
let fee_rate = if network == Network::Regtest {
Feerate { sats_per_kvb: 1000 }
} else {
rpc.get_feerate().await?.context("Feerate not available")?
};
Ok(ServerBitcoinRpcStatus {
network,
block_count,
fee_rate,
sync_progress,
})
}
pub fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
self.rpc.get_bitcoin_rpc_config()
}
pub fn url(&self) -> SafeUrl {
self.rpc.get_url()
}
pub fn status(&self) -> Option<ServerBitcoinRpcStatus> {
self.status_receiver.borrow().clone()
}
pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
ensure!(
self.status_receiver.borrow().is_some(),
"Not connected to bitcoin backend"
);
self.rpc.get_block(hash).await
}
pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
ensure!(
self.status_receiver.borrow().is_some(),
"Not connected to bitcoin backend"
);
self.rpc.get_block_hash(height).await
}
pub async fn submit_transaction(&self, tx: Transaction) -> Result<()> {
ensure!(
self.status_receiver.borrow().is_some(),
"Not connected to bitcoin backend"
);
self.rpc.submit_transaction(tx).await
}
pub async fn get_chain_id(&self) -> Result<ChainId> {
if let Some(chain_id) = self.chain_id.get() {
return Ok(*chain_id);
}
ensure!(
self.status_receiver.borrow().is_some(),
"Not connected to bitcoin backend"
);
let chain_id = self.rpc.get_chain_id().await?;
let _ = self.chain_id.set(chain_id);
Ok(chain_id)
}
}
impl Clone for ServerBitcoinRpcMonitor {
fn clone(&self) -> Self {
Self {
rpc: self.rpc.clone(),
status_receiver: self.status_receiver.clone(),
chain_id: self
.chain_id
.get()
.copied()
.map(|h| {
let lock = OnceLock::new();
let _ = lock.set(h);
lock
})
.unwrap_or_default(),
}
}
}
pub type DynServerBitcoinRpc = Arc<dyn IServerBitcoinRpc>;
#[async_trait::async_trait]
pub trait IServerBitcoinRpc: Debug + Send + Sync + 'static {
fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig;
fn get_url(&self) -> SafeUrl;
async fn get_block_count(&self) -> Result<u64>;
async fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
async fn get_block(&self, block_hash: &BlockHash) -> Result<Block>;
async fn get_feerate(&self) -> Result<Option<Feerate>>;
async fn submit_transaction(&self, transaction: Transaction) -> Result<()>;
async fn get_sync_progress(&self) -> Result<Option<f64>>;
async fn get_chain_id(&self) -> Result<ChainId>;
fn into_dyn(self) -> DynServerBitcoinRpc
where
Self: Sized,
{
Arc::new(self)
}
}