diff --git a/src/client.rs b/src/client.rs index 965ffd2..077db71 100644 --- a/src/client.rs +++ b/src/client.rs @@ -263,6 +263,26 @@ impl Client { self.call("getblock", &[json!(block_hash), json!(1)])?; block_info.into_model().map_err(Error::GetBlockVerboseOne) } + + /// Retrieves the prune height of the `bitcoind` instance this client is connected to. + /// + /// # Returns + /// + /// The prune height of the `bitcoind` instance, as an `Option`. + pub fn get_prune_height(&self) -> Result, Error> { + let res = { + let res: v30::GetBlockchainInfo = self.call("getblockchaininfo", &[])?; + res.into_model().map_err(Error::GetBlockchainInfo)? + }; + + if res.pruned { + Ok(Some( + res.prune_height.expect("pruned=true implies a pruneheight"), + )) + } else { + Ok(None) + } + } } #[cfg(test)] diff --git a/src/client/v28.rs b/src/client/v28.rs index 0b09be5..1e7a8f3 100644 --- a/src/client/v28.rs +++ b/src/client/v28.rs @@ -44,4 +44,24 @@ impl Client { self.call("getblock", &[json!(block_hash), json!(1)])?; block_info.into_model().map_err(Error::GetBlockVerboseOne) } + + /// Retrieves the prune height of the `bitcoind` instance this client is connected to. + /// + /// # Returns + /// + /// The prune height of the `bitcoind` instance, as an `Option`. + pub fn get_prune_height(&self) -> Result, Error> { + let res = { + let res: v28::GetBlockchainInfo = self.call("getblockchaininfo", &[])?; + res.into_model().map_err(Error::GetBlockchainInfo)? + }; + + if res.pruned { + Ok(Some( + res.prune_height.expect("pruned=true implies a pruneheight"), + )) + } else { + Ok(None) + } + } } diff --git a/src/error.rs b/src/error.rs index 3e8f3ba..daf8cea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,8 +3,12 @@ use bitcoin::{consensus::encode::FromHexError, hex::HexToArrayError}; #[cfg(feature = "28_0")] use corepc_types::v17::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; +#[cfg(feature = "28_0")] +use corepc_types::v19::GetBlockchainInfoError; #[cfg(not(feature = "28_0"))] -use corepc_types::v30::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; +use corepc_types::v30::{ + GetBlockHeaderVerboseError, GetBlockVerboseOneError, GetBlockchainInfoError, +}; use corepc_types::{bitcoin, v30::GetBlockFilterError}; use jsonrpc::serde_json; use std::{fmt, io, num::TryFromIntError}; @@ -27,6 +31,9 @@ pub enum Error { /// Error modeling [`GetBlockFilter`](corepc_types::model::GetBlockFilter) GetBlockFilter(GetBlockFilterError), + /// Error modeling [`GetBlockchainInfo`](corepc_types::model::GetBlockchainInfo). + GetBlockchainInfo(GetBlockchainInfoError), + /// Invalid or corrupted cookie file. InvalidCookieFile, @@ -56,6 +63,7 @@ impl fmt::Display for Error { Error::GetBlockVerboseOne(e) => write!(f, "block verbose error: {e}"), Error::GetBlockHeaderVerbose(e) => write!(f, "block header verbose error: {e}"), Error::GetBlockFilter(e) => write!(f, "block filter error: {e}"), + Error::GetBlockchainInfo(e) => write!(f, "getblockchaininfo error: {e}"), Error::InvalidCookieFile => write!(f, "invalid or missing cookie file"), Error::InvalidUrl(e) => write!(f, "invalid RPC URL: {e}"), Error::HexToArray(e) => write!(f, "hash parsing error: {e}"), diff --git a/tests/test_rpc_client.rs b/tests/test_rpc_client.rs index 676b408..1d334fc 100644 --- a/tests/test_rpc_client.rs +++ b/tests/test_rpc_client.rs @@ -3,6 +3,7 @@ //! These tests require a running Bitcoin Core node in regtest mode. To setup refer to [`corepc_node`]. use bdk_bitcoind_client::{Auth, Client, Error}; +use corepc_node::Conf; use corepc_types::bitcoin::{Amount, BlockHash, Txid}; use std::str::FromStr; @@ -12,7 +13,8 @@ use testenv::TestEnv; #[test] fn test_invalid_credentials() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); + let client = Client::with_auth( &env.node.rpc_url(), Auth::UserPass("wrong".to_string(), "credentials".to_string()), @@ -28,7 +30,7 @@ fn test_invalid_credentials() { fn test_client_with_custom_transport() { use jsonrpc::http::bitreq_http::Builder; - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let rpc_url = env.node.rpc_url(); let cookie = env @@ -54,7 +56,7 @@ fn test_client_with_custom_transport() { #[test] fn test_get_block_count() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let block_count = env .client @@ -66,7 +68,7 @@ fn test_get_block_count() { #[test] fn test_get_block_hash() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let _genesis_hash = env .client @@ -76,21 +78,22 @@ fn test_get_block_hash() { #[test] fn test_get_block_hash_for_current_height() { - let TestEnv { - client, - node: _node, - } = TestEnv::setup().unwrap(); + let env = TestEnv::new(); - let block_count = client.get_block_count().expect("failed to get block count"); + let block_count = env + .client + .get_block_count() + .expect("failed to get block count"); - let _block_hash = client + let _block_hash = env + .client .get_block_hash(block_count) .expect("failed to get block hash"); } #[test] fn test_get_block_hash_invalid_height() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let result = env.client.get_block_hash(999_999_999); @@ -99,17 +102,19 @@ fn test_get_block_hash_invalid_height() { #[test] fn test_get_best_block_hash() { - let TestEnv { - client, - node: _node, - } = TestEnv::setup().unwrap(); + let env = TestEnv::new(); - let best_block_hash = client + let best_block_hash = env + .client .get_best_block_hash() .expect("failed to get best block hash"); - let block_count = client.get_block_count().expect("failed to get block count"); - let block_hash = client + let block_count = env + .client + .get_block_count() + .expect("failed to get block count"); + let block_hash = env + .client .get_block_hash(block_count) .expect("failed to get block hash"); @@ -118,16 +123,15 @@ fn test_get_best_block_hash() { #[test] fn test_get_block() { - let TestEnv { - client, - node: _node, - } = TestEnv::setup().unwrap(); + let env = TestEnv::new(); - let genesis_hash = client + let genesis_hash = env + .client .get_block_hash(0) .expect("failed to get genesis hash"); - let block = client + let block = env + .client .get_block(&genesis_hash) .expect("failed to get block"); @@ -137,7 +141,7 @@ fn test_get_block() { #[test] fn test_get_block_after_mining() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let hashes = env.mine_blocks(1, None).expect("failed to mine block"); let block_hash = hashes[0]; @@ -153,7 +157,7 @@ fn test_get_block_after_mining() { #[test] fn test_get_block_verbose() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let hashes = env.mine_blocks(1, None).expect("failed to mine block"); let block_hash = hashes[0]; @@ -169,7 +173,7 @@ fn test_get_block_verbose() { #[test] fn test_get_block_invalid_hash() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let invalid_hash = BlockHash::from_str("0000000000000000000000000000000000000000000000000000000000000000") @@ -182,16 +186,15 @@ fn test_get_block_invalid_hash() { #[test] fn test_get_block_header() { - let TestEnv { - client, - node: _node, - } = TestEnv::setup().unwrap(); + let env = TestEnv::new(); - let genesis_hash = client + let genesis_hash = env + .client .get_block_hash(0) .expect("failed to get genesis hash"); - let header = client + let header = env + .client .get_block_header(&genesis_hash) .expect("failed to get block header"); @@ -200,16 +203,15 @@ fn test_get_block_header() { #[test] fn test_get_block_header_verbose() { - let TestEnv { - client, - node: _node, - } = TestEnv::setup().unwrap(); + let env = TestEnv::new(); - let genesis_hash = client + let genesis_hash = env + .client .get_block_hash(0) .expect("failed to get genesis hash"); - let header = client + let header = env + .client .get_block_header_verbose(&genesis_hash) .expect("failed to get block header verbose"); @@ -218,7 +220,7 @@ fn test_get_block_header_verbose() { #[test] fn test_get_raw_mempool_empty() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let _hashes = env.mine_blocks(1, None).expect("failed to mine block"); @@ -231,7 +233,7 @@ fn test_get_raw_mempool_empty() { #[test] fn test_get_raw_mempool_with_transaction() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let _hashes = env.mine_blocks(101, None).expect("failed to mine block"); @@ -251,7 +253,7 @@ fn test_get_raw_mempool_with_transaction() { #[test] fn test_get_raw_transaction() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let _hashes = env.mine_blocks(1, None).expect("failed to mine block"); @@ -279,7 +281,7 @@ fn test_get_raw_transaction() { #[test] fn test_get_raw_transaction_invalid_txid() { - let env = TestEnv::setup().unwrap(); + let env = TestEnv::new(); let fake_txid = Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(); @@ -291,18 +293,50 @@ fn test_get_raw_transaction_invalid_txid() { #[test] fn test_get_block_filter() { - let TestEnv { - client, - node: _node, - } = TestEnv::setup().unwrap(); + let env = TestEnv::new(); - let genesis_hash = client + let genesis_hash = env + .client .get_block_hash(0) .expect("failed to get genesis hash"); - let result = client + let result = env + .client .get_block_filter(&genesis_hash) .expect("failed to get block filter"); assert!(!result.filter.is_empty()); } + +#[test] +fn test_get_prune_height() { + // Spawn an unpruned node. + let env_unpruned = TestEnv::new(); + + // Assert that `getblockchaininfo.pruned` is `None`. + let unpruned_res = env_unpruned.client.get_prune_height().unwrap(); + assert_eq!(unpruned_res, None); + + // Spawn a node with manual pruning enabled. + let mut node_config = Conf::default(); + node_config.args.push("-prune=1"); + node_config.args.push("-fastprune"); + let env_pruned = TestEnv::new_with_config(&node_config); + + // Mine 1000 blocks. + let block_count = 1000; + let _hashes = env_pruned.mine_blocks(block_count as usize, None); + + // Assert that `getblockchaininfo.pruned` is `Some(0)`. + let pruned_res = env_pruned.client.get_prune_height().unwrap(); + assert_eq!(pruned_res, Some(0)); + + // Prune the last 2 blocks. + let _ = env_pruned.corepc_client.prune_blockchain(block_count - 2); + + // Assert that `getblockchaininfo.prunedheight` is > 0. + // Note: it's not possible to assert on a specific block height since Bitcoin Core + // prunes at the block file level (`blkXXXX.dat`), and not at block height level. + let pruned_res = env_pruned.client.get_prune_height().unwrap(); + assert!(pruned_res > Some(0)); +} diff --git a/tests/testenv.rs b/tests/testenv.rs index 691976c..beeb758 100644 --- a/tests/testenv.rs +++ b/tests/testenv.rs @@ -1,9 +1,10 @@ use bdk_bitcoind_client::{Auth, Client}; use bitcoin::{Address, BlockHash}; +use corepc_node::client::client_sync::Auth as CorepcAuth; use corepc_node::{exe_path, Conf, Node}; use corepc_types::bitcoin; -/// Test environment for running integration tests. +/// Test Environment for running integration tests. /// /// [`TestEnv`] exposes the [`Client`] API defined by this crate to be tested against /// a running [`corepc_node::Node`] instance. @@ -13,10 +14,18 @@ pub struct TestEnv { pub client: Client, /// [`corepc_node::Node`] pub node: Node, + /// [`corepc_node::Client] + pub corepc_client: corepc_node::Client, +} + +impl Default for TestEnv { + fn default() -> Self { + Self::new() + } } impl TestEnv { - /// Create new [`TestEnv`]. + /// Create new [`TestEnv`] with a default [configuration](Config). /// /// This will first look for the path of the `bitcoind` executable using [`corepc_node::exe_path`] /// before returning a new [`TestEnv`] with [`Client`] connected to it. @@ -24,21 +33,48 @@ impl TestEnv { /// Note that [`Node`] also exposes its own RPC [`client`](Node::client) which may help with /// creating different test cases, but be aware that this is different from the client we're /// actually testing. - pub fn setup() -> anyhow::Result { - let exe = exe_path()?; + pub fn new() -> Self { + // Enable `txindex` and `blockfilterindex` by default. + let mut bitcoind_config = Conf::default(); + bitcoind_config.args.push("-txindex=1"); + bitcoind_config.args.push("-blockfilterindex=1"); - let mut conf = Conf::default(); - conf.args.push("-blockfilterindex=1"); - conf.args.push("-txindex=1"); + TestEnv::new_with_config(&bitcoind_config) + } + + /// Create new [`TestEnv`] with a custom [configuration](Config) for the [node](Node). + /// + /// This will first look for the path of the `bitcoind` executable using [`corepc_node::exe_path`] + /// before returning a new [`TestEnv`] with [`Client`] connected to it. + /// + /// Note that [`Node`] also exposes its own RPC [`client`](Node::client) which may help with + /// creating different test cases, but be aware that this is different from the client we're + /// actually testing. + pub fn new_with_config(config: &Conf) -> Self { + // Try to get `BITCOIN_EXE` from the environment. + let bitcoind_exe = exe_path().unwrap(); - let node = Node::with_conf(exe, &conf)?; + // Spawn a `corepc::Node` with the default configuration. + let node = Node::with_conf(bitcoind_exe, config).unwrap(); + // Get the URL for the RPC server. let rpc_url = node.rpc_url(); + // Get the location for the cookie file. let cookie_file = &node.params.cookie_file; + + // Setup authentication and create the `bdk_client`. let auth = Auth::CookieFile(cookie_file.clone()); - let client = Client::with_auth(&rpc_url, auth)?; + let client = Client::with_auth(&rpc_url, auth).unwrap(); + + // Setup authentication and create the `corepc_client`. + let corepc_auth = CorepcAuth::CookieFile(cookie_file.clone()); + let corepc_client = corepc_node::Client::new_with_auth(&rpc_url, corepc_auth).unwrap(); - Ok(Self { client, node }) + Self { + client, + node, + corepc_client, + } } /// Mines `nblocks` blocks to the given `address`, or an address controlled