fedimint_bitcoind/
esplora.rs1use std::collections::HashMap;
2
3use anyhow::{Context, bail, format_err};
4use bitcoin::{BlockHash, Network, ScriptBuf, Transaction, Txid};
5use fedimint_core::envs::BitcoinRpcConfig;
6use fedimint_core::txoproof::TxOutProof;
7use fedimint_core::util::SafeUrl;
8use fedimint_core::{Feerate, apply, async_trait_maybe_send};
9use fedimint_logging::LOG_BITCOIND_ESPLORA;
10use tracing::info;
11
12use crate::{DynBitcoindRpc, IBitcoindRpc, IBitcoindRpcFactory};
13
14#[derive(Debug)]
15pub struct EsploraFactory;
16
17impl IBitcoindRpcFactory for EsploraFactory {
18 fn create_connection(&self, url: &SafeUrl) -> anyhow::Result<DynBitcoindRpc> {
19 Ok(EsploraClient::new(url)?.into())
20 }
21}
22
23#[derive(Debug)]
24struct EsploraClient {
25 client: esplora_client::AsyncClient,
26 url: SafeUrl,
27}
28
29impl EsploraClient {
30 fn new(url: &SafeUrl) -> anyhow::Result<Self> {
31 let without_trailing = url.as_str().trim_end_matches('/');
33
34 let builder = esplora_client::Builder::new(without_trailing);
35 let client = builder.build_async()?;
36 Ok(Self {
37 client,
38 url: url.clone(),
39 })
40 }
41}
42
43#[apply(async_trait_maybe_send!)]
44impl IBitcoindRpc for EsploraClient {
45 async fn get_network(&self) -> anyhow::Result<Network> {
46 let genesis_height: u32 = 0;
47 let genesis_hash = self.client.get_block_hash(genesis_height).await?;
48
49 let network = match genesis_hash.to_string().as_str() {
50 crate::MAINNET_GENESIS_BLOCK_HASH => Network::Bitcoin,
51 crate::TESTNET_GENESIS_BLOCK_HASH => Network::Testnet,
52 crate::SIGNET_GENESIS_BLOCK_HASH => Network::Signet,
53 crate::REGTEST_GENESIS_BLOCK_HASH => Network::Regtest,
54 hash => {
55 bail!("Unknown genesis hash {hash}");
56 }
57 };
58
59 Ok(network)
60 }
61
62 async fn get_block_count(&self) -> anyhow::Result<u64> {
63 match self.client.get_height().await {
64 Ok(height) => Ok(u64::from(height) + 1),
65 Err(e) => Err(e.into()),
66 }
67 }
68
69 async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
70 Ok(self.client.get_block_hash(u32::try_from(height)?).await?)
71 }
72
73 async fn get_block(&self, block_hash: &BlockHash) -> anyhow::Result<bitcoin::Block> {
74 self.client
75 .get_block_by_hash(block_hash)
76 .await?
77 .context("Block with this hash is not available")
78 }
79
80 async fn get_fee_rate(&self, confirmation_target: u16) -> anyhow::Result<Option<Feerate>> {
81 let fee_estimates: HashMap<u16, f64> = self.client.get_fee_estimates().await?;
82
83 let fee_rate_vb =
84 esplora_client::convert_fee_rate(confirmation_target.into(), fee_estimates)
85 .unwrap_or(1.0);
86
87 let fee_rate_kvb = fee_rate_vb * 1_000f32;
88
89 Ok(Some(Feerate {
90 sats_per_kvb: (fee_rate_kvb).ceil() as u64,
91 }))
92 }
93
94 async fn submit_transaction(&self, transaction: Transaction) {
95 let _ = self.client.broadcast(&transaction).await.map_err(|error| {
96 info!(target: LOG_BITCOIND_ESPLORA, ?error, "Error broadcasting transaction");
102 });
103 }
104
105 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
106 Ok(self
107 .client
108 .get_tx_status(txid)
109 .await?
110 .block_height
111 .map(u64::from))
112 }
113
114 async fn is_tx_in_block(
115 &self,
116 txid: &Txid,
117 block_hash: &BlockHash,
118 block_height: u64,
119 ) -> anyhow::Result<bool> {
120 let tx_status = self.client.get_tx_status(txid).await?;
121
122 let is_in_block_height = tx_status
123 .block_height
124 .is_some_and(|height| u64::from(height) == block_height);
125
126 if is_in_block_height {
127 let tx_block_hash = tx_status.block_hash.ok_or(anyhow::format_err!(
128 "Tx has a block height without a block hash"
129 ))?;
130 anyhow::ensure!(
131 block_hash == &tx_block_hash,
132 "Block height for block hash does not match expected height"
133 );
134 }
135
136 Ok(is_in_block_height)
137 }
138
139 async fn watch_script_history(&self, _: &ScriptBuf) -> anyhow::Result<()> {
140 Ok(())
142 }
143
144 async fn get_script_history(
145 &self,
146 script: &ScriptBuf,
147 ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
148 let transactions = self
149 .client
150 .scripthash_txs(script, None)
151 .await?
152 .into_iter()
153 .map(|tx| tx.to_tx())
154 .collect::<Vec<_>>();
155
156 Ok(transactions)
157 }
158
159 async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
160 let proof = self
161 .client
162 .get_merkle_block(&txid)
163 .await?
164 .ok_or(format_err!("No merkle proof found"))?;
165
166 Ok(TxOutProof {
167 block_header: proof.header,
168 merkle_proof: proof.txn,
169 })
170 }
171
172 async fn get_sync_percentage(&self) -> anyhow::Result<Option<f64>> {
173 Ok(None)
174 }
175
176 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
177 BitcoinRpcConfig {
178 kind: "esplora".to_string(),
179 url: self.url.clone(),
180 }
181 }
182}