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 }