fedimint_server_bitcoin_rpc/
bitcoind.rs1#[cfg(test)]
2mod tests;
3
4use anyhow::anyhow;
5use bitcoin::{BlockHash, Transaction};
6use bitcoincore_rpc::Error::JsonRpc;
7use bitcoincore_rpc::bitcoincore_rpc_json::EstimateMode;
8use bitcoincore_rpc::jsonrpc::Error::Rpc;
9use bitcoincore_rpc::{Auth, Client, RpcApi};
10use fedimint_core::envs::BitcoinRpcConfig;
11use fedimint_core::runtime::block_in_place;
12use fedimint_core::util::SafeUrl;
13use fedimint_core::{ChainId, Feerate};
14use fedimint_logging::LOG_SERVER;
15use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
16use tracing::info;
17
18#[derive(Debug)]
19pub struct BitcoindClient {
20 client: Client,
21 url: SafeUrl,
22}
23
24impl BitcoindClient {
25 pub fn new(username: String, password: String, url: &SafeUrl) -> anyhow::Result<Self> {
26 let auth = Auth::UserPass(username, password);
27
28 let url = url
29 .without_auth()
30 .map_err(|()| anyhow!("Failed to strip auth from Bitcoin Rpc Url"))?;
31
32 info!(
33 target: LOG_SERVER,
34 %url,
35 "Initializing bitcoin bitcoind backend"
36 );
37 Ok(Self {
38 client: Client::new(url.as_str(), auth)?,
39 url,
40 })
41 }
42}
43
44#[async_trait::async_trait]
45impl IServerBitcoinRpc for BitcoindClient {
46 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
47 BitcoinRpcConfig {
48 kind: "bitcoind".to_string(),
49 url: self.url.clone(),
50 }
51 }
52
53 fn get_url(&self) -> SafeUrl {
54 self.url.clone()
55 }
56
57 async fn get_block_count(&self) -> anyhow::Result<u64> {
58 block_in_place(|| self.client.get_block_count())
60 .map(|height| height + 1)
61 .map_err(anyhow::Error::from)
62 }
63
64 async fn get_block_count_and_initial_block_download(&self) -> anyhow::Result<(u64, bool)> {
65 let info = block_in_place(|| self.client.get_blockchain_info())?;
66 Ok((info.blocks + 1, info.initial_block_download))
67 }
68
69 async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
70 block_in_place(|| self.client.get_block_hash(height)).map_err(anyhow::Error::from)
71 }
72
73 async fn get_block(&self, hash: &BlockHash) -> anyhow::Result<bitcoin::Block> {
74 block_in_place(|| self.client.get_block(hash)).map_err(anyhow::Error::from)
75 }
76
77 async fn get_feerate(&self) -> anyhow::Result<Option<Feerate>> {
78 let feerate = block_in_place(|| {
79 self.client
80 .estimate_smart_fee(1, Some(EstimateMode::Conservative))
81 })?
82 .fee_rate
83 .map(|per_kb| Feerate {
84 sats_per_kvb: per_kb.to_sat(),
85 });
86
87 Ok(feerate)
88 }
89
90 async fn submit_transaction(&self, transaction: Transaction) -> anyhow::Result<()> {
91 match block_in_place(|| self.client.send_raw_transaction(&transaction)) {
92 Err(JsonRpc(Rpc(e))) if e.code == -27 => Ok(()),
97 Err(e) => Err(e.into()),
98 Ok(_) => Ok(()),
99 }
100 }
101
102 async fn get_sync_progress(&self) -> anyhow::Result<Option<f64>> {
103 Ok(Some(
104 block_in_place(|| self.client.get_blockchain_info())?.verification_progress,
105 ))
106 }
107
108 async fn get_chain_id(&self) -> anyhow::Result<ChainId> {
109 self.get_block_hash(1).await.map(ChainId::new)
110 }
111}