clementine_core/
extended_bitcoin_rpc.rs

1//! # Bitcoin Extended RPC Interface
2//!
3//! Extended RPC interface communicates with the Bitcoin node. It features some
4//! common wrappers around typical RPC operations as well as direct
5//! communication interface with the Bitcoin node.
6//!
7//! ## Tests
8//!
9//! In tests, Bitcoind node and client are usually created using
10//! [`crate::test::common::create_regtest_rpc`]. Please refer to
11//! [`crate::test::common`] for using [`ExtendedBitcoinRpc`] in tests.
12
13use async_trait::async_trait;
14use bitcoin::Address;
15use bitcoin::Amount;
16use bitcoin::BlockHash;
17use bitcoin::FeeRate;
18use bitcoin::Network;
19use bitcoin::OutPoint;
20use bitcoin::ScriptBuf;
21use bitcoin::TxOut;
22use bitcoin::Txid;
23use bitcoincore_rpc::Auth;
24use bitcoincore_rpc::Client;
25use bitcoincore_rpc::RpcApi;
26use eyre::eyre;
27use eyre::Context;
28use eyre::OptionExt;
29use http::StatusCode;
30use reqwest;
31use secrecy::ExposeSecret;
32use secrecy::SecretString;
33use std::iter::Take;
34use std::str::FromStr;
35use std::sync::Arc;
36use std::time::Duration;
37use tokio::time::timeout;
38use tokio_retry::strategy::{jitter, ExponentialBackoff};
39use tokio_retry::RetryIf;
40
41use crate::builder::address::create_taproot_address;
42use crate::builder::transaction::create_round_txhandlers;
43use crate::builder::transaction::input::UtxoVout;
44use crate::builder::transaction::KickoffWinternitzKeys;
45use crate::builder::transaction::TransactionType;
46use crate::builder::transaction::TxHandler;
47use crate::config::protocol::ProtocolParamset;
48use crate::deposit::OperatorData;
49use crate::errors::{BridgeError, FeeErr};
50use crate::operator::RoundIndex;
51
52#[cfg(test)]
53use crate::test::common::citrea::CitreaE2EData;
54#[cfg(test)]
55use crate::{
56    citrea::CitreaClientT,
57    test::common::{are_all_state_managers_synced, test_actors::TestActors},
58};
59
60type Result<T> = std::result::Result<T, BitcoinRPCError>;
61
62const MAX_RETRY_ATTEMPTS: usize = 50;
63
64#[derive(Clone)]
65pub struct RetryConfig {
66    pub initial_delay_millis: u64,
67    pub max_delay: Duration,
68    pub max_attempts: usize,
69    pub backoff_multiplier: u64,
70    pub is_jitter: bool,
71    // Store the base iterator configuration
72    base_strategy: Arc<Take<ExponentialBackoff>>,
73}
74
75impl RetryConfig {
76    pub fn new(
77        initial_delay_millis: u64,
78        max_delay: Duration,
79        max_attempts: usize,
80        backoff_multiplier: u64,
81        is_jitter: bool,
82    ) -> Self {
83        // The crate use is confusing. ExponentialBackoff::from_millis defines the base,
84        // given the backoff_multiplier (this is supposed to be the initial delay), the
85        // starting factor becomes backoff_multiplier / initial_delay_millis.
86        let factor: u64 = initial_delay_millis / backoff_multiplier;
87
88        let max_attempts = std::cmp::min(max_attempts, MAX_RETRY_ATTEMPTS);
89
90        // Create the base strategy once
91        let base_strategy = Arc::new(
92            ExponentialBackoff::from_millis(backoff_multiplier)
93                .max_delay(max_delay)
94                .factor(factor)
95                .take(max_attempts),
96        );
97
98        Self {
99            initial_delay_millis,
100            max_delay,
101            max_attempts,
102            backoff_multiplier,
103            is_jitter,
104            base_strategy,
105        }
106    }
107
108    pub fn get_strategy(&self) -> Box<dyn Iterator<Item = Duration> + Send> {
109        // Clone the base strategy to get a fresh iterator with the same initial state
110        let base_strategy = (*self.base_strategy).clone();
111
112        if self.is_jitter {
113            Box::new(base_strategy.map(jitter))
114        } else {
115            Box::new(base_strategy)
116        }
117    }
118}
119
120impl Default for RetryConfig {
121    fn default() -> Self {
122        Self::new(100, Duration::from_secs(30), 5, 2, false)
123    }
124}
125
126impl std::fmt::Debug for RetryConfig {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        f.debug_struct("RetryConfig")
129            .field("initial_delay_millis", &self.initial_delay_millis)
130            .field("max_delay", &self.max_delay)
131            .field("max_attempts", &self.max_attempts)
132            .field("backoff_multiplier", &self.backoff_multiplier)
133            .field("is_jitter", &self.is_jitter)
134            .finish()
135    }
136}
137
138/// Trait to determine if an error is retryable
139pub trait RetryableError {
140    fn is_retryable(&self) -> bool;
141}
142
143impl RetryableError for bitcoincore_rpc::Error {
144    fn is_retryable(&self) -> bool {
145        tracing::trace!("Checking if error is retryable: {:?}", self);
146        let result = match self {
147            // JSON-RPC errors - check specific error patterns
148            bitcoincore_rpc::Error::JsonRpc(jsonrpc_error) => {
149                let error_str = jsonrpc_error.to_string().to_lowercase();
150                tracing::trace!("JsonRpc error string (lowercase): {}", error_str);
151                // Retry on connection issues, timeouts, temporary failures
152                let is_retryable = error_str.contains("timeout")
153                    || error_str.contains("connection")
154                    || error_str.contains("temporary")
155                    || error_str.contains("busy")
156                    || error_str.contains("unavailable")
157                    || error_str.contains("network")
158                    || error_str.contains("broken pipe")
159                    || error_str.contains("connection reset")
160                    || error_str.contains("connection refused")
161                    || error_str.contains("host unreachable");
162                tracing::trace!("JsonRpc error is_retryable: {}", is_retryable);
163                is_retryable
164            }
165
166            // I/O errors are typically network-related and retryable
167            bitcoincore_rpc::Error::Io(io_error) => {
168                use std::io::ErrorKind;
169                match io_error.kind() {
170                    // These are typically temporary network issues
171                    ErrorKind::ConnectionRefused
172                    | ErrorKind::ConnectionReset
173                    | ErrorKind::ConnectionAborted
174                    | ErrorKind::NotConnected
175                    | ErrorKind::BrokenPipe
176                    | ErrorKind::TimedOut
177                    | ErrorKind::Interrupted
178                    | ErrorKind::UnexpectedEof => true,
179
180                    // These are typically permanent issues
181                    ErrorKind::PermissionDenied
182                    | ErrorKind::NotFound
183                    | ErrorKind::InvalidInput
184                    | ErrorKind::InvalidData => false,
185
186                    // For other kinds, be conservative and retry
187                    _ => true,
188                }
189            }
190
191            // Authentication errors are typically permanent
192            bitcoincore_rpc::Error::Auth(_) => false,
193
194            // URL parse errors are permanent
195            bitcoincore_rpc::Error::UrlParse(_) => false,
196
197            // Invalid cookie file is usually a config issue (permanent)
198            bitcoincore_rpc::Error::InvalidCookieFile => false,
199
200            // Daemon returned error - check the error message
201            bitcoincore_rpc::Error::ReturnedError(error_msg) => {
202                let error_str = error_msg.to_lowercase();
203                // Retry on temporary RPC errors
204                error_str.contains("loading") ||
205                error_str.contains("warming up") ||
206                error_str.contains("verifying") ||
207                error_str.contains("busy") ||
208                error_str.contains("temporary") ||
209                error_str.contains("try again") ||
210                error_str.contains("timeout") ||
211                // Don't retry on wallet/transaction specific errors
212                !(error_str.contains("insufficient funds") ||
213                  error_str.contains("transaction already") ||
214                  error_str.contains("invalid") ||
215                  error_str.contains("not found") ||
216                  error_str.contains("conflict"))
217            }
218
219            // Unexpected structure might be due to version mismatch or temporary parsing issues
220            // Be conservative and retry once
221            bitcoincore_rpc::Error::UnexpectedStructure => true,
222
223            // Serialization errors are typically permanent
224            bitcoincore_rpc::Error::BitcoinSerialization(_) => false,
225            bitcoincore_rpc::Error::Hex(_) => false,
226            bitcoincore_rpc::Error::Json(_) => false,
227            bitcoincore_rpc::Error::Secp256k1(_) => false,
228            bitcoincore_rpc::Error::InvalidAmount(_) => false,
229        };
230        tracing::trace!("Final is_retryable result: {}", result);
231        result
232    }
233}
234
235impl RetryableError for BitcoinRPCError {
236    fn is_retryable(&self) -> bool {
237        match self {
238            BitcoinRPCError::TransactionNotConfirmed => true,
239            BitcoinRPCError::TransactionAlreadyInBlock(_) => false,
240            BitcoinRPCError::BumpFeeUTXOSpent(_) => false,
241
242            // These might be temporary - retry
243            BitcoinRPCError::BumpFeeError(_, _) => true,
244
245            // Check underlying error
246            BitcoinRPCError::Other(err) => {
247                let err_str = err.to_string().to_lowercase();
248                err_str.contains("timeout")
249                    || err_str.contains("connection")
250                    || err_str.contains("temporary")
251                    || err_str.contains("busy")
252                    || err_str.contains("network")
253            }
254        }
255    }
256}
257
258/// Bitcoin RPC wrapper. Extended RPC provides useful wrapper functions for
259/// common operations, as well as direct access to Bitcoin RPC.
260#[derive(Clone)]
261pub struct ExtendedBitcoinRpc {
262    url: String,
263    client: Arc<Client>,
264    retry_config: RetryConfig,
265
266    #[cfg(test)]
267    cached_mining_address: Arc<tokio::sync::RwLock<Option<String>>>,
268}
269
270impl std::fmt::Debug for ExtendedBitcoinRpc {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        f.debug_struct("ExtendedBitcoinRpc")
273            .field("url", &self.url)
274            .finish()
275    }
276}
277
278/// Errors that can occur during Bitcoin RPC operations.
279#[derive(Debug, thiserror::Error)]
280pub enum BitcoinRPCError {
281    #[error("Failed to bump fee for Txid of {0} and feerate of {1}")]
282    BumpFeeError(Txid, FeeRate),
283    #[error("Failed to bump fee: UTXO is already spent")]
284    BumpFeeUTXOSpent(OutPoint),
285    #[error("Transaction is already in block: {0}")]
286    TransactionAlreadyInBlock(BlockHash),
287    #[error("Transaction is not confirmed")]
288    TransactionNotConfirmed,
289
290    #[error(transparent)]
291    Other(#[from] eyre::Report),
292}
293
294impl ExtendedBitcoinRpc {
295    /// Connects to Bitcoin RPC server with built-in retry mechanism.
296    ///
297    /// This method attempts to connect to the Bitcoin RPC server and creates a new
298    /// [`ExtendedBitcoinRpc`] instance. It includes retry logic that will retry
299    /// connection attempts for retryable errors using exponential backoff.
300    ///
301    /// # Parameters
302    ///
303    /// * `url` - The RPC server URL
304    /// * `user` - Username for RPC authentication
305    /// * `password` - Password for RPC authentication
306    /// * `retry_config` - Optional retry configuration. If None, uses default config.
307    ///
308    /// # Returns
309    ///
310    /// - [`Result<ExtendedBitcoinRpc>`]: A new ExtendedBitcoinRpc instance on success
311    ///
312    /// # Errors
313    ///
314    /// - [`BitcoinRPCError`]: If connection fails after all retry attempts or ping fails
315    pub async fn connect(
316        url: String,
317        user: SecretString,
318        password: SecretString,
319        retry_config: Option<RetryConfig>,
320    ) -> Result<Self> {
321        let config = retry_config.clone().unwrap_or_default();
322
323        let url_clone = url.clone();
324        let user_clone = user.clone();
325        let password_clone = password.clone();
326
327        let retry_strategy = config.get_strategy();
328
329        RetryIf::spawn(
330            retry_strategy,
331            || async {
332                let auth = Auth::UserPass(
333                    user_clone.expose_secret().to_string(),
334                    password_clone.expose_secret().to_string(),
335                );
336
337                let retry_config = retry_config.clone().unwrap_or_default();
338
339                tracing::debug!(
340                    "Attempting to connect to Bitcoin RPC at {} with retry config: {:?}",
341                    &url_clone,
342                    &retry_config
343                );
344                let rpc = Client::new(&url_clone, auth)
345                    .await
346                    .wrap_err("Failed to connect to Bitcoin RPC")?;
347
348                // Since this is a lazy connection, we should ping it to ensure it works
349                tracing::debug!(
350                    "Pinging Bitcoin RPC at {} to make sure it's alive",
351                    &url_clone
352                );
353                rpc.ping()
354                    .await
355                    .map_err(|e| eyre::eyre!("Failed to ping Bitcoin RPC: {}", e))?;
356
357                let result: Result<ExtendedBitcoinRpc> = Ok(Self {
358                    url: url_clone.clone(),
359                    client: Arc::new(rpc),
360                    retry_config,
361                    #[cfg(test)]
362                    cached_mining_address: Arc::new(tokio::sync::RwLock::new(None)),
363                });
364
365                match &result {
366                    Ok(_) => tracing::debug!("Connected to Bitcoin RPC successfully"),
367                    Err(error) => {
368                        if !error.is_retryable() {
369                            tracing::debug!("Non-retryable connection error: {}", error);
370                        } else {
371                            tracing::debug!("Bitcoin RPC connection failed, will retry: {}", error);
372                        }
373                    }
374                }
375
376                result
377            },
378            |error: &BitcoinRPCError| error.is_retryable(),
379        )
380        .await
381    }
382
383    /// Generates a new Bitcoin address for the wallet.
384    pub async fn get_new_wallet_address(&self) -> Result<Address> {
385        self.get_new_address(None, None)
386            .await
387            .wrap_err("Failed to get new address")
388            .map(|addr| addr.assume_checked())
389            .map_err(Into::into)
390    }
391
392    /// Returns the number of confirmations for a transaction.
393    ///
394    /// # Parameters
395    ///
396    /// * `txid`: TXID of the transaction to check.
397    ///
398    /// # Returns
399    ///
400    /// - [`u32`]: The number of confirmations for the transaction.
401    ///
402    /// # Errors
403    ///
404    /// - [`BitcoinRPCError`]: If the transaction is not confirmed (0) or if
405    ///   there was an error retrieving the transaction info.
406    pub async fn confirmation_blocks(&self, txid: &bitcoin::Txid) -> Result<u32> {
407        let raw_tx_res = self
408            .get_raw_transaction_info(txid, None)
409            .await
410            .wrap_err("Failed to get transaction info")?;
411        raw_tx_res
412            .confirmations
413            .ok_or_else(|| eyre::eyre!("No confirmation data for transaction {}", txid))
414            .map_err(Into::into)
415    }
416
417    /// Retrieves the current blockchain height (number of blocks).
418    ///
419    /// # Returns
420    ///
421    /// - [`u32`]: Current block height
422    pub async fn get_current_chain_height(&self) -> Result<u32> {
423        let height = self
424            .get_block_count()
425            .await
426            .wrap_err("Failed to get current chain height")?;
427        Ok(u32::try_from(height).wrap_err("Failed to convert block count to u32")?)
428    }
429
430    /// Checks if an operator's collateral is valid and available for use.
431    ///
432    /// This function validates the operator's collateral by:
433    /// 1. Verifying the collateral UTXO exists and has the correct amount
434    /// 2. Creating the round transaction chain to track current collateral position
435    /// 3. Determining if the current collateral UTXO in the chain is spent in a non-protocol tx, signaling the exit of operator from the protocol
436    ///
437    /// # Parameters
438    ///
439    /// * `operator_data`: Data about the operator including collateral funding outpoint
440    /// * `kickoff_wpks`: Kickoff Winternitz public keys for round transaction creation
441    /// * `paramset`: Protocol parameters
442    ///
443    /// # Returns
444    ///
445    /// - [`bool`]: `true` if the collateral is still usable, thus operator is still in protocol, `false` if the collateral is spent, thus operator is not in protocol anymore
446    ///
447    /// # Errors
448    ///
449    /// - [`BridgeError`]: If there was an error retrieving transaction data, creating round transactions,
450    ///   or checking UTXO status
451    pub async fn collateral_check(
452        &self,
453        operator_data: &OperatorData,
454        kickoff_wpks: &KickoffWinternitzKeys,
455        paramset: &'static ProtocolParamset,
456    ) -> std::result::Result<bool, BridgeError> {
457        // first check if the collateral utxo is on chain or mempool
458        let tx = self
459            .get_tx_of_txid(&operator_data.collateral_funding_outpoint.txid)
460            .await
461            .wrap_err(format!(
462                "Failed to find collateral utxo in chain for outpoint {:?}",
463                operator_data.collateral_funding_outpoint
464            ))?;
465        let collateral_outpoint = match tx
466            .output
467            .get(operator_data.collateral_funding_outpoint.vout as usize)
468        {
469            Some(output) => output,
470            None => {
471                tracing::warn!(
472                    "No output at index {} for txid {} while checking for collateral existence",
473                    operator_data.collateral_funding_outpoint.vout,
474                    operator_data.collateral_funding_outpoint.txid
475                );
476                return Ok(false);
477            }
478        };
479
480        if collateral_outpoint.value != paramset.collateral_funding_amount {
481            tracing::error!(
482                "Collateral amount for collateral {:?} is not correct: expected {}, got {}",
483                operator_data.collateral_funding_outpoint,
484                paramset.collateral_funding_amount,
485                collateral_outpoint.value
486            );
487            return Ok(false);
488        }
489
490        let operator_tpr_address =
491            create_taproot_address(&[], Some(operator_data.xonly_pk), paramset.network).0;
492
493        if collateral_outpoint.script_pubkey != operator_tpr_address.script_pubkey() {
494            tracing::error!(
495                "Collateral script pubkey for collateral {:?} is not correct: expected {}, got {}",
496                operator_data.collateral_funding_outpoint,
497                operator_tpr_address.script_pubkey(),
498                collateral_outpoint.script_pubkey
499            );
500            return Ok(false);
501        }
502
503        // we additionally check if collateral utxo is on chain (so not in mempool)
504        // on mainnet we fail if collateral utxo is not on chain because if it is in mempool,
505        // the txid of the utxo can change if the fee is bumped
506        // on other networks, we allow collateral to be in mempool to not wait for collateral to be on chain to do deposits for faster testing
507        let is_on_chain = self
508            .is_tx_on_chain(&operator_data.collateral_funding_outpoint.txid)
509            .await?;
510        if !is_on_chain {
511            return match paramset.network {
512                bitcoin::Network::Bitcoin => Ok(false),
513                _ => Ok(true),
514            };
515        }
516
517        let mut current_collateral_outpoint: OutPoint = operator_data.collateral_funding_outpoint;
518        let mut prev_ready_to_reimburse: Option<TxHandler> = None;
519        // iterate over all rounds
520        for round_idx in RoundIndex::iter_rounds(paramset.num_round_txs) {
521            // create round and ready to reimburse txs for the round
522            let txhandlers = create_round_txhandlers(
523                paramset,
524                round_idx,
525                operator_data,
526                kickoff_wpks,
527                prev_ready_to_reimburse.as_ref(),
528            )?;
529
530            let mut round_txhandler_opt = None;
531            let mut ready_to_reimburse_txhandler_opt = None;
532            for txhandler in &txhandlers {
533                match txhandler.get_transaction_type() {
534                    TransactionType::Round => round_txhandler_opt = Some(txhandler),
535                    TransactionType::ReadyToReimburse => {
536                        ready_to_reimburse_txhandler_opt = Some(txhandler)
537                    }
538                    _ => {}
539                }
540            }
541            if round_txhandler_opt.is_none() || ready_to_reimburse_txhandler_opt.is_none() {
542                return Err(eyre!(
543                    "Failed to create round and ready to reimburse txs for round {:?} for operator {}",
544                    round_idx,
545                    operator_data.xonly_pk
546                ).into());
547            }
548
549            let round_txid = round_txhandler_opt
550                .expect("Round txhandler should exist, checked above")
551                .get_cached_tx()
552                .compute_txid();
553            let is_round_tx_on_chain = self.is_tx_on_chain(&round_txid).await?;
554            if !is_round_tx_on_chain {
555                break;
556            }
557            let block_hash = self.get_blockhash_of_tx(&round_txid).await?;
558            let block_height = self
559                .get_block_info(&block_hash)
560                .await
561                .wrap_err(format!(
562                    "Failed to get block info for block hash {block_hash}"
563                ))?
564                .height;
565            if block_height < paramset.start_height as usize {
566                tracing::warn!(
567                    "Collateral utxo of operator {operator_data:?} is spent in a block before paramset start height: {block_height} < {0}",
568                    paramset.start_height
569                );
570                return Ok(false);
571            }
572            current_collateral_outpoint = OutPoint {
573                txid: round_txid,
574                vout: UtxoVout::CollateralInRound.get_vout(),
575            };
576            if round_idx == RoundIndex::Round(paramset.num_round_txs - 1) {
577                // for the last round, only check round tx, as if the operator sent the ready to reimburse tx of last round,
578                // it cannot create more kickoffs anymore
579                break;
580            }
581            let ready_to_reimburse_txhandler = ready_to_reimburse_txhandler_opt
582                .expect("Ready to reimburse txhandler should exist");
583            let ready_to_reimburse_txid =
584                ready_to_reimburse_txhandler.get_cached_tx().compute_txid();
585            let is_ready_to_reimburse_tx_on_chain =
586                self.is_tx_on_chain(&ready_to_reimburse_txid).await?;
587            if !is_ready_to_reimburse_tx_on_chain {
588                break;
589            }
590
591            current_collateral_outpoint = OutPoint {
592                txid: ready_to_reimburse_txid,
593                vout: UtxoVout::CollateralInReadyToReimburse.get_vout(),
594            };
595
596            prev_ready_to_reimburse = Some(ready_to_reimburse_txhandler.clone());
597        }
598
599        // if the collateral utxo we found latest in the round tx chain is spent, operators collateral is spent from Clementine
600        // bridge protocol, thus it is unusable and operator cannot fulfill withdrawals anymore
601        // if not spent, it should exist in chain, which is checked below
602        Ok(!self.is_utxo_spent(&current_collateral_outpoint).await?)
603    }
604
605    /// Returns block hash of a transaction, if confirmed.
606    ///
607    /// # Parameters
608    ///
609    /// * `txid`: TXID of the transaction to check.
610    ///
611    /// # Returns
612    ///
613    /// - [`bitcoin::BlockHash`]: Block hash of the block that the transaction
614    ///   is in.
615    ///
616    /// # Errors
617    ///
618    /// - [`BitcoinRPCError`]: If the transaction is not confirmed (0) or if
619    ///   there was an error retrieving the transaction info.
620    pub async fn get_blockhash_of_tx(&self, txid: &bitcoin::Txid) -> Result<bitcoin::BlockHash> {
621        let raw_transaction_results = self
622            .get_raw_transaction_info(txid, None)
623            .await
624            .wrap_err("Failed to get transaction info")?;
625        let Some(blockhash) = raw_transaction_results.blockhash else {
626            return Err(eyre::eyre!("Transaction not confirmed: {0}", txid).into());
627        };
628        Ok(blockhash)
629    }
630
631    /// Retrieves the block header and hash for a given block height.
632    ///
633    /// # Arguments
634    ///
635    /// * `height`: Target block height.
636    ///
637    /// # Returns
638    ///
639    /// - ([`bitcoin::BlockHash`], [`bitcoin::block::Header`]): A tuple
640    ///   containing the block hash and header.
641    pub async fn get_block_info_by_height(
642        &self,
643        height: u64,
644    ) -> Result<(bitcoin::BlockHash, bitcoin::block::Header)> {
645        let block_hash = self.get_block_hash(height).await.wrap_err(format!(
646            "Couldn't retrieve block hash from height {height} from rpc"
647        ))?;
648        let block_header = self.get_block_header(&block_hash).await.wrap_err(format!(
649            "Couldn't retrieve block header with block hash {block_hash} from rpc"
650        ))?;
651
652        Ok((block_hash, block_header))
653    }
654
655    /// Gets the transactions that created the inputs of a given transaction.
656    ///
657    /// # Arguments
658    ///
659    /// * `tx` - The transaction to get the previous transactions for
660    ///
661    /// # Returns
662    ///
663    /// A vector of transactions that created the inputs of the given transaction.
664    #[tracing::instrument(skip(self), err(level = tracing::Level::ERROR), ret(level = tracing::Level::TRACE))]
665    pub async fn get_prevout_txs(
666        &self,
667        tx: &bitcoin::Transaction,
668    ) -> Result<Vec<bitcoin::Transaction>> {
669        let mut prevout_txs = Vec::new();
670        for input in &tx.input {
671            let txid = input.previous_output.txid;
672            prevout_txs.push(self.get_tx_of_txid(&txid).await?);
673        }
674        Ok(prevout_txs)
675    }
676
677    /// Gets the transaction data for a given transaction ID.
678    ///
679    /// # Parameters
680    ///
681    /// * `txid`: TXID of the transaction to check.
682    ///
683    /// # Returns
684    ///
685    /// - [`bitcoin::Transaction`]: Transaction itself.
686    pub async fn get_tx_of_txid(&self, txid: &bitcoin::Txid) -> Result<bitcoin::Transaction> {
687        let raw_transaction = self
688            .get_raw_transaction(txid, None)
689            .await
690            .wrap_err("Failed to get raw transaction")?;
691        Ok(raw_transaction)
692    }
693
694    /// Checks if a transaction is on-chain.
695    ///
696    /// # Parameters
697    ///
698    /// * `txid`: TXID of the transaction to check.
699    ///
700    /// # Returns
701    ///
702    /// - [`bool`]: `true` if the transaction is on-chain, `false` otherwise.
703    pub async fn is_tx_on_chain(&self, txid: &bitcoin::Txid) -> Result<bool> {
704        Ok(self
705            .get_raw_transaction_info(txid, None)
706            .await
707            .ok()
708            .and_then(|s| s.blockhash)
709            .is_some())
710    }
711
712    /// Checks if a transaction UTXO has expected address and amount.
713    ///
714    /// # Parameters
715    ///
716    /// * `outpoint` - The outpoint to check
717    /// * `address` - Expected script pubkey
718    /// * `amount_sats` - Expected amount in satoshis
719    ///
720    /// # Returns
721    ///
722    /// - [`bool`]: `true` if the UTXO has the expected address and amount, `false` otherwise.
723    pub async fn check_utxo_address_and_amount(
724        &self,
725        outpoint: &OutPoint,
726        address: &ScriptBuf,
727        amount_sats: Amount,
728    ) -> Result<bool> {
729        let tx = self
730            .get_raw_transaction(&outpoint.txid, None)
731            .await
732            .wrap_err("Failed to get transaction")?;
733
734        let current_output = tx
735            .output
736            .get(outpoint.vout as usize)
737            .ok_or(eyre!(
738                "No output at index {} for txid {}",
739                outpoint.vout,
740                outpoint.txid
741            ))?
742            .to_owned();
743
744        let expected_output = TxOut {
745            script_pubkey: address.clone(),
746            value: amount_sats,
747        };
748
749        Ok(expected_output == current_output)
750    }
751
752    /// Checks if an UTXO is spent.
753    ///
754    /// # Parameters
755    ///
756    /// * `outpoint`: The outpoint to check
757    ///
758    /// # Returns
759    ///
760    /// - [`bool`]: `true` if the UTXO is spent, `false` otherwise.
761    ///
762    /// # Errors
763    ///
764    /// - [`BitcoinRPCError`]: If the transaction is not confirmed or if there
765    ///   was an error retrieving the transaction output.
766    pub async fn is_utxo_spent(&self, outpoint: &OutPoint) -> Result<bool> {
767        if !self.is_tx_on_chain(&outpoint.txid).await? {
768            return Err(BitcoinRPCError::TransactionNotConfirmed);
769        }
770
771        let res = self
772            .get_tx_out(&outpoint.txid, outpoint.vout, Some(false))
773            .await
774            .wrap_err("Failed to get transaction output")?;
775
776        Ok(res.is_none())
777    }
778
779    /// Attempts to mine the specified number of blocks and returns their hashes.
780    ///
781    /// This test-only async function will mine `block_num` blocks on the Bitcoin regtest network
782    /// using a cached mining address or a newly generated one. It retries up to 5 times on failure
783    /// with exponential backoff.
784    ///
785    /// # Parameters
786    /// - `block_num`: The number of blocks to mine.
787    ///
788    /// # Returns
789    /// - `Ok(Vec<BlockHash>)`: A vector of block hashes for the mined blocks.
790    /// - `Err`: If mining fails after all retry attempts.
791    #[cfg(test)]
792    pub async fn mine_blocks(&self, block_num: u64) -> Result<Vec<BlockHash>> {
793        if block_num == 0 {
794            return Ok(vec![]);
795        }
796
797        self.try_mine(block_num).await
798    }
799
800    /// A helper fn to safely mine blocks while waiting for all actors to be synced
801    /// If CitreaE2EData is provided and there are multiple DA nodes, it will additionally perform a reorg. Reorg will cause chain size to increase by number of reorged blocks in addition to block_num.
802    #[cfg(test)]
803    pub async fn mine_blocks_while_synced<C: CitreaClientT>(
804        &self,
805        block_num: u64,
806        actors: &TestActors<C>,
807        e2e: Option<&CitreaE2EData<'_>>,
808    ) -> Result<Vec<BlockHash>> {
809        match e2e {
810            // do a reorg if there are multiple nodes, the actors are currently always using node 0
811            Some(e2e) if e2e.bitcoin_nodes.iter().count() > 1 => {
812                use bitcoin::secp256k1::rand::{thread_rng, Rng};
813                e2e.bitcoin_nodes
814                    .disconnect_nodes()
815                    .await
816                    .map_err(|e| eyre::eyre!("Failed to disconnect nodes: {}", e))?;
817                let reorg_blocks =
818                    thread_rng().gen_range(0..e2e.config.protocol_paramset().finality_depth as u64);
819                let da0 = e2e.bitcoin_nodes.get(0).expect("node 0 should exist");
820                let da1 = e2e.bitcoin_nodes.get(1).expect("node 1 should exist");
821
822                let mut mined_blocks = Vec::new();
823                while mined_blocks.len() < reorg_blocks as usize {
824                    if !are_all_state_managers_synced(self, actors).await? {
825                        // wait until they are synced
826                        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
827                        continue;
828                    }
829                    da0.generate(1)
830                        .await
831                        .wrap_err("Failed to generate blocks")?;
832                    // da1 will be canonical
833                    let new_blocks = da1
834                        .generate(1)
835                        .await
836                        .wrap_err("Failed to generate blocks")?;
837                    mined_blocks.extend(new_blocks);
838                }
839                mined_blocks.extend(
840                    da1.generate(1)
841                        .await
842                        .wrap_err("Failed to generate blocks")?,
843                );
844                // connect and reorg, da0's blocks will be non-canonical
845                e2e.bitcoin_nodes
846                    .connect_nodes()
847                    .await
848                    .map_err(|e| eyre::eyre!("Failed to connect nodes: {}", e))?;
849                e2e.bitcoin_nodes
850                    .wait_for_sync(None)
851                    .await
852                    .map_err(|e| eyre::eyre!("Failed to wait for sync: {}", e))?;
853                // mined blocks has to be block_num higher than reorg_blocks + 1, because this callers of this fn expects
854                // chain size to increase by at least block_num starting from where cpfp fee payer is in mempool.
855                while mined_blocks.len() != (reorg_blocks + block_num + 1) as usize {
856                    if !are_all_state_managers_synced(self, actors).await? {
857                        // wait until they are synced
858                        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
859                        continue;
860                    }
861                    mined_blocks.extend(self.mine_blocks(1).await?);
862                }
863                Ok(mined_blocks)
864            }
865            _ => {
866                // do not do a reorg here, just mine blocks
867                let mut mined_blocks = Vec::new();
868                while mined_blocks.len() < block_num as usize {
869                    if !are_all_state_managers_synced(self, actors).await? {
870                        // wait until they are synced
871                        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
872                        continue;
873                    }
874                    let new_blocks = self.mine_blocks(1).await?;
875                    mined_blocks.extend(new_blocks);
876                }
877                Ok(mined_blocks)
878            }
879        }
880    }
881
882    /// Internal helper that performs the actual block mining logic.
883    ///
884    /// It uses a cached mining address if available, otherwise it generates and caches
885    /// a new one. It then uses the address to mine `block_num` blocks.
886    ///
887    /// # Parameters
888    /// - `block_num`: The number of blocks to mine.
889    ///
890    /// # Returns
891    /// - `Ok(Vec<BlockHash>)`: The list of block hashes.
892    /// - `Err`: If the client fails to get a new address or mine the blocks.
893    #[cfg(test)]
894    async fn try_mine(&self, block_num: u64) -> Result<Vec<BlockHash>> {
895        let address = {
896            let read = self.cached_mining_address.read().await;
897            if let Some(addr) = &*read {
898                addr.clone()
899            } else {
900                drop(read);
901                let mut write = self.cached_mining_address.write().await;
902
903                if let Some(addr) = &*write {
904                    addr.clone()
905                } else {
906                    let new_addr = self
907                        .get_new_address(None, None)
908                        .await
909                        .wrap_err("Failed to get new address")?
910                        .assume_checked()
911                        .to_string();
912                    *write = Some(new_addr.clone());
913                    new_addr
914                }
915            }
916        };
917
918        let address = Address::from_str(&address)
919            .wrap_err("Invalid address format")?
920            .assume_checked();
921        let blocks = self
922            .generate_to_address(block_num, &address)
923            .await
924            .wrap_err("Failed to generate to address")?;
925
926        Ok(blocks)
927    }
928
929    /// Gets the number of transactions in the mempool.
930    ///
931    /// # Returns
932    ///
933    /// - [`usize`]: The number of transactions in the mempool.
934    pub async fn mempool_size(&self) -> Result<usize> {
935        let mempool_info = self
936            .get_mempool_info()
937            .await
938            .wrap_err("Failed to get mempool info")?;
939        Ok(mempool_info.size)
940    }
941
942    /// Sends a specified amount of Bitcoins to the given address.
943    ///
944    /// # Parameters
945    ///
946    /// * `address` - The recipient address
947    /// * `amount_sats` - The amount to send in satoshis
948    ///
949    /// # Returns
950    ///
951    /// - [`OutPoint`]: The outpoint (txid and vout) of the newly created output.
952    pub async fn send_to_address(
953        &self,
954        address: &Address,
955        amount_sats: Amount,
956    ) -> Result<OutPoint> {
957        let txid = self
958            .client
959            .send_to_address(
960                address,
961                amount_sats,
962                None,
963                None,
964                None,
965                Some(true),
966                Some(2),
967                Some(bitcoincore_rpc::json::EstimateMode::Conservative),
968            )
969            .await
970            .wrap_err("Failed to send to address")?;
971
972        let tx_result = self
973            .get_transaction(&txid, None)
974            .await
975            .wrap_err("Failed to get transaction")?;
976        let vout = tx_result.details[0].vout;
977
978        Ok(OutPoint { txid, vout })
979    }
980
981    /// Retrieves the transaction output for a given outpoint.
982    ///
983    /// # Arguments
984    ///
985    /// * `outpoint` - The outpoint (txid and vout) to retrieve
986    ///
987    /// # Returns
988    ///
989    /// - [`TxOut`]: The transaction output at the specified outpoint.
990    pub async fn get_txout_from_outpoint(&self, outpoint: &OutPoint) -> Result<TxOut> {
991        let tx = self
992            .get_raw_transaction(&outpoint.txid, None)
993            .await
994            .wrap_err("Failed to get transaction")?;
995        let txout = tx
996            .output
997            .get(outpoint.vout as usize)
998            .ok_or(eyre!(
999                "No output at index {} for txid {}",
1000                outpoint.vout,
1001                outpoint.txid
1002            ))?
1003            .to_owned();
1004
1005        Ok(txout)
1006    }
1007
1008    /// Bumps the fee of a transaction to meet or exceed a target fee rate. Does
1009    /// nothing if the transaction is already confirmed. Returns the original
1010    /// txid if no bump was needed.
1011    ///
1012    /// This function implements Replace-By-Fee (RBF) to increase the fee of an unconfirmed transaction.
1013    /// It works as follows:
1014    /// 1. If the transaction is already confirmed, returns Err(TransactionAlreadyInBlock)
1015    /// 2. If the current fee rate is already >= the requested fee rate, returns the original txid
1016    /// 3. Otherwise, increases the fee rate by adding the node's incremental fee to the current fee rate, then `bump_fee`s the transaction
1017    ///
1018    /// Note: This function currently only supports fee payer TXs.
1019    ///
1020    /// # Arguments
1021    /// * `txid` - The transaction ID to bump
1022    /// * `fee_rate` - The target fee rate to achieve
1023    ///
1024    /// # Returns
1025    ///
1026    /// - [`Txid`]: The txid of the bumped transaction (which may be the same as the input txid if no bump was needed).
1027    ///
1028    /// # Errors
1029    ///
1030    ///  * `TransactionAlreadyInBlock` - If the transaction is already confirmed
1031    /// * `BumpFeeUTXOSpent` - If the UTXO being spent by the transaction is already spent
1032    /// * `BumpFeeError` - For other errors with fee bumping
1033    pub async fn bump_fee_with_fee_rate(&self, txid: Txid, fee_rate: FeeRate) -> Result<Txid> {
1034        // Check if transaction is already confirmed
1035        let transaction_info = self
1036            .get_transaction(&txid, None)
1037            .await
1038            .wrap_err("Failed to get transaction")?;
1039        if transaction_info.info.blockhash.is_some() {
1040            return Err(BitcoinRPCError::TransactionAlreadyInBlock(
1041                transaction_info
1042                    .info
1043                    .blockhash
1044                    .expect("Blockhash should be present"),
1045            ));
1046        }
1047
1048        // Calculate current fee rate
1049        let tx = transaction_info
1050            .transaction()
1051            .wrap_err("Failed to get transaction")?;
1052        let tx_weight = tx.weight().to_wu();
1053        let current_fee_sat = u64::try_from(
1054            transaction_info
1055                .fee
1056                .expect("Fee should be present")
1057                .to_sat()
1058                .abs(),
1059        )
1060        .wrap_err("Failed to convert fee to sat")?;
1061
1062        let current_fee_rate_sat_kwu = current_fee_sat as f64 * 1000.0 / tx_weight as f64;
1063
1064        tracing::trace!(
1065            "Bump fee with fee rate txid: {txid} - Current fee sat: {current_fee_sat} - current fee rate: {current_fee_rate_sat_kwu}"
1066        );
1067
1068        // If current fee rate is already sufficient, return original txid
1069        if current_fee_rate_sat_kwu >= fee_rate.to_sat_per_kwu() as f64 {
1070            return Ok(txid);
1071        }
1072
1073        tracing::trace!(
1074            "Bump fee with fee rate txid: {txid} - Current fee rate: {current_fee_rate_sat_kwu} sat/kwu, target fee rate: {fee_rate} sat/kwu"
1075        );
1076
1077        // Get node's incremental fee to determine how much to increase
1078        let network_info = self
1079            .get_network_info()
1080            .await
1081            .wrap_err("Failed to get network info")?;
1082        // incremental fee is in BTC/kvB
1083        let incremental_fee = network_info.incremental_fee;
1084        // Convert from sat/kvB to sat/kwu by dividing by 4.0, since 1 kvB = 4 kwu.
1085        let incremental_fee_rate_sat_kwu = incremental_fee.to_sat() as f64 / 4.0;
1086
1087        // Calculate new fee rate by adding incremental fee to current fee rate, or use the target fee rate if it's higher
1088        let new_fee_rate = FeeRate::from_sat_per_kwu(std::cmp::max(
1089            (current_fee_rate_sat_kwu + incremental_fee_rate_sat_kwu).ceil() as u64,
1090            fee_rate.to_sat_per_kwu(),
1091        ));
1092
1093        tracing::debug!(
1094            "Bumping fee for txid: {txid} from {current_fee_rate_sat_kwu} to {new_fee_rate} with incremental fee {incremental_fee_rate_sat_kwu} - Final fee rate: {new_fee_rate}, current chain fee rate: {fee_rate}"
1095        );
1096
1097        // Call Bitcoin Core's bumpfee RPC
1098        let bump_fee_result = match self
1099            .bump_fee(
1100                &txid,
1101                Some(&bitcoincore_rpc::json::BumpFeeOptions {
1102                    fee_rate: Some(bitcoincore_rpc::json::FeeRate::per_vbyte(Amount::from_sat(
1103                        new_fee_rate.to_sat_per_vb_ceil(),
1104                    ))),
1105                    replaceable: Some(true),
1106                    ..Default::default()
1107                }),
1108            )
1109            .await
1110        {
1111            Ok(bump_fee_result) => bump_fee_result,
1112            // Attempt to parse the error message to get the outpoint if the UTXO is already spent
1113            Err(e) => match e {
1114                bitcoincore_rpc::Error::JsonRpc(json_rpc_error) => match json_rpc_error {
1115                    bitcoincore_rpc::RpcError::Rpc(rpc_error) => {
1116                        if let Some((outpoint_str, _)) =
1117                            rpc_error.message.split_once(" is already spent")
1118                        {
1119                            let outpoint = OutPoint::from_str(outpoint_str)
1120                                .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))?;
1121
1122                            return Err(BitcoinRPCError::BumpFeeUTXOSpent(outpoint));
1123                        }
1124
1125                        return Err(eyre::eyre!("{:?}", rpc_error)
1126                            .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))
1127                            .into());
1128                    }
1129                    _ => {
1130                        return Err(eyre::eyre!(json_rpc_error)
1131                            .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))
1132                            .into());
1133                    }
1134                },
1135                _ => {
1136                    return Err(eyre::eyre!(e)
1137                        .wrap_err(BitcoinRPCError::BumpFeeError(txid, fee_rate))
1138                        .into())
1139                }
1140            },
1141        };
1142
1143        // Return the new txid
1144        Ok(bump_fee_result
1145            .txid
1146            .ok_or_eyre("Failed to get Txid from bump_fee_result")?)
1147    }
1148
1149    /// Creates a new instance of the [`ExtendedBitcoinRpc`] with a new client
1150    /// connection for cloning. This is needed when you need a separate
1151    /// connection to the Bitcoin RPC server.
1152    ///
1153    /// # Returns
1154    ///
1155    /// - [`ExtendedBitcoinRpc`]: A new instance of ExtendedBitcoinRpc with a new client connection.
1156    pub async fn clone_inner(&self) -> std::result::Result<Self, bitcoincore_rpc::Error> {
1157        Ok(Self {
1158            url: self.url.clone(),
1159            client: self.client.clone(),
1160            retry_config: self.retry_config.clone(),
1161            #[cfg(test)]
1162            cached_mining_address: self.cached_mining_address.clone(),
1163        })
1164    }
1165
1166    /// Retrieves the block for a given height.
1167    ///
1168    /// # Arguments
1169    ///
1170    /// * `height` - The target block height.
1171    ///
1172    /// # Returns
1173    ///
1174    /// - [`bitcoin::Block`]: The block at the specified height.
1175    pub async fn get_block_by_height(&self, height: u64) -> Result<bitcoin::Block> {
1176        let hash = self
1177            .get_block_info_by_height(height)
1178            .await
1179            .wrap_err("Failed to get block info by height")?
1180            .0;
1181
1182        Ok(self
1183            .get_block(&hash)
1184            .await
1185            .wrap_err("Failed to get block by height")?)
1186    }
1187
1188    /// Gets the current recommended fee rate in sat/vb from Mempool Space and Bitcoin Core and selects the minimum.
1189    /// For Regtest and Signet, it uses a fixed fee rate of 1 sat/vB.
1190    /// # Logic
1191    /// *   **Regtest:** Uses a fixed fee rate of 1 sat/vB for simplicity.
1192    /// *   **Mainnet, Testnet4 and Signet:** Fetches fee rates from both Mempool Space API and Bitcoin Core RPC and takes the minimum.
1193    /// *   **Hard Cap:** Applies a hard cap from configuration to prevent excessive fees.
1194    /// # Fallbacks
1195    /// *   If one source fails, it uses the other.
1196    /// *   If both fail, it falls back to a default of 1 sat/vB.
1197    pub async fn get_fee_rate(
1198        &self,
1199        network: Network,
1200        mempool_api_host: &Option<String>,
1201        mempool_api_endpoint: &Option<String>,
1202        mempool_fee_rate_multiplier: u64,
1203        mempool_fee_rate_offset_sat_kvb: u64,
1204        fee_rate_hard_cap: u64,
1205    ) -> Result<FeeRate> {
1206        match network {
1207            // Regtest use a fixed, low fee rate.
1208            Network::Regtest => {
1209                tracing::debug!("Using fixed fee rate of 1 sat/vB for {network} network");
1210                Ok(FeeRate::from_sat_per_vb_unchecked(1))
1211            }
1212
1213            // Mainnet and Testnet4 fetch fees from Mempool Space and Bitcoin Core RPC.
1214            Network::Bitcoin | Network::Testnet4 | Network::Signet => {
1215                tracing::debug!("Fetching fee rate for {network} network...");
1216
1217                // Fetch fees from both mempool.space and Bitcoin Core RPC
1218                let mempool_fee = get_fee_rate_from_mempool_space(
1219                    mempool_api_host,
1220                    mempool_api_endpoint,
1221                    network,
1222                )
1223                .await;
1224
1225                let rpc_fee = timeout(Duration::from_secs(30), self.estimate_smart_fee(1, None))
1226                    .await
1227                    .map_err(|_| eyre!("RPC estimate_smart_fee timed out after 30 seconds"))
1228                    .and_then(|result| {
1229                        result.wrap_err("Failed to estimate smart fee using Bitcoin Core RPC")
1230                    })
1231                    .and_then(|estimate| {
1232                        estimate.fee_rate.ok_or_else(|| {
1233                            eyre!("Failed to extract fee rate from Bitcoin Core RPC response")
1234                        })
1235                    });
1236
1237                // Use the minimum of both fee sources, with fallback logic, carefully avoiding overflow
1238                let selected_fee_amount = match (mempool_fee, rpc_fee) {
1239                    (Ok(mempool_amt), Ok(rpc_amt)) => {
1240                        // Use checked arithmetic to avoid overflow
1241                        let multiplier = mempool_fee_rate_multiplier;
1242                        let offset = mempool_fee_rate_offset_sat_kvb;
1243                        let rpc_amt_sat = rpc_amt.to_sat();
1244
1245                        let threshold_sat = multiplier
1246                            .checked_mul(rpc_amt_sat)
1247                            .and_then(|v| v.checked_add(offset))
1248                            .ok_or_else(|| {
1249                                eyre!("Overflow when calculating threshold_sat in fee selection ({multiplier} * {rpc_amt_sat} + {offset})")
1250                            })?;
1251
1252                        let threshold = Amount::from_sat(threshold_sat);
1253
1254                        if mempool_amt <= threshold {
1255                            tracing::debug!(
1256                                "Selected mempool.space fee rate: {} sat/kvB (mempool: {}, rpc: {}, threshold: {})",
1257                                mempool_amt.to_sat(),
1258                                mempool_amt.to_sat(),
1259                                rpc_amt.to_sat(),
1260                                threshold
1261                            );
1262                            mempool_amt
1263                        } else {
1264                            tracing::debug!(
1265                                "Selected Bitcoin Core RPC fee rate: {} sat/kvB (mempool: {}, rpc: {}, threshold: {})",
1266                                rpc_amt.to_sat(),
1267                                mempool_amt.to_sat(),
1268                                rpc_amt.to_sat(),
1269                                threshold
1270                            );
1271                            rpc_amt
1272                        }
1273                    }
1274                    (Ok(mempool_amt), Err(rpc_err)) => {
1275                        tracing::warn!(
1276                            "RPC fee estimation failed, using mempool.space: {:#}",
1277                            rpc_err
1278                        );
1279                        mempool_amt
1280                    }
1281                    (Err(mempool_err), Ok(rpc_amt)) => {
1282                        tracing::warn!(
1283                            "Mempool.space fee fetch failed, using Bitcoin Core RPC: {:#}",
1284                            mempool_err
1285                        );
1286                        rpc_amt
1287                    }
1288                    (Err(mempool_err), Err(rpc_err)) => {
1289                        tracing::warn!(
1290                            "Both fee sources failed (mempool: {:#}, rpc: {:#}), using default of 1 sat/vB",
1291                            mempool_err, rpc_err
1292                        );
1293                        Amount::from_sat(1000) // 1 sat/vB * 1000 = 1000 sat/kvB
1294                    }
1295                };
1296
1297                // Convert sat/kvB to sat/vB and apply hard cap
1298                let mut fee_sat_kvb = selected_fee_amount.to_sat();
1299
1300                // Apply hard cap from config
1301                if fee_sat_kvb > fee_rate_hard_cap * 1000 {
1302                    tracing::warn!(
1303                        "Fee rate {} sat/kvb exceeds hard cap {} sat/kvb, using hard cap",
1304                        fee_sat_kvb,
1305                        fee_rate_hard_cap * 1000
1306                    );
1307                    fee_sat_kvb = fee_rate_hard_cap * 1000;
1308                }
1309
1310                tracing::debug!("Final fee rate: {} sat/kvb", fee_sat_kvb);
1311                Ok(FeeRate::from_sat_per_kwu(fee_sat_kvb.div_ceil(4)))
1312            }
1313
1314            // All other network types are unsupported.
1315            _ => Err(eyre!(
1316                "Fee rate estimation is not supported for network: {:?}",
1317                network
1318            )
1319            .into()),
1320        }
1321    }
1322}
1323
1324/// Fetches the current recommended fee rate from the provider. Currently only supports
1325/// Mempool Space API.
1326/// This function is used to get the fee rate in sat/vkb (satoshis per kilovbyte).
1327/// See [Mempool Space API](https://mempool.space/docs/api/rest#get-recommended-fees) for more details.
1328async fn get_fee_rate_from_mempool_space(
1329    url: &Option<String>,
1330    endpoint: &Option<String>,
1331    network: Network,
1332) -> Result<Amount> {
1333    let url = url
1334        .as_ref()
1335        .ok_or_else(|| eyre!("Fee rate API host is not configured"))?;
1336
1337    let endpoint = endpoint
1338        .as_ref()
1339        .ok_or_else(|| eyre!("Fee rate API endpoint is not configured"))?;
1340
1341    let url = match network {
1342        Network::Bitcoin => format!(
1343            // If the variables are not, return Error to fallback to Bitcoin Core RPC.
1344            "{url}{endpoint}",
1345        ),
1346        Network::Testnet4 => format!("{url}testnet4/{endpoint}"),
1347        // Return early with error for unsupported networks
1348        Network::Signet => {
1349            tracing::warn!("You should use Citrea signet url for mempool.space");
1350            format!("{url}{endpoint}")
1351        }
1352        _ => return Err(eyre!("Unsupported network for mempool.space: {network:?}").into()),
1353    };
1354
1355    let retry_config = RetryConfig::new(250, Duration::from_secs(5), 4, 2, true);
1356
1357    let retry_strategy = retry_config.get_strategy();
1358
1359    // Retry predicate: only retry on timeouts, connect errors, and 5xx/429 statuses.
1360    let should_retry = |e: &FeeErr| match e {
1361        FeeErr::Timeout => true,
1362        FeeErr::Transport(req_err) => req_err.is_timeout() || req_err.is_connect(),
1363        FeeErr::Status(code) => code.is_server_error() || *code == StatusCode::TOO_MANY_REQUESTS,
1364        FeeErr::JsonDecode(_) | FeeErr::MissingField => false,
1365    };
1366
1367    let fee_sat_per_vb: u64 = RetryIf::spawn(
1368        retry_strategy,
1369        || {
1370            let url = url.clone();
1371            async move {
1372                let resp = timeout(Duration::from_secs(5), reqwest::get(&url))
1373                    .await
1374                    .map_err(|_| FeeErr::Timeout)?
1375                    .map_err(FeeErr::Transport)?;
1376
1377                let status = resp.status();
1378                if !status.is_success() {
1379                    return Err(FeeErr::Status(status));
1380                }
1381
1382                let json: serde_json::Value = timeout(Duration::from_secs(5), resp.json())
1383                    .await
1384                    .map_err(|_| FeeErr::Timeout)?
1385                    .map_err(FeeErr::JsonDecode)?;
1386
1387                json.get("fastestFee")
1388                    .and_then(|fee| fee.as_u64())
1389                    .ok_or(FeeErr::MissingField)
1390            }
1391        },
1392        should_retry,
1393    )
1394    .await
1395    .map_err(|e| eyre::eyre!(e))
1396    .wrap_err_with(|| format!("Failed to fetch/parse fees from {url}"))?;
1397
1398    // The API returns the fee rate in sat/vB. We multiply by 1000 to get sat/kvB.
1399    let fee_rate = Amount::from_sat(fee_sat_per_vb * 1000);
1400
1401    Ok(fee_rate)
1402}
1403
1404#[async_trait]
1405/// Implementation of the `RpcApi` trait for `ExtendedBitcoinRpc`. All RPC calls
1406/// are made with retry logic that only retries when errors are retryable.
1407impl RpcApi for ExtendedBitcoinRpc {
1408    async fn call<T: for<'a> serde::de::Deserialize<'a>>(
1409        &self,
1410        cmd: &str,
1411        args: &[serde_json::Value],
1412    ) -> std::result::Result<T, bitcoincore_rpc::Error> {
1413        tracing::trace!("Calling Bitcoin RPC command: {}", cmd);
1414        let strategy = self.retry_config.get_strategy();
1415
1416        let condition = |error: &bitcoincore_rpc::Error| error.is_retryable();
1417
1418        RetryIf::spawn(
1419            strategy,
1420            || async { self.client.call(cmd, args).await },
1421            condition,
1422        )
1423        .await
1424    }
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429    use std::collections::HashMap;
1430
1431    use crate::actor::Actor;
1432    use crate::config::protocol::{ProtocolParamset, REGTEST_PARAMSET};
1433    use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
1434    use crate::test::common::{citrea, create_test_config_with_thread_name};
1435    use crate::{
1436        bitvm_client::SECP, extended_bitcoin_rpc::BitcoinRPCError, test::common::create_regtest_rpc,
1437    };
1438    use bitcoin::Amount;
1439    use bitcoin::{amount, key::Keypair, Address, FeeRate, XOnlyPublicKey};
1440    use bitcoincore_rpc::RpcApi;
1441    use citrea_e2e::bitcoin::DEFAULT_FINALITY_DEPTH;
1442    use citrea_e2e::config::{BitcoinConfig, TestCaseDockerConfig};
1443    use citrea_e2e::node::NodeKind;
1444    use citrea_e2e::test_case::TestCaseRunner;
1445    use citrea_e2e::Result;
1446    use citrea_e2e::{config::TestCaseConfig, framework::TestFramework, test_case::TestCase};
1447    use tonic::async_trait;
1448
1449    #[tokio::test]
1450    async fn new_extended_rpc_with_clone() {
1451        let mut config = create_test_config_with_thread_name().await;
1452        let regtest = create_regtest_rpc(&mut config).await;
1453        let rpc = regtest.rpc();
1454
1455        rpc.mine_blocks(101).await.unwrap();
1456        let height = rpc.get_block_count().await.unwrap();
1457        let hash = rpc.get_block_hash(height).await.unwrap();
1458
1459        let cloned_rpc = rpc.clone_inner().await.unwrap();
1460        assert_eq!(cloned_rpc.url, rpc.url);
1461        assert_eq!(cloned_rpc.get_block_count().await.unwrap(), height);
1462        assert_eq!(cloned_rpc.get_block_hash(height).await.unwrap(), hash);
1463    }
1464
1465    #[tokio::test]
1466    async fn tx_checks_in_mempool_and_on_chain() {
1467        let mut config = create_test_config_with_thread_name().await;
1468        let regtest = create_regtest_rpc(&mut config).await;
1469        let rpc = regtest.rpc();
1470
1471        let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1472        let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1473        let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1474
1475        let amount = amount::Amount::from_sat(10000);
1476
1477        // Prepare a transaction.
1478        let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1479        let tx = rpc.get_tx_of_txid(&utxo.txid).await.unwrap();
1480        let txid = tx.compute_txid();
1481        tracing::debug!("TXID: {}", txid);
1482
1483        assert_eq!(tx.output[utxo.vout as usize].value, amount);
1484        assert_eq!(utxo.txid, txid);
1485        assert!(rpc
1486            .check_utxo_address_and_amount(&utxo, &address.script_pubkey(), amount)
1487            .await
1488            .unwrap());
1489
1490        // In mempool.
1491        assert!(rpc.confirmation_blocks(&utxo.txid).await.is_err());
1492        assert!(rpc.get_blockhash_of_tx(&utxo.txid).await.is_err());
1493        assert!(!rpc.is_tx_on_chain(&txid).await.unwrap());
1494        assert!(rpc.is_utxo_spent(&utxo).await.is_err());
1495
1496        rpc.mine_blocks(1).await.unwrap();
1497        let height = rpc.get_block_count().await.unwrap();
1498        assert_eq!(height as u32, rpc.get_current_chain_height().await.unwrap());
1499        let blockhash = rpc.get_block_hash(height).await.unwrap();
1500
1501        // On chain.
1502        assert_eq!(rpc.confirmation_blocks(&utxo.txid).await.unwrap(), 1);
1503        assert_eq!(
1504            rpc.get_blockhash_of_tx(&utxo.txid).await.unwrap(),
1505            blockhash
1506        );
1507        assert_eq!(rpc.get_tx_of_txid(&txid).await.unwrap(), tx);
1508        assert!(rpc.is_tx_on_chain(&txid).await.unwrap());
1509        assert!(!rpc.is_utxo_spent(&utxo).await.unwrap());
1510
1511        // Doesn't matter if in mempool or on chain.
1512        let txout = rpc.get_txout_from_outpoint(&utxo).await.unwrap();
1513        assert_eq!(txout.value, amount);
1514        assert_eq!(rpc.get_tx_of_txid(&txid).await.unwrap(), tx);
1515
1516        let height = rpc.get_current_chain_height().await.unwrap();
1517        let (hash, header) = rpc.get_block_info_by_height(height.into()).await.unwrap();
1518        assert_eq!(blockhash, hash);
1519        assert_eq!(rpc.get_block_header(&hash).await.unwrap(), header);
1520    }
1521
1522    #[tokio::test]
1523    async fn bump_fee_with_fee_rate() {
1524        let mut config = create_test_config_with_thread_name().await;
1525        let regtest = create_regtest_rpc(&mut config).await;
1526        let rpc = regtest.rpc();
1527
1528        let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1529        let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1530        let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1531
1532        let amount = amount::Amount::from_sat(10000);
1533
1534        // Confirmed transaction cannot be fee bumped.
1535        let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1536        rpc.mine_blocks(1).await.unwrap();
1537        assert!(rpc
1538            .bump_fee_with_fee_rate(utxo.txid, FeeRate::from_sat_per_vb(1).unwrap())
1539            .await
1540            .inspect_err(|e| {
1541                match e {
1542                    BitcoinRPCError::TransactionAlreadyInBlock(_) => {}
1543                    _ => panic!("Unexpected error: {e:?}"),
1544                }
1545            })
1546            .is_err());
1547
1548        let current_fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1549
1550        // Trying to bump a transaction with a fee rate that is already enough
1551        // should return the original txid.
1552        let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1553        let txid = rpc
1554            .bump_fee_with_fee_rate(utxo.txid, current_fee_rate)
1555            .await
1556            .unwrap();
1557        assert_eq!(txid, utxo.txid);
1558
1559        // A bigger fee rate should return a different txid.
1560        let new_fee_rate = FeeRate::from_sat_per_vb_unchecked(10000);
1561        let txid = rpc
1562            .bump_fee_with_fee_rate(utxo.txid, new_fee_rate)
1563            .await
1564            .unwrap();
1565        assert_ne!(txid, utxo.txid);
1566    }
1567
1568    struct ReorgChecks;
1569    #[async_trait]
1570    impl TestCase for ReorgChecks {
1571        fn bitcoin_config() -> BitcoinConfig {
1572            BitcoinConfig {
1573                extra_args: vec![
1574                    "-txindex=1",
1575                    "-fallbackfee=0.000001",
1576                    "-rpcallowip=0.0.0.0/0",
1577                    "-dustrelayfee=0",
1578                ],
1579                ..Default::default()
1580            }
1581        }
1582
1583        fn test_config() -> TestCaseConfig {
1584            TestCaseConfig {
1585                with_sequencer: true,
1586                with_batch_prover: false,
1587                n_nodes: HashMap::from([(NodeKind::Bitcoin, 2)]),
1588                docker: TestCaseDockerConfig {
1589                    bitcoin: true,
1590                    citrea: true,
1591                },
1592                ..Default::default()
1593            }
1594        }
1595
1596        async fn run_test(&mut self, f: &mut TestFramework) -> Result<()> {
1597            let (da0, da1) = (
1598                f.bitcoin_nodes.get(0).unwrap(),
1599                f.bitcoin_nodes.get(1).unwrap(),
1600            );
1601
1602            let mut config = create_test_config_with_thread_name().await;
1603            const PARAMSET: ProtocolParamset = ProtocolParamset {
1604                finality_depth: DEFAULT_FINALITY_DEPTH as u32,
1605                ..REGTEST_PARAMSET
1606            };
1607            config.protocol_paramset = &PARAMSET;
1608            citrea::update_config_with_citrea_e2e_values(
1609                &mut config,
1610                da0,
1611                f.sequencer.as_ref().expect("Sequencer is present"),
1612                None,
1613            );
1614
1615            let rpc = ExtendedBitcoinRpc::connect(
1616                config.bitcoin_rpc_url.clone(),
1617                config.bitcoin_rpc_user.clone(),
1618                config.bitcoin_rpc_password.clone(),
1619                None,
1620            )
1621            .await
1622            .unwrap();
1623
1624            // Reorg starts here.
1625            f.bitcoin_nodes.disconnect_nodes().await?;
1626
1627            let before_reorg_tip_height = rpc.get_block_count().await?;
1628            let before_reorg_tip_hash = rpc.get_block_hash(before_reorg_tip_height).await?;
1629
1630            let address = Actor::new(config.secret_key, config.protocol_paramset.network).address;
1631            let tx = rpc
1632                .send_to_address(&address, Amount::from_sat(10000))
1633                .await?;
1634
1635            assert!(!rpc.is_tx_on_chain(&tx.txid).await?);
1636            rpc.mine_blocks(1).await?;
1637            assert!(rpc.is_tx_on_chain(&tx.txid).await?);
1638
1639            // Make the second branch longer and perform a reorg.
1640            let reorg_depth = 4;
1641            da1.generate(reorg_depth).await.unwrap();
1642            f.bitcoin_nodes.connect_nodes().await?;
1643            f.bitcoin_nodes.wait_for_sync(None).await?;
1644
1645            // Check that reorg happened.
1646            let current_tip_height = rpc.get_block_count().await?;
1647            assert_eq!(
1648                before_reorg_tip_height + reorg_depth,
1649                current_tip_height,
1650                "Re-org did not occur"
1651            );
1652            let current_tip_hash = rpc.get_block_hash(current_tip_height).await?;
1653            assert_ne!(
1654                before_reorg_tip_hash, current_tip_hash,
1655                "Re-org did not occur"
1656            );
1657
1658            assert!(!rpc.is_tx_on_chain(&tx.txid).await?);
1659
1660            Ok(())
1661        }
1662    }
1663
1664    #[tokio::test]
1665    async fn reorg_checks() -> Result<()> {
1666        TestCaseRunner::new(ReorgChecks).run().await
1667    }
1668
1669    mod retry_config_tests {
1670        use crate::extended_bitcoin_rpc::RetryConfig;
1671
1672        use std::time::Duration;
1673
1674        #[test]
1675        fn test_retry_config_default() {
1676            let config = RetryConfig::default();
1677            assert_eq!(config.initial_delay_millis, 100);
1678            assert_eq!(config.max_delay, Duration::from_secs(30));
1679            assert_eq!(config.max_attempts, 5);
1680            assert_eq!(config.backoff_multiplier, 2);
1681            assert!(!config.is_jitter);
1682        }
1683
1684        #[test]
1685        fn test_retry_config_custom() {
1686            let initial = 200;
1687            let max = Duration::from_secs(10);
1688            let attempts = 7;
1689            let backoff_multiplier = 3;
1690            let jitter = true;
1691            let config = RetryConfig::new(initial, max, attempts, backoff_multiplier, jitter);
1692            assert_eq!(config.initial_delay_millis, initial);
1693            assert_eq!(config.max_delay, max);
1694            assert_eq!(config.max_attempts, attempts);
1695            assert_eq!(config.backoff_multiplier, backoff_multiplier);
1696            assert!(config.is_jitter);
1697        }
1698
1699        #[test]
1700        fn test_retry_strategy_initial_delay() {
1701            // Test that the first delay matches the expected initial_delay_millis
1702            // when initial_delay_millis is divisible by backoff_multiplier
1703            let initial_delay_millis = 100;
1704            let backoff_multiplier = 2;
1705            let config = RetryConfig::new(
1706                initial_delay_millis,
1707                Duration::from_secs(30),
1708                5,
1709                backoff_multiplier,
1710                false, // no jitter for predictable testing
1711            );
1712
1713            let mut strategy = config.get_strategy();
1714            let first_delay = strategy.next().expect("Should have first delay");
1715
1716            // The formula is: first_delay = base * factor
1717            // We set base = initial_delay_millis / backoff_multiplier
1718            // So: first_delay = (initial_delay_millis / backoff_multiplier) * backoff_multiplier = initial_delay_millis
1719            assert_eq!(
1720                first_delay,
1721                Duration::from_millis(initial_delay_millis),
1722                "First delay should match initial_delay_millis"
1723            );
1724
1725            // Verify the second delay is approximately initial_delay_millis * backoff_multiplier
1726            let second_delay = strategy.next().expect("Should have second delay");
1727            assert_eq!(
1728                second_delay,
1729                Duration::from_millis(initial_delay_millis * backoff_multiplier),
1730                "Second delay should be initial_delay_millis * backoff_multiplier"
1731            );
1732        }
1733    }
1734
1735    mod retryable_error_tests {
1736        use bitcoin::{hashes::Hash, BlockHash, Txid};
1737
1738        use crate::extended_bitcoin_rpc::RetryableError;
1739
1740        use super::*;
1741        use std::io::{Error as IoError, ErrorKind};
1742
1743        #[test]
1744        fn test_bitcoin_rpc_error_retryable_io_errors() {
1745            let retryable_kinds = [
1746                ErrorKind::ConnectionRefused,
1747                ErrorKind::ConnectionReset,
1748                ErrorKind::ConnectionAborted,
1749                ErrorKind::NotConnected,
1750                ErrorKind::BrokenPipe,
1751                ErrorKind::TimedOut,
1752                ErrorKind::Interrupted,
1753                ErrorKind::UnexpectedEof,
1754            ];
1755
1756            for kind in retryable_kinds {
1757                let io_error = IoError::new(kind, "test error");
1758                let rpc_error = bitcoincore_rpc::Error::Io(io_error);
1759                assert!(
1760                    rpc_error.is_retryable(),
1761                    "ErrorKind::{kind:?} should be retryable"
1762                );
1763            }
1764        }
1765
1766        #[test]
1767        fn test_bitcoin_rpc_error_non_retryable_io_errors() {
1768            let non_retryable_kinds = [
1769                ErrorKind::PermissionDenied,
1770                ErrorKind::NotFound,
1771                ErrorKind::InvalidInput,
1772                ErrorKind::InvalidData,
1773            ];
1774
1775            for kind in non_retryable_kinds {
1776                let io_error = IoError::new(kind, "test error");
1777                let rpc_error = bitcoincore_rpc::Error::Io(io_error);
1778                assert!(
1779                    !rpc_error.is_retryable(),
1780                    "ErrorKind::{kind:?} should not be retryable"
1781                );
1782            }
1783        }
1784
1785        #[test]
1786        fn test_bitcoin_rpc_error_auth_not_retryable() {
1787            let auth_error = bitcoincore_rpc::Error::Auth("Invalid credentials".to_string());
1788            assert!(!auth_error.is_retryable());
1789        }
1790
1791        #[test]
1792        fn test_bitcoin_rpc_error_url_parse_not_retryable() {
1793            let url_error = url::ParseError::EmptyHost;
1794            let rpc_error = bitcoincore_rpc::Error::UrlParse(url_error);
1795            assert!(!rpc_error.is_retryable());
1796        }
1797
1798        #[test]
1799        fn test_bitcoin_rpc_error_invalid_cookie_not_retryable() {
1800            let rpc_error = bitcoincore_rpc::Error::InvalidCookieFile;
1801            assert!(!rpc_error.is_retryable());
1802        }
1803
1804        #[test]
1805        fn test_bitcoin_rpc_error_returned_error_non_retryable_patterns() {
1806            let non_retryable_messages = [
1807                "insufficient funds",
1808                "transaction already in blockchain",
1809                "invalid transaction",
1810                "not found in mempool",
1811                "transaction conflict",
1812            ];
1813
1814            for msg in non_retryable_messages {
1815                let rpc_error = bitcoincore_rpc::Error::ReturnedError(msg.to_string());
1816                assert!(
1817                    !rpc_error.is_retryable(),
1818                    "Message '{msg}' should not be retryable"
1819                );
1820            }
1821        }
1822
1823        #[test]
1824        fn test_bitcoin_rpc_error_unexpected_structure_retryable() {
1825            let rpc_error = bitcoincore_rpc::Error::UnexpectedStructure;
1826            assert!(rpc_error.is_retryable());
1827        }
1828
1829        #[test]
1830        fn test_bitcoin_rpc_error_serialization_errors_not_retryable() {
1831            use bitcoin::consensus::encode::Error as EncodeError;
1832
1833            let serialization_errors = [
1834                bitcoincore_rpc::Error::BitcoinSerialization(EncodeError::Io(
1835                    IoError::other("test").into(),
1836                )),
1837                // bitcoincore_rpc::Error::Hex(HexToBytesError::InvalidChar(InvalidCharError{invalid: 0})),
1838                bitcoincore_rpc::Error::Json(serde_json::Error::io(IoError::other("test"))),
1839            ];
1840
1841            for error in serialization_errors {
1842                assert!(
1843                    !error.is_retryable(),
1844                    "Serialization error should not be retryable"
1845                );
1846            }
1847        }
1848
1849        #[test]
1850        fn test_bridge_rpc_error_retryable() {
1851            // Test permanent errors
1852            assert!(
1853                !BitcoinRPCError::TransactionAlreadyInBlock(BlockHash::all_zeros()).is_retryable()
1854            );
1855            assert!(!BitcoinRPCError::BumpFeeUTXOSpent(Default::default()).is_retryable());
1856
1857            // Test potentially retryable errors
1858            let txid = Txid::all_zeros();
1859            let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1860            assert!(BitcoinRPCError::BumpFeeError(txid, fee_rate).is_retryable());
1861
1862            // Test Other error with retryable patterns
1863            let retryable_other = BitcoinRPCError::Other(eyre::eyre!("timeout occurred"));
1864            assert!(retryable_other.is_retryable());
1865
1866            let non_retryable_other = BitcoinRPCError::Other(eyre::eyre!("permission denied"));
1867            assert!(!non_retryable_other.is_retryable());
1868        }
1869    }
1870
1871    mod rpc_call_retry_tests {
1872
1873        use crate::extended_bitcoin_rpc::RetryableError;
1874
1875        use super::*;
1876        use secrecy::SecretString;
1877
1878        #[tokio::test]
1879        async fn test_rpc_call_retry_with_invalid_credentials() {
1880            let mut config = create_test_config_with_thread_name().await;
1881            let regtest = create_regtest_rpc(&mut config).await;
1882
1883            // Get a working connection first
1884            let working_rpc = regtest.rpc();
1885            let url = working_rpc.url.clone();
1886
1887            // Create connection with invalid credentials
1888            let invalid_user = SecretString::new("invalid_user".to_string().into());
1889            let invalid_password = SecretString::new("invalid_password".to_string().into());
1890
1891            let res = ExtendedBitcoinRpc::connect(url, invalid_user, invalid_password, None).await;
1892
1893            assert!(res.is_err());
1894            assert!(!res.unwrap_err().is_retryable());
1895        }
1896
1897        #[tokio::test]
1898        async fn test_rpc_call_retry_with_invalid_host() {
1899            let user = SecretString::new("user".to_string().into());
1900            let password = SecretString::new("password".to_string().into());
1901            let invalid_url = "http://nonexistent-host:8332".to_string();
1902
1903            let res = ExtendedBitcoinRpc::connect(invalid_url, user, password, None).await;
1904
1905            assert!(res.is_err());
1906            assert!(!res.unwrap_err().is_retryable());
1907        }
1908    }
1909
1910    mod convenience_method_tests {
1911        use super::*;
1912
1913        #[tokio::test]
1914        async fn test_get_block_hash_with_retry() {
1915            let mut config = create_test_config_with_thread_name().await;
1916            let regtest = create_regtest_rpc(&mut config).await;
1917            let rpc = regtest.rpc();
1918
1919            // Mine a block first
1920            rpc.mine_blocks(1).await.unwrap();
1921            let height = rpc.get_block_count().await.unwrap();
1922
1923            let result = rpc.get_block_hash(height).await;
1924            assert!(result.is_ok());
1925
1926            let expected_hash = rpc.get_block_hash(height).await.unwrap();
1927            assert_eq!(result.unwrap(), expected_hash);
1928        }
1929
1930        #[tokio::test]
1931        async fn test_get_tx_out_with_retry() {
1932            let mut config = create_test_config_with_thread_name().await;
1933            let regtest = create_regtest_rpc(&mut config).await;
1934            let rpc = regtest.rpc();
1935
1936            // Create a transaction
1937            let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1938            let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1939            let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1940            let amount = Amount::from_sat(10000);
1941
1942            let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1943
1944            let result = rpc.get_tx_of_txid(&utxo.txid).await;
1945            assert!(result.is_ok());
1946
1947            let tx = result.unwrap();
1948            assert_eq!(tx.compute_txid(), utxo.txid);
1949        }
1950
1951        #[tokio::test]
1952        async fn test_send_to_address_with_retry() {
1953            let mut config = create_test_config_with_thread_name().await;
1954            let regtest = create_regtest_rpc(&mut config).await;
1955            let rpc = regtest.rpc();
1956
1957            let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1958            let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1959            let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1960            let amount = Amount::from_sat(10000);
1961
1962            let result = rpc.send_to_address(&address, amount).await;
1963            assert!(result.is_ok());
1964
1965            let outpoint = result.unwrap();
1966
1967            // Verify the transaction exists
1968            let tx = rpc.get_tx_of_txid(&outpoint.txid).await.unwrap();
1969            assert_eq!(tx.output[outpoint.vout as usize].value, amount);
1970        }
1971
1972        #[tokio::test]
1973        async fn test_bump_fee_with_retry() {
1974            let mut config = create_test_config_with_thread_name().await;
1975            let regtest = create_regtest_rpc(&mut config).await;
1976            let rpc = regtest.rpc();
1977
1978            let keypair = Keypair::from_secret_key(&SECP, &config.secret_key);
1979            let (xonly, _parity) = XOnlyPublicKey::from_keypair(&keypair);
1980            let address = Address::p2tr(&SECP, xonly, None, config.protocol_paramset.network);
1981            let amount = Amount::from_sat(10000);
1982
1983            // Create an unconfirmed transaction
1984            let utxo = rpc.send_to_address(&address, amount).await.unwrap();
1985            let new_fee_rate = FeeRate::from_sat_per_vb_unchecked(10000);
1986
1987            let result = rpc.bump_fee_with_fee_rate(utxo.txid, new_fee_rate).await;
1988            assert!(result.is_ok());
1989
1990            let new_txid = result.unwrap();
1991            // Should return a different txid since fee was actually bumped
1992            assert_ne!(new_txid, utxo.txid);
1993        }
1994    }
1995}