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
12// <https://blockstream.info/api/block-height/0>
13const MAINNET: &str = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
14
15// <https://blockstream.info/testnet/api/block-height/0>
16const TESTNET: &str = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
17
18// <https://mempool.space/signet/api/block-height/0>
19const SIGNET: &str = "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6";
20
21// See <https://bitcoin.stackexchange.com/questions/122778/is-the-regtest-genesis-hash-always-the-same-or-not>
22// <https://github.com/bitcoin/bitcoin/blob/d82283950f5ff3b2116e705f931c6e89e5fdd0be/src/kernel/chainparams.cpp#L478>
23const 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        // URL needs to have any trailing path including '/' removed
34        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            // `esplora-client` v0.6.0 only surfaces HTTP error codes, which prevents us
107            // from detecting errors for transactions already submitted.
108            // TODO: Suppress `esplora-client` already submitted errors when client is
109            // updated
110            // https://github.com/fedimint/fedimint/issues/3732
111            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}