diff --git a/src/client.rs b/src/client.rs index 1427e06..073610b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,12 +2,22 @@ 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::Header, consensus::deserialize, hex::FromHex, Block, BlockHash, Transaction, Txid, + }, + model::{GetBlockCount, GetBlockFilter, GetBlockVerboseOne, GetRawMempool}, +}; +use jsonrpc::{ + serde, + serde_json::{self, json}, + Transport, +}; /// client authentication methods #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -101,11 +111,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, } diff --git a/tests/test_rpc_client.rs b/tests/test_rpc_client.rs index 89dde64..ff5cb7b 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)"); + } + } +}