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