fedimint_bitcoind/
lib.rs
1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_sign_loss)]
4#![allow(clippy::missing_errors_doc)]
5#![allow(clippy::missing_panics_doc)]
6#![allow(clippy::module_name_repetitions)]
7#![allow(clippy::similar_names)]
8
9use std::env;
10use std::fmt::Debug;
11use std::sync::Arc;
12
13use anyhow::{Result, format_err};
14use bitcoin::{ScriptBuf, Transaction, Txid};
15use esplora_client::{AsyncClient, Builder};
16use fedimint_core::envs::FM_FORCE_BITCOIN_RPC_URL_ENV;
17use fedimint_core::txoproof::TxOutProof;
18use fedimint_core::util::SafeUrl;
19use fedimint_core::{apply, async_trait_maybe_send};
20
21pub fn create_esplora_rpc(url: &SafeUrl) -> Result<DynBitcoindRpc> {
22 let url = env::var(FM_FORCE_BITCOIN_RPC_URL_ENV)
23 .ok()
24 .map(|s| SafeUrl::parse(&s))
25 .transpose()?
26 .unwrap_or_else(|| url.clone());
27
28 Ok(EsploraClient::new(&url)?.into_dyn())
29}
30
31pub type DynBitcoindRpc = Arc<dyn IBitcoindRpc + Send + Sync>;
32
33#[apply(async_trait_maybe_send!)]
37pub trait IBitcoindRpc: Debug + Send + Sync + 'static {
38 async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>>;
40
41 async fn get_script_history(&self, script: &ScriptBuf) -> Result<Vec<Transaction>>;
43
44 async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof>;
46
47 fn into_dyn(self) -> DynBitcoindRpc
48 where
49 Self: Sized,
50 {
51 Arc::new(self)
52 }
53}
54
55#[derive(Debug)]
56pub struct EsploraClient {
57 client: AsyncClient,
58}
59
60impl EsploraClient {
61 pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
62 Ok(Self {
63 client: Builder::new(url.as_str().trim_end_matches('/')).build_async()?,
65 })
66 }
67}
68
69#[apply(async_trait_maybe_send!)]
70impl IBitcoindRpc for EsploraClient {
71 async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
72 Ok(self
73 .client
74 .get_tx_status(txid)
75 .await?
76 .block_height
77 .map(u64::from))
78 }
79
80 async fn get_script_history(
81 &self,
82 script: &ScriptBuf,
83 ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
84 let transactions = self
85 .client
86 .scripthash_txs(script, None)
87 .await?
88 .into_iter()
89 .map(|tx| tx.to_tx())
90 .collect::<Vec<_>>();
91
92 Ok(transactions)
93 }
94
95 async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
96 let proof = self
97 .client
98 .get_merkle_block(&txid)
99 .await?
100 .ok_or(format_err!("No merkle proof found"))?;
101
102 Ok(TxOutProof {
103 block_header: proof.header,
104 merkle_proof: proof.txn,
105 })
106 }
107}