Skip to main content

fedimint_server_bitcoin_rpc/
bitcoind.rs

1#[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        // The RPC function is confusingly named and actually returns the block height
59        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            // Bitcoin core's RPC will return error code -27 if a transaction is already in a block.
93            // This is considered a success case, so we don't surface it as an error.
94            //
95            // https://github.com/bitcoin/bitcoin/blob/daa56f7f665183bcce3df146f143be37f33c123e/src/rpc/protocol.h#L48
96            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}