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