fedimint_server_bitcoin_rpc/
esplora.rs1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use anyhow::Context;
5use bitcoin::{BlockHash, Transaction};
6use fedimint_core::envs::BitcoinRpcConfig;
7use fedimint_core::util::{FmtCompact, SafeUrl};
8use fedimint_core::{ChainId, Feerate};
9use fedimint_logging::{LOG_BITCOIND_ESPLORA, LOG_SERVER};
10use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
11use tracing::info;
12
13#[derive(Debug)]
14pub struct EsploraClient {
15 client: esplora_client::AsyncClient,
16 url: SafeUrl,
17 cached_chain_id: OnceLock<ChainId>,
18}
19
20impl EsploraClient {
21 pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
22 info!(
23 target: LOG_SERVER,
24 %url,
25 "Initializing bitcoin esplora backend"
26 );
27 let without_trailing = url.as_str().trim_end_matches('/');
29
30 let builder = esplora_client::Builder::new(without_trailing);
31 let client = builder.build_async()?;
32 Ok(Self {
33 client,
34 url: url.clone(),
35 cached_chain_id: OnceLock::new(),
36 })
37 }
38}
39
40#[async_trait::async_trait]
41impl IServerBitcoinRpc for EsploraClient {
42 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
43 BitcoinRpcConfig {
44 kind: "esplora".to_string(),
45 url: self.url.clone(),
46 }
47 }
48
49 fn get_url(&self) -> SafeUrl {
50 self.url.clone()
51 }
52
53 async fn get_block_count(&self) -> anyhow::Result<u64> {
54 match self.client.get_height().await {
55 Ok(height) => Ok(u64::from(height) + 1),
56 Err(e) => Err(e.into()),
57 }
58 }
59
60 async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
61 Ok(self.client.get_block_hash(u32::try_from(height)?).await?)
62 }
63
64 async fn get_block(&self, block_hash: &BlockHash) -> anyhow::Result<bitcoin::Block> {
65 self.client
66 .get_block_by_hash(block_hash)
67 .await?
68 .context("Block with this hash is not available")
69 }
70
71 async fn get_feerate(&self) -> anyhow::Result<Option<Feerate>> {
72 let fee_estimates: HashMap<u16, f64> = self.client.get_fee_estimates().await?;
73
74 let fee_rate_vb = esplora_client::convert_fee_rate(1, fee_estimates).unwrap_or(1.0);
75
76 let fee_rate_kvb = fee_rate_vb * 1_000f32;
77
78 Ok(Some(Feerate {
79 sats_per_kvb: (fee_rate_kvb).ceil() as u64,
80 }))
81 }
82
83 async fn submit_transaction(&self, transaction: Transaction) {
84 let _ = self.client.broadcast(&transaction).await.map_err(|err| {
85 info!(target: LOG_BITCOIND_ESPLORA, err = %err.fmt_compact(), "Error broadcasting transaction");
91 });
92 }
93
94 async fn get_sync_progress(&self) -> anyhow::Result<Option<f64>> {
95 Ok(None)
96 }
97
98 async fn get_chain_id(&self) -> anyhow::Result<ChainId> {
99 if let Some(chain_id) = self.cached_chain_id.get() {
100 return Ok(*chain_id);
101 }
102
103 let chain_id = ChainId::new(self.get_block_hash(1).await?);
104 let _ = self.cached_chain_id.set(chain_id);
105 Ok(chain_id)
106 }
107}