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 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 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 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}