fedimint_server_bitcoin_rpc/
bitcoind.rs

1use anyhow::{Context, anyhow};
2use bitcoin::{BlockHash, Network, Transaction};
3use bitcoincore_rpc::Error::JsonRpc;
4use bitcoincore_rpc::bitcoincore_rpc_json::EstimateMode;
5use bitcoincore_rpc::jsonrpc::Error::Rpc;
6use bitcoincore_rpc::{Auth, Client, RpcApi};
7use fedimint_core::Feerate;
8use fedimint_core::envs::BitcoinRpcConfig;
9use fedimint_core::runtime::block_in_place;
10use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl};
11use fedimint_logging::{LOG_BITCOIND_CORE, LOG_SERVER};
12use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
13use tracing::{debug, info};
14
15#[derive(Debug)]
16pub struct BitcoindClient {
17    client: Client,
18    url: SafeUrl,
19}
20
21impl BitcoindClient {
22    pub fn new(url: &SafeUrl) -> anyhow::Result<Self> {
23        let auth = Auth::UserPass(
24            url.username().to_owned(),
25            url.password()
26                .context("Bitcoin RPC URL is missing password")?
27                .to_owned(),
28        );
29
30        let url = url
31            .without_auth()
32            .map_err(|()| anyhow!("Failed to strip auth from Bitcoin Rpc Url"))?;
33
34        info!(
35            target: LOG_SERVER,
36            %url,
37            "Initiallizing bitcoin bitcoind backend"
38        );
39        Ok(Self {
40            client: Client::new(url.as_str(), auth)?,
41            url,
42        })
43    }
44}
45
46#[async_trait::async_trait]
47impl IServerBitcoinRpc for BitcoindClient {
48    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
49        BitcoinRpcConfig {
50            kind: "bitcoind".to_string(),
51            url: self.url.clone(),
52        }
53    }
54
55    fn get_url(&self) -> SafeUrl {
56        self.url.clone()
57    }
58
59    async fn get_network(&self) -> anyhow::Result<Network> {
60        block_in_place(|| self.client.get_blockchain_info())
61            .map(|network| network.chain)
62            .map_err(anyhow::Error::from)
63            .inspect_err(|err| {
64                debug!(
65                target: LOG_BITCOIND_CORE,
66                err = %err.fmt_compact_anyhow(),
67                "Error getting network from bitcoind");
68            })
69    }
70
71    async fn get_block_count(&self) -> anyhow::Result<u64> {
72        // The RPC function is confusingly named and actually returns the block height
73        block_in_place(|| self.client.get_block_count())
74            .map(|height| height + 1)
75            .map_err(anyhow::Error::from)
76    }
77
78    async fn get_block_hash(&self, height: u64) -> anyhow::Result<BlockHash> {
79        block_in_place(|| self.client.get_block_hash(height)).map_err(anyhow::Error::from)
80    }
81
82    async fn get_block(&self, hash: &BlockHash) -> anyhow::Result<bitcoin::Block> {
83        block_in_place(|| self.client.get_block(hash)).map_err(anyhow::Error::from)
84    }
85
86    async fn get_feerate(&self) -> anyhow::Result<Option<Feerate>> {
87        let feerate = block_in_place(|| {
88            self.client
89                .estimate_smart_fee(1, Some(EstimateMode::Conservative))
90        })?
91        .fee_rate
92        .map(|per_kb| Feerate {
93            sats_per_kvb: per_kb.to_sat(),
94        });
95
96        Ok(feerate)
97    }
98
99    async fn submit_transaction(&self, transaction: Transaction) {
100        match block_in_place(|| self.client.send_raw_transaction(&transaction)) {
101            // Bitcoin core's RPC will return error code -27 if a transaction is already in a block.
102            // This is considered a success case, so we don't surface the error log.
103            //
104            // https://github.com/bitcoin/bitcoin/blob/daa56f7f665183bcce3df146f143be37f33c123e/src/rpc/protocol.h#L48
105            Err(JsonRpc(Rpc(e))) if e.code == -27 => (),
106            Err(e) => info!(target: LOG_BITCOIND_CORE, ?e, "Error broadcasting transaction"),
107            Ok(_) => (),
108        }
109    }
110
111    async fn get_sync_percentage(&self) -> anyhow::Result<Option<f64>> {
112        Ok(Some(
113            block_in_place(|| self.client.get_blockchain_info())?.verification_progress,
114        ))
115    }
116}