Skip to main content

fedimint_bitcoind/
bitcoincore.rs

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