From 1a04495b9649b50c1ae87a01b60441058e5eab49 Mon Sep 17 00:00:00 2001 From: Brian Cloutier Date: Fri, 22 Oct 2021 12:15:06 -0700 Subject: [PATCH] Initial cli for running JSON-RPC against trin --- Cargo.lock | 46 +++++++++++++-- Cargo.toml | 2 + trin-cli/Cargo.toml | 18 ++++++ trin-cli/src/main.rs | 137 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 trin-cli/Cargo.toml create mode 100644 trin-cli/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 7085b1e4f..b37b32e5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,15 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64-compat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" +dependencies = [ + "byteorder", +] + [[package]] name = "bindgen" version = "0.57.0" @@ -1343,6 +1352,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad24d69a8a0698db8ffb9048e937e8ae3ee3bc45772a5d7b6979b1d2d5b6a9f7" +dependencies = [ + "base64-compat", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "k256" version = "0.8.1" @@ -2494,9 +2515,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -2639,9 +2660,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.22" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b041cdcb67226aca307e6e7be44c8806423d83e018bd662360a93dabce4d71" +checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" dependencies = [ "clap", "lazy_static 1.4.0", @@ -2650,9 +2671,9 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.15" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7813934aecf5f51a54775e00068c237de98489463968231a51746bbbc03f9c10" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck", "proc-macro-error", @@ -3140,11 +3161,24 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "trin-cli", "trin-core", "trin-history", "trin-state", ] +[[package]] +name = "trin-cli" +version = "0.1.0" +dependencies = [ + "jsonrpc", + "serde", + "serde_json", + "structopt", + "thiserror", + "trin-core", +] + [[package]] name = "trin-core" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7a3f7aa7e..0a0bb18d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ tracing-subscriber = "0.2.18" trin-core = { path = "trin-core" } trin-history = { path = "trin-history" } trin-state = { path = "trin-state" } +trin-cli = { path = "trin-cli" } [dependencies.discv5] version = "0.1.0-beta.10" @@ -26,5 +27,6 @@ members = [ "trin-history", "trin-state", "trin-core", + "trin-cli", "ethportal-peertest" ] diff --git a/trin-cli/Cargo.toml b/trin-cli/Cargo.toml new file mode 100644 index 000000000..a121d6932 --- /dev/null +++ b/trin-cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "trin-cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +structopt="0.3.25" +trin-core = { path = "../trin-core" } +jsonrpc = "0.12.0" +serde_json = "1.0.68" +serde = "1.0.117" +thiserror = "1.0.29" + +[[bin]] +name="trin-cli" +path="src/main.rs" diff --git a/trin-cli/src/main.rs b/trin-cli/src/main.rs new file mode 100644 index 000000000..33cd7b59c --- /dev/null +++ b/trin-cli/src/main.rs @@ -0,0 +1,137 @@ +use std::os::unix::net::UnixStream; +use std::path::PathBuf; +use structopt::StructOpt; +use thiserror::Error; + +use trin_core::cli::DEFAULT_WEB3_IPC_PATH; + +#[derive(StructOpt, Debug)] +#[structopt( + name = "trin-cli", + version = "0.0.1", + author = "Ethereum Foundation", + about = "Run JSON-RPC commands against trin nodes" +)] +struct Config { + #[structopt( + default_value(DEFAULT_WEB3_IPC_PATH), + long, + help = "path to JSON-RPC endpoint" + )] + ipc: PathBuf, + + #[structopt(help = "e.g. discv5_routingTableInfo", required = true)] + endpoint: String, +} + +fn main() { + let Config { ipc, endpoint } = Config::from_args(); + + eprintln!( + "Attempting RPC. endpoint={} file={}", + endpoint, + ipc.to_string_lossy() + ); + let mut client = match TrinClient::from_ipc(&ipc) { + Ok(client) => client, + Err(err) => { + eprintln!("Could not connect. err={:?}", err); + return; + } + }; + + let req = client.build_request(endpoint.as_str()); + let resp = client.make_request(req); + + match resp { + Err(error) => { + eprintln!("error: {}", error); + } + Ok(value) => { + // unwrap: this value is safe to serialize, it was just deserialized! + println!("{}", serde_json::to_string_pretty(&value).unwrap()); + } + } +} + +fn build_request<'a>(method: &'a str, request_id: u64) -> jsonrpc::Request<'a> { + jsonrpc::Request { + method: method, + params: &[], + id: serde_json::json!(request_id), + jsonrpc: Some("2.0"), + } +} + +pub trait TryClone { + fn try_clone(&self) -> std::io::Result + where + Self: Sized; +} + +impl TryClone for UnixStream { + fn try_clone(&self) -> std::io::Result { + UnixStream::try_clone(self) + } +} + +pub struct TrinClient +where + S: std::io::Read + std::io::Write + TryClone, +{ + stream: S, + request_id: u64, +} + +impl TrinClient { + fn from_ipc(path: &PathBuf) -> std::io::Result { + // TODO: a nice error if this file does not exist + Ok(Self { + stream: UnixStream::connect(path)?, + request_id: 0, + }) + } +} + +#[derive(Error, Debug)] +pub enum JsonRpcError { + #[error("Received malformed response: {0}")] + Malformed(serde_json::Error), + + #[error("Received empty response")] + Empty, +} + +// TryClone is used because JSON-RPC responses are not followed by EOF. We must read bytes +// from the stream until a complete object is detected, and the simplest way of doing that +// with available APIs is to give ownership of a Read to a serde_json::Deserializer. If we +// gave it exclusive ownership that would require us to open a new connection for every +// command we wanted to send! By making a clone (or, by trying to) we can have our cake +// and eat it too. +impl<'a, S> TrinClient +where + S: std::io::Read + std::io::Write + TryClone, +{ + fn build_request(&mut self, method: &'a str) -> jsonrpc::Request<'a> { + let result = build_request(method, self.request_id); + self.request_id += 1; + + result + } + + fn make_request(&mut self, req: jsonrpc::Request) -> Result { + let data = serde_json::to_vec(&req).unwrap(); + + self.stream.write_all(&data).unwrap(); + self.stream.flush().unwrap(); + + let clone = self.stream.try_clone().unwrap(); + let deser = serde_json::Deserializer::from_reader(clone); + for obj in deser.into_iter::() { + return obj.map_err(|err| JsonRpcError::Malformed(err)); + } + + // this should only happen when they immediately send EOF + Err(JsonRpcError::Empty) + } +}