diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index c9ab518c2..920af3884 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, @@ -117,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()?)?, @@ -340,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/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..30cf78bf2 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; @@ -320,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 @@ -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..bb24aca88 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -6,6 +6,9 @@ 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; @@ -116,12 +119,17 @@ 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. /// 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 @@ -132,12 +140,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 @@ -157,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 { @@ -166,53 +174,46 @@ 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 { 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 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 { @@ -246,15 +247,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 { @@ -262,32 +257,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() } @@ -304,27 +285,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 { @@ -332,7 +314,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> { @@ -343,3 +326,769 @@ 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 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()); + 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); + } +} 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