fedimint_server_bitcoin_rpc/
esplora.rs
1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use anyhow::{Context, bail};
5use bitcoin::{BlockHash, Network, Transaction};
6use fedimint_core::Feerate;
7use fedimint_core::envs::BitcoinRpcConfig;
8use fedimint_core::util::{FmtCompact as _, SafeUrl};
9use fedimint_logging::{LOG_BITCOIND_ESPLORA, LOG_SERVER};
10use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
11use tracing::{debug, info};
12
13const MAINNET: &str = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
15
16const TESTNET: &str = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
18
19const SIGNET: &str = "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6";
21
22const REGTEST: &str = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206";
25
26#[derive(Debug)]
27pub struct EsploraClient {
28 client: esplora_client::AsyncClient,
29 url: SafeUrl,
30 cached_network: OnceLock<Network>,
31}
32
33impl EsploraClient {
34 pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
35 info!(
36 target: LOG_SERVER,
37 %url,
38 "Initiallizing bitcoin esplora backend"
39 );
40 let without_trailing = url.as_str().trim_end_matches('/');
42
43 let builder = esplora_client::Builder::new(without_trailing);
44 let client = builder.build_async()?;
45 Ok(Self {
46 client,
47 url: url.clone(),
48 cached_network: OnceLock::new(),
49 })
50 }
51}
52
53#[async_trait::async_trait]
54impl IServerBitcoinRpc for EsploraClient {
55 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
56 BitcoinRpcConfig {
57 kind: "esplora".to_string(),
58 url: self.url.clone(),
59 }
60 }
61
62 fn get_url(&self) -> SafeUrl {
63 self.url.clone()
64 }
65
66 async fn get_network(&self) -> anyhow::Result<Network> {
67 if let Some(network) = self.cached_network.get() {
69 return Ok(*network);
70 }
71
72 let genesis_hash = self.client.get_block_hash(0).await.inspect_err(|err| {
74 debug!(
75 target: LOG_BITCOIND_ESPLORA,
76 err = %err.fmt_compact(),
77 "Error getting network (genesis hash) from esplora backend");
78 })?;
79
80 let network = match genesis_hash.to_string().as_str() {
81 MAINNET => Network::Bitcoin,
82 TESTNET => Network::Testnet,
83 SIGNET => Network::Signet,
84 REGTEST => Network::Regtest,
85 hash => {
86 bail!("Unknown genesis hash {hash}");
87 }
88 };
89
90 let _ = self.cached_network.set(network);
92 Ok(network)
93 }
94
95 async fn get_block_count(&self) -> anyhow::Result<u64> {
96 match self.client.get_height().await {
97 Ok(height) => Ok(u64::from(height) + 1),
98 Err(e) => Err(e.into()),
99 }
100 }
101
102 async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
103 Ok(self.client.get_block_hash(u32::try_from(height)?).await?)
104 }
105
106 async fn get_block(&self, block_hash: &BlockHash) -> anyhow::Result<bitcoin::Block> {
107 self.client
108 .get_block_by_hash(block_hash)
109 .await?
110 .context("Block with this hash is not available")
111 }
112
113 async fn get_feerate(&self) -> anyhow::Result<Option<Feerate>> {
114 let fee_estimates: HashMap<u16, f64> = self.client.get_fee_estimates().await?;
115
116 let fee_rate_vb = esplora_client::convert_fee_rate(1, fee_estimates).unwrap_or(1.0);
117
118 let fee_rate_kvb = fee_rate_vb * 1_000f32;
119
120 Ok(Some(Feerate {
121 sats_per_kvb: (fee_rate_kvb).ceil() as u64,
122 }))
123 }
124
125 async fn submit_transaction(&self, transaction: Transaction) {
126 let _ = self.client.broadcast(&transaction).await.map_err(|error| {
127 info!(target: LOG_BITCOIND_ESPLORA, ?error, "Error broadcasting transaction");
133 });
134 }
135
136 async fn get_sync_percentage(&self) -> anyhow::Result<Option<f64>> {
137 Ok(None)
138 }
139}