fedimint_server_bitcoin_rpc/
bitcoind.rs

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