From b7c85fdf50799f8aa9e1b37d8b52c89ea6d56b79 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 18 Nov 2025 18:02:21 +0100 Subject: [PATCH 1/5] feat(add-blockchain-mthds): add blockchain methods - getblockcount - getblockhash - getblockfilter - getblockheader - getrawmempool - getrawtransaction --- src/client.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/error.rs | 7 +++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1427e06..df22842 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,12 +2,21 @@ use std::{ fs::File, io::{BufRead, BufReader}, path::PathBuf, + str::FromStr, }; use crate::error::Error; use crate::jsonrpc::minreq_http::Builder; -use corepc_types::bitcoin::BlockHash; -use jsonrpc::{serde, serde_json, Transport}; +use corepc_types::{ + bitcoin::{ + Block, BlockHash, Transaction, Txid, block::Header, consensus::deserialize, hex::FromHex, + }, + model::{GetBlockCount, GetBlockFilter, GetBlockVerboseOne, GetRawMempool}, +}; +use jsonrpc::{ + Transport, serde, + serde_json::{self, json}, +}; /// client authentication methods #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -101,11 +110,94 @@ impl Client { // `bitcoind` RPC methods impl Client { - /// Get best block hash. + /// Get block + pub fn get_block(&self, block_hash: &BlockHash) -> Result { + let hex_string: String = self.call("getblock", &[json!(block_hash), json!(0)])?; + + let bytes: Vec = Vec::::from_hex(&hex_string).map_err(Error::HexToBytes)?; + + let block: Block = deserialize(&bytes) + .map_err(|e| Error::InvalidResponse(format!("failed to deserialize block: {e}")))?; + + Ok(block) + } + + /// Get block verboseone + pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { + let res: GetBlockVerboseOne = self.call("getblock", &[json!(block_hash), json!(1)])?; + Ok(res) + } + + /// Get best block hash pub fn get_best_block_hash(&self) -> Result { let res: String = self.call("getbestblockhash", &[])?; Ok(res.parse()?) } + + /// Get block count + pub fn get_block_count(&self) -> Result { + let res: GetBlockCount = self.call("getblockcount", &[])?; + Ok(res.0) + } + + /// Get block hash + pub fn get_block_hash(&self, height: u32) -> Result { + let raw: serde_json::Value = self.call("getblockhash", &[json!(height)])?; + + let hash_str = match raw { + serde_json::Value::String(s) => s, + serde_json::Value::Object(obj) => obj + .get("hash") + .and_then(|v| v.as_str()) + .ok_or_else(|| Error::InvalidResponse("getblockhash: missing 'hash' field".into()))? + .to_string(), + _ => { + return Err(Error::InvalidResponse( + "getblockhash: unexpected response type".into(), + )); + } + }; + + BlockHash::from_str(&hash_str).map_err(Error::HexToArray) + } + + /// Get block filter + pub fn get_block_filter(&self, block_hash: BlockHash) -> Result { + let res: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; + Ok(res) + } + + /// Get block header + pub fn get_block_header(&self, block_hash: &BlockHash) -> Result { + let hex_string: String = self.call("getblockheader", &[json!(block_hash), json!(false)])?; + + let bytes = Vec::::from_hex(&hex_string).map_err(Error::HexToBytes)?; + + let header = deserialize(&bytes).map_err(|e| { + Error::InvalidResponse(format!("failed to deserialize block header: {e}")) + })?; + + Ok(header) + } + + /// Get raw mempool + pub fn get_raw_mempool(&self) -> Result, Error> { + let res: GetRawMempool = self.call("getrawmempool", &[])?; + Ok(res.0) + } + + /// Get raw transaction + pub fn get_raw_transaction(&self, txid: &Txid) -> Result { + let hex_string: String = self.call("getrawtransaction", &[json!(txid)])?; + + let bytes = Vec::::from_hex(&hex_string).map_err(Error::HexToBytes)?; + + let transaction = deserialize(&bytes).map_err(|e| { + Error::InvalidResponse(format!("transaction deserialization failed: {e}")) + })?; + + Ok(transaction) + } } #[cfg(test)] diff --git a/src/error.rs b/src/error.rs index 1e11f5a..bcec548 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,7 @@ use std::{fmt, io}; -use corepc_types::bitcoin::hex::HexToArrayError; +use corepc_types::bitcoin::hex::{HexToArrayError, HexToBytesError}; use jsonrpc::serde_json; /// Result type alias for the RPC client. @@ -23,6 +23,9 @@ pub enum Error { /// JSON-RPC error from the server. JsonRpc(jsonrpc::Error), + /// Hex decoding error for byte vectors (used in get_block, etc.) + HexToBytes(HexToBytesError), + /// Hash parsing error. HexToArray(HexToArrayError), @@ -41,6 +44,7 @@ impl fmt::Display for Error { } Error::InvalidCookieFile => write!(f, "invalid cookie file"), Error::InvalidResponse(e) => write!(f, "invalid response: {e}"), + Error::HexToBytes(e) => write!(f, "Hex to bytes error: {e}"), Error::HexToArray(e) => write!(f, "Hash parsing eror: {e}"), Error::JsonRpc(e) => write!(f, "JSON-RPC error: {e}"), Error::Json(e) => write!(f, "JSON error: {e}"), @@ -55,6 +59,7 @@ impl std::error::Error for Error { Error::JsonRpc(e) => Some(e), Error::Json(e) => Some(e), Error::Io(e) => Some(e), + Error::HexToBytes(e) => Some(e), Error::HexToArray(e) => Some(e), _ => None, } From 291f42af585602a30096c67306e5b1565c5c1823 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 18 Nov 2025 18:04:44 +0100 Subject: [PATCH 2/5] feat: Add integration tests - add integration tests for blockchain methods --- src/client.rs | 138 ++++++++++++++------- src/error.rs | 10 +- tests/test_rpc_client.rs | 262 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 362 insertions(+), 48 deletions(-) diff --git a/src/client.rs b/src/client.rs index df22842..beaa4ec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,32 +2,43 @@ use std::{ fs::File, io::{BufRead, BufReader}, path::PathBuf, - str::FromStr, }; use crate::error::Error; use crate::jsonrpc::minreq_http::Builder; use corepc_types::{ bitcoin::{ - Block, BlockHash, Transaction, Txid, block::Header, consensus::deserialize, hex::FromHex, + block::Header, + consensus::{deserialize, encode::deserialize_hex}, + hex::FromHex, + Block, BlockHash, Transaction, Txid, }, - model::{GetBlockCount, GetBlockFilter, GetBlockVerboseOne, GetRawMempool}, + model::{GetBlockCount, GetBlockFilter, GetRawMempool}, + v29::GetBlockVerboseOne, }; use jsonrpc::{ - Transport, serde, + serde, serde_json::{self, json}, + Transport, }; -/// client authentication methods +/// Client authentication methods for the Bitcoin Core JSON-RPC server #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum Auth { + /// No authentication (not recommended) None, + /// Username and password authentication (RPC user/pass) UserPass(String, String), + /// Authentication via a cookie file CookieFile(PathBuf), } impl Auth { - /// Convert into the arguments that jsonrpc::Client needs. + /// Converts `Auth` enum into the optional username and password strings + /// required by JSON-RPC client transport. + /// + /// # Errors + /// Returns an error if the `CookieFile` cannot be read or invalid pub fn get_user_pass(self) -> Result<(Option, Option), Error> { match self { Auth::None => Ok((None, None)), @@ -44,7 +55,9 @@ impl Auth { } } -// RPC Client. +/// Bitcoin Core JSON-RPC Client. +/// +/// A wrapper for JSON-RPC client for interacting with the `bitcoind` RPC interface. #[derive(Debug)] pub struct Client { /// The inner JSON-RPC client. @@ -52,10 +65,18 @@ pub struct Client { } impl Client { - /// Creates a client to a bitcoind JSON-RPC server. + /// Creates a client connection to a bitcoind JSON-RPC server with authentication /// /// Requires authentication via username/password or cookie file. /// For connections without authentication, use `with_transport` instead. + /// # Arguments + /// * `url` - URL of the RPC server + /// * `auth` - authentication method (`UserPass` or `CookieFile`). + /// + /// # Errors + /// * Returns `Error::MissingAuthentication` if `Auth::None` is provided. + /// * Returns `Error::InvalidResponse` if the URL is invalid. + /// * Returns errors related to reading the cookie file. pub fn with_auth(url: &str, auth: Auth) -> Result { if matches!(auth, Auth::None) { return Err(Error::MissingAuthentication); @@ -95,7 +116,9 @@ impl Client { } } - /// Calls the RPC `method` with a given `args` list. + /// Calls the underlying RPC `method` with given `args` list + /// + /// This is the generic function used by all specific RPC methods. pub fn call(&self, method: &str, args: &[serde_json::Value]) -> Result where T: for<'de> serde::Deserialize<'de>, @@ -108,66 +131,82 @@ impl Client { } } -// `bitcoind` RPC methods +// `bitcoind` RPC methods implementation for `Client` impl Client { - /// Get block + /// Retrieves the raw block data for a given block hash (verbosity 0) + /// + /// # Arguments + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// The deserialized `Block` struct. pub fn get_block(&self, block_hash: &BlockHash) -> Result { let hex_string: String = self.call("getblock", &[json!(block_hash), json!(0)])?; - - let bytes: Vec = Vec::::from_hex(&hex_string).map_err(Error::HexToBytes)?; - - let block: Block = deserialize(&bytes) - .map_err(|e| Error::InvalidResponse(format!("failed to deserialize block: {e}")))?; - + let block = deserialize_hex(&hex_string).map_err(Error::DecodeHex)?; Ok(block) } - /// Get block verboseone + /// Retrieves the verbose JSON representation of a block (verbosity 1) + /// # Arguments + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// The verbose block data as a `GetBlockVerboseOne` struct. pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { - let res: GetBlockVerboseOne = self.call("getblock", &[json!(block_hash), json!(1)])?; - Ok(res) + let block_verbose_one: GetBlockVerboseOne = + self.call("getblock", &[json!(block_hash), json!(1)])?; + Ok(block_verbose_one) } - /// Get best block hash + /// Retrieves the hash of the tip of the best block chain. + /// + /// # Returns + /// The `BlockHash` of the chain tip. pub fn get_best_block_hash(&self) -> Result { let res: String = self.call("getbestblockhash", &[])?; Ok(res.parse()?) } - /// Get block count + /// Retrieves the number of blocks in the longest chain + /// + /// # Returns + /// The block count as a `u64` pub fn get_block_count(&self) -> Result { let res: GetBlockCount = self.call("getblockcount", &[])?; Ok(res.0) } - /// Get block hash + /// Retrieves the block hash at a given height + /// + /// # Arguments + /// * `height`: The block height + /// + /// # Returns + /// The `BlockHash` for the given height pub fn get_block_hash(&self, height: u32) -> Result { - let raw: serde_json::Value = self.call("getblockhash", &[json!(height)])?; - - let hash_str = match raw { - serde_json::Value::String(s) => s, - serde_json::Value::Object(obj) => obj - .get("hash") - .and_then(|v| v.as_str()) - .ok_or_else(|| Error::InvalidResponse("getblockhash: missing 'hash' field".into()))? - .to_string(), - _ => { - return Err(Error::InvalidResponse( - "getblockhash: unexpected response type".into(), - )); - } - }; - - BlockHash::from_str(&hash_str).map_err(Error::HexToArray) + let hex: String = self.call("getblockhash", &[json!(height)])?; + Ok(hex.parse()?) } - /// Get block filter - pub fn get_block_filter(&self, block_hash: BlockHash) -> Result { + /// Retrieves the compact block filter for a given block + /// + /// # Arguments + /// * `block_hash`: The hash of the block whose filter is requested + /// + /// # Returns + /// The `GetBlockFilter` structure containing the filter data + pub fn get_block_filter(&self, block_hash: &BlockHash) -> Result { let res: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; Ok(res) } - /// Get block header + /// Retrieves the raw block header for a given block hash. + /// + /// # Arguments + /// * `block_hash`: The hash of the block whose header is requested. + /// + /// # Returns + /// The deserialized `Header` struct pub fn get_block_header(&self, block_hash: &BlockHash) -> Result { let hex_string: String = self.call("getblockheader", &[json!(block_hash), json!(false)])?; @@ -180,13 +219,22 @@ impl Client { Ok(header) } - /// Get raw mempool + /// Retrieves the transaction IDs of all transactions currently in the mempool + /// + /// # Returns + /// A vector of `Txid`s in the raw mempool pub fn get_raw_mempool(&self) -> Result, Error> { let res: GetRawMempool = self.call("getrawmempool", &[])?; Ok(res.0) } - /// Get raw transaction + /// Retrieves the raw transaction data for a given transaction ID. + /// + /// # Arguments + /// * `txid`: The transaction ID to retrieve. + /// + /// # Returns + /// The deserialized `Transaction` struct pub fn get_raw_transaction(&self, txid: &Txid) -> Result { let hex_string: String = self.call("getrawtransaction", &[json!(txid)])?; diff --git a/src/error.rs b/src/error.rs index bcec548..52ff48d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,10 @@ use std::{fmt, io}; -use corepc_types::bitcoin::hex::{HexToArrayError, HexToBytesError}; +use corepc_types::bitcoin::{ + consensus, + hex::{HexToArrayError, HexToBytesError}, +}; use jsonrpc::serde_json; /// Result type alias for the RPC client. @@ -11,6 +14,9 @@ pub type Result = std::result::Result; /// Errors that can occur when using the Bitcoin RPC client. #[derive(Debug)] pub enum Error { + /// Hex deserialization error + DecodeHex(consensus::encode::FromHexError), + /// Missing authentication credentials. MissingAuthentication, @@ -49,6 +55,7 @@ impl fmt::Display for Error { Error::JsonRpc(e) => write!(f, "JSON-RPC error: {e}"), Error::Json(e) => write!(f, "JSON error: {e}"), Error::Io(e) => write!(f, "I/O error: {e}"), + Error::DecodeHex(e) => write!(f, "Hex deserialization error: {e}"), } } } @@ -61,6 +68,7 @@ impl std::error::Error for Error { Error::Io(e) => Some(e), Error::HexToBytes(e) => Some(e), Error::HexToArray(e) => Some(e), + Error::DecodeHex(e) => Some(e), _ => None, } } diff --git a/tests/test_rpc_client.rs b/tests/test_rpc_client.rs index 89dde64..e681d71 100644 --- a/tests/test_rpc_client.rs +++ b/tests/test_rpc_client.rs @@ -8,8 +8,9 @@ //! ``` use bdk_bitcoind_client::{Auth, Client, Error}; -use corepc_types::bitcoin::BlockHash; -use std::path::PathBuf; +use corepc_types::bitcoin::{BlockHash, Txid}; +use jsonrpc::serde_json::json; +use std::{path::PathBuf, str::FromStr}; /// Helper to get the test RPC URL fn test_url() -> String { @@ -23,6 +24,17 @@ fn test_auth() -> Auth { Auth::UserPass(user, pass) } +/// Helper to create a test client +fn test_client() -> Client { + Client::with_auth(&test_url(), test_auth()).expect("failed to create client") +} + +/// Helper to mine blocks +fn mine_blocks(client: &Client, n: u64) -> Result, Error> { + let address: String = client.call("getnewaddress", &[])?; + client.call("generatetoaddress", &[json!(n), json!(address)]) +} + #[test] #[ignore] fn test_client_with_user_pass() { @@ -105,3 +117,249 @@ fn test_client_with_custom_transport() { "block hash should be 64 characters" ); } + +#[test] +#[ignore] +fn test_get_block_count() { + let client = test_client(); + + let block_count = client.get_block_count().expect("failed to get block count"); + + assert!(block_count >= 1); +} + +#[test] +#[ignore] +fn test_get_block_hash() { + let client = test_client(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis block hash"); + + assert_eq!(genesis_hash.to_string().len(), 64); +} + +#[test] +#[ignore] +fn test_get_block_hash_for_current_height() { + let client = test_client(); + + let block_count = client.get_block_count().expect("failed to get block count"); + + let block_hash = client + .get_block_hash(block_count.try_into().unwrap()) + .expect("failed to get block hash"); + + assert_eq!(block_hash.to_string().len(), 64); +} + +#[test] +#[ignore] +fn test_get_block_hash_invalid_height() { + let client = test_client(); + + let result = client.get_block_hash(999999999); + + assert!(result.is_err()); +} + +#[test] +#[ignore] +fn test_get_best_block_hash() { + let client = test_client(); + + let best_hash = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + assert_eq!(best_hash.to_string().len(), 64); +} + +#[test] +#[ignore] +fn test_get_best_block_hash_changes_after_mining() { + let client = test_client(); + + let hash_before = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + mine_blocks(&client, 1).expect("failed to mine block"); + + let hash_after = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + assert_ne!(hash_before, hash_after); +} + +#[test] +#[ignore] +fn test_get_block() { + let client = test_client(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let block = client + .get_block(&genesis_hash) + .expect("failed to get block"); + + assert_eq!(block.block_hash(), genesis_hash); + assert!(!block.txdata.is_empty()); +} + +#[test] +#[ignore] +fn test_get_block_after_mining() { + let client = test_client(); + + let hashes = mine_blocks(&client, 1).expect("failed to mine block"); + let block_hash = BlockHash::from_str(&hashes[0]).expect("invalid hash"); + + let block = client.get_block(&block_hash).expect("failed to get block"); + + assert_eq!(block.block_hash(), block_hash); + assert!(!block.txdata.is_empty()); +} + +#[test] +#[ignore] +fn test_get_block_invalid_hash() { + let client = test_client(); + + let invalid_hash = + BlockHash::from_str("0000000000000000000000000000000000000000000000000000000000000000") + .unwrap(); + + let result = client.get_block(&invalid_hash); + + assert!(result.is_err()); +} + +#[test] +#[ignore] +fn test_get_block_header() { + let client = test_client(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let header = client + .get_block_header(&genesis_hash) + .expect("failed to get block header"); + + assert_eq!(header.block_hash(), genesis_hash); +} + +#[test] +#[ignore] +fn test_get_block_header_has_valid_fields() { + let client = test_client(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let header = client + .get_block_header(&genesis_hash) + .expect("failed to get block header"); + + assert!(header.time > 0); + assert!(header.nonce >= 1); +} + +#[test] +#[ignore] +fn test_get_raw_mempool_empty() { + let client = test_client(); + + mine_blocks(&client, 1).expect("failed to mine block"); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + let mempool = client.get_raw_mempool().expect("failed to get mempool"); + + assert!(mempool.is_empty()); +} + +#[test] +#[ignore] +fn test_get_raw_mempool_with_transaction() { + let client = test_client(); + + mine_blocks(&client, 101).expect("failed to mine blocks"); + + let address: String = client + .call("getnewaddress", &[]) + .expect("failed to get address"); + let txid: String = client + .call("sendtoaddress", &[json!(address), json!(0.001)]) + .expect("failed to send transaction"); + + let mempool = client.get_raw_mempool().expect("failed to get mempool"); + + let txid_parsed = Txid::from_str(&txid).unwrap(); + assert!(mempool.contains(&txid_parsed)); +} + +#[test] +#[ignore] +fn test_get_raw_transaction() { + let client = test_client(); + + mine_blocks(&client, 1).expect("failed to mine block"); + + let best_hash = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + let block = client.get_block(&best_hash).expect("failed to get block"); + + let txid = &block.txdata[0].compute_txid(); + + let tx = client + .get_raw_transaction(txid) + .expect("failed to get raw transaction"); + + assert_eq!(tx.compute_txid(), *txid); + assert!(!tx.input.is_empty()); + assert!(!tx.output.is_empty()); +} + +#[test] +#[ignore] +fn test_get_raw_transaction_invalid_txid() { + let client = test_client(); + + let fake_txid = + Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(); + + let result = client.get_raw_transaction(&fake_txid); + + assert!(result.is_err()); +} + +#[test] +#[ignore] +fn test_get_block_filter() { + let client = test_client(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let result = client.get_block_filter(&genesis_hash); + + match result { + Ok(filter) => { + assert!(!filter.filter.is_empty()); + } + Err(_) => { + println!("Block filters not enabled (requires -blockfilterindex=1)"); + } + } +} From b0731c15ef67020c5436b3aff297ceff372629b8 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Thu, 11 Dec 2025 14:55:48 +0100 Subject: [PATCH 3/5] feat: Update `corepc-types` to v0.11.0 - update `corepc-types` to v0.11.0 to support Bitcoind v30 --- Cargo.lock | 4 +-- Cargo.toml | 2 +- src/client.rs | 68 +++++++++++++++++----------------------- src/error.rs | 43 ++++++++++++++++++++++--- tests/test_rpc_client.rs | 2 +- 5 files changed, 71 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21e0ffd..32944fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "corepc-types" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22db78b0223b66f82f92b14345f06307078f76d94b18280431ea9bc6cd9cbb6" +checksum = "fc6ea6101b2da248ff9c7e0ead02c6e0b8243db140d86c6190e1b043c306d97a" dependencies = [ "bitcoin", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5b21b93..35db0a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = ["Bitcoin Dev Kit Developers"] readme = "README.md" [dependencies] -corepc-types = { version = "0.10.1", features = ["default"]} +corepc-types = { version = "0.11.0", features = ["default"]} jsonrpc = { version = "0.18.0", features = ["simple_http", "simple_tcp", "minreq_http", "simple_uds", "proxy"] } [features] diff --git a/src/client.rs b/src/client.rs index beaa4ec..1aff24b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,13 +8,9 @@ use crate::error::Error; use crate::jsonrpc::minreq_http::Builder; use corepc_types::{ bitcoin::{ - block::Header, - consensus::{deserialize, encode::deserialize_hex}, - hex::FromHex, - Block, BlockHash, Transaction, Txid, + block::Header, consensus::encode::deserialize_hex, Block, BlockHash, Transaction, Txid, }, - model::{GetBlockCount, GetBlockFilter, GetRawMempool}, - v29::GetBlockVerboseOne, + model::{GetBlockCount, GetBlockFilter, GetBlockVerboseOne, GetRawMempool}, }; use jsonrpc::{ serde, @@ -131,7 +127,7 @@ impl Client { } } -// `bitcoind` RPC methods implementation for `Client` +/// `Bitcoind` RPC methods implementation for `Client` impl Client { /// Retrieves the raw block data for a given block hash (verbosity 0) /// @@ -141,21 +137,24 @@ impl Client { /// # Returns /// The deserialized `Block` struct. pub fn get_block(&self, block_hash: &BlockHash) -> Result { - let hex_string: String = self.call("getblock", &[json!(block_hash), json!(0)])?; - let block = deserialize_hex(&hex_string).map_err(Error::DecodeHex)?; + let block_string: String = self.call("getblock", &[json!(block_hash), json!(0)])?; + let block = deserialize_hex(&block_string)?; Ok(block) } /// Retrieves the verbose JSON representation of a block (verbosity 1) + /// /// # Arguments /// * `block_hash`: The hash of the block to retrieve. /// /// # Returns /// The verbose block data as a `GetBlockVerboseOne` struct. pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { - let block_verbose_one: GetBlockVerboseOne = + let block: corepc_types::v30::GetBlockVerboseOne = self.call("getblock", &[json!(block_hash), json!(1)])?; - Ok(block_verbose_one) + let block_model = block.into_model()?; + + Ok(block_model) } /// Retrieves the hash of the tip of the best block chain. @@ -163,17 +162,19 @@ impl Client { /// # Returns /// The `BlockHash` of the chain tip. pub fn get_best_block_hash(&self) -> Result { - let res: String = self.call("getbestblockhash", &[])?; - Ok(res.parse()?) + let best_block_hash: String = self.call("getbestblockhash", &[])?; + Ok(best_block_hash.parse()?) } /// Retrieves the number of blocks in the longest chain /// /// # Returns - /// The block count as a `u64` - pub fn get_block_count(&self) -> Result { - let res: GetBlockCount = self.call("getblockcount", &[])?; - Ok(res.0) + /// The block count as a `u32` + pub fn get_block_count(&self) -> Result { + let block_count: GetBlockCount = self.call("getblockcount", &[])?; + let block_count_u64 = block_count.0; + let block_count_u32 = block_count_u64.try_into()?; + Ok(block_count_u32) } /// Retrieves the block hash at a given height @@ -184,8 +185,8 @@ impl Client { /// # Returns /// The `BlockHash` for the given height pub fn get_block_hash(&self, height: u32) -> Result { - let hex: String = self.call("getblockhash", &[json!(height)])?; - Ok(hex.parse()?) + let block_hash: String = self.call("getblockhash", &[json!(height)])?; + Ok(block_hash.parse()?) } /// Retrieves the compact block filter for a given block @@ -196,8 +197,8 @@ impl Client { /// # Returns /// The `GetBlockFilter` structure containing the filter data pub fn get_block_filter(&self, block_hash: &BlockHash) -> Result { - let res: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; - Ok(res) + let block_filter: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; + Ok(block_filter) } /// Retrieves the raw block header for a given block hash. @@ -208,14 +209,9 @@ impl Client { /// # Returns /// The deserialized `Header` struct pub fn get_block_header(&self, block_hash: &BlockHash) -> Result { - let hex_string: String = self.call("getblockheader", &[json!(block_hash), json!(false)])?; - - let bytes = Vec::::from_hex(&hex_string).map_err(Error::HexToBytes)?; - - let header = deserialize(&bytes).map_err(|e| { - Error::InvalidResponse(format!("failed to deserialize block header: {e}")) - })?; - + let header_string: String = + self.call("getblockheader", &[json!(block_hash), json!(false)])?; + let header = deserialize_hex(&header_string)?; Ok(header) } @@ -224,11 +220,11 @@ impl Client { /// # Returns /// A vector of `Txid`s in the raw mempool pub fn get_raw_mempool(&self) -> Result, Error> { - let res: GetRawMempool = self.call("getrawmempool", &[])?; - Ok(res.0) + let txids: GetRawMempool = self.call("getrawmempool", &[])?; + Ok(txids.0) } - /// Retrieves the raw transaction data for a given transaction ID. + /// Retrieves the raw transaction data for a given transaction ID /// /// # Arguments /// * `txid`: The transaction ID to retrieve. @@ -237,13 +233,7 @@ impl Client { /// The deserialized `Transaction` struct pub fn get_raw_transaction(&self, txid: &Txid) -> Result { let hex_string: String = self.call("getrawtransaction", &[json!(txid)])?; - - let bytes = Vec::::from_hex(&hex_string).map_err(Error::HexToBytes)?; - - let transaction = deserialize(&bytes).map_err(|e| { - Error::InvalidResponse(format!("transaction deserialization failed: {e}")) - })?; - + let transaction = deserialize_hex(&hex_string)?; Ok(transaction) } } diff --git a/src/error.rs b/src/error.rs index 52ff48d..0528a30 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,13 @@ //! Error types for the Bitcoin RPC client. -use std::{fmt, io}; - -use corepc_types::bitcoin::{ - consensus, - hex::{HexToArrayError, HexToBytesError}, +use std::{fmt, io, num::TryFromIntError}; + +use corepc_types::{ + bitcoin::{ + consensus::{self, encode::FromHexError}, + hex::{HexToArrayError, HexToBytesError}, + }, + v30::GetBlockVerboseOneError, }; use jsonrpc::serde_json; @@ -17,6 +20,9 @@ pub enum Error { /// Hex deserialization error DecodeHex(consensus::encode::FromHexError), + /// Error converting `GetBlockVersboseOne` type into the model type + GetBlockVerboseOneError(GetBlockVerboseOneError), + /// Missing authentication credentials. MissingAuthentication, @@ -40,6 +46,9 @@ pub enum Error { /// I/O error (e.g., reading cookie file, network issues). Io(io::Error), + + /// Error when converting an integer type to a smaller type due to overflow. + Overflow(TryFromIntError), } impl fmt::Display for Error { @@ -56,6 +65,10 @@ impl fmt::Display for Error { Error::Json(e) => write!(f, "JSON error: {e}"), Error::Io(e) => write!(f, "I/O error: {e}"), Error::DecodeHex(e) => write!(f, "Hex deserialization error: {e}"), + Error::GetBlockVerboseOneError(e) => { + write!(f, "Error converting getblockverboseone: {e}") + } + Error::Overflow(e) => write!(f, "Integer conversion overflow error: {e}"), } } } @@ -69,6 +82,8 @@ impl std::error::Error for Error { Error::HexToBytes(e) => Some(e), Error::HexToArray(e) => Some(e), Error::DecodeHex(e) => Some(e), + Error::GetBlockVerboseOneError(e) => Some(e), + Error::Overflow(e) => Some(e), _ => None, } } @@ -98,3 +113,21 @@ impl From for Error { Error::Io(e) } } + +impl From for Error { + fn from(e: TryFromIntError) -> Self { + Error::Overflow(e) + } +} + +impl From for Error { + fn from(e: GetBlockVerboseOneError) -> Self { + Error::GetBlockVerboseOneError(e) + } +} + +impl From for Error { + fn from(e: FromHexError) -> Self { + Error::DecodeHex(e) + } +} diff --git a/tests/test_rpc_client.rs b/tests/test_rpc_client.rs index e681d71..6c6d352 100644 --- a/tests/test_rpc_client.rs +++ b/tests/test_rpc_client.rs @@ -148,7 +148,7 @@ fn test_get_block_hash_for_current_height() { let block_count = client.get_block_count().expect("failed to get block count"); let block_hash = client - .get_block_hash(block_count.try_into().unwrap()) + .get_block_hash(block_count) .expect("failed to get block hash"); assert_eq!(block_hash.to_string().len(), 64); From df393f8263411fb71015720e46e858b46b2ffc92 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 13 Dec 2025 15:48:20 -0500 Subject: [PATCH 4/5] feat(client): Add v28 module --- Cargo.toml | 3 ++ src/client.rs | 91 ++++++++++++++++++++++++++++++++-------- src/client/v28.rs | 49 ++++++++++++++++++++++ src/client/v29.rs | 21 ++++++++++ src/error.rs | 23 ++++++---- tests/test_rpc_client.rs | 5 +-- 6 files changed, 163 insertions(+), 29 deletions(-) create mode 100644 src/client/v28.rs create mode 100644 src/client/v29.rs diff --git a/Cargo.toml b/Cargo.toml index 35db0a7..adcf860 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,7 @@ corepc-types = { version = "0.11.0", features = ["default"]} jsonrpc = { version = "0.18.0", features = ["simple_http", "simple_tcp", "minreq_http", "simple_uds", "proxy"] } [features] +default = ["30_0"] 30_0 = [] +29_0 = [] +28_0 = [] diff --git a/src/client.rs b/src/client.rs index 1aff24b..ad959b6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,11 +6,13 @@ use std::{ use crate::error::Error; use crate::jsonrpc::minreq_http::Builder; +#[cfg(not(feature = "29_0"))] +use corepc_types::v30::GetBlockFilter; use corepc_types::{ bitcoin::{ block::Header, consensus::encode::deserialize_hex, Block, BlockHash, Transaction, Txid, }, - model::{GetBlockCount, GetBlockFilter, GetBlockVerboseOne, GetRawMempool}, + model::{GetBlockCount, GetRawMempool}, }; use jsonrpc::{ serde, @@ -18,6 +20,12 @@ use jsonrpc::{ Transport, }; +#[cfg(feature = "28_0")] +pub mod v28; + +#[cfg(feature = "29_0")] +pub mod v29; + /// Client authentication methods for the Bitcoin Core JSON-RPC server #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum Auth { @@ -34,6 +42,7 @@ impl Auth { /// required by JSON-RPC client transport. /// /// # Errors + /// /// Returns an error if the `CookieFile` cannot be read or invalid pub fn get_user_pass(self) -> Result<(Option, Option), Error> { match self { @@ -65,11 +74,14 @@ impl Client { /// /// Requires authentication via username/password or cookie file. /// For connections without authentication, use `with_transport` instead. + /// /// # Arguments + /// /// * `url` - URL of the RPC server /// * `auth` - authentication method (`UserPass` or `CookieFile`). /// /// # Errors + /// /// * Returns `Error::MissingAuthentication` if `Auth::None` is provided. /// * Returns `Error::InvalidResponse` if the URL is invalid. /// * Returns errors related to reading the cookie file. @@ -132,9 +144,11 @@ impl Client { /// Retrieves the raw block data for a given block hash (verbosity 0) /// /// # Arguments + /// /// * `block_hash`: The hash of the block to retrieve. /// /// # Returns + /// /// The deserialized `Block` struct. pub fn get_block(&self, block_hash: &BlockHash) -> Result { let block_string: String = self.call("getblock", &[json!(block_hash), json!(0)])?; @@ -142,24 +156,10 @@ impl Client { Ok(block) } - /// Retrieves the verbose JSON representation of a block (verbosity 1) - /// - /// # Arguments - /// * `block_hash`: The hash of the block to retrieve. - /// - /// # Returns - /// The verbose block data as a `GetBlockVerboseOne` struct. - pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { - let block: corepc_types::v30::GetBlockVerboseOne = - self.call("getblock", &[json!(block_hash), json!(1)])?; - let block_model = block.into_model()?; - - Ok(block_model) - } - /// Retrieves the hash of the tip of the best block chain. /// /// # Returns + /// /// The `BlockHash` of the chain tip. pub fn get_best_block_hash(&self) -> Result { let best_block_hash: String = self.call("getbestblockhash", &[])?; @@ -169,6 +169,7 @@ impl Client { /// Retrieves the number of blocks in the longest chain /// /// # Returns + /// /// The block count as a `u32` pub fn get_block_count(&self) -> Result { let block_count: GetBlockCount = self.call("getblockcount", &[])?; @@ -180,22 +181,27 @@ impl Client { /// Retrieves the block hash at a given height /// /// # Arguments + /// /// * `height`: The block height /// /// # Returns + /// /// The `BlockHash` for the given height pub fn get_block_hash(&self, height: u32) -> Result { let block_hash: String = self.call("getblockhash", &[json!(height)])?; Ok(block_hash.parse()?) } - /// Retrieves the compact block filter for a given block + /// Retrieve the `basic` BIP 157 content filter for a particular block /// /// # Arguments + /// /// * `block_hash`: The hash of the block whose filter is requested /// /// # Returns + /// /// The `GetBlockFilter` structure containing the filter data + #[cfg(not(feature = "29_0"))] pub fn get_block_filter(&self, block_hash: &BlockHash) -> Result { let block_filter: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; Ok(block_filter) @@ -204,9 +210,11 @@ impl Client { /// Retrieves the raw block header for a given block hash. /// /// # Arguments + /// /// * `block_hash`: The hash of the block whose header is requested. /// /// # Returns + /// /// The deserialized `Header` struct pub fn get_block_header(&self, block_hash: &BlockHash) -> Result { let header_string: String = @@ -218,6 +226,7 @@ impl Client { /// Retrieves the transaction IDs of all transactions currently in the mempool /// /// # Returns + /// /// A vector of `Txid`s in the raw mempool pub fn get_raw_mempool(&self) -> Result, Error> { let txids: GetRawMempool = self.call("getrawmempool", &[])?; @@ -227,9 +236,11 @@ impl Client { /// Retrieves the raw transaction data for a given transaction ID /// /// # Arguments + /// /// * `txid`: The transaction ID to retrieve. /// /// # Returns + /// /// The deserialized `Transaction` struct pub fn get_raw_transaction(&self, txid: &Txid) -> Result { let hex_string: String = self.call("getrawtransaction", &[json!(txid)])?; @@ -238,6 +249,52 @@ impl Client { } } +#[cfg(not(feature = "28_0"))] +use corepc_types::{ + model::{GetBlockHeaderVerbose, GetBlockVerboseOne}, + v30, +}; + +#[cfg(not(feature = "28_0"))] +impl Client { + /// Retrieves the verbose JSON representation of a block header (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose header as a `GetBlockHeaderVerbose` struct. + pub fn get_block_header_verbose( + &self, + hash: &BlockHash, + ) -> Result { + let header_info: v30::GetBlockHeaderVerbose = + self.call("getblockheader", &[json!(hash)])?; + header_info + .into_model() + .map_err(Error::GetBlockHeaderVerboseError) + } + + /// Retrieves the verbose JSON representation of a block (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose block data as a `GetBlockVerboseOne` struct. + pub fn get_block_verbose(&self, hash: &BlockHash) -> Result { + let block_info: v30::GetBlockVerboseOne = + self.call("getblock", &[json!(hash), json!(1)])?; + block_info + .into_model() + .map_err(Error::GetBlockVerboseOneError) + } +} + #[cfg(test)] mod test_auth { use super::*; diff --git a/src/client/v28.rs b/src/client/v28.rs new file mode 100644 index 0000000..036188b --- /dev/null +++ b/src/client/v28.rs @@ -0,0 +1,49 @@ +use bitcoin::BlockHash; +use corepc_types::{ + bitcoin, + model::{GetBlockHeaderVerbose, GetBlockVerboseOne}, + v28, +}; + +use jsonrpc::serde_json::json; + +use crate::{Client, Error}; + +impl Client { + /// Retrieves the verbose JSON representation of a block header (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose header as a `GetBlockHeaderVerbose` struct. + pub fn get_block_header_verbose( + &self, + hash: &BlockHash, + ) -> Result { + let header_info: v28::GetBlockHeaderVerbose = + self.call("getblockheader", &[json!(hash)])?; + header_info + .into_model() + .map_err(Error::GetBlockHeaderVerboseError) + } + + /// Retrieves the verbose JSON representation of a block (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose block data as a `GetBlockVerboseOne` struct. + pub fn get_block_verbose(&self, hash: &BlockHash) -> Result { + let block_info: v28::GetBlockVerboseOne = + self.call("getblock", &[json!(hash), json!(1)])?; + block_info + .into_model() + .map_err(Error::GetBlockVerboseOneError) + } +} diff --git a/src/client/v29.rs b/src/client/v29.rs new file mode 100644 index 0000000..9c6f149 --- /dev/null +++ b/src/client/v29.rs @@ -0,0 +1,21 @@ +use corepc_types::{bitcoin::BlockHash, v29::GetBlockFilter}; + +use jsonrpc::serde_json::json; + +use crate::{Client, Error}; + +impl Client { + /// Retrieve the `basic` BIP 157 content filter for a particular block + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block whose filter is requested + /// + /// # Returns + /// + /// The `GetBlockFilter` structure containing the filter data + pub fn get_block_filter(&self, block_hash: &BlockHash) -> Result { + let block_filter: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; + Ok(block_filter) + } +} diff --git a/src/error.rs b/src/error.rs index 0528a30..a6179e1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,15 +1,16 @@ //! Error types for the Bitcoin RPC client. -use std::{fmt, io, num::TryFromIntError}; - -use corepc_types::{ - bitcoin::{ - consensus::{self, encode::FromHexError}, - hex::{HexToArrayError, HexToBytesError}, - }, - v30::GetBlockVerboseOneError, +use bitcoin::{ + consensus::encode::FromHexError, + hex::{HexToArrayError, HexToBytesError}, }; +use corepc_types::bitcoin; +#[cfg(feature = "28_0")] +use corepc_types::v17::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; +#[cfg(not(feature = "28_0"))] +use corepc_types::v30::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; use jsonrpc::serde_json; +use std::{fmt, io, num::TryFromIntError}; /// Result type alias for the RPC client. pub type Result = std::result::Result; @@ -18,11 +19,14 @@ pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { /// Hex deserialization error - DecodeHex(consensus::encode::FromHexError), + DecodeHex(FromHexError), /// Error converting `GetBlockVersboseOne` type into the model type GetBlockVerboseOneError(GetBlockVerboseOneError), + /// Error modeling [`GetBlockHeaderVerbose`](corepc_types::model::GetBlockHeaderVerbose). + GetBlockHeaderVerboseError(GetBlockHeaderVerboseError), + /// Missing authentication credentials. MissingAuthentication, @@ -65,6 +69,7 @@ impl fmt::Display for Error { Error::Json(e) => write!(f, "JSON error: {e}"), Error::Io(e) => write!(f, "I/O error: {e}"), Error::DecodeHex(e) => write!(f, "Hex deserialization error: {e}"), + Error::GetBlockHeaderVerboseError(e) => write!(f, "{e}"), Error::GetBlockVerboseOneError(e) => { write!(f, "Error converting getblockverboseone: {e}") } diff --git a/tests/test_rpc_client.rs b/tests/test_rpc_client.rs index 6c6d352..beeea1c 100644 --- a/tests/test_rpc_client.rs +++ b/tests/test_rpc_client.rs @@ -169,11 +169,10 @@ fn test_get_block_hash_invalid_height() { fn test_get_best_block_hash() { let client = test_client(); - let best_hash = client + let best_block_hash = client .get_best_block_hash() .expect("failed to get best block hash"); - - assert_eq!(best_hash.to_string().len(), 64); + assert_eq!(best_block_hash.to_string().len(), 64); } #[test] From 17266ca7aa903c1b051eea1d94fa12dad2b487d4 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Sat, 20 Dec 2025 20:36:52 +0100 Subject: [PATCH 5/5] test: Use `corepc-node` for integration tests - refactor methods --- Cargo.lock | 512 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/client.rs | 65 +++-- src/client/v28.rs | 8 +- src/client/v29.rs | 21 -- src/error.rs | 11 +- tests/test_rpc_client.rs | 169 ++++++++----- 7 files changed, 660 insertions(+), 129 deletions(-) delete mode 100644 src/client/v29.rs diff --git a/Cargo.lock b/Cargo.lock index 32944fd..b97511c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arrayvec" version = "0.7.6" @@ -34,7 +46,8 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" name = "bdk-bitcoind-client" version = "0.1.0" dependencies = [ - "corepc-types", + "corepc-node", + "corepc-types 0.11.0", "jsonrpc", ] @@ -99,12 +112,38 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cc" version = "1.2.47" @@ -115,6 +154,56 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "corepc-client" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7755b8b9219b23d166a5897b5e2d8266cbdd0de5861d351b96f6db26bcf415f3" +dependencies = [ + "bitcoin", + "corepc-types 0.10.1", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "corepc-node" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768391062ec3812e223bb3031c5b2fcdd6e0e60b816157f21df82fd3e6617dc0" +dependencies = [ + "anyhow", + "bitcoin_hashes", + "corepc-client", + "flate2", + "log", + "minreq", + "serde_json", + "tar", + "tempfile", + "which", + "zip", +] + +[[package]] +name = "corepc-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22db78b0223b66f82f92b14345f06307078f76d94b18280431ea9bc6cd9cbb6" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + [[package]] name = "corepc-types" version = "0.11.0" @@ -126,12 +215,76 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hex-conservative" version = "0.2.1" @@ -172,22 +325,70 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libredox" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "minreq" version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" dependencies = [ + "rustls", + "rustls-webpki", "serde", "serde_json", + "webpki-roots", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -206,12 +407,80 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.29.1" @@ -281,6 +550,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "socks" version = "0.3.4" @@ -303,12 +578,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -330,3 +655,188 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "crossbeam-utils", + "flate2", +] diff --git a/Cargo.toml b/Cargo.toml index adcf860..bde205e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,6 @@ default = ["30_0"] 30_0 = [] 29_0 = [] 28_0 = [] + +[dev-dependencies] +corepc-node = { version = "0.10.1", features = ["download", "29_0"] } diff --git a/src/client.rs b/src/client.rs index ad959b6..5f0b70d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,13 +6,12 @@ use std::{ use crate::error::Error; use crate::jsonrpc::minreq_http::Builder; -#[cfg(not(feature = "29_0"))] -use corepc_types::v30::GetBlockFilter; use corepc_types::{ bitcoin::{ block::Header, consensus::encode::deserialize_hex, Block, BlockHash, Transaction, Txid, }, - model::{GetBlockCount, GetRawMempool}, + model::{GetBlockCount, GetBlockFilter, GetRawMempool}, + v30, }; use jsonrpc::{ serde, @@ -23,9 +22,6 @@ use jsonrpc::{ #[cfg(feature = "28_0")] pub mod v28; -#[cfg(feature = "29_0")] -pub mod v29; - /// Client authentication methods for the Bitcoin Core JSON-RPC server #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum Auth { @@ -151,9 +147,8 @@ impl Client { /// /// The deserialized `Block` struct. pub fn get_block(&self, block_hash: &BlockHash) -> Result { - let block_string: String = self.call("getblock", &[json!(block_hash), json!(0)])?; - let block = deserialize_hex(&block_string)?; - Ok(block) + self.call::("getblock", &[json!(block_hash), json!(0)]) + .and_then(|block_hex| deserialize_hex(&block_hex).map_err(Error::DecodeHex)) } /// Retrieves the hash of the tip of the best block chain. @@ -162,8 +157,8 @@ impl Client { /// /// The `BlockHash` of the chain tip. pub fn get_best_block_hash(&self) -> Result { - let best_block_hash: String = self.call("getbestblockhash", &[])?; - Ok(best_block_hash.parse()?) + self.call::("getbestblockhash", &[]) + .and_then(|block_hex| block_hex.parse().map_err(Error::from)) } /// Retrieves the number of blocks in the longest chain @@ -172,10 +167,10 @@ impl Client { /// /// The block count as a `u32` pub fn get_block_count(&self) -> Result { - let block_count: GetBlockCount = self.call("getblockcount", &[])?; - let block_count_u64 = block_count.0; - let block_count_u32 = block_count_u64.try_into()?; - Ok(block_count_u32) + self.call::("getblockcount", &[])? + .0 + .try_into() + .map_err(Error::Overflow) } /// Retrieves the block hash at a given height @@ -188,8 +183,8 @@ impl Client { /// /// The `BlockHash` for the given height pub fn get_block_hash(&self, height: u32) -> Result { - let block_hash: String = self.call("getblockhash", &[json!(height)])?; - Ok(block_hash.parse()?) + self.call::("getblockhash", &[json!(height)]) + .and_then(|block_hex| block_hex.parse().map_err(Error::from)) } /// Retrieve the `basic` BIP 157 content filter for a particular block @@ -201,10 +196,12 @@ impl Client { /// # Returns /// /// The `GetBlockFilter` structure containing the filter data - #[cfg(not(feature = "29_0"))] pub fn get_block_filter(&self, block_hash: &BlockHash) -> Result { - let block_filter: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; - Ok(block_filter) + let block_filter: v30::GetBlockFilter = + self.call("getblockfilter", &[json!(block_hash)])?; + block_filter + .into_model() + .map_err(Error::GetBlockFilterError) } /// Retrieves the raw block header for a given block hash. @@ -217,10 +214,8 @@ impl Client { /// /// The deserialized `Header` struct pub fn get_block_header(&self, block_hash: &BlockHash) -> Result { - let header_string: String = - self.call("getblockheader", &[json!(block_hash), json!(false)])?; - let header = deserialize_hex(&header_string)?; - Ok(header) + self.call::("getblockheader", &[json!(block_hash), json!(false)]) + .and_then(|header_hex: String| deserialize_hex(&header_hex).map_err(Error::DecodeHex)) } /// Retrieves the transaction IDs of all transactions currently in the mempool @@ -229,8 +224,8 @@ impl Client { /// /// A vector of `Txid`s in the raw mempool pub fn get_raw_mempool(&self) -> Result, Error> { - let txids: GetRawMempool = self.call("getrawmempool", &[])?; - Ok(txids.0) + self.call::("getrawmempool", &[]) + .map(|txids| txids.0) } /// Retrieves the raw transaction data for a given transaction ID @@ -243,17 +238,13 @@ impl Client { /// /// The deserialized `Transaction` struct pub fn get_raw_transaction(&self, txid: &Txid) -> Result { - let hex_string: String = self.call("getrawtransaction", &[json!(txid)])?; - let transaction = deserialize_hex(&hex_string)?; - Ok(transaction) + self.call::("getrawtransaction", &[json!(txid)]) + .and_then(|tx_hex| deserialize_hex(&tx_hex).map_err(Error::DecodeHex)) } } #[cfg(not(feature = "28_0"))] -use corepc_types::{ - model::{GetBlockHeaderVerbose, GetBlockVerboseOne}, - v30, -}; +use corepc_types::model::{GetBlockHeaderVerbose, GetBlockVerboseOne}; #[cfg(not(feature = "28_0"))] impl Client { @@ -268,10 +259,10 @@ impl Client { /// The verbose header as a `GetBlockHeaderVerbose` struct. pub fn get_block_header_verbose( &self, - hash: &BlockHash, + block_hash: &BlockHash, ) -> Result { let header_info: v30::GetBlockHeaderVerbose = - self.call("getblockheader", &[json!(hash)])?; + self.call("getblockheader", &[json!(block_hash)])?; header_info .into_model() .map_err(Error::GetBlockHeaderVerboseError) @@ -286,9 +277,9 @@ impl Client { /// # Returns /// /// The verbose block data as a `GetBlockVerboseOne` struct. - pub fn get_block_verbose(&self, hash: &BlockHash) -> Result { + pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { let block_info: v30::GetBlockVerboseOne = - self.call("getblock", &[json!(hash), json!(1)])?; + self.call("getblock", &[json!(block_hash), json!(1)])?; block_info .into_model() .map_err(Error::GetBlockVerboseOneError) diff --git a/src/client/v28.rs b/src/client/v28.rs index 036188b..a822627 100644 --- a/src/client/v28.rs +++ b/src/client/v28.rs @@ -21,10 +21,10 @@ impl Client { /// The verbose header as a `GetBlockHeaderVerbose` struct. pub fn get_block_header_verbose( &self, - hash: &BlockHash, + block_hash: &BlockHash, ) -> Result { let header_info: v28::GetBlockHeaderVerbose = - self.call("getblockheader", &[json!(hash)])?; + self.call("getblockheader", &[json!(block_hash)])?; header_info .into_model() .map_err(Error::GetBlockHeaderVerboseError) @@ -39,9 +39,9 @@ impl Client { /// # Returns /// /// The verbose block data as a `GetBlockVerboseOne` struct. - pub fn get_block_verbose(&self, hash: &BlockHash) -> Result { + pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { let block_info: v28::GetBlockVerboseOne = - self.call("getblock", &[json!(hash), json!(1)])?; + self.call("getblock", &[json!(block_hash), json!(1)])?; block_info .into_model() .map_err(Error::GetBlockVerboseOneError) diff --git a/src/client/v29.rs b/src/client/v29.rs deleted file mode 100644 index 9c6f149..0000000 --- a/src/client/v29.rs +++ /dev/null @@ -1,21 +0,0 @@ -use corepc_types::{bitcoin::BlockHash, v29::GetBlockFilter}; - -use jsonrpc::serde_json::json; - -use crate::{Client, Error}; - -impl Client { - /// Retrieve the `basic` BIP 157 content filter for a particular block - /// - /// # Arguments - /// - /// * `block_hash`: The hash of the block whose filter is requested - /// - /// # Returns - /// - /// The `GetBlockFilter` structure containing the filter data - pub fn get_block_filter(&self, block_hash: &BlockHash) -> Result { - let block_filter: GetBlockFilter = self.call("getblockfilter", &[json!(block_hash)])?; - Ok(block_filter) - } -} diff --git a/src/error.rs b/src/error.rs index a6179e1..57c353b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,11 +4,11 @@ use bitcoin::{ consensus::encode::FromHexError, hex::{HexToArrayError, HexToBytesError}, }; -use corepc_types::bitcoin; #[cfg(feature = "28_0")] use corepc_types::v17::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; #[cfg(not(feature = "28_0"))] use corepc_types::v30::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; +use corepc_types::{bitcoin, v30::GetBlockFilterError}; use jsonrpc::serde_json; use std::{fmt, io, num::TryFromIntError}; @@ -27,6 +27,9 @@ pub enum Error { /// Error modeling [`GetBlockHeaderVerbose`](corepc_types::model::GetBlockHeaderVerbose). GetBlockHeaderVerboseError(GetBlockHeaderVerboseError), + /// Error modeling [`GetBlockFilter`](corepc_types::model::GetBlockFilter) + GetBlockFilterError(GetBlockFilterError), + /// Missing authentication credentials. MissingAuthentication, @@ -70,10 +73,9 @@ impl fmt::Display for Error { Error::Io(e) => write!(f, "I/O error: {e}"), Error::DecodeHex(e) => write!(f, "Hex deserialization error: {e}"), Error::GetBlockHeaderVerboseError(e) => write!(f, "{e}"), - Error::GetBlockVerboseOneError(e) => { - write!(f, "Error converting getblockverboseone: {e}") - } + Error::GetBlockVerboseOneError(e) => write!(f, "{e}"), Error::Overflow(e) => write!(f, "Integer conversion overflow error: {e}"), + Error::GetBlockFilterError(e) => write!(f, "{e}"), } } } @@ -89,6 +91,7 @@ impl std::error::Error for Error { Error::DecodeHex(e) => Some(e), Error::GetBlockVerboseOneError(e) => Some(e), Error::Overflow(e) => Some(e), + Error::GetBlockFilterError(e) => Some(e), _ => None, } } diff --git a/tests/test_rpc_client.rs b/tests/test_rpc_client.rs index beeea1c..d95ab98 100644 --- a/tests/test_rpc_client.rs +++ b/tests/test_rpc_client.rs @@ -8,25 +8,39 @@ //! ``` use bdk_bitcoind_client::{Auth, Client, Error}; +use corepc_node::{exe_path, Conf, Node}; use corepc_types::bitcoin::{BlockHash, Txid}; use jsonrpc::serde_json::json; use std::{path::PathBuf, str::FromStr}; -/// Helper to get the test RPC URL -fn test_url() -> String { - std::env::var("BITCOIN_RPC_URL").unwrap_or_else(|_| "http://localhost:18443".to_string()) +/// Helper to initialize the bitcoind executable path +fn init() -> String { + exe_path().expect("bitcoind executable not found. Set BITCOIND_EXE or enable download feature.") } -/// Helper to get test credentials -fn test_auth() -> Auth { - let user = std::env::var("BITCOIN_RPC_USER").unwrap_or_else(|_| "bitcoin".to_string()); - let pass = std::env::var("BITCOIN_RPC_PASS").unwrap_or_else(|_| "bitcoin".to_string()); - Auth::UserPass(user, pass) -} +/// Helper to set up a clean bitcoind node and return the client. +fn setup() -> (Client, Node) { + let exe = init(); + + let mut conf = Conf::default(); + + conf.args.push("-blockfilterindex=1"); + conf.args.push("-txindex=1"); + + let node = Node::with_conf(exe, &conf).expect("Failed to start node"); + + let rpc_url = node.rpc_url(); + let cookie = node + .params + .get_cookie_values() + .expect("Failed to read cookie") + .expect("Cookie file empty"); + + let auth = Auth::UserPass(cookie.user, cookie.password); + + let client = Client::with_auth(&rpc_url, auth).expect("failed to create client"); -/// Helper to create a test client -fn test_client() -> Client { - Client::with_auth(&test_url(), test_auth()).expect("failed to create client") + (client, node) } /// Helper to mine blocks @@ -36,28 +50,32 @@ fn mine_blocks(client: &Client, n: u64) -> Result, Error> { } #[test] -#[ignore] fn test_client_with_user_pass() { - let client = Client::with_auth(&test_url(), test_auth()).expect("failed to create client"); + let (client, mut node) = setup(); - let result = client + let block_hash = client .get_best_block_hash() - .expect("failed to call getblockchaininfo"); + .expect("failed to call getbestblockhash"); assert_eq!( - result.to_string().len(), + block_hash.to_string().len(), 64, "block hash should be 64 characters" ); assert!( - result.to_string().chars().all(|c| c.is_ascii_hexdigit()), + block_hash + .to_string() + .chars() + .all(|c| c.is_ascii_hexdigit()), "hash should only contain hex digits" ); + + node.stop().expect("failed to stop node"); } #[test] fn test_auth_none_returns_error() { - let result = Client::with_auth(&test_url(), Auth::None); + let result = Client::with_auth("http://invalid-url", Auth::None); assert!(result.is_err()); match result { @@ -67,10 +85,10 @@ fn test_auth_none_returns_error() { } #[test] -#[ignore] fn test_invalid_credentials() { + let (_, mut node) = setup(); let client = Client::with_auth( - &test_url(), + &node.rpc_url(), Auth::UserPass("wrong".to_string(), "credentials".to_string()), ) .expect("client creation should succeed"); @@ -78,38 +96,55 @@ fn test_invalid_credentials() { let result: Result = client.get_best_block_hash(); assert!(result.is_err()); + + node.stop().expect("failed to stop node"); } #[test] fn test_invalid_cookie_file() { + let dummy_url = "http://127.0.0.1:18443"; let cookie_path = PathBuf::from("/nonexistent/path/to/cookie"); - let result = Client::with_auth(&test_url(), Auth::CookieFile(cookie_path)); - assert!(result.is_err()); + let result = Client::with_auth(dummy_url, Auth::CookieFile(cookie_path)); + + assert!( + result.is_err(), + "Client should fail when cookie file is missing" + ); + match result { Err(Error::InvalidCookieFile) => (), - Err(Error::Io(_)) => (), - _ => panic!("expected InvalidCookieFile or Io error"), + Err(Error::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound => (), + Err(e) => panic!("Expected InvalidCookieFile or NotFound Io error, got: {e:?}"), + _ => panic!("Expected an error but got Ok"), } } #[test] -#[ignore] fn test_client_with_custom_transport() { use jsonrpc::http::minreq_http::Builder; + let (_, node) = setup(); + + let rpc_url = node.rpc_url(); + let cookie = node + .params + .get_cookie_values() + .expect("Failed to read cookie") + .expect("Cookie file empty"); + let transport = Builder::new() - .url(&test_url()) + .url(&rpc_url) .expect("invalid URL") .timeout(std::time::Duration::from_secs(30)) - .basic_auth("bitcoin".to_string(), Some("bitcoin".to_string())) + .basic_auth(cookie.user, Some(cookie.password)) .build(); let client = Client::with_transport(transport); let result = client .get_best_block_hash() - .expect("failed to call getblockchaininfo"); + .expect("failed to call getbestblockhash"); assert_eq!( result.to_string().len(), @@ -119,31 +154,32 @@ fn test_client_with_custom_transport() { } #[test] -#[ignore] fn test_get_block_count() { - let client = test_client(); + let (client, mut node) = setup(); let block_count = client.get_block_count().expect("failed to get block count"); - assert!(block_count >= 1); + assert_eq!(block_count, 0); + + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_hash() { - let client = test_client(); + let (client, mut node) = setup(); let genesis_hash = client .get_block_hash(0) .expect("failed to get genesis block hash"); assert_eq!(genesis_hash.to_string().len(), 64); + + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_hash_for_current_height() { - let client = test_client(); + let (client, mut node) = setup(); let block_count = client.get_block_count().expect("failed to get block count"); @@ -152,33 +188,41 @@ fn test_get_block_hash_for_current_height() { .expect("failed to get block hash"); assert_eq!(block_hash.to_string().len(), 64); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_hash_invalid_height() { - let client = test_client(); + let (client, mut node) = setup(); let result = client.get_block_hash(999999999); assert!(result.is_err()); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_best_block_hash() { - let client = test_client(); + let (client, mut node) = setup(); let best_block_hash = client .get_best_block_hash() .expect("failed to get best block hash"); + assert_eq!(best_block_hash.to_string().len(), 64); + + let block_count = client.get_block_count().expect("failed to get block count"); + let block_hash = client + .get_block_hash(block_count) + .expect("failed to get block hash"); + + assert_eq!(best_block_hash, block_hash); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_best_block_hash_changes_after_mining() { - let client = test_client(); + let (client, mut node) = setup(); let hash_before = client .get_best_block_hash() @@ -191,12 +235,12 @@ fn test_get_best_block_hash_changes_after_mining() { .expect("failed to get best block hash"); assert_ne!(hash_before, hash_after); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block() { - let client = test_client(); + let (client, mut node) = setup(); let genesis_hash = client .get_block_hash(0) @@ -208,12 +252,12 @@ fn test_get_block() { assert_eq!(block.block_hash(), genesis_hash); assert!(!block.txdata.is_empty()); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_after_mining() { - let client = test_client(); + let (client, mut node) = setup(); let hashes = mine_blocks(&client, 1).expect("failed to mine block"); let block_hash = BlockHash::from_str(&hashes[0]).expect("invalid hash"); @@ -222,12 +266,12 @@ fn test_get_block_after_mining() { assert_eq!(block.block_hash(), block_hash); assert!(!block.txdata.is_empty()); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_invalid_hash() { - let client = test_client(); + let (client, mut node) = setup(); let invalid_hash = BlockHash::from_str("0000000000000000000000000000000000000000000000000000000000000000") @@ -236,12 +280,12 @@ fn test_get_block_invalid_hash() { let result = client.get_block(&invalid_hash); assert!(result.is_err()); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_header() { - let client = test_client(); + let (client, mut node) = setup(); let genesis_hash = client .get_block_hash(0) @@ -252,12 +296,12 @@ fn test_get_block_header() { .expect("failed to get block header"); assert_eq!(header.block_hash(), genesis_hash); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_header_has_valid_fields() { - let client = test_client(); + let (client, mut node) = setup(); let genesis_hash = client .get_block_hash(0) @@ -269,12 +313,12 @@ fn test_get_block_header_has_valid_fields() { assert!(header.time > 0); assert!(header.nonce >= 1); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_raw_mempool_empty() { - let client = test_client(); + let (client, mut node) = setup(); mine_blocks(&client, 1).expect("failed to mine block"); @@ -283,12 +327,12 @@ fn test_get_raw_mempool_empty() { let mempool = client.get_raw_mempool().expect("failed to get mempool"); assert!(mempool.is_empty()); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_raw_mempool_with_transaction() { - let client = test_client(); + let (client, mut node) = setup(); mine_blocks(&client, 101).expect("failed to mine blocks"); @@ -303,12 +347,12 @@ fn test_get_raw_mempool_with_transaction() { let txid_parsed = Txid::from_str(&txid).unwrap(); assert!(mempool.contains(&txid_parsed)); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_raw_transaction() { - let client = test_client(); + let (client, mut node) = setup(); mine_blocks(&client, 1).expect("failed to mine block"); @@ -327,12 +371,12 @@ fn test_get_raw_transaction() { assert_eq!(tx.compute_txid(), *txid); assert!(!tx.input.is_empty()); assert!(!tx.output.is_empty()); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_raw_transaction_invalid_txid() { - let client = test_client(); + let (client, mut node) = setup(); let fake_txid = Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(); @@ -340,12 +384,12 @@ fn test_get_raw_transaction_invalid_txid() { let result = client.get_raw_transaction(&fake_txid); assert!(result.is_err()); + node.stop().expect("failed to stop node"); } #[test] -#[ignore] fn test_get_block_filter() { - let client = test_client(); + let (client, mut node) = setup(); let genesis_hash = client .get_block_hash(0) @@ -361,4 +405,5 @@ fn test_get_block_filter() { println!("Block filters not enabled (requires -blockfilterindex=1)"); } } + node.stop().expect("failed to stop node"); }