Skip to main content

fedimint_bitcoind/
bitcoincore.rs

1use bitcoin::{Address, ScriptBuf, Txid};
2use bitcoincore_rpc::json::ImportDescriptors;
3use bitcoincore_rpc::jsonrpc::error::Error as JsonRpcError;
4use bitcoincore_rpc::{Auth, Error as RpcError, RpcApi};
5use fedimint_core::encoding::Decodable;
6use fedimint_core::module::registry::ModuleDecoderRegistry;
7use fedimint_core::task::block_in_place;
8use fedimint_core::txoproof::TxOutProof;
9use fedimint_core::util::{FmtCompact, SafeUrl};
10use fedimint_core::{apply, async_trait_maybe_send};
11use fedimint_logging::LOG_BITCOIND_CORE;
12use tracing::{debug, warn};
13
14use crate::{BitcoinRpcError, BlockchainInfo, IBitcoindRpc};
15
16#[derive(Debug)]
17pub struct BitcoindClient {
18    client: ::bitcoincore_rpc::Client,
19    network: bitcoin::Network,
20}
21
22impl BitcoindClient {
23    pub fn new(
24        url: &SafeUrl,
25        username: String,
26        password: String,
27        wallet_name: &str,
28        network: bitcoin::Network,
29    ) -> Result<Self, BitcoinRpcError> {
30        let auth = Auth::UserPass(username, password);
31        let url_str = if let Some(port) = url.port() {
32            format!(
33                "{}://{}:{port}",
34                url.scheme(),
35                url.host_str().unwrap_or("127.0.0.1")
36            )
37        } else {
38            format!(
39                "{}://{}",
40                url.scheme(),
41                url.host_str().unwrap_or("127.0.0.1")
42            )
43        };
44
45        let default_url_str = format!("{url_str}/wallet/");
46        let default_client = ::bitcoincore_rpc::Client::new(&default_url_str, auth.clone())
47            .map_err(|source| BitcoinRpcError::InvalidUrl {
48                url: default_url_str.clone(),
49                source: Box::new(source),
50            })?;
51        Self::create_watch_only_wallet(&default_client, wallet_name)?;
52
53        let wallet_url_str = format!("{url_str}/wallet/{wallet_name}");
54        let client = ::bitcoincore_rpc::Client::new(&wallet_url_str, auth).map_err(|source| {
55            BitcoinRpcError::InvalidUrl {
56                url: wallet_url_str.clone(),
57                source: Box::new(source),
58            }
59        })?;
60        Ok(Self { client, network })
61    }
62
63    fn create_watch_only_wallet(
64        client: &::bitcoincore_rpc::Client,
65        wallet_name: &str,
66    ) -> Result<(), BitcoinRpcError> {
67        let create_wallet = block_in_place(|| {
68            client.create_wallet(wallet_name, Some(true), Some(true), None, None)
69        });
70
71        match create_wallet {
72            Ok(_) => Ok(()),
73            Err(RpcError::JsonRpc(JsonRpcError::Rpc(rpc_err))) if rpc_err.code == -4 => {
74                // Wallet already exists → treat as success
75                Ok(())
76            }
77            Err(e) => Err(BitcoinRpcError::Backend(Box::new(e))),
78        }
79    }
80}
81
82#[apply(async_trait_maybe_send!)]
83impl IBitcoindRpc for BitcoindClient {
84    async fn get_tx_block_height(&self, txid: &Txid) -> Result<Option<u64>, BitcoinRpcError> {
85        let info = block_in_place(|| self.client.get_transaction(txid, Some(true)))
86            .map_err(|err| warn!(target: LOG_BITCOIND_CORE, err = %err.fmt_compact(), "Unable to get transaction"));
87        let height = match info.ok().and_then(|info| info.info.blockhash) {
88            None => None,
89            Some(hash) => Some(
90                block_in_place(|| self.client.get_block_header_info(&hash))
91                    .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?
92                    .height,
93            ),
94        };
95        Ok(height.map(|h| h as u64))
96    }
97
98    async fn watch_script_history(&self, script: &ScriptBuf) -> Result<(), BitcoinRpcError> {
99        let address = Address::from_script(script, self.network)
100            .map_err(|err| BitcoinRpcError::NonStandardScript(Box::new(err)))?
101            .to_string();
102        debug!(target: LOG_BITCOIND_CORE, %address, "Watching script history");
103
104        // First get the checksum for the descriptor
105        let descriptor = format!("addr({address})");
106        let descriptor_info = block_in_place(|| self.client.get_descriptor_info(&descriptor))
107            .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
108        let checksum =
109            descriptor_info
110                .checksum
111                .ok_or_else(|| BitcoinRpcError::InvalidResponse {
112                    message: "Descriptor info carries no checksum".to_string(),
113                })?;
114
115        // Import the descriptor
116        let import_results = block_in_place(|| {
117            self.client.import_descriptors(ImportDescriptors {
118                descriptor: format!("{descriptor}#{checksum}"),
119                timestamp: bitcoincore_rpc::json::Timestamp::Now,
120                active: Some(false),
121                range: None,
122                next_index: None,
123                internal: None,
124                label: Some(address.clone()),
125            })
126        })
127        .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
128
129        // Verify that the import was successful
130        if import_results.iter().all(|r| r.success) {
131            Ok(())
132        } else {
133            Err(BitcoinRpcError::InvalidResponse {
134                message: format!(
135                    "Importing descriptor failed: {:?}",
136                    import_results
137                        .into_iter()
138                        .filter(|r| !r.success)
139                        .collect::<Vec<_>>()
140                ),
141            })
142        }
143    }
144
145    async fn get_script_history(
146        &self,
147        script: &ScriptBuf,
148    ) -> Result<Vec<bitcoin::Transaction>, BitcoinRpcError> {
149        let address = Address::from_script(script, self.network)
150            .map_err(|err| BitcoinRpcError::NonStandardScript(Box::new(err)))?
151            .to_string();
152        let mut results = vec![];
153        let list = block_in_place(|| {
154            self.client
155                .list_transactions(Some(&address), None, None, Some(true))
156        })
157        .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
158        for tx in list {
159            let tx = block_in_place(|| self.client.get_transaction(&tx.info.txid, Some(true)))
160                .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
161            let raw_tx = tx
162                .transaction()
163                .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
164            results.push(raw_tx);
165        }
166        Ok(results)
167    }
168
169    async fn get_txout_proof(&self, txid: Txid) -> Result<TxOutProof, BitcoinRpcError> {
170        TxOutProof::consensus_decode_whole(
171            &block_in_place(|| self.client.get_tx_out_proof(&[txid], None))
172                .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?,
173            &ModuleDecoderRegistry::default(),
174        )
175        .map_err(BitcoinRpcError::Decode)
176    }
177
178    async fn get_info(&self) -> Result<BlockchainInfo, BitcoinRpcError> {
179        let info = block_in_place(|| self.client.get_blockchain_info())
180            .map_err(|err| BitcoinRpcError::Backend(Box::new(err)))?;
181        Ok(BlockchainInfo {
182            block_height: info.blocks,
183            synced: !info.initial_block_download,
184        })
185    }
186}