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::SafeUrl;
10use fedimint_core::{apply, async_trait_maybe_send};
11use fedimint_logging::LOG_BITCOIND_CORE;
12use tracing::{debug, warn};
13
14use crate::{IBitcoindRpc, format_err};
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    ) -> anyhow::Result<Self> {
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        Self::create_watch_only_wallet(&default_client, wallet_name)?;
48
49        let wallet_url_str = format!("{url_str}/wallet/{wallet_name}");
50        let client = ::bitcoincore_rpc::Client::new(&wallet_url_str, auth)?;
51        Ok(Self { client, network })
52    }
53
54    fn create_watch_only_wallet(
55        client: &::bitcoincore_rpc::Client,
56        wallet_name: &str,
57    ) -> anyhow::Result<()> {
58        let create_wallet = block_in_place(|| {
59            client.create_wallet(wallet_name, Some(true), Some(true), None, None)
60        });
61
62        match create_wallet {
63            Ok(_) => Ok(()),
64            Err(RpcError::JsonRpc(JsonRpcError::Rpc(rpc_err))) if rpc_err.code == -4 => {
65                // Wallet already exists → treat as success
66                Ok(())
67            }
68            Err(e) => Err(e.into()),
69        }
70    }
71}
72
73#[apply(async_trait_maybe_send!)]
74impl IBitcoindRpc for BitcoindClient {
75    async fn get_tx_block_height(&self, txid: &Txid) -> anyhow::Result<Option<u64>> {
76        let info = block_in_place(|| self.client.get_transaction(txid, Some(true)))
77            .map_err(|error| warn!(target: LOG_BITCOIND_CORE, ?error, "Unable to get transaction"));
78        let height = match info.ok().and_then(|info| info.info.blockhash) {
79            None => None,
80            Some(hash) => Some(block_in_place(|| self.client.get_block_header_info(&hash))?.height),
81        };
82        Ok(height.map(|h| h as u64))
83    }
84
85    async fn watch_script_history(&self, script: &ScriptBuf) -> anyhow::Result<()> {
86        let address = Address::from_script(script, self.network)?.to_string();
87        debug!(target: LOG_BITCOIND_CORE, %address, "Watching script history");
88
89        // First get the checksum for the descriptor
90        let descriptor = format!("addr({address})");
91        let descriptor_info = block_in_place(|| self.client.get_descriptor_info(&descriptor))?;
92        let checksum = descriptor_info
93            .checksum
94            .ok_or(anyhow::anyhow!("No checksum"))?;
95
96        // Import the descriptor
97        let import_results = block_in_place(|| {
98            self.client.import_descriptors(ImportDescriptors {
99                descriptor: format!("{descriptor}#{checksum}"),
100                timestamp: bitcoincore_rpc::json::Timestamp::Now,
101                active: Some(false),
102                range: None,
103                next_index: None,
104                internal: None,
105                label: Some(address.clone()),
106            })
107        })?;
108
109        // Verify that the import was successful
110        if import_results.iter().all(|r| r.success) {
111            Ok(())
112        } else {
113            Err(anyhow::anyhow!(
114                "Importing descriptor failed: {:?}",
115                import_results
116                    .into_iter()
117                    .filter(|r| !r.success)
118                    .collect::<Vec<_>>()
119            ))
120        }
121    }
122
123    async fn get_script_history(
124        &self,
125        script: &ScriptBuf,
126    ) -> anyhow::Result<Vec<bitcoin::Transaction>> {
127        let address = Address::from_script(script, self.network)?.to_string();
128        let mut results = vec![];
129        let list = block_in_place(|| {
130            self.client
131                .list_transactions(Some(&address), None, None, Some(true))
132        })?;
133        for tx in list {
134            let tx = block_in_place(|| self.client.get_transaction(&tx.info.txid, Some(true)))?;
135            let raw_tx = tx.transaction()?;
136            results.push(raw_tx);
137        }
138        Ok(results)
139    }
140
141    async fn get_txout_proof(&self, txid: Txid) -> anyhow::Result<TxOutProof> {
142        TxOutProof::consensus_decode_whole(
143            &block_in_place(|| self.client.get_tx_out_proof(&[txid], None))?,
144            &ModuleDecoderRegistry::default(),
145        )
146        .map_err(|error| format_err!("Could not decode tx: {}", error))
147    }
148}