From 4e3f8bab10387bd7b1d4baa1b4715d4d2d84a9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Str=C3=B6m?= Date: Tue, 20 Sep 2016 12:19:07 +0200 Subject: [PATCH] Add Ws Json rpc client and command line utils --- parity/cli/mod.rs | 3 + parity/cli/usage.txt | 3 + parity/configuration.rs | 51 +++- parity/main.rs | 8 + rpc/src/v1/types/confirmations.rs | 25 +- rpc/src/v1/types/transaction_request.rs | 12 +- rpc/src/v1/types/uint.rs | 8 + rpc_cli/Cargo.toml | 15 ++ rpc_cli/src/lib.rs | 168 ++++++++++++ rpc_client/Cargo.toml | 27 ++ rpc_client/src/client.rs | 332 ++++++++++++++++++++++++ rpc_client/src/lib.rs | 74 ++++++ rpc_client/src/mock.rs | 30 +++ rpc_client/src/signer.rs | 47 ++++ 14 files changed, 796 insertions(+), 7 deletions(-) create mode 100644 rpc_cli/Cargo.toml create mode 100644 rpc_cli/src/lib.rs create mode 100644 rpc_client/Cargo.toml create mode 100644 rpc_client/src/client.rs create mode 100644 rpc_client/src/lib.rs create mode 100644 rpc_client/src/mock.rs create mode 100644 rpc_client/src/signer.rs diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index f5e2cbf4a..a535d4195 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -31,6 +31,8 @@ usage! { cmd_import: bool, cmd_signer: bool, cmd_new_token: bool, + cmd_sign: bool, + cmd_reject: bool, cmd_snapshot: bool, cmd_restore: bool, cmd_ui: bool, @@ -41,6 +43,7 @@ usage! { arg_pid_file: String, arg_file: Option, arg_path: Vec, + arg_id: Option, // Flags // -- Legacy Options diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index 58518bd48..34efad9c6 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -12,6 +12,9 @@ Usage: parity import [ ] [options] parity export (blocks | state) [ ] [options] parity signer new-token [options] + parity signer list [options] + parity signer sign [ ] [ --password FILE ] [options] + parity signer reject [options] parity snapshot [options] parity restore [ ] [options] parity tools hash diff --git a/parity/configuration.rs b/parity/configuration.rs index 60116ef99..0d1c2c690 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -51,6 +51,21 @@ pub enum Cmd { ImportPresaleWallet(ImportWallet), Blockchain(BlockchainCmd), SignerToken(SignerConfiguration), + SignerSign { + id: Option, + pwfile: Option, + port: u16, + authfile: PathBuf, + }, + SignerList { + port: u16, + authfile: PathBuf + }, + SignerReject { + id: usize, + port: u16, + authfile: PathBuf + }, Snapshot(SnapshotCommand), Hash(Option), } @@ -103,8 +118,43 @@ impl Configuration { let cmd = if self.args.flag_version { Cmd::Version +<<<<<<< HEAD } else if self.args.cmd_signer && self.args.cmd_new_token { Cmd::SignerToken(signer_conf) +======= + } else if self.args.cmd_signer { + let mut authfile = PathBuf::from(signer_conf.signer_path); + authfile.push("authcodes"); + + if self.args.cmd_new_token { + Cmd::SignerToken(dirs.signer) + } else if self.args.cmd_sign { + let pwfile = match self.args.flag_password.get(0) { + Some(pwfile) => Some(PathBuf::from(pwfile)), + None => None, + }; + Cmd::SignerSign { + id: self.args.arg_id, + pwfile: pwfile, + port: signer_conf.port, + authfile: authfile, + } + } else if self.args.cmd_reject { + Cmd::SignerReject { + // id is a required field for this command + id: self.args.arg_id.unwrap(), + port: signer_conf.port, + authfile: authfile, + } + } else if self.args.cmd_list { + Cmd::SignerList { + port: signer_conf.port, + authfile: authfile, + } + } else { + unreachable!(); + } +>>>>>>> Add Ws Json rpc client and command line utils } else if self.args.cmd_tools && self.args.cmd_hash { Cmd::Hash(self.args.arg_file) } else if self.args.cmd_account { @@ -1126,4 +1176,3 @@ mod tests { assert!(conf.init_reserved_nodes().is_ok()); } } - diff --git a/parity/main.rs b/parity/main.rs index c125e87f6..6d1ddbffb 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -69,6 +69,8 @@ extern crate ethcore_stratum; #[cfg(feature = "dapps")] extern crate ethcore_dapps; +extern crate rpc_cli; + macro_rules! dependency { ($dep_ty:ident, $url:expr) => { { @@ -145,6 +147,12 @@ fn execute(command: Execute) -> Result { Cmd::ImportPresaleWallet(presale_cmd) => presale::execute(presale_cmd), Cmd::Blockchain(blockchain_cmd) => blockchain::execute(blockchain_cmd), Cmd::SignerToken(signer_cmd) => signer::execute(signer_cmd), + Cmd::SignerSign { id, pwfile, port, authfile } => + rpc_cli::cmd_signer_sign(id, pwfile, port, authfile), + Cmd::SignerList { port, authfile } => + rpc_cli::cmd_signer_list(port, authfile), + Cmd::SignerReject { id, port, authfile } => + rpc_cli::cmd_signer_reject(id, port, authfile), Cmd::Snapshot(snapshot_cmd) => snapshot::execute(snapshot_cmd), } } diff --git a/rpc/src/v1/types/confirmations.rs b/rpc/src/v1/types/confirmations.rs index d8cfa14d6..6ea01db64 100644 --- a/rpc/src/v1/types/confirmations.rs +++ b/rpc/src/v1/types/confirmations.rs @@ -22,7 +22,7 @@ use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, use v1::helpers; /// Confirmation waiting in a queue -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ConfirmationRequest { /// Id of this confirmation pub id: U256, @@ -39,8 +39,24 @@ impl From for ConfirmationRequest { } } +impl fmt::Display for ConfirmationRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "id: {:?}, {}", self.id, self.payload) + } +} + +impl fmt::Display for ConfirmationPayload { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &ConfirmationPayload::Transaction(ref transaction) + => write!(f, "{}", transaction), + &ConfirmationPayload::Sign(_) => write!(f, "TODO: data"), + } + } +} + /// Sign request -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct SignRequest { /// Address pub address: H160, @@ -102,7 +118,7 @@ impl Serialize for ConfirmationResponse { } /// Confirmation payload, i.e. the thing to be confirmed -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum ConfirmationPayload { /// Send Transaction #[serde(rename="sendTransaction")] @@ -136,7 +152,7 @@ impl From for ConfirmationPayload { } /// Possible modifications to the confirmed transaction sent by `Trusted Signer` -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TransactionModification { /// Modified gas price @@ -290,4 +306,3 @@ mod tests { }); } } - diff --git a/rpc/src/v1/types/transaction_request.rs b/rpc/src/v1/types/transaction_request.rs index a4f8e6387..5ee484a01 100644 --- a/rpc/src/v1/types/transaction_request.rs +++ b/rpc/src/v1/types/transaction_request.rs @@ -19,6 +19,8 @@ use v1::types::{Bytes, H160, U256}; use v1::helpers; +use std::fmt; + /// Transaction request coming from RPC #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -40,6 +42,15 @@ pub struct TransactionRequest { pub nonce: Option, } +impl fmt::Display for TransactionRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?} from {:?} to {:?}", + self.value.unwrap_or(U256::from(0)), + self.from, + self.to) + } +} + impl From for TransactionRequest { fn from(r: helpers::TransactionRequest) -> Self { TransactionRequest { @@ -192,4 +203,3 @@ mod tests { assert!(deserialized.is_err(), "Should be error because to is empty"); } } - diff --git a/rpc/src/v1/types/uint.rs b/rpc/src/v1/types/uint.rs index 245348709..e7363275e 100644 --- a/rpc/src/v1/types/uint.rs +++ b/rpc/src/v1/types/uint.rs @@ -15,6 +15,8 @@ // along with Parity. If not, see . use std::str::FromStr; +use std::fmt; +use rustc_serialize::hex::ToHex; use serde; use util::{U256 as EthU256, U128 as EthU128, Uint}; @@ -46,6 +48,12 @@ macro_rules! impl_uint { } } + impl fmt::LowerHex for $name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:#x}", self.0) + } + } + impl serde::Serialize for $name { fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: serde::Serializer { serializer.serialize_str(&format!("0x{}", self.0.to_hex())) diff --git a/rpc_cli/Cargo.toml b/rpc_cli/Cargo.toml new file mode 100644 index 000000000..8169d3b71 --- /dev/null +++ b/rpc_cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +authors = ["Ethcore "] +description = "Parity Cli Tool" +homepage = "http://ethcore.io" +license = "GPL-3.0" +name = "rpc-cli" +version = "1.4.0" + +[dependencies] +futures = "0.1" +rpassword = "0.3.0" +ethcore-bigint = { path = "../util/bigint" } +ethcore-rpc = { path = "../rpc" } +parity-rpc-client = { path = "../rpc_client" } +ethcore-util = { path = "../util" } diff --git a/rpc_cli/src/lib.rs b/rpc_cli/src/lib.rs new file mode 100644 index 000000000..a69351fd0 --- /dev/null +++ b/rpc_cli/src/lib.rs @@ -0,0 +1,168 @@ +extern crate futures; + +extern crate ethcore_util as util; +extern crate ethcore_rpc as rpc; +extern crate ethcore_bigint as bigint; +extern crate rpassword; + +extern crate parity_rpc_client as client; + +use rpc::v1::types::{U256, ConfirmationRequest}; +use client::signer::SignerRpc; +use std::io::{Write, BufRead, BufReader, stdout, stdin}; +use std::path::PathBuf; +use std::fs::File; + +use futures::Future; + +fn sign_interactive(signer: &mut SignerRpc, pwd: &String, request: ConfirmationRequest) + -> Result +{ + print!("\n{}\nSign this transaction? (y)es/(N)o/(r)eject: ", request); + stdout().flush(); + match BufReader::new(stdin()).lines().next() { + Some(Ok(line)) => { + match line.to_lowercase().chars().nth(0) { + Some('y') => { + match sign_transaction(signer, request.id, pwd) { + Ok(s) | Err(s) => println!("{}", s), + } + } + Some('r') => { + match reject_transaction(signer, request.id) { + Ok(s) | Err(s) => println!("{}", s), + } + } + _ => () + } + } + _ => return Err("Could not read from stdin".to_string()) + } + Ok("Finished".to_string()) +} + +fn sign_transactions(signer: &mut SignerRpc, pwd: String) -> Result { + signer.requests_to_confirm().map(|reqs| { + match reqs { + Ok(reqs) => { + if reqs.len() == 0 { + Ok("No transactions in signing queue".to_string()) + } else { + for r in reqs { + sign_interactive(signer, &pwd, r); + } + Ok("".to_string()) + } + } + Err(err) => { + Err(format!("error: {:?}", err)) + } + } + }).wait().unwrap() +} + +fn list_transactions(signer: &mut SignerRpc) -> Result { + signer.requests_to_confirm().map(|reqs| { + match reqs { + Ok(reqs) => { + let mut s = "Transaction queue:".to_string(); + if reqs.len() == 0 { + s = s + &"No transactions in signing queue"; + } else { + for r in reqs { + s = s + &format!("\n{}", r); + } + } + Ok(s) + } + Err(err) => { + Err(format!("error: {:?}", err)) + } + } + }).wait().unwrap() +} + +fn sign_transaction(signer: &mut SignerRpc, + id: U256, + pwd: &String) -> Result { + signer.confirm_request(id, None, &pwd).map(|res| { + match res { + Ok(u) => Ok(format!("Signed transaction id: {:#x}", u)), + Err(e) => Err(format!("{:?}", e)), + } + }).wait().unwrap() +} + +fn reject_transaction(signer: &mut SignerRpc, + id: U256) -> Result { + signer.reject_request(id).map(|res| { + match res { + Ok(true) => Ok(format!("Rejected transaction id {:#x}", id)), + Ok(false) => Err(format!("No such request")), + Err(e) => Err(format!("{:?}", e)), + } + }).wait().unwrap() +} + +// cmds + +pub fn cmd_signer_list(signerport: u16, + authfile: PathBuf) -> Result { + match SignerRpc::new(&format!("ws://127.0.0.1:{}", signerport), + &authfile) { + Ok(mut signer) => { + list_transactions(&mut signer) + } + Err(e) => Err(format!("{:?}", e)) + } +} + +pub fn cmd_signer_reject(id: usize, + signerport: u16, + authfile: PathBuf) -> Result { + match SignerRpc::new(&format!("ws://127.0.0.1:{}", signerport), + &authfile) { + Ok(mut signer) => { + reject_transaction(&mut signer, U256::from(id)) + }, + Err(e) => Err(format!("{:?}", e)) + } +} + +pub fn cmd_signer_sign(id: Option, + pwfile: Option, + signerport: u16, + authfile: PathBuf) -> Result { + let pwd; + match pwfile { + Some(pwfile) => { + match File::open(pwfile) { + Ok(fd) => { + match BufReader::new(fd).lines().next() { + Some(Ok(line)) => pwd = line, + _ => return Err(format!("No password in file")) + } + }, + Err(e) => return Err(format!("Could not open pwfile: {}", e)) + } + } + None => { + pwd = rpassword::prompt_password_stdout("Password: ").unwrap(); + } + } + + match SignerRpc::new(&format!("ws://127.0.0.1:{}", signerport), + &authfile) { + Ok(mut signer) => { + match id { + Some(id) => { + sign_transaction(&mut signer, U256::from(id), &pwd) + }, + None => { + sign_transactions(&mut signer, pwd) + } + } + } + Err(e) => return Err(format!("{:?}", e)) + } +} diff --git a/rpc_client/Cargo.toml b/rpc_client/Cargo.toml new file mode 100644 index 000000000..24f8b8522 --- /dev/null +++ b/rpc_client/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = ["Ethcore "] +description = "Parity Rpc Client" +homepage = "http://ethcore.io" +license = "GPL-3.0" +name = "parity-rpc-client" +version = "1.4.0" + +[dependencies] +futures = "0.1" +lazy_static = "0.2.1" +matches = "0.1.2" +rand = "0.3.14" +serde = "0.8" +serde_json = "0.8" +tempdir = "0.3.5" +url = "1.2.0" +ws = "0.5.3" + +[dependencies.ethcore-rpc] +path = "../rpc" + +[dependencies.ethcore-signer] +path = "../signer" + +[dependencies.ethcore-util] +path = "../util" diff --git a/rpc_client/src/client.rs b/rpc_client/src/client.rs new file mode 100644 index 000000000..15d83ee18 --- /dev/null +++ b/rpc_client/src/client.rs @@ -0,0 +1,332 @@ +use std::fmt::{Debug, Formatter, Error as FmtError}; +use std::io::{BufReader, BufRead}; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::collections::BTreeMap; +use std::thread; +use std::mem; +use std::time; + +use std::path::PathBuf; +use util::Hashable; +use url::Url; +use std::fs::File; + +use ws::{connect, + Request, + Handler, + Sender, + Handshake, + Error as WsError, + ErrorKind as WsErrorKind, + Message, + Result as WsResult}; + +use serde::Serialize; +use serde::Deserialize; +use serde::ser::Serializer; +use serde_json::{from_str, + to_string, + from_value, + Value as JsonValue, + Error as JsonError}; + +use futures::{BoxFuture, Canceled, Complete, Future, oneshot, done}; + +/// The actual websocket connection handler, passed into the +/// event loop of ws-rs +struct RpcHandler { + pending: Pending, + // Option is used here as + // temporary storage until + // connection is setup + // and the values are moved into + // the new `Rpc` + complete: Option>>, + auth_code: String, + out: Option, +} + +impl RpcHandler { + fn new(out: Sender, + auth_code: String, + complete: Complete>) + -> Self { + RpcHandler { + out: Some(out), + auth_code: auth_code, + pending: Pending::new(), + complete: Some(complete), + } + } +} + +impl Handler for RpcHandler { + fn build_request(&mut self, url: &Url) -> WsResult { + match Request::from_url(url) { + Ok(mut r) => { + let timestamp = time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + let hashed = format!("{}:{}", self.auth_code, timestamp).sha3(); + let proto = format!("{:?}_{}", hashed, timestamp); + r.add_protocol(&proto); + Ok(r) + }, + Err(e) => Err(WsError::new(WsErrorKind::Internal, format!("{}", e))), + } + } + fn on_error(&mut self, err: WsError) { + match mem::replace(&mut self.complete, None) { + Some(c) => c.complete(Err(RpcError::WsError(err))), + None => println!("warning: unexpected error"), + } + } + fn on_open(&mut self, _: Handshake) -> WsResult<()> { + match mem::replace(&mut self.complete, None) { + Some(c) => c.complete(Ok(Rpc { + out: mem::replace(&mut self.out, None).unwrap(), + auth_code: self.auth_code.clone(), + counter: AtomicUsize::new(0), + pending: self.pending.clone(), + })), + // Should not be reachable + None => (), + } + Ok(()) + } + fn on_message(&mut self, msg: Message) -> WsResult<()> { + match parse_response(&msg.to_string()) { + (Some(id), response) => { + match self.pending.remove(id) { + Some(c) => c.complete(response), + None => println!("warning: unexpected id: {}", id), + } + } + (None, response) => println!("warning: error: {:?}, {}", response, msg.to_string()), + } + Ok(()) + } +} + +/// Keeping track of issued requests to be matched up with responses +#[derive(Clone)] +struct Pending(Arc>>>>); + +impl Pending { + fn new() -> Self { + Pending(Arc::new(Mutex::new(BTreeMap::new()))) + } + fn insert(&mut self, k: usize, v: Complete>) { + self.0.lock().unwrap().insert(k, v); + } + fn remove(&mut self, k: usize) -> Option>> { + self.0.lock().unwrap().remove(&k) + } +} + +fn get_authcode(path: &PathBuf) -> Result { + match File::open(path) { + Ok(fd) => match BufReader::new(fd).lines().next() { + Some(Ok(code)) => Ok(code), + _ => Err(RpcError::NoAuthCode), + }, + Err(_) => Err(RpcError::NoAuthCode) + } +} + +/// The handle to the connection +pub struct Rpc { + out: Sender, + counter: AtomicUsize, + pending: Pending, + auth_code: String, +} + +impl Rpc { + /// Blocking, returns a new initialized connection or RpcError + pub fn new(url: &str, authpath: &PathBuf) -> Result { + let rpc = try!(Self::connect(url, authpath).map(|rpc| rpc).wait()); + rpc + } + /// Non-blocking, returns a future + pub fn connect(url: &str, authpath: &PathBuf) + -> BoxFuture, Canceled> { + let (c, p) = oneshot::>(); + match get_authcode(authpath) { + Err(e) => return done(Ok(Err(e))).boxed(), + Ok(code) => { + let url = String::from(url); + thread::spawn(move || { + // mem:replace Option hack to move `c` out + // of the FnMut closure + let mut swap = Some(c); + match connect(url, |out| { + let c = mem::replace(&mut swap, None).unwrap(); + RpcHandler::new(out, code.clone(), c) + }) { + Err(err) => { + let c = mem::replace(&mut swap, None).unwrap(); + c.complete(Err(RpcError::WsError(err))); + }, + // c will complete on the `on_open` event in the Handler + _ => () + } + }); + p.boxed() + } + } + } + /// Non-blocking, returns a future of the request response + pub fn request(&mut self, method: &'static str, params: Vec) + -> BoxFuture, Canceled> + where T: Deserialize + Send + Sized { + + let (c, p) = oneshot::>(); + + let id = self.counter.fetch_add(1, Ordering::Relaxed); + self.pending.insert(id, c); + + let serialized = to_string(&RpcRequest::new(id, method, params)).unwrap(); + let _ = self.out.send(serialized); + + p.map(|result| { + match result { + Ok(json) => { + let t: T = try!(from_value(json)); + Ok(t) + }, + Err(err) => Err(err) + } + }).boxed() + } +} + + +struct RpcRequest { + method: &'static str, + params: Vec, + id: usize, +} + +impl RpcRequest { + fn new(id: usize, method: &'static str, params: Vec) -> Self { + RpcRequest { + method: method, + id: id, + params: params, + } + } +} + +impl Serialize for RpcRequest { + fn serialize(&self, s: &mut S) + -> Result<(), S::Error> + where S: Serializer { + let mut state = try!(s.serialize_struct("RpcRequest" , 3)); + try!(s.serialize_struct_elt(&mut state ,"jsonrpc", "2.0")); + try!(s.serialize_struct_elt(&mut state ,"id" , &self.id)); + try!(s.serialize_struct_elt(&mut state ,"method" , &self.method)); + try!(s.serialize_struct_elt(&mut state ,"params" , &self.params)); + s.serialize_struct_end(state) + } +} + +pub enum RpcError { + WrongVersion(String), + ParseError(JsonError), + MalformedResponse(String), + Remote(String), + WsError(WsError), + Canceled(Canceled), + UnexpectedId, + NoAuthCode, +} + +impl Debug for RpcError { + fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> { + match self { + &RpcError::WrongVersion(ref s) + => write!(f, "Expected version 2.0, got {}", s), + &RpcError::ParseError(ref err) + => write!(f, "ParseError: {}", err), + &RpcError::MalformedResponse(ref s) + => write!(f, "Malformed response: {}", s), + &RpcError::Remote(ref s) + => write!(f, "Remote error: {}", s), + &RpcError::WsError(ref s) + => write!(f, "Websocket error: {}", s), + &RpcError::Canceled(ref s) + => write!(f, "Futures error: {:?}", s), + &RpcError::UnexpectedId + => write!(f, "Unexpected response id"), + &RpcError::NoAuthCode + => write!(f, "No authcodes available"), + } + } +} + +impl From for RpcError { + fn from(err: JsonError) -> RpcError { + RpcError::ParseError(err) + } +} + +impl From for RpcError { + fn from(err: WsError) -> RpcError { + RpcError::WsError(err) + } +} + +impl From for RpcError { + fn from(err: Canceled) -> RpcError { + RpcError::Canceled(err) + } +} + +fn parse_response(s: &str) -> (Option, Result) { + let mut json: JsonValue = match from_str(s) { + Err(e) => return (None, Err(RpcError::ParseError(e))), + Ok(json) => json, + }; + + let obj = match json.as_object_mut() { + Some(o) => o, + None => return + (None, + Err(RpcError::MalformedResponse("Not a JSON object".to_string()))), + }; + + let id; + match obj.get("id") { + Some(&JsonValue::U64(u)) => { + id = u as usize; + }, + _ => return (None, + Err(RpcError::MalformedResponse("Missing id".to_string()))), + } + + match obj.get("jsonrpc") { + Some(&JsonValue::String(ref s)) => { + if *s != "2.0".to_string() { + return (Some(id), + Err(RpcError::WrongVersion(s.clone()))) + } + }, + _ => return + (Some(id), + Err(RpcError::MalformedResponse("Not a jsonrpc object".to_string()))), + } + + match obj.get("error") { + Some(err) => return + (Some(id), + Err(RpcError::Remote(format!("{}", err)))), + None => (), + }; + + match obj.remove("result") { + None => (Some(id), + Err(RpcError::MalformedResponse("No result".to_string()))), + Some(result) => (Some(id), + Ok(result)), + } +} diff --git a/rpc_client/src/lib.rs b/rpc_client/src/lib.rs new file mode 100644 index 000000000..345526828 --- /dev/null +++ b/rpc_client/src/lib.rs @@ -0,0 +1,74 @@ +pub mod client; +pub mod signer; +mod mock; + +extern crate ws; +extern crate ethcore_signer; +extern crate url; +extern crate futures; +extern crate ethcore_util as util; +extern crate ethcore_rpc as rpc; +extern crate serde; +extern crate serde_json; +extern crate rand; +extern crate tempdir; + +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate matches; + +mod test { + use futures::Future; + use url::Url; + use std::path::PathBuf; + + use client::{Rpc, RpcError}; + + use mock; + + #[test] + fn test_connection_refused() { + let (srv, port, tmpdir, _) = mock::serve(); + + let mut path = PathBuf::from(tmpdir.path()); + path.push("authcodes"); + let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port - 1), &path); + + connect.map(|conn| { + assert!(matches!(&conn, &Err(RpcError::WsError(_)))); + }).wait(); + + drop(srv); + } + + #[test] + fn test_authcode_fail() { + let (srv, port, _, _) = mock::serve(); + let path = PathBuf::from("nonexist"); + + let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port), &path); + + connect.map(|conn| { + assert!(matches!(&conn, &Err(RpcError::NoAuthCode))); + }).wait(); + + drop(srv); + } + + #[test] + fn test_authcode_correct() { + let (srv, port, tmpdir, _) = mock::serve(); + + let mut path = PathBuf::from(tmpdir.path()); + path.push("authcodes"); + let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port), &path); + + connect.map(|conn| { + assert!(conn.is_ok()) + }).wait(); + + drop(srv); + } + +} diff --git a/rpc_client/src/mock.rs b/rpc_client/src/mock.rs new file mode 100644 index 000000000..9ac52f605 --- /dev/null +++ b/rpc_client/src/mock.rs @@ -0,0 +1,30 @@ +use ethcore_signer::ServerBuilder; +use ethcore_signer::Server; +use rpc::ConfirmationsQueue; +use std::sync::Arc; +use std::time::{Duration}; +use std::thread; +use rand; +use tempdir::TempDir; +use std::path::PathBuf; +use std::fs::{File, create_dir_all}; +use std::io::Write; + +// mock server +pub fn serve() -> (Server, usize, TempDir, Arc) { + let queue = Arc::new(ConfirmationsQueue::default()); + let dir = TempDir::new("auth").unwrap(); + + let mut authpath = PathBuf::from(dir.path()); + create_dir_all(&authpath).unwrap(); + authpath.push("authcodes"); + let mut authfile = File::create(&authpath).unwrap(); + authfile.write_all(b"zzzRo0IzGi04mzzz\n").unwrap(); + + let builder = ServerBuilder::new(queue.clone(), authpath); + let port = 35000 + rand::random::() % 10000; + let res = builder.start(format!("127.0.0.1:{}", port).parse().unwrap()).unwrap(); + + thread::sleep(Duration::from_millis(25)); + (res, port, dir, queue) +} diff --git a/rpc_client/src/signer.rs b/rpc_client/src/signer.rs new file mode 100644 index 000000000..8ca4724a1 --- /dev/null +++ b/rpc_client/src/signer.rs @@ -0,0 +1,47 @@ + +use client::{Rpc, RpcError}; +use rpc::v1::types::{ConfirmationRequest, + ConfirmationPayload, + TransactionModification, + U256}; +use serde_json::{Value as JsonValue, to_value}; +use std::path::PathBuf; +use futures::{BoxFuture, Canceled}; + +pub struct SignerRpc { + rpc: Rpc, +} + +impl SignerRpc { + pub fn new(url: &str, authfile: &PathBuf) -> Result { + match Rpc::new(&url, authfile) { + Ok(rpc) => Ok(SignerRpc { rpc: rpc }), + Err(e) => Err(e), + } + } + pub fn requests_to_confirm(&mut self) -> + BoxFuture, RpcError>, Canceled> + { + self.rpc.request::> + ("personal_requestsToConfirm", vec![]) + } + pub fn confirm_request(&mut self, + id: U256, + new_gas_price: Option, + pwd: &str) -> + BoxFuture, Canceled> + { + self.rpc.request::("personal_confirmRequest", vec![ + to_value(&format!("{:#x}", id)), + to_value(&TransactionModification { gas_price: new_gas_price }), + to_value(&pwd), + ]) + } + pub fn reject_request(&mut self, id: U256) -> + BoxFuture, Canceled> + { + self.rpc.request::("personal_rejectRequest", vec![ + JsonValue::String(format!("{:#x}", id)) + ]) + } +}