/ fedimint-bitcoind / src / esplora.rs
esplora.rs
  1  use std::collections::HashMap;
  2  
  3  use anyhow::format_err;
  4  use bitcoin::{BlockHash, Network, ScriptBuf, Transaction, Txid};
  5  use fedimint_core::task::TaskHandle;
  6  use fedimint_core::txoproof::TxOutProof;
  7  use fedimint_core::util::SafeUrl;
  8  use fedimint_core::{apply, async_trait_maybe_send, Feerate};
  9  use hex::ToHex;
 10  use tracing::{info, warn};
 11  
 12  use crate::{DynBitcoindRpc, IBitcoindRpc, IBitcoindRpcFactory, RetryClient};
 13  
 14  #[derive(Debug)]
 15  pub struct EsploraFactory;
 16  
 17  impl IBitcoindRpcFactory for EsploraFactory {
 18      fn create_connection(
 19          &self,
 20          url: &SafeUrl,
 21          handle: TaskHandle,
 22      ) -> anyhow::Result<DynBitcoindRpc> {
 23          Ok(RetryClient::new(EsploraClient::new(url)?, handle).into())
 24      }
 25  }
 26  
 27  #[derive(Debug)]
 28  pub struct EsploraClient(esplora_client::AsyncClient);
 29  
 30  impl EsploraClient {
 31      fn new(url: &SafeUrl) -> anyhow::Result<Self> {
 32          // URL needs to have any trailing path including '/' removed
 33          let without_trailing = url.as_str().trim_end_matches('/');
 34  
 35          let builder = esplora_client::Builder::new(without_trailing);
 36          let client = builder.build_async()?;
 37          Ok(Self(client))
 38      }
 39  }
 40  
 41  #[apply(async_trait_maybe_send!)]
 42  impl IBitcoindRpc for EsploraClient {
 43      async fn get_network(&self) -> anyhow::Result<Network> {
 44          let genesis_height: u32 = 0;
 45          let genesis_hash = self.0.get_block_hash(genesis_height).await?;
 46  
 47          let network = match genesis_hash.encode_hex::<String>().as_str() {
 48              crate::MAINNET_GENESIS_BLOCK_HASH => Network::Bitcoin,
 49              crate::TESTNET_GENESIS_BLOCK_HASH => Network::Testnet,
 50              crate::SIGNET_GENESIS_BLOCK_HASH => Network::Signet,
 51              hash => {
 52                  warn!("Unknown genesis hash {hash} - assuming regtest");
 53                  Network::Regtest
 54              }
 55          };
 56  
 57          Ok(network)
 58      }
 59  
 60      async fn get_block_count(&self) -> anyhow::Result<u64> {
 61          match self.0.get_height().await {
 62              Ok(height) => Ok(height as u64 + 1),
 63              Err(e) => Err(e.into()),
 64          }
 65      }
 66  
 67      async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
 68          Ok(self.0.get_block_hash(height as u32).await?)
 69      }
 70  
 71      async fn get_fee_rate(&self, confirmation_target: u16) -> anyhow::Result<Option<Feerate>> {
 72          let fee_estimates: HashMap<String, f64> = self.0.get_fee_estimates().await?;
 73  
 74          let fee_rate_vb =
 75              esplora_client::convert_fee_rate(confirmation_target.into(), fee_estimates)?;
 76  
 77          let fee_rate_kvb = fee_rate_vb * 1_000f32;
 78  
 79          Ok(Some(Feerate {
 80              sats_per_kvb: (fee_rate_kvb).ceil() as u64,
 81          }))
 82      }
 83  
 84      async fn submit_transaction(&self, transaction: Transaction) {
 85          let _ = self.0.broadcast(&transaction).await.map_err(|error| {
 86              // `esplora-client` v0.6.0 only surfaces HTTP error codes, which prevents us
 87              // from detecting errors for transactions already submitted.
 88              // TODO: Suppress `esplora-client` already submitted errors when client is
 89              // updated
 90              // https://github.com/fedimint/fedimint/issues/3732
 91              info!(?error, "Error broadcasting transaction");
 92          });
 93      }
 94  
 95      async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
 96          Ok(self
 97              .0
 98              .get_tx_status(txid)
 99              .await?
100              .block_height
101              .map(|height| height as u64))
102      }
103  
104      async fn watch_script_history(&self, _: &ScriptBuf) -> anyhow::Result<()> {
105          // no watching needed, has all the history already
106          Ok(())
107      }
108  
109      async fn get_script_history(
110          &self,
111          script: &ScriptBuf,
112      ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
113          let transactions = self
114              .0
115              .scripthash_txs(script, None)
116              .await?
117              .into_iter()
118              .map(|tx| tx.to_tx())
119              .collect::<Vec<_>>();
120  
121          Ok(transactions)
122      }
123      async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
124          let proof = self
125              .0
126              .get_merkle_block(&txid)
127              .await?
128              .ok_or(format_err!("No merkle proof found"))?;
129  
130          Ok(TxOutProof {
131              block_header: proof.header,
132              merkle_proof: proof.txn,
133          })
134      }
135  }