1use bitcoin::{Address, Amount, Network, Txid};
4use bitcoincore_rpc::json::{
5 GetBlockchainInfoResult, GetNetworkInfoResult, GetRawTransactionResult, GetTransactionResult,
6};
7use bitcoincore_rpc::{Auth, Client, RpcApi};
8use serde::Serialize;
9use std::sync::{Arc, RwLock};
10use std::time::Duration;
11
12use crate::error::{BitcoinError, Result};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum BitcoinNetwork {
17 Mainnet,
18 Testnet,
19 Testnet4,
21 Regtest,
22 Signet,
23}
24
25impl From<BitcoinNetwork> for Network {
26 fn from(network: BitcoinNetwork) -> Self {
27 match network {
28 BitcoinNetwork::Mainnet => Network::Bitcoin,
29 BitcoinNetwork::Testnet => Network::Testnet,
30 BitcoinNetwork::Testnet4 => Network::Testnet,
32 BitcoinNetwork::Regtest => Network::Regtest,
33 BitcoinNetwork::Signet => Network::Signet,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct ReconnectConfig {
41 pub max_retries: u32,
43 pub initial_delay: Duration,
45 pub max_delay: Duration,
47 pub backoff_multiplier: f64,
49}
50
51impl Default for ReconnectConfig {
52 fn default() -> Self {
53 Self {
54 max_retries: 5,
55 initial_delay: Duration::from_millis(500),
56 max_delay: Duration::from_secs(30),
57 backoff_multiplier: 2.0,
58 }
59 }
60}
61
62#[derive(Clone)]
64struct ConnectionParams {
65 url: String,
66 user: String,
67 password: String,
68}
69
70pub struct BitcoinClient {
92 client: Arc<RwLock<Client>>,
93 network: BitcoinNetwork,
94 connection_params: ConnectionParams,
95 reconnect_config: ReconnectConfig,
96}
97
98impl BitcoinClient {
99 pub fn new(url: &str, user: &str, password: &str, network: BitcoinNetwork) -> Result<Self> {
117 Self::with_config(url, user, password, network, ReconnectConfig::default())
118 }
119
120 pub fn with_config(
122 url: &str,
123 user: &str,
124 password: &str,
125 network: BitcoinNetwork,
126 reconnect_config: ReconnectConfig,
127 ) -> Result<Self> {
128 let client = Client::new(url, Auth::UserPass(user.to_string(), password.to_string()))?;
129
130 tracing::info!(url = url, network = ?network, "Bitcoin RPC client connected");
131
132 Ok(Self {
133 client: Arc::new(RwLock::new(client)),
134 network,
135 connection_params: ConnectionParams {
136 url: url.to_string(),
137 user: user.to_string(),
138 password: password.to_string(),
139 },
140 reconnect_config,
141 })
142 }
143
144 fn reconnect(&self) -> Result<()> {
146 let params = &self.connection_params;
147 let new_client = Client::new(
148 ¶ms.url,
149 Auth::UserPass(params.user.clone(), params.password.clone()),
150 )?;
151
152 let mut client = self.client.write().unwrap();
153 *client = new_client;
154
155 tracing::info!("Bitcoin RPC client reconnected");
156 Ok(())
157 }
158
159 fn with_retry<T, F>(&self, operation: F) -> Result<T>
161 where
162 F: Fn(&Client) -> std::result::Result<T, bitcoincore_rpc::Error>,
163 {
164 let mut last_error = None;
165 let mut delay = self.reconnect_config.initial_delay;
166
167 for attempt in 0..=self.reconnect_config.max_retries {
168 let client = self.client.read().unwrap();
169 match operation(&client) {
170 Ok(result) => return Ok(result),
171 Err(e) => {
172 last_error = Some(e);
173 drop(client); if attempt < self.reconnect_config.max_retries {
176 tracing::warn!(
177 attempt = attempt + 1,
178 max_retries = self.reconnect_config.max_retries,
179 delay_ms = delay.as_millis(),
180 "Bitcoin RPC failed, attempting reconnection"
181 );
182
183 std::thread::sleep(delay);
184
185 if let Err(reconnect_err) = self.reconnect() {
187 tracing::warn!(error = %reconnect_err, "Reconnection failed");
188 }
189
190 delay = std::cmp::min(
192 Duration::from_secs_f64(
193 delay.as_secs_f64() * self.reconnect_config.backoff_multiplier,
194 ),
195 self.reconnect_config.max_delay,
196 );
197 }
198 }
199 }
200 }
201
202 Err(BitcoinError::Rpc(last_error.unwrap()))
203 }
204
205 pub fn network(&self) -> BitcoinNetwork {
207 self.network
208 }
209
210 pub fn health_check(&self) -> Result<bool> {
212 match self.with_retry(|c| c.get_blockchain_info()) {
213 Ok(_) => Ok(true),
214 Err(e) => {
215 tracing::warn!(error = %e, "Bitcoin RPC health check failed");
216 Ok(false)
217 }
218 }
219 }
220
221 pub fn get_blockchain_info(&self) -> Result<GetBlockchainInfoResult> {
223 self.with_retry(|c| c.get_blockchain_info())
224 }
225
226 pub fn get_network_info(&self) -> Result<GetNetworkInfoResult> {
228 self.with_retry(|c| c.get_network_info())
229 }
230
231 pub fn get_block_height(&self) -> Result<u64> {
233 let info = self.with_retry(|c| c.get_blockchain_info())?;
234 Ok(info.blocks)
235 }
236
237 pub fn get_best_block_hash(&self) -> Result<bitcoin::BlockHash> {
239 self.with_retry(|c| c.get_best_block_hash())
240 }
241
242 pub fn get_new_address(
244 &self,
245 label: Option<&str>,
246 ) -> Result<Address<bitcoin::address::NetworkUnchecked>> {
247 self.with_retry(|c| c.get_new_address(label, None))
248 }
249
250 pub fn get_received_by_address(
252 &self,
253 address: &Address,
254 min_confirmations: Option<u32>,
255 ) -> Result<Amount> {
256 self.with_retry(|c| c.get_received_by_address(address, min_confirmations))
257 }
258
259 pub fn get_transaction(&self, txid: &Txid) -> Result<GetTransactionResult> {
261 self.with_retry(|c| c.get_transaction(txid, None))
262 }
263
264 pub fn get_raw_transaction(&self, txid: &Txid) -> Result<GetRawTransactionResult> {
266 self.with_retry(|c| c.get_raw_transaction_info(txid, None))
267 }
268
269 pub fn list_unspent(
271 &self,
272 min_conf: Option<usize>,
273 max_conf: Option<usize>,
274 addresses: Option<&[&Address<bitcoin::address::NetworkChecked>]>,
275 ) -> Result<Vec<bitcoincore_rpc::json::ListUnspentResultEntry>> {
276 self.with_retry(|c| c.list_unspent(min_conf, max_conf, addresses, None, None))
277 }
278
279 pub fn get_balance(&self) -> Result<Amount> {
281 self.with_retry(|c| c.get_balance(None, None))
282 }
283
284 pub fn validate_address(&self, address: &str) -> Result<AddressValidation> {
286 let parsed = address
288 .parse::<Address<bitcoin::address::NetworkUnchecked>>()
289 .map_err(|e| BitcoinError::InvalidAddress(e.to_string()));
290
291 match parsed {
292 Ok(_addr) => Ok(AddressValidation {
293 is_valid: true,
294 is_mine: false, is_script: address.starts_with("3") || address.starts_with("bc1q"),
296 }),
297 Err(_) => Ok(AddressValidation {
298 is_valid: false,
299 is_mine: false,
300 is_script: false,
301 }),
302 }
303 }
304
305 pub fn get_mempool_info(&self) -> Result<bitcoincore_rpc::json::GetMempoolInfoResult> {
307 self.with_retry(|c| c.get_mempool_info())
308 }
309
310 pub fn estimate_smart_fee(&self, conf_target: u16) -> Result<Option<f64>> {
312 let result = self.with_retry(|c| c.estimate_smart_fee(conf_target, None))?;
313 Ok(result.fee_rate.map(|amt| {
314 amt.to_sat() as f64 / 1000.0
316 }))
317 }
318}
319
320#[derive(Debug, Clone, Serialize)]
322pub struct AddressValidation {
323 pub is_valid: bool,
324 pub is_mine: bool,
325 pub is_script: bool,
326}
327
328#[derive(Debug, Clone, Serialize)]
330pub struct NodeStatus {
331 pub connected: bool,
332 pub block_height: u64,
333 pub network: String,
334 pub version: u64,
335 pub connections: usize,
336 pub mempool_size: u64,
337}
338
339impl BitcoinClient {
340 pub fn get_node_status(&self) -> Result<NodeStatus> {
342 let blockchain_info = self.with_retry(|c| c.get_blockchain_info())?;
343 let network_info = self.with_retry(|c| c.get_network_info())?;
344 let mempool_info = self.with_retry(|c| c.get_mempool_info())?;
345
346 Ok(NodeStatus {
347 connected: true,
348 block_height: blockchain_info.blocks,
349 network: blockchain_info.chain.to_string(),
350 version: network_info.version as u64,
351 connections: network_info.connections,
352 mempool_size: mempool_info.size as u64,
353 })
354 }
355
356 pub fn list_since_block(
358 &self,
359 block_hash: Option<&bitcoin::BlockHash>,
360 target_confirmations: Option<usize>,
361 ) -> Result<ListSinceBlockResult> {
362 let result =
363 self.with_retry(|c| c.list_since_block(block_hash, target_confirmations, None, None))?;
364
365 Ok(ListSinceBlockResult {
366 transactions: result
367 .transactions
368 .into_iter()
369 .map(|tx| TransactionInfo {
370 txid: tx.info.txid,
371 address: tx.detail.address.map(|a| a.assume_checked().to_string()),
372 category: format!("{:?}", tx.detail.category),
373 amount: tx.detail.amount.to_sat(),
374 confirmations: tx.info.confirmations,
375 block_hash: tx.info.blockhash,
376 block_time: tx.info.blocktime,
377 time: tx.info.time,
378 })
379 .collect(),
380 last_block: result.lastblock,
381 })
382 }
383
384 pub fn get_address_info(&self, address: &str) -> Result<AddressInfo> {
386 let parsed: Address<bitcoin::address::NetworkUnchecked> =
388 address.parse().map_err(|e: bitcoin::address::ParseError| {
389 BitcoinError::InvalidAddress(e.to_string())
390 })?;
391
392 let checked_addr = parsed.assume_checked();
393
394 let received = self.with_retry(|c| c.get_received_by_address(&checked_addr, Some(0)))?;
396 let received_confirmed =
397 self.with_retry(|c| c.get_received_by_address(&checked_addr, Some(1)))?;
398
399 Ok(AddressInfo {
400 address: address.to_string(),
401 is_valid: true,
402 total_received_sats: received.to_sat(),
403 confirmed_received_sats: received_confirmed.to_sat(),
404 unconfirmed_sats: received
405 .to_sat()
406 .saturating_sub(received_confirmed.to_sat()),
407 })
408 }
409
410 pub fn send_raw_transaction(&self, tx_hex: &str) -> Result<Txid> {
412 let tx_hex_owned = tx_hex.to_string();
413 self.with_retry(|c| c.send_raw_transaction(tx_hex_owned.clone()))
414 }
415
416 pub fn get_block_hash(&self, height: u64) -> Result<bitcoin::BlockHash> {
418 self.with_retry(|c| c.get_block_hash(height))
419 }
420
421 pub fn test_mempool_accept(&self, tx_hex: &str) -> Result<bool> {
423 let rawtxs = vec![tx_hex.to_string()];
424 let results = self.with_retry(|c| c.test_mempool_accept(&rawtxs))?;
425 Ok(results.first().map(|r| r.allowed).unwrap_or(false))
426 }
427
428 pub fn generate_to_address(
430 &self,
431 blocks: u64,
432 address: &bitcoin::Address,
433 ) -> Result<Vec<bitcoin::BlockHash>> {
434 self.with_retry(|c| c.generate_to_address(blocks, address))
435 }
436
437 pub fn send_to_address(
439 &self,
440 address: &bitcoin::Address,
441 amount: bitcoin::Amount,
442 ) -> Result<Txid> {
443 self.with_retry(|c| c.send_to_address(address, amount, None, None, None, None, None, None))
444 }
445
446 pub fn invalidate_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> {
448 self.with_retry(|c| c.invalidate_block(block_hash))
449 }
450
451 pub fn reconsider_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> {
453 self.with_retry(|c| c.reconsider_block(block_hash))
454 }
455}
456
457#[derive(Debug, Clone, Serialize)]
459pub struct ListSinceBlockResult {
460 pub transactions: Vec<TransactionInfo>,
461 pub last_block: bitcoin::BlockHash,
462}
463
464#[derive(Debug, Clone, Serialize)]
466pub struct TransactionInfo {
467 pub txid: Txid,
468 pub address: Option<String>,
469 pub category: String,
470 pub amount: i64,
471 pub confirmations: i32,
472 pub block_hash: Option<bitcoin::BlockHash>,
473 pub block_time: Option<u64>,
474 pub time: u64,
475}
476
477#[derive(Debug, Clone, Serialize)]
479pub struct AddressInfo {
480 pub address: String,
481 pub is_valid: bool,
482 pub total_received_sats: u64,
483 pub confirmed_received_sats: u64,
484 pub unconfirmed_sats: u64,
485}