From 6c1cacd2fa4e7b8782f5448803ba604a4fe7ea57 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:05:44 -0300 Subject: [PATCH 1/3] fix(node): make v2transport optional in addnode, allowing server-side defaults The v2transport parameter in addnode is now optional. When omitted, the server applies its configured default value: - true (unless --allow-v1-fallback is set) - false (if --allow-v1-fallback is configured) This removes the requirement for explicit specification in the CLI while allowing the server's configuration to determine the connection behavior. --- bin/floresta-cli/src/main.rs | 5 +--- crates/floresta-node/src/florestad.rs | 1 + crates/floresta-node/src/json_rpc/network.rs | 4 ++- crates/floresta-node/src/json_rpc/server.rs | 6 ++-- crates/floresta-rpc/src/rpc.rs | 29 +++++++++++++------- doc/rpc/addnode.md | 2 +- 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index c9ab518c2..7df502723 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -104,10 +104,7 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { node, command, v2transport, - } => { - let transport = v2transport.unwrap_or(false); - serde_json::to_string_pretty(&client.add_node(node, command, transport)?)? - } + } => serde_json::to_string_pretty(&client.add_node(node, command, v2transport)?)?, Methods::DisconnectNode { node_address, node_id, diff --git a/crates/floresta-node/src/florestad.rs b/crates/floresta-node/src/florestad.rs index 4f1bb250f..cb7eac7fd 100644 --- a/crates/floresta-node/src/florestad.rs +++ b/crates/floresta-node/src/florestad.rs @@ -478,6 +478,7 @@ impl Florestad { .map(|x| Self::resolve_hostname(x, 8332)) .transpose()?, format!("{data_dir}/debug.log"), + !self.config.allow_v1_fallback, )); if self.json_rpc.set(server).is_err() { diff --git a/crates/floresta-node/src/json_rpc/network.rs b/crates/floresta-node/src/json_rpc/network.rs index 21921ea5c..7121f08ce 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -28,7 +28,7 @@ impl RpcImpl { &self, node_address: String, command: String, - v2transport: bool, + v2transport: Option, ) -> Result { // Try to parse both IP address and port. let (addr, port) = if let Ok(socket_addr) = node_address.parse::() { @@ -52,6 +52,8 @@ impl RpcImpl { (ip, default_port) }; + let v2transport = v2transport.unwrap_or(self.default_connection_is_v2); + let _ = match command.as_str() { "add" => self.node.add_peer(addr, port, v2transport).await, "remove" => self.node.remove_peer(addr, port).await, diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index b960f62e5..31b03cfce 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -83,6 +83,7 @@ pub struct RpcImpl { pub(super) inflight: Arc>>, pub(super) log_path: String, pub(super) start_time: Instant, + pub(super) default_connection_is_v2: bool, } type Result = std::result::Result; @@ -364,8 +365,7 @@ async fn handle_json_rpc_request( "addnode" => { let node = get_string(¶ms, 0, "node")?; let command = get_string(¶ms, 1, "command")?; - let v2transport = - get_optional_field(¶ms, 2, "V2transport", get_bool)?.unwrap_or(false); + let v2transport = get_optional_field(¶ms, 2, "V2transport", get_bool)?; state .add_node(node, command, v2transport) @@ -749,6 +749,7 @@ impl RpcImpl { block_filter_storage: Option>>, address: Option, log_path: String, + default_connection_is_v2: bool, ) { let address = address.unwrap_or_else(|| { format!("127.0.0.1:{}", Self::get_port(&network)) @@ -789,6 +790,7 @@ impl RpcImpl { inflight: Arc::new(RwLock::new(HashMap::new())), log_path, start_time: Instant::now(), + default_connection_is_v2, })); axum::serve(listener, router) diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 6c419af32..c957df2b5 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -116,7 +116,12 @@ pub trait FlorestaRPC { /// If the `v2transport` option is set, we won't retry connecting using the old, unencrypted /// P2P protocol. #[doc = include_str!("../../../doc/rpc/addnode.md")] - fn add_node(&self, node: String, command: AddNodeCommand, v2transport: bool) -> Result; + fn add_node( + &self, + node: String, + command: AddNodeCommand, + v2transport: Option, + ) -> Result; /// Immediately disconnect from a peer. /// /// The peer can be referenced either by node_address or node_id. @@ -191,15 +196,19 @@ impl FlorestaRPC for T { self.call("getrpcinfo", &[]) } - fn add_node(&self, node: String, command: AddNodeCommand, v2transport: bool) -> Result { - self.call( - "addnode", - &[ - Value::String(node), - Value::String(command.to_string()), - Value::Bool(v2transport), - ], - ) + fn add_node( + &self, + node: String, + command: AddNodeCommand, + v2transport: Option, + ) -> Result { + let mut params = vec![Value::String(node), Value::String(command.to_string())]; + + if let Some(v2transport) = v2transport { + params.push(Value::Bool(v2transport)); + } + + self.call("addnode", ¶ms) } fn disconnect_node(&self, node_address: String, node_id: Option) -> Result { diff --git a/doc/rpc/addnode.md b/doc/rpc/addnode.md index 708b2284d..86a7c69ad 100644 --- a/doc/rpc/addnode.md +++ b/doc/rpc/addnode.md @@ -27,7 +27,7 @@ floresta-cli addnode 192.168.0.1 onetry false `command` - (string, required) 'add' to add a node to the list, 'remove' to remove a node from the list, 'onetry' to try a connection to the node once. -`v2transport` - (boolean, optional) Only tries to connect with this address using BIP0324 P2P V2 protocol. ignored for 'remove' command. +`v2transport` - (boolean, optional, default=set by --allow-v1-fallback) Only tries to connect with this address using BIP0324 P2P V2 protocol. Ignored for 'remove' command. Default: `true` unless `--allow-v1-fallback` is used (then defaults to `false`). ## Returns From b17939b43f64f34924e5c121bf0949d9771a7ab1 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:16:18 -0300 Subject: [PATCH 2/3] chore(rpc): remove client-side defaults via RpcArg parameter abstraction Remove unwrap_or() calls that force default values on the client side. Optional parameters are now omitted when None, letting the server determine appropriate defaults instead of the CLI forcing them. Introduce RpcArg enum and rpc_params() helper function to enable this: - RpcArg::Value wraps required parameters - RpcArg::Optional wraps optional parameters and filters None values - rpc_params() automatically collects parameters and handles filtering Implement From trait conversions for String, bool, u32, Txid, BlockHash, Vec, and Option types to provide seamless parameter construction across the RPC layer without manual error handling. Add comprehensive unit tests validating RpcArg conversions, parameter filtering behavior. --- bin/floresta-cli/src/main.rs | 10 +- crates/floresta-node/src/json_rpc/server.rs | 2 +- crates/floresta-rpc/src/rpc.rs | 242 ++++++++++++++------ 3 files changed, 177 insertions(+), 77 deletions(-) diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 7df502723..920af3884 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -114,14 +114,8 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { vout, script, height_hint, - } => serde_json::to_string_pretty(&client.find_tx_out( - txid, - vout, - script, - height_hint.unwrap_or(0), - )?)?, + } => serde_json::to_string_pretty(&client.find_tx_out(txid, vout, script, height_hint)?)?, Methods::GetMemoryInfo { mode } => { - let mode = mode.unwrap_or("stats".to_string()); serde_json::to_string_pretty(&client.get_memory_info(mode)?)? } Methods::GetRpcInfo => serde_json::to_string_pretty(&client.get_rpc_info()?)?, @@ -337,7 +331,7 @@ pub enum Methods { )] DisconnectNode { node_address: String, - node_id: Option, + node_id: Option, }, #[command(name = "findtxout")] diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 31b03cfce..30cf78bf2 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -321,7 +321,7 @@ async fn handle_json_rpc_request( let vout = get_numeric(¶ms, 1, "vout")?; let script = get_string(¶ms, 2, "script")?; let script = ScriptBuf::from_hex(&script).map_err(|_| JsonRpcError::InvalidScript)?; - let height = get_numeric(¶ms, 3, "height")?; + let height = get_optional_field(¶ms, 3, "height", get_numeric)?.unwrap_or(0); let state = state.clone(); state.find_tx_out(txid, vout, script, height).await diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index c957df2b5..e91ab90c0 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -6,6 +6,7 @@ use bitcoin::block::Header as BlockHeader; use bitcoin::BlockHash; use bitcoin::Txid; use corepc_types::v29::GetTxOut; +use serde::Serialize; use serde_json::Number; use serde_json::Value; @@ -126,7 +127,7 @@ pub trait FlorestaRPC { /// /// The peer can be referenced either by node_address or node_id. /// If referencing by node_id, an empty string must be passed as the node_address. - fn disconnect_node(&self, node_address: String, node_id: Option) -> Result; + fn disconnect_node(&self, node_address: String, node_id: Option) -> Result; /// Finds an specific utxo in the chain /// /// You can use this to look for a utxo. If it exists, it will return the amount and @@ -137,12 +138,12 @@ pub trait FlorestaRPC { tx_id: Txid, outpoint: u32, script: String, - height_hint: u32, + height_hint: Option, ) -> Result; /// Returns statistics about Floresta's memory usage. /// /// Returns zeroed values for all runtimes that are not *-gnu or MacOS. - fn get_memory_info(&self, mode: String) -> Result; + fn get_memory_info(&self, mode: Option) -> Result; /// Returns stats about our RPC server fn get_rpc_info(&self) -> Result; /// Returns for how long florestad has been running, in seconds @@ -171,25 +172,25 @@ impl FlorestaRPC for T { tx_id: Txid, outpoint: u32, script: String, - height_hint: u32, + height_hint: Option, ) -> Result { - self.call( - "findtxout", - &[ - Value::String(tx_id.to_string()), - Value::Number(Number::from(outpoint)), - Value::String(script), - Value::Number(Number::from(height_hint)), - ], - ) + let params = rpc_params([ + tx_id.into(), + outpoint.into(), + script.into(), + height_hint.into(), + ]); + + self.call("findtxout", ¶ms) } fn uptime(&self) -> Result { self.call("uptime", &[]) } - fn get_memory_info(&self, mode: String) -> Result { - self.call("getmemoryinfo", &[Value::String(mode)]) + fn get_memory_info(&self, mode: Option) -> Result { + let params = rpc_params([mode.into()]); + self.call("getmemoryinfo", ¶ms) } fn get_rpc_info(&self) -> Result { @@ -202,26 +203,15 @@ impl FlorestaRPC for T { command: AddNodeCommand, v2transport: Option, ) -> Result { - let mut params = vec![Value::String(node), Value::String(command.to_string())]; - - if let Some(v2transport) = v2transport { - params.push(Value::Bool(v2transport)); - } + let params = rpc_params([node.into(), command.to_string().into(), v2transport.into()]); self.call("addnode", ¶ms) } - fn disconnect_node(&self, node_address: String, node_id: Option) -> Result { - match node_id { - Some(node_id) => self.call( - "disconnectnode", - &[ - Value::String(node_address), - Value::Number(Number::from(node_id)), - ], - ), - None => self.call("disconnectnode", &[Value::String(node_address)]), - } + fn disconnect_node(&self, node_address: String, node_id: Option) -> Result { + let params = rpc_params([node_address.into(), node_id.into()]); + + self.call("disconnectnode", ¶ms) } fn stop(&self) -> Result { @@ -255,15 +245,9 @@ impl FlorestaRPC for T { } fn get_block(&self, hash: BlockHash, verbosity: Option) -> Result { - let verbosity = verbosity.unwrap_or(1); + let params = rpc_params([hash.into(), verbosity.into()]); - self.call( - "getblock", - &[ - Value::String(hash.to_string()), - Value::Number(Number::from(verbosity)), - ], - ) + self.call("getblock", ¶ms) } fn get_block_count(&self) -> Result { @@ -271,32 +255,18 @@ impl FlorestaRPC for T { } fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result { - let result: serde_json::Value = self.call( - "gettxout", - &[ - Value::String(tx_id.to_string()), - Value::Number(Number::from(outpoint)), - ], - )?; + let params = rpc_params([tx_id.into(), outpoint.into()]); + + let result: serde_json::Value = self.call("gettxout", ¶ms)?; if result.is_null() { return Err(Error::TxOutNotFound); } + serde_json::from_value(result).map_err(Error::Serde) } fn get_txout_proof(&self, txids: Vec, blockhash: Option) -> Option { - let params: Vec = match blockhash { - Some(blockhash) => vec![ - serde_json::to_value(txids) - .expect("Unreachable, Vec can be parsed into a json value"), - Value::String(blockhash.to_string()), - ], - None => { - let txids = serde_json::to_value(txids) - .expect("Unreachable, Vec can be parsed into a json value"); - vec![txids] - } - }; + let params = rpc_params([txids.into(), blockhash.into()]); self.call("gettxoutproof", ¶ms).ok() } @@ -313,27 +283,28 @@ impl FlorestaRPC for T { } fn get_block_hash(&self, height: u32) -> Result { - self.call("getblockhash", &[Value::Number(Number::from(height))]) + let params = rpc_params([height.into()]); + self.call("getblockhash", ¶ms) } fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { - let verbosity = verbosity.unwrap_or(false); - self.call( - "gettransaction", - &[Value::String(tx_id.to_string()), Value::Bool(verbosity)], - ) + let params = rpc_params([tx_id.into(), verbosity.into()]); + self.call("gettransaction", ¶ms) } fn load_descriptor(&self, descriptor: String) -> Result { - self.call("loaddescriptor", &[Value::String(descriptor)]) + let params = rpc_params([descriptor.into()]); + self.call("loaddescriptor", ¶ms) } fn get_block_filter(&self, height: u32) -> Result { - self.call("getblockfilter", &[Value::Number(Number::from(height))]) + let params = rpc_params([height.into()]); + self.call("getblockfilter", ¶ms) } fn get_block_header(&self, hash: BlockHash) -> Result { - self.call("getblockheader", &[Value::String(hash.to_string())]) + let params = rpc_params([hash.into()]); + self.call("getblockheader", ¶ms) } fn get_blockchain_info(&self) -> Result { @@ -341,7 +312,8 @@ impl FlorestaRPC for T { } fn send_raw_transaction(&self, tx: String) -> Result { - self.call("sendrawtransaction", &[Value::String(tx)]) + let params = rpc_params([tx.into()]); + self.call("sendrawtransaction", ¶ms) } fn list_descriptors(&self) -> Result> { @@ -352,3 +324,137 @@ impl FlorestaRPC for T { self.call("ping", &[]) } } + +enum RpcArg { + Value(Value), + Optional(Option), +} + +impl From for RpcArg { + fn from(v: String) -> Self { + RpcArg::Value(Value::String(v)) + } +} + +impl From<&str> for RpcArg { + fn from(v: &str) -> Self { + RpcArg::Value(Value::String(v.to_owned())) + } +} + +impl From for RpcArg { + fn from(v: bool) -> Self { + RpcArg::Value(Value::Bool(v)) + } +} + +impl From for RpcArg { + fn from(v: u32) -> Self { + RpcArg::Value(Value::Number(Number::from(v))) + } +} + +impl From for RpcArg { + fn from(value: Txid) -> Self { + RpcArg::Value(Value::String(value.to_string())) + } +} + +impl From for RpcArg { + fn from(value: BlockHash) -> Self { + RpcArg::Value(Value::String(value.to_string())) + } +} + +impl From> for RpcArg { + fn from(v: Vec) -> Self { + let values: Vec = v + .into_iter() + .filter_map(|item| serde_json::to_value(item).ok()) + .collect(); + RpcArg::Value(Value::Array(values)) + } +} + +impl From> for RpcArg { + fn from(v: Option) -> Self { + RpcArg::Optional(v.and_then(|x| serde_json::to_value(x).ok())) + } +} + +fn rpc_params(args: impl IntoIterator) -> Vec { + args.into_iter() + .filter_map(|arg| match arg { + RpcArg::Value(v) => Some(v), + RpcArg::Optional(v) => v, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rpc_arg_from_string() { + let arg = RpcArg::from("test".to_string()); + match arg { + RpcArg::Value(Value::String(s)) => assert_eq!(s, "test"), + _ => panic!("Expected RpcArg::Value"), + } + } + + #[test] + fn test_rpc_arg_from_bool() { + let arg = RpcArg::from(true); + match arg { + RpcArg::Value(Value::Bool(b)) => assert!(b), + _ => panic!("Expected RpcArg::Value with bool"), + } + } + + #[test] + fn test_rpc_arg_from_option_some() { + let opt: Option = Some(42); + let arg = RpcArg::from(opt); + match arg { + RpcArg::Optional(Some(Value::Number(n))) => { + assert_eq!(n.as_u64(), Some(42)); + } + _ => panic!("Expected RpcArg::Optional(Some(...))"), + } + } + + #[test] + fn test_rpc_arg_from_option_none() { + let opt: Option = None; + let arg = RpcArg::from(opt); + match arg { + RpcArg::Optional(None) => {} + _ => panic!("Expected RpcArg::Optional(None)"), + } + } + + #[test] + fn test_rpc_params_filters_nones() { + let params = rpc_params([ + "node1".into(), + true.into(), + Some(123u32).into(), + None::.into(), + ]); + + // Should have only 3 elements (None was filtered) + assert_eq!(params.len(), 3); + assert!(matches!(params[0], Value::String(ref s) if s == "node1")); + assert!(matches!(params[1], Value::Bool(true))); + assert!(matches!(params[2], Value::Number(_))); + } + + #[test] + fn test_rpc_params_all_none() { + let params = rpc_params([None::.into(), None::.into()]); + + assert_eq!(params.len(), 0); + } +} From e077196ed01933ea18b939934d8c70b9002a64bb Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:41:46 -0300 Subject: [PATCH 3/3] test(rpc): add comprehensive unit tests for all RPC methods Add unit tests validating the behavior and correctness of all FlorestaRPC trait methods. Tests verify that method calls are properly forwarded to the JSON-RPC client, parameters are correctly serialized and passed, and returned values match expected results. Includes tests for all RPC methods including parameter handling, optional arguments filtering, and deserialization of responses. --- crates/floresta-rpc/src/rpc.rs | 636 ++++++++++++++++++++++++++++++++- 1 file changed, 635 insertions(+), 1 deletion(-) diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index e91ab90c0..bb24aca88 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -6,6 +6,8 @@ use bitcoin::block::Header as BlockHeader; use bitcoin::BlockHash; use bitcoin::Txid; use corepc_types::v29::GetTxOut; +use serde::de::Deserialize; +use serde::de::DeserializeOwned; use serde::Serialize; use serde_json::Number; use serde_json::Value; @@ -163,7 +165,7 @@ pub trait JsonRPCClient: Sized { /// This should call the appropriated rpc method and return a parsed response or error. fn call(&self, method: &str, params: &[Value]) -> Result where - T: for<'a> serde::de::Deserialize<'a> + serde::de::DeserializeOwned + Debug; + T: for<'a> Deserialize<'a> + DeserializeOwned + Debug; } impl FlorestaRPC for T { @@ -393,8 +395,640 @@ fn rpc_params(args: impl IntoIterator) -> Vec { #[cfg(test)] mod tests { + use std::cell::RefCell; + use std::vec; + + use bitcoin::block::Version; + use bitcoin::hashes::Hash; + use bitcoin::CompactTarget; + use super::*; + struct MockRpcClient { + method: RefCell, + params: RefCell>, + result: RefCell>, + } + + impl MockRpcClient { + fn set_result(&self, result: Value) { + *self.result.borrow_mut() = Some(result); + } + } + + impl MockRpcClient { + fn new() -> Self { + Self { + method: RefCell::new(String::new()), + params: RefCell::new(Vec::new()), + result: RefCell::new(None), + } + } + } + + impl JsonRPCClient for MockRpcClient { + fn call(&self, method: &str, params: &[Value]) -> Result + where + T: for<'a> Deserialize<'a> + DeserializeOwned + Debug, + { + *self.method.borrow_mut() = method.to_string(); + *self.params.borrow_mut() = params.to_vec(); + + let result = self + .result + .borrow() + .clone() + .unwrap_or(serde_json::json!(null)); + + serde_json::from_value(result) + .map_err(|_| Error::Api(Value::String("Result parsing error".to_string()))) + } + } + + #[test] + fn test_get_block_filter_params() { + let client = MockRpcClient::new(); + let expected_result = "abcdef1234567890".to_string(); + client.set_result(Value::String(expected_result.clone())); + + let height = 500u32; + + let result = client.get_block_filter(height).unwrap(); + assert_eq!(result, expected_result); + + let expected_params = rpc_params([height.into()]); + + assert_eq!(*client.method.borrow(), "getblockfilter"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_blockchain_info() { + let client = MockRpcClient::new(); + let get_blockchain_info_res = GetBlockchainInfoRes { + chain: "main".to_string(), + best_block: "best_block".to_string(), + difficulty: 123456789, + height: 1000, + ibd: false, + latest_block_time: 1234567890, + latest_work: "latest_work".to_string(), + leaf_count: 1000, + progress: 0.9999, + root_count: 1000, + root_hashes: vec!["root_hash".to_string()], + validated: 10, + }; + let expected_result = serde_json::to_value(get_blockchain_info_res).unwrap(); + client.set_result(expected_result.clone()); + + let result = client.get_blockchain_info().unwrap(); + let result_serialized = serde_json::to_value(result).unwrap(); + + assert_eq!(result_serialized, expected_result); + + assert_eq!(*client.method.borrow(), "getblockchaininfo"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_get_best_block_hash() { + let client = MockRpcClient::new(); + let expected_result = BlockHash::all_zeros(); + client.set_result(Value::String(expected_result.to_string())); + + let result = client.get_best_block_hash().unwrap(); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "getbestblockhash"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_get_block_hash() { + let client = MockRpcClient::new(); + let expected_result = BlockHash::all_zeros(); + client.set_result(Value::String(expected_result.to_string())); + + let height = 100u32; + + let result = client.get_block_hash(height).unwrap(); + + let expected_params = rpc_params([height.into()]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "getblockhash"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_block_header() { + let client = MockRpcClient::new(); + let block_header = BlockHeader { + version: Version::from_consensus(21), + prev_blockhash: BlockHash::all_zeros(), + merkle_root: bitcoin::TxMerkleNode::all_zeros(), + time: 1234567890, + bits: CompactTarget::from_consensus(21), + nonce: 0, + }; + let expected_result = serde_json::to_value(block_header).unwrap(); + client.set_result(expected_result.clone()); + + let block_hash = BlockHash::all_zeros(); + + let result = client.get_block_header(block_hash).unwrap(); + let result_serialized = serde_json::to_value(result).unwrap(); + + let expected_params = rpc_params([block_hash.into()]); + + assert_eq!(result_serialized, expected_result); + assert_eq!(*client.method.borrow(), "getblockheader"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_transaction() { + let client = MockRpcClient::new(); + let expected_result = Value::String("transaction".to_string()); + client.set_result(expected_result.clone()); + + let tx_id = Txid::all_zeros(); + let verbosity = Some(true); + + let result = client.get_transaction(tx_id, verbosity).unwrap(); + + let expected_params = rpc_params([tx_id.into(), true.into()]); // verbosity is always passed + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "gettransaction"); + assert_eq!(client.params.borrow().len(), 2); + assert_eq!(*client.params.borrow(), expected_params); + + // Test with None verbosity (defaults to false) + let _ = client.get_transaction(tx_id, None); + + let expected_params = rpc_params([tx_id.into(), None::.into()]); + + assert_eq!(*client.method.borrow(), "gettransaction"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_txout_proof() { + let client = MockRpcClient::new(); + let expected_result = "proof".to_string(); + client.set_result(Value::String(expected_result.clone())); + + let txids = vec![Txid::all_zeros()]; + let blockhash = Some(BlockHash::all_zeros()); + + let result = client.get_txout_proof(txids.clone(), blockhash).unwrap(); + + let expected_params = rpc_params([txids.clone().into(), blockhash.into()]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "gettxoutproof"); + assert_eq!(client.params.borrow().len(), 2); + assert_eq!(*client.params.borrow(), expected_params); + + // Test without blockhash parameter + let _ = client.get_txout_proof(txids.clone(), None); + + let expected_params = rpc_params([txids.clone().into(), None::.into()]); + + assert_eq!(*client.method.borrow(), "gettxoutproof"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_load_descriptor() { + let client = MockRpcClient::new(); + let expected_result = true; + client.set_result(Value::Bool(expected_result)); + + let descriptor = "wpkh([aabbccdd/84'/0'/0']xpub6CatD...)".to_string(); + + let result = client.load_descriptor(descriptor.clone()).unwrap(); + + let expected_params = rpc_params([descriptor.clone().into()]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "loaddescriptor"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_rescanblockchain() { + let client = MockRpcClient::new(); + let expected_result = true; + client.set_result(Value::Bool(expected_result)); + + let start_height = 100u32; + let stop_height = 200u32; + let use_timestamp = true; + let confidence = RescanConfidence::High; + + let result = client + .rescanblockchain( + Some(start_height), + Some(stop_height), + use_timestamp, + confidence.clone(), + ) + .unwrap(); + + let expected_params = [ + Value::Number(Number::from(start_height)), + Value::Number(Number::from(stop_height)), + Value::Bool(use_timestamp), + serde_json::to_value(&confidence).expect("RescanConfidence implements Ser/De"), + ]; + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "rescanblockchain"); + assert_eq!(client.params.borrow().len(), 4); + assert_eq!(*client.params.borrow(), expected_params); + + // Test with None parameters + let _ = client.rescanblockchain(None, None, use_timestamp, confidence.clone()); + + let expected_params = [ + Value::Number(Number::from(0u32)), // Default start height + Value::Number(Number::from(0u32)), // Default stop height + Value::Bool(use_timestamp), + serde_json::to_value(&confidence).expect("RescanConfidence implements Ser/De"), + ]; + + assert_eq!(*client.method.borrow(), "rescanblockchain"); + assert_eq!(client.params.borrow().len(), 4); // All parameters are passed, but start/stop heights are defaulted + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_block_count() { + let client = MockRpcClient::new(); + let expected_result = 1000u32; + client.set_result(Value::Number(Number::from(expected_result))); + + let result = client.get_block_count().unwrap(); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "getblockcount"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_send_raw_transaction() { + let client = MockRpcClient::new(); + let expected_result = Txid::all_zeros(); + client.set_result(Value::String(expected_result.to_string())); + + let tx = "02000000010123456789abcdef...".to_string(); + + let result = client.send_raw_transaction(tx.clone()).unwrap(); + + let expected_params = rpc_params([tx.clone().into()]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "sendrawtransaction"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_roots() { + let client = MockRpcClient::new(); + let expected_result = vec!["root1".to_string(), "root2".to_string()]; + client.set_result(Value::Array( + expected_result + .iter() + .map(|root| Value::String(root.clone())) + .collect(), + )); + + let result = client.get_roots().unwrap(); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "getroots"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_get_peer_info() { + let client = MockRpcClient::new(); + let peer_info = vec![PeerInfo { + address: "address".to_string(), + id: 1, + initial_height: 100, + kind: "kind".to_string(), + services: "services".to_string(), + state: "state".to_string(), + transport_protocol: "transport_protocol".to_string(), + user_agent: "user_agent".to_string(), + }]; + let expected_result = serde_json::to_value(&peer_info).unwrap(); + client.set_result(expected_result.clone()); + + let result = client.get_peer_info().unwrap(); + let result_serialized = serde_json::to_value(result).unwrap(); + + assert_eq!(result_serialized, expected_result); + assert_eq!(*client.method.borrow(), "getpeerinfo"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_get_connection_count() { + let client = MockRpcClient::new(); + let expected_result = 8usize; + client.set_result(Value::Number(Number::from(expected_result))); + + let result = client.get_connection_count().unwrap(); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "getconnectioncount"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_get_block() { + let client = MockRpcClient::new(); + let get_block = GetBlockRes::Zero("block".to_string()); + let expected_result = serde_json::to_value(&get_block).unwrap(); + client.set_result(expected_result.clone()); + + let block_hash = BlockHash::all_zeros(); + let verbosity = Some(1u32); + + let result = client.get_block(block_hash, verbosity).unwrap(); + let result_serialized = serde_json::to_value(result).unwrap(); + + let expected_params = rpc_params([block_hash.into(), verbosity.into()]); + + assert_eq!(result_serialized, expected_result); + assert_eq!(*client.method.borrow(), "getblock"); + assert_eq!(client.params.borrow().len(), 2); + assert_eq!(*client.params.borrow(), expected_params); + + // Test without verbosity parameter + let _ = client.get_block(block_hash, None); + + let expected_params = rpc_params([block_hash.into(), None::.into()]); + + assert_eq!(*client.method.borrow(), "getblock"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_tx_out() { + let client = MockRpcClient::new(); + let expected_result = GetTxOut { + best_block: "best_block".to_string(), + confirmations: 10, + value: 0.1, + coinbase: false, + script_pubkey: corepc_types::ScriptPubkey { + address: Some("address".to_string()), + asm: "asm".to_string(), + hex: "hex".to_string(), + type_: "type".to_string(), + addresses: None, + descriptor: None, + required_signatures: None, + }, + }; + client.set_result(serde_json::to_value(&expected_result).unwrap()); + + let tx_id = Txid::all_zeros(); + let outpoint = 0u32; + + let result = client.get_tx_out(tx_id, outpoint).unwrap(); + + let expected_params = rpc_params([tx_id.into(), outpoint.into()]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "gettxout"); + assert_eq!(client.params.borrow().len(), 2); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_stop() { + let client = MockRpcClient::new(); + let _ = client.stop(); + + assert_eq!(*client.method.borrow(), "stop"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_add_node() { + let client = MockRpcClient::new(); + let expected_result = serde_json::json!({"success": true}); + client.set_result(expected_result.clone()); + + let node = "192.168.1.1:8333".to_string(); + let command = AddNodeCommand::Add; + let v2transport = Some(true); + + let result = client + .add_node(node.clone(), command.clone(), v2transport) + .unwrap(); + + let expected_params = rpc_params([ + node.clone().into(), + command.clone().to_string().into(), + v2transport.into(), + ]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "addnode"); + assert_eq!(client.params.borrow().len(), 3); + assert_eq!(*client.params.borrow(), expected_params); + + // Test without v2transport parameter + let _ = client.add_node(node.clone(), command.clone(), None); + + let expected_params = rpc_params([ + node.clone().into(), + command.to_string().into(), + None::.into(), + ]); + + assert_eq!(*client.method.borrow(), "addnode"); + assert_eq!(client.params.borrow().len(), 2); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_disconnect_node() { + let client = MockRpcClient::new(); + let expected_result = serde_json::json!({"success": true}); + client.set_result(expected_result.clone()); + + let node_address = "192.168.1.1".to_string(); + let node_id = Some(1u32); + + let result = client + .disconnect_node(node_address.clone(), node_id) + .unwrap(); + + let expected_params = rpc_params([node_address.clone().into(), node_id.into()]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "disconnectnode"); + assert_eq!(client.params.borrow().len(), 2); + assert_eq!(*client.params.borrow(), expected_params); + + // Test with None node_id + let _ = client.disconnect_node(node_address.clone(), None); + + let expected_params = rpc_params([node_address.clone().into(), None::.into()]); + + assert_eq!(*client.method.borrow(), "disconnectnode"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_find_tx_out() { + let client = MockRpcClient::new(); + let expected_result = serde_json::json!({"success": true}); + client.set_result(expected_result.clone()); + + let txid = Txid::all_zeros(); + let outpoint = 0; + let script = "76a91488ac".to_string(); + let mut height_hint = Some(100); + + let result = client + .find_tx_out(txid, outpoint, script.clone(), height_hint) + .unwrap(); + + let expetecd_params = rpc_params([ + txid.into(), + outpoint.into(), + script.clone().into(), + height_hint.into(), + ]); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "findtxout"); + assert_eq!(client.params.borrow().len(), 4); + assert_eq!(*client.params.borrow(), expetecd_params); + + // Test that None height hint is filtered out + height_hint = None; + + let _ = client.find_tx_out(txid, outpoint, script.clone(), height_hint); + + let expetecd_params = rpc_params([ + txid.into(), + outpoint.into(), + script.clone().into(), + height_hint.into(), + ]); + + assert_eq!(*client.method.borrow(), "findtxout"); + assert_eq!(client.params.borrow().len(), 3); + assert_eq!(*client.params.borrow(), expetecd_params); + } + + #[test] + fn test_get_memory_info() { + let client = MockRpcClient::new(); + let memory_info = GetMemInfoRes::MallocInfo("Malloc".to_string()); + let expected_result = serde_json::to_value(&memory_info).unwrap(); + client.set_result(expected_result.clone()); + + let mode = Some("all".to_string()); + + let result = client.get_memory_info(mode.clone()).unwrap(); + let result_serialized = serde_json::to_value(result).unwrap(); + + let expected_params = rpc_params([mode.clone().into()]); + + assert_eq!(result_serialized, expected_result); + assert_eq!(*client.method.borrow(), "getmemoryinfo"); + assert_eq!(client.params.borrow().len(), 1); + assert_eq!(*client.params.borrow(), expected_params); + + // Test without mode parameter + let _ = client.get_memory_info(None); + + let expected_params = rpc_params([None::.into()]); + + assert_eq!(*client.method.borrow(), "getmemoryinfo"); + assert_eq!(client.params.borrow().len(), 0); + assert_eq!(*client.params.borrow(), expected_params); + } + + #[test] + fn test_get_rpc_info() { + let client = MockRpcClient::new(); + let rpc_info = GetRpcInfoRes { + logpath: "logpath".to_string(), + active_commands: Vec::new(), + }; + let expected_result = serde_json::to_value(&rpc_info).unwrap(); + client.set_result(expected_result.clone()); + + let result = client.get_rpc_info().unwrap(); + let result_serialized = serde_json::to_value(result).unwrap(); + + assert_eq!(result_serialized, expected_result); + assert_eq!(*client.method.borrow(), "getrpcinfo"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_uptime() { + let client = MockRpcClient::new(); + let expected_result = 3600u32; + client.set_result(Value::Number(Number::from(expected_result))); + + let result = client.uptime().unwrap(); + + assert_eq!(result, expected_result); + assert_eq!(*client.method.borrow(), "uptime"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_list_descriptors() { + let client = MockRpcClient::new(); + let expect_ed_result = vec!["desc1".to_string(), "desc2".to_string()]; + client.set_result(Value::Array( + expect_ed_result + .iter() + .map(|desc| Value::String(desc.clone())) + .collect(), + )); + + let result = client.list_descriptors().unwrap(); + + assert_eq!(result, expect_ed_result); + assert_eq!(*client.method.borrow(), "listdescriptors"); + assert!(client.params.borrow().is_empty()); + } + + #[test] + fn test_ping() { + let client = MockRpcClient::new(); + client.ping().unwrap(); + + assert_eq!(*client.method.borrow(), "ping"); + assert!(client.params.borrow().is_empty()); + } + #[test] fn test_rpc_arg_from_string() { let arg = RpcArg::from("test".to_string());