From 1e8e3660808001aaa5f7ddff546df888adc3c5a2 Mon Sep 17 00:00:00 2001 From: Riccardo Casatta Date: Mon, 16 Sep 2024 14:33:28 +0200 Subject: [PATCH 1/4] add electrum raw test --- tests/electrum.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/electrum.rs b/tests/electrum.rs index b2bab95f9..9c6ce5b7c 100644 --- a/tests/electrum.rs +++ b/tests/electrum.rs @@ -1,4 +1,7 @@ pub mod common; +use std::io::{Read, Write}; +use std::net::TcpStream; + use common::Result; use bitcoind::bitcoincore_rpc::RpcApi; @@ -137,3 +140,38 @@ fn test_electrum() -> Result<()> { Ok(()) } + +/// Test the Electrum RPC server using an headless Electrum wallet +/// This only runs on Bitcoin (non-Liquid) mode. +#[cfg_attr(not(feature = "liquid"), test)] +#[cfg_attr(feature = "liquid", allow(dead_code))] +fn test_electrum_raw() { + // Spawn an Electrs Electrum RPC server + let (_electrum_server, electrum_addr, mut _tester) = common::init_electrum_tester().unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1000)); + + let mut stream = TcpStream::connect(electrum_addr).unwrap(); + let write = "{\"jsonrpc\": \"2.0\", \"method\": \"server.version\", \"id\": 0}"; + + let s = write_and_read(&mut stream, write); + let expected = "{\"id\":0,\"jsonrpc\":\"2.0\",\"result\":[\"electrs-esplora 0.4.1\",\"1.4\"]}"; + assert_eq!(s, expected); +} + +fn write_and_read(stream: &mut TcpStream, write: &str) -> String { + stream.write_all(write.as_bytes()).unwrap(); + stream.write(b"\n").unwrap(); + stream.flush().unwrap(); + let mut result = vec![]; + loop { + let mut buf = [0u8]; + stream.read_exact(&mut buf).unwrap(); + + if buf[0] == b'\n' { + break; + } else { + result.push(buf[0]); + } + } + std::str::from_utf8(&result).unwrap().to_string() +} From 192ba8d4750a2b7ebab9a5eb1a5beadd0c3b874c Mon Sep 17 00:00:00 2001 From: Riccardo Casatta Date: Mon, 16 Sep 2024 15:14:28 +0200 Subject: [PATCH 2/4] refactor out handling a single Value --- src/electrum/server.rs | 93 +++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/src/electrum/server.rs b/src/electrum/server.rs index 2beae0855..ec491edf8 100644 --- a/src/electrum/server.rs +++ b/src/electrum/server.rs @@ -546,49 +546,7 @@ impl Connection { match msg { Message::Request(line) => { let cmd: Value = from_str(&line).chain_err(|| "invalid JSON format")?; - match ( - cmd.get("method"), - cmd.get("params").unwrap_or_else(|| &empty_params), - cmd.get("id"), - ) { - ( - Some(&Value::String(ref method)), - &Value::Array(ref params), - Some(ref id), - ) => { - conditionally_log_rpc_event!( - self, - json!({ - "event": "rpc request", - "id": id, - "method": method, - "params": if let Some(RpcLogging::Full) = self.rpc_logging { - json!(params) - } else { - Value::Null - } - }) - ); - - let reply = self.handle_command(method, params, id)?; - - conditionally_log_rpc_event!( - self, - json!({ - "event": "rpc response", - "method": method, - "payload_size": reply.to_string().as_bytes().len(), - "duration_micros": start_time.elapsed().as_micros(), - "id": id, - }) - ); - - self.send_values(&[reply])? - } - _ => { - bail!("invalid command: {}", cmd) - } - } + self.handle_value(cmd, &empty_params, start_time)?; } Message::PeriodicUpdate => { let values = self @@ -601,6 +559,55 @@ impl Connection { } } + fn handle_value( + &mut self, + cmd: Value, + empty_params: &Value, + start_time: Instant, + ) -> Result<()> { + Ok( + match ( + cmd.get("method"), + cmd.get("params").unwrap_or_else(|| empty_params), + cmd.get("id"), + ) { + (Some(&Value::String(ref method)), &Value::Array(ref params), Some(ref id)) => { + conditionally_log_rpc_event!( + self, + json!({ + "event": "rpc request", + "id": id, + "method": method, + "params": if let Some(RpcLogging::Full) = self.rpc_logging { + json!(params) + } else { + Value::Null + } + }) + ); + + let reply = self.handle_command(method, params, id)?; + + conditionally_log_rpc_event!( + self, + json!({ + "event": "rpc response", + "method": method, + "payload_size": reply.to_string().as_bytes().len(), + "duration_micros": start_time.elapsed().as_micros(), + "id": id, + }) + ); + + self.send_values(&[reply])? + } + _ => { + bail!("invalid command: {}", cmd) + } + }, + ) + } + fn parse_requests(mut reader: BufReader, tx: &SyncSender) -> Result<()> { loop { let mut line = Vec::::new(); From 99f5a8b4ff132d846dfecd2e24f146d34682ff1a Mon Sep 17 00:00:00 2001 From: Riccardo Casatta Date: Mon, 16 Sep 2024 15:17:41 +0200 Subject: [PATCH 3/4] handle_value return result instead of sending --- src/electrum/server.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/electrum/server.rs b/src/electrum/server.rs index ec491edf8..cc8e2136b 100644 --- a/src/electrum/server.rs +++ b/src/electrum/server.rs @@ -546,7 +546,8 @@ impl Connection { match msg { Message::Request(line) => { let cmd: Value = from_str(&line).chain_err(|| "invalid JSON format")?; - self.handle_value(cmd, &empty_params, start_time)?; + let reply = self.handle_value(cmd, &empty_params, start_time)?; + self.send_values(&[reply])? } Message::PeriodicUpdate => { let values = self @@ -564,7 +565,7 @@ impl Connection { cmd: Value, empty_params: &Value, start_time: Instant, - ) -> Result<()> { + ) -> Result { Ok( match ( cmd.get("method"), @@ -599,7 +600,7 @@ impl Connection { }) ); - self.send_values(&[reply])? + reply } _ => { bail!("invalid command: {}", cmd) From 4b66d0a3e9527e786e75a0d58450ece8f81f7268 Mon Sep 17 00:00:00 2001 From: Riccardo Casatta Date: Mon, 16 Sep 2024 15:26:10 +0200 Subject: [PATCH 4/4] handle batch requests as array --- src/electrum/server.rs | 13 +++++++++++-- tests/electrum.rs | 6 ++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/electrum/server.rs b/src/electrum/server.rs index cc8e2136b..8b8b2b93e 100644 --- a/src/electrum/server.rs +++ b/src/electrum/server.rs @@ -546,8 +546,17 @@ impl Connection { match msg { Message::Request(line) => { let cmd: Value = from_str(&line).chain_err(|| "invalid JSON format")?; - let reply = self.handle_value(cmd, &empty_params, start_time)?; - self.send_values(&[reply])? + if let Value::Array(arr) = cmd { + let mut result = Vec::with_capacity(arr.len()); + for el in arr { + let reply = self.handle_value(el, &empty_params, start_time)?; + result.push(reply) + } + self.send_values(&[Value::Array(result)])? + } else { + let reply = self.handle_value(cmd, &empty_params, start_time)?; + self.send_values(&[reply])? + } } Message::PeriodicUpdate => { let values = self diff --git a/tests/electrum.rs b/tests/electrum.rs index 9c6ce5b7c..3dacf74b3 100644 --- a/tests/electrum.rs +++ b/tests/electrum.rs @@ -156,6 +156,12 @@ fn test_electrum_raw() { let s = write_and_read(&mut stream, write); let expected = "{\"id\":0,\"jsonrpc\":\"2.0\",\"result\":[\"electrs-esplora 0.4.1\",\"1.4\"]}"; assert_eq!(s, expected); + + let write = "[{\"jsonrpc\": \"2.0\", \"method\": \"server.version\", \"id\": 0}]"; + let s = write_and_read(&mut stream, write); + let expected = + "[{\"id\":0,\"jsonrpc\":\"2.0\",\"result\":[\"electrs-esplora 0.4.1\",\"1.4\"]}]"; + assert_eq!(s, expected); } fn write_and_read(stream: &mut TcpStream, write: &str) -> String {