diff --git a/Cargo.lock b/Cargo.lock index 3767f38f2..e8bf3f0b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -859,6 +859,7 @@ dependencies = [ "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-retry 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/api/src/owner.rs b/api/src/owner.rs index bc6d96617..5a5e8fa9a 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use uuid::Uuid; use crate::core::core::Transaction; -use crate::impls::{HTTPWalletCommAdapter, KeybaseWalletCommAdapter}; +use crate::impls::create_sender; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::api_impl::owner; use crate::libwallet::{ @@ -498,21 +498,20 @@ where // Helper functionality. If send arguments exist, attempt to send match send_args { Some(sa) => { + //TODO: in case of keybase, the response might take 60s and leave the service hanging match sa.method.as_ref() { - "http" => { - slate = HTTPWalletCommAdapter::new().send_tx_sync(&sa.dest, &slate)? - } - "keybase" => { - //TODO: in case of keybase, the response might take 60s and leave the service hanging - slate = KeybaseWalletCommAdapter::new().send_tx_sync(&sa.dest, &slate)?; - } + "http" | "keybase" => {} _ => { error!("unsupported payment method: {}", sa.method); return Err(ErrorKind::ClientCallback( "unsupported payment method".to_owned(), - ))?; + ) + .into()); } - } + }; + let comm_adapter = create_sender(&sa.method, &sa.dest) + .map_err(|e| ErrorKind::GenericError(format!("{}", e)))?; + slate = comm_adapter.send_tx(&slate)?; self.tx_lock_outputs(&slate, 0)?; let slate = match sa.finalize { true => self.finalize_tx(&slate)?, diff --git a/controller/src/command.rs b/controller/src/command.rs index 9a13731c0..994934e17 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -12,31 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::util::{Mutex, ZeroingString}; -use std::collections::HashMap; -/// Grin wallet command-line function implementations -use std::fs::File; -use std::io::Write; -use std::sync::Arc; -use std::thread; -use std::time::Duration; - -use serde_json as json; -use uuid::Uuid; +//! Grin wallet command-line function implementations use crate::api::TLSConfig; -use crate::core::core; -use crate::keychain; - use crate::config::WalletConfig; +use crate::core::core; use crate::error::{Error, ErrorKind}; use crate::impls::{ - instantiate_wallet, FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, - LMDBBackend, NullWalletCommAdapter, + create_sender, HTTPNodeClient, KeybaseAllChannels, SlateGetter as _, SlateReceiver as _, + WalletSeed, }; -use crate::impls::{HTTPNodeClient, WalletSeed}; +use crate::impls::{instantiate_wallet, LMDBBackend, PathToSlate, SlatePutter}; +use crate::keychain; use crate::libwallet::{InitTxArgs, IssueInvoiceTxArgs, NodeClient, WalletInst}; +use crate::util::{Mutex, ZeroingString}; use crate::{controller, display}; +use serde_json as json; +use std::fs::File; +use std::io::Write; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use uuid::Uuid; /// Arguments common to all wallet commands #[derive(Clone)] @@ -116,12 +113,6 @@ pub struct ListenArgs { } pub fn listen(config: &WalletConfig, args: &ListenArgs, g_args: &GlobalArgs) -> Result<(), Error> { - let mut params = HashMap::new(); - params.insert("api_listen_addr".to_owned(), config.api_listen_addr()); - if let Some(t) = g_args.tls_conf.as_ref() { - params.insert("certificate".to_owned(), t.certificate.clone()); - params.insert("private_key".to_owned(), t.private_key.clone()); - } let res = match args.method.as_str() { "http" => { // HTTP adapter can't use the listen trait method because of the @@ -136,28 +127,24 @@ pub fn listen(config: &WalletConfig, args: &ListenArgs, g_args: &GlobalArgs) -> &g_args.password.clone().unwrap(), &g_args.account, )?; - let listen_addr = params.get("api_listen_addr").unwrap(); - let tls_conf = match params.get("certificate") { - Some(s) => Some(TLSConfig::new( - s.to_owned(), - params.get("private_key").unwrap().to_owned(), - )), - None => None, - }; - controller::foreign_listener(wallet.clone(), &listen_addr, tls_conf)?; - Ok(()) - } - "keybase" => { - let adapter = KeybaseWalletCommAdapter::new(); - adapter.listen( - params, - config.clone(), - &g_args.password.clone().unwrap(), - &g_args.account, - g_args.node_api_secret.clone(), + controller::foreign_listener( + wallet.clone(), + &config.api_listen_addr(), + g_args.tls_conf.clone(), ) } - _ => Ok(()), + "keybase" => KeybaseAllChannels::new()?.listen( + config.clone(), + &g_args.password.clone().unwrap(), + &g_args.account, + g_args.node_api_secret.clone(), + ), + method => { + return Err(ErrorKind::ArgumentError(format!( + "No listener for method \"{}\".", + method + )).into()); + } }; if let Err(e) = res { @@ -291,42 +278,41 @@ pub fn send( return Err(e); } }; - let adapter = match args.method.as_str() { - "http" => HTTPWalletCommAdapter::new(), - "file" => FileWalletCommAdapter::new(), - "keybase" => KeybaseWalletCommAdapter::new(), - "self" => NullWalletCommAdapter::new(), - _ => NullWalletCommAdapter::new(), - }; - if adapter.supports_sync() { - slate = adapter.send_tx_sync(&args.dest, &slate)?; - api.tx_lock_outputs(&slate, 0)?; - if args.method == "self" { + + match args.method.as_str() { + "file" => { + PathToSlate((&args.dest).into()).put_tx(&slate)?; + api.tx_lock_outputs(&slate, 0)?; + return Ok(()); + } + "self" => { + api.tx_lock_outputs(&slate, 0)?; controller::foreign_single_use(wallet, |api| { slate = api.receive_tx(&slate, Some(&args.dest), None)?; Ok(()) })?; } - if let Err(e) = api.verify_slate_messages(&slate) { - error!("Error validating participant messages: {}", e); - return Err(e); + method => { + let sender = create_sender(method, &args.dest)?; + slate = sender.send_tx(&slate)?; + api.tx_lock_outputs(&slate, 0)?; } - slate = api.finalize_tx(&slate)?; - } else { - adapter.send_tx_async(&args.dest, &slate)?; - api.tx_lock_outputs(&slate, 0)?; } - if adapter.supports_sync() { - let result = api.post_tx(&slate.tx, args.fluff); - match result { - Ok(_) => { - info!("Tx sent ok",); - return Ok(()); - } - Err(e) => { - error!("Tx sent fail: {}", e); - return Err(e); - } + + api.verify_slate_messages(&slate).map_err(|e| { + error!("Error validating participant messages: {}", e); + e + })?; + slate = api.finalize_tx(&slate)?; + let result = api.post_tx(&slate.tx, args.fluff); + match result { + Ok(_) => { + info!("Tx sent ok",); + return Ok(()); + } + Err(e) => { + error!("Tx sent fail: {}", e); + return Err(e); } } } @@ -346,8 +332,7 @@ pub fn receive( g_args: &GlobalArgs, args: ReceiveArgs, ) -> Result<(), Error> { - let adapter = FileWalletCommAdapter::new(); - let mut slate = adapter.receive_tx_async(&args.input)?; + let mut slate = PathToSlate((&args.input).into()).get_tx()?; controller::foreign_single_use(wallet, |api| { if let Err(e) = api.verify_slate_messages(&slate) { error!("Error validating participant messages: {}", e); @@ -356,8 +341,7 @@ pub fn receive( slate = api.receive_tx(&slate, Some(&g_args.account), args.message.clone())?; Ok(()) })?; - let send_tx = format!("{}.response", args.input); - adapter.send_tx_async(&send_tx, &slate)?; + PathToSlate(format!("{}.response", args.input).into()).put_tx(&slate)?; info!( "Response file {}.response generated, and can be sent back to the transaction originator.", args.input @@ -375,8 +359,8 @@ pub fn finalize( wallet: Arc>>, args: FinalizeArgs, ) -> Result<(), Error> { - let adapter = FileWalletCommAdapter::new(); - let mut slate = adapter.receive_tx_async(&args.input)?; + let mut slate = PathToSlate((&args.input).into()).get_tx()?; + // Rather than duplicating the entire command, we'll just // try to determine what kind of finalization this is // based on the slate contents @@ -472,8 +456,7 @@ pub fn process_invoice( args: ProcessInvoiceArgs, dark_scheme: bool, ) -> Result<(), Error> { - let adapter = FileWalletCommAdapter::new(); - let slate = adapter.receive_tx_async(&args.input)?; + let slate = PathToSlate((&args.input).into()).get_tx()?; controller::owner_single_use(wallet.clone(), |api| { if args.estimate_selection_strategies { let strategies = vec!["smallest", "all"] @@ -526,24 +509,25 @@ pub fn process_invoice( return Err(e); } }; - let adapter = match args.method.as_str() { - "http" => HTTPWalletCommAdapter::new(), - "file" => FileWalletCommAdapter::new(), - "self" => NullWalletCommAdapter::new(), - _ => NullWalletCommAdapter::new(), - }; - if adapter.supports_sync() { - slate = adapter.send_tx_sync(&args.dest, &slate)?; - api.tx_lock_outputs(&slate, 0)?; - if args.method == "self" { + + match args.method.as_str() { + "file" => { + let slate_putter = PathToSlate((&args.dest).into()); + slate_putter.put_tx(&slate)?; + api.tx_lock_outputs(&slate, 0)?; + } + "self" => { + api.tx_lock_outputs(&slate, 0)?; controller::foreign_single_use(wallet, |api| { slate = api.finalize_invoice_tx(&slate)?; Ok(()) })?; } - } else { - adapter.send_tx_async(&args.dest, &slate)?; - api.tx_lock_outputs(&slate, 0)?; + method => { + let sender = create_sender(method, &args.dest)?; + slate = sender.send_tx(&slate)?; + api.tx_lock_outputs(&slate, 0)?; + } } } Ok(()) diff --git a/controller/tests/check.rs b/controller/tests/check.rs index cd17ad942..9996ca20c 100644 --- a/controller/tests/check.rs +++ b/controller/tests/check.rs @@ -27,7 +27,7 @@ use self::core::global::ChainTypes; use self::keychain::ExtKeychain; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient, WalletProxy}; -use impls::FileWalletCommAdapter; +use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; use libwallet::{InitTxArgs, WalletInst}; use std::fs; use std::thread; @@ -189,9 +189,8 @@ fn check_repair_impl(test_dir: &str) -> Result<(), libwallet::Error> { }; let mut slate = api.init_send_tx(args)?; // output tx file - let file_adapter = FileWalletCommAdapter::new(); let send_file = format!("{}/part_tx_1.tx", test_dir); - file_adapter.send_tx_async(&send_file, &mut slate)?; + PathToSlate(send_file.into()).put_tx(&slate)?; api.tx_lock_outputs(&slate, 0)?; Ok(()) })?; diff --git a/controller/tests/file.rs b/controller/tests/file.rs index 0ac743814..28a7b81ff 100644 --- a/controller/tests/file.rs +++ b/controller/tests/file.rs @@ -26,7 +26,7 @@ use self::core::global::ChainTypes; use self::keychain::ExtKeychain; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient, WalletProxy}; -use impls::FileWalletCommAdapter; +use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; use std::fs; use std::thread; use std::time::Duration; @@ -119,8 +119,7 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { }; let mut slate = api.init_send_tx(args)?; // output tx file - let file_adapter = FileWalletCommAdapter::new(); - file_adapter.send_tx_async(&send_file, &mut slate)?; + PathToSlate((&send_file).into()).put_tx(&mut slate)?; api.tx_lock_outputs(&slate, 0)?; Ok(()) })?; @@ -131,8 +130,7 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { w.set_parent_key_id_by_name("account1")?; } - let adapter = FileWalletCommAdapter::new(); - let mut slate = adapter.receive_tx_async(&send_file)?; + let mut slate = PathToSlate((&send_file).into()).get_tx()?; let mut naughty_slate = slate.clone(); naughty_slate.participant_data[0].message = Some("I changed the message".to_owned()); @@ -148,14 +146,13 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { // wallet 2 receives file, completes, sends file back wallet::controller::foreign_single_use(wallet2.clone(), |api| { slate = api.receive_tx(&slate, None, Some(sender2_message.clone()))?; - adapter.send_tx_async(&receive_file, &mut slate)?; + PathToSlate((&receive_file).into()).put_tx(&slate)?; Ok(()) })?; // wallet 1 finalises and posts wallet::controller::owner_single_use(wallet1.clone(), |api| { - let adapter = FileWalletCommAdapter::new(); - let mut slate = adapter.receive_tx_async(&receive_file)?; + let mut slate = PathToSlate(receive_file.into()).get_tx()?; api.verify_slate_messages(&slate)?; slate = api.finalize_tx(&slate)?; api.post_tx(&slate.tx, false)?; diff --git a/controller/tests/repost.rs b/controller/tests/repost.rs index 856dc472a..167d4d0fa 100644 --- a/controller/tests/repost.rs +++ b/controller/tests/repost.rs @@ -27,7 +27,7 @@ use self::core::global::ChainTypes; use self::keychain::ExtKeychain; use self::libwallet::{InitTxArgs, Slate}; use impls::test_framework::{self, LocalWalletClient, WalletProxy}; -use impls::FileWalletCommAdapter; +use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; use std::fs; use std::thread; use std::time::Duration; @@ -112,10 +112,8 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { selection_strategy_is_use_all: true, ..Default::default() }; - let mut slate = api.init_send_tx(args)?; - // output tx file - let file_adapter = FileWalletCommAdapter::new(); - file_adapter.send_tx_async(&send_file, &mut slate)?; + let slate = api.init_send_tx(args)?; + PathToSlate((&send_file).into()).put_tx(&slate)?; api.tx_lock_outputs(&slate, 0)?; Ok(()) })?; @@ -130,10 +128,9 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { } wallet::controller::foreign_single_use(wallet1.clone(), |api| { - let adapter = FileWalletCommAdapter::new(); - slate = adapter.receive_tx_async(&send_file)?; + slate = PathToSlate((&send_file).into()).get_tx()?; slate = api.receive_tx(&slate, None, None)?; - adapter.send_tx_async(&receive_file, &mut slate)?; + PathToSlate((&receive_file).into()).put_tx(&slate)?; Ok(()) })?; @@ -145,8 +142,7 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { // wallet 1 finalize wallet::controller::owner_single_use(wallet1.clone(), |api| { - let adapter = FileWalletCommAdapter::new(); - slate = adapter.receive_tx_async(&receive_file)?; + slate = PathToSlate((&receive_file).into()).get_tx()?; slate = api.finalize_tx(&slate)?; Ok(()) })?; diff --git a/impls/Cargo.toml b/impls/Cargo.toml index c6cf7a5a7..1d431c69e 100644 --- a/impls/Cargo.toml +++ b/impls/Cargo.toml @@ -30,3 +30,4 @@ grin_wallet_util = { path = "../util", version = "2.0.1-beta.1" } grin_wallet_libwallet = { path = "../libwallet", version = "2.0.1-beta.1" } grin_wallet_config = { path = "../config", version = "2.0.1-beta.1" } +url = "1.7.2" diff --git a/impls/src/adapters/file.rs b/impls/src/adapters/file.rs index d63024f0c..04a9031b5 100644 --- a/impls/src/adapters/file.rs +++ b/impls/src/adapters/file.rs @@ -16,32 +16,16 @@ use std::fs::File; use std::io::{Read, Write}; -use crate::config::WalletConfig; use crate::libwallet::{Error, ErrorKind, Slate}; -use crate::WalletCommAdapter; -use std::collections::HashMap; +use crate::{SlateGetter, SlatePutter}; +use std::path::PathBuf; #[derive(Clone)] -pub struct FileWalletCommAdapter {} +pub struct PathToSlate(pub PathBuf); -impl FileWalletCommAdapter { - /// Create - pub fn new() -> Box { - Box::new(FileWalletCommAdapter {}) - } -} - -impl WalletCommAdapter for FileWalletCommAdapter { - fn supports_sync(&self) -> bool { - false - } - - fn send_tx_sync(&self, _dest: &str, _slate: &Slate) -> Result { - unimplemented!(); - } - - fn send_tx_async(&self, dest: &str, slate: &Slate) -> Result<(), Error> { - let mut pub_tx = File::create(dest)?; +impl SlatePutter for PathToSlate { + fn put_tx(&self, slate: &Slate) -> Result<(), Error> { + let mut pub_tx = File::create(&self.0)?; pub_tx.write_all( serde_json::to_string(slate) .map_err(|_| ErrorKind::SlateSer)? @@ -50,22 +34,13 @@ impl WalletCommAdapter for FileWalletCommAdapter { pub_tx.sync_all()?; Ok(()) } +} - fn receive_tx_async(&self, params: &str) -> Result { - let mut pub_tx_f = File::open(params)?; +impl SlateGetter for PathToSlate { + fn get_tx(&self) -> Result { + let mut pub_tx_f = File::open(&self.0)?; let mut content = String::new(); pub_tx_f.read_to_string(&mut content)?; Ok(Slate::deserialize_upgrade(&content)?) } - - fn listen( - &self, - _params: HashMap, - _config: WalletConfig, - _passphrase: &str, - _account: &str, - _node_api_secret: Option, - ) -> Result<(), Error> { - unimplemented!(); - } } diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/http.rs index fbefcc89a..327990732 100644 --- a/impls/src/adapters/http.rs +++ b/impls/src/adapters/http.rs @@ -15,23 +15,28 @@ /// HTTP Wallet 'plugin' implementation use crate::api; use crate::libwallet::{Error, ErrorKind, Slate}; -use crate::WalletCommAdapter; -use config::WalletConfig; +use crate::SlateSender; use serde::Serialize; use serde_json::{json, Value}; -use std::collections::HashMap; +use url::Url; #[derive(Clone)] -pub struct HTTPWalletCommAdapter {} +pub struct HttpSlateSender { + base_url: Url, +} -impl HTTPWalletCommAdapter { - /// Create - pub fn new() -> Box { - Box::new(HTTPWalletCommAdapter {}) +impl HttpSlateSender { + /// Create, return Err if scheme is not "http" + pub fn new(base_url: Url) -> Result { + if base_url.scheme() != "http" { + Err(SchemeNotHttp) + } else { + Ok(HttpSlateSender { base_url }) + } } - /// Check version of the other wallet - fn check_other_version(&self, url: &str) -> Result<(), Error> { + /// Check version of the listening wallet + fn check_other_version(&self) -> Result<(), Error> { let req = json!({ "jsonrpc": "2.0", "method": "check_version", @@ -39,7 +44,7 @@ impl HTTPWalletCommAdapter { "params": [] }); - let res: String = post(url, None, &req).map_err(|e| { + let res: String = post(&self.base_url, None, &req).map_err(|e| { let mut report = format!("Performing version check (is recipient listening?): {}", e); let err_string = format!("{}", e); if err_string.contains("404") { @@ -88,24 +93,15 @@ impl HTTPWalletCommAdapter { } } -impl WalletCommAdapter for HTTPWalletCommAdapter { - fn supports_sync(&self) -> bool { - true - } - - fn send_tx_sync(&self, dest: &str, slate: &Slate) -> Result { - if &dest[..4] != "http" { - let err_str = format!( - "dest formatted as {} but send -d expected stdout or http://IP:port", - dest - ); - error!("{}", err_str,); - Err(ErrorKind::Uri)? - } - let url = format!("{}/v2/foreign", dest); +impl SlateSender for HttpSlateSender { + fn send_tx(&self, slate: &Slate) -> Result { + let url: Url = self + .base_url + .join("/v2/foreign") + .expect("/v2/foreign is an invalid url path"); debug!("Posting transaction slate to {}", url); - self.check_other_version(&url)?; + self.check_other_version()?; // Note: not using easy-jsonrpc as don't want the dependencies in this crate let req = json!({ @@ -120,7 +116,7 @@ impl WalletCommAdapter for HTTPWalletCommAdapter { }); trace!("Sending receive_tx request: {}", req); - let res: String = post(url.as_str(), None, &req).map_err(|e| { + let res: String = post(&url, None, &req).map_err(|e| { let report = format!("Posting transaction slate (is recipient listening?): {}", e); error!("{}", report); ErrorKind::ClientCallback(report) @@ -144,32 +140,24 @@ impl WalletCommAdapter for HTTPWalletCommAdapter { Ok(slate) } +} - fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), Error> { - unimplemented!(); - } - - fn receive_tx_async(&self, _params: &str) -> Result { - unimplemented!(); - } +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct SchemeNotHttp; - fn listen( - &self, - _params: HashMap, - _config: WalletConfig, - _passphrase: &str, - _account: &str, - _node_api_secret: Option, - ) -> Result<(), Error> { - unimplemented!(); +impl Into for SchemeNotHttp { + fn into(self) -> Error { + let err_str = format!("url scheme must be http",); + ErrorKind::GenericError(err_str).into() } } -pub fn post(url: &str, api_secret: Option, input: &IN) -> Result +pub fn post(url: &Url, api_secret: Option, input: &IN) -> Result where IN: Serialize, { - let req = api::client::create_post_request(url, api_secret, input)?; + // TODO: change create_post_request to accept a url instead of a &str + let req = api::client::create_post_request(url.as_str(), api_secret, input)?; let res = api::client::send_request(req)?; Ok(res) } diff --git a/impls/src/adapters/keybase.rs b/impls/src/adapters/keybase.rs index 79cf25bfe..25f357d4a 100644 --- a/impls/src/adapters/keybase.rs +++ b/impls/src/adapters/keybase.rs @@ -14,10 +14,11 @@ // Keybase Wallet Plugin +use crate::adapters::{SlateReceiver, SlateSender}; use crate::config::WalletConfig; use crate::libwallet::api_impl::foreign; use crate::libwallet::{Error, ErrorKind, Slate}; -use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter}; +use crate::{instantiate_wallet, HTTPNodeClient}; use failure::ResultExt; use serde::Serialize; use serde_json::{from_str, json, to_string, Value}; @@ -36,25 +37,40 @@ const SLATE_NEW: &str = "grin_slate_new"; const SLATE_SIGNED: &str = "grin_slate_signed"; #[derive(Clone)] -pub struct KeybaseWalletCommAdapter {} +pub struct KeybaseChannel(String); -impl KeybaseWalletCommAdapter { +impl KeybaseChannel { /// Check if keybase is installed and return an adapter object. - pub fn new() -> Box { - let mut proc = if cfg!(target_os = "windows") { - Command::new("where") - } else { - Command::new("which") - }; - proc.arg("keybase") - .stdout(Stdio::null()) - .status() - .expect("Keybase executable not found, make sure it is installed and in your PATH"); - - Box::new(KeybaseWalletCommAdapter {}) + pub fn new(channel: String) -> Result { + // Limit only one recipient + if channel.matches(",").count() > 0 { + return Err( + ErrorKind::GenericError("Only one recipient is supported!".to_owned()).into(), + ); + } + + if !keybase_installed() { + return Err(ErrorKind::GenericError( + "Keybase executable not found, make sure it is installed and in your PATH" + .to_owned(), + ) + .into()); + } + + Ok(KeybaseChannel(channel)) } } +/// Check if keybase executable exists in path +fn keybase_installed() -> bool { + let mut proc = if cfg!(target_os = "windows") { + Command::new("where") + } else { + Command::new("which") + }; + proc.arg("keybase").stdout(Stdio::null()).status().is_ok() +} + /// Send a json object to the keybase process. Type `keybase chat api --help` for a list of available methods. fn api_send(payload: &str) -> Result { let mut proc = Command::new("keybase"); @@ -280,23 +296,13 @@ fn poll(nseconds: u64, channel: &str) -> Option { None } -impl WalletCommAdapter for KeybaseWalletCommAdapter { - fn supports_sync(&self) -> bool { - true - } - - // Send a slate to a keybase username then wait for a response for TTL seconds. - fn send_tx_sync(&self, addr: &str, slate: &Slate) -> Result { - // Limit only one recipient - if addr.matches(",").count() > 0 { - error!("Only one recipient is supported!"); - return Err(ErrorKind::GenericError("Tx rejected".to_owned()))?; - } - +impl SlateSender for KeybaseChannel { + /// Send a slate to a keybase username then wait for a response for TTL seconds. + fn send_tx(&self, slate: &Slate) -> Result { let id = slate.id; // Send original slate to recipient with the SLATE_NEW topic - match send(&slate, addr, SLATE_NEW, TTL) { + match send(&slate, &self.0, SLATE_NEW, TTL) { true => (), false => { return Err(ErrorKind::ClientCallback( @@ -304,9 +310,9 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter { ))?; } } - info!("tx request has been sent to @{}, tx uuid: {}", addr, id); + info!("tx request has been sent to @{}, tx uuid: {}", &self.0, id); // Wait for response from recipient with SLATE_SIGNED topic - match poll(TTL as u64, addr) { + match poll(TTL as u64, &self.0) { Some(slate) => return Ok(slate), None => { return Err(ErrorKind::ClientCallback( @@ -315,22 +321,33 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter { } } } +} - /// Send a transaction asynchronously (result will be returned via the listener) - fn send_tx_async(&self, _addr: &str, _slate: &Slate) -> Result<(), Error> { - unimplemented!(); - } +/// Receives slates on all channels with topic SLATE_NEW +pub struct KeybaseAllChannels { + _priv: (), // makes KeybaseAllChannels unconstructable without checking for existence of keybase executable +} - /// Receive a transaction async. (Actually just read it from wherever and return the slate) - fn receive_tx_async(&self, _params: &str) -> Result { - unimplemented!(); +impl KeybaseAllChannels { + /// Create a KeybaseAllChannels, return error if keybase executable is not present + pub fn new() -> Result { + if !keybase_installed() { + Err(ErrorKind::GenericError( + "Keybase executable not found, make sure it is installed and in your PATH" + .to_owned(), + ) + .into()) + } else { + Ok(KeybaseAllChannels { _priv: () }) + } } +} +impl SlateReceiver for KeybaseAllChannels { /// Start a listener, passing received messages to the wallet api directly #[allow(unreachable_code)] fn listen( &self, - _params: HashMap, config: WalletConfig, passphrase: &str, account: &str, diff --git a/impls/src/adapters/mod.rs b/impls/src/adapters/mod.rs index fa4ce0fd7..50655c8b5 100644 --- a/impls/src/adapters/mod.rs +++ b/impls/src/adapters/mod.rs @@ -15,41 +15,82 @@ mod file; mod http; mod keybase; -mod null; +mod server_auth; -pub use self::file::FileWalletCommAdapter; -pub use self::http::HTTPWalletCommAdapter; -pub use self::keybase::KeybaseWalletCommAdapter; -pub use self::null::NullWalletCommAdapter; +pub use self::file::PathToSlate; +pub use self::http::HttpSlateSender; +pub use self::keybase::{KeybaseAllChannels, KeybaseChannel}; +pub use self::server_auth::AuthenticatedHttpsSlateSender; use crate::config::WalletConfig; -use crate::libwallet::{Error, Slate}; -use std::collections::HashMap; - -/// Encapsulate wallet to wallet communication functions -pub trait WalletCommAdapter { - /// Whether this adapter supports sync mode - fn supports_sync(&self) -> bool; +use crate::libwallet::{Error, ErrorKind, Slate}; +/// Sends transactions to a corresponding SlateReceiver +pub trait SlateSender { /// Send a transaction slate to another listening wallet and return result /// TODO: Probably need a slate wrapper type - fn send_tx_sync(&self, addr: &str, slate: &Slate) -> Result; - - /// Send a transaction asynchronously (result will be returned via the listener) - fn send_tx_async(&self, addr: &str, slate: &Slate) -> Result<(), Error>; - - /// Receive a transaction async. (Actually just read it from wherever and return the slate) - fn receive_tx_async(&self, params: &str) -> Result; + fn send_tx(&self, slate: &Slate) -> Result; +} +pub trait SlateReceiver { /// Start a listener, passing received messages to the wallet api directly /// Takes a wallet config for now to avoid needing all sorts of awkward /// type parameters on this trait fn listen( &self, - params: HashMap, config: WalletConfig, passphrase: &str, account: &str, node_api_secret: Option, ) -> Result<(), Error>; } + +/// Posts slates to be read later by a corresponding getter +pub trait SlatePutter { + /// Send a transaction asynchronously + fn put_tx(&self, slate: &Slate) -> Result<(), Error>; +} + +/// Checks for a transaction from a corresponding SlatePutter, returns the transaction if it exists +pub trait SlateGetter { + /// Receive a transaction async. (Actually just read it from wherever and return the slate) + fn get_tx(&self) -> Result; +} + +/// select a SlateSender based on method and dest fields from, e.g., SendArgs +pub fn create_sender(method: &str, dest: &str) -> Result, Error> { + use url::Url; + + let invalid = || { + ErrorKind::WalletComms(format!( + "Invalid wallet comm type and destination. method: {}, dest: {}", + method, dest + )) + }; + Ok(match method { + "http" => { + let url: Url = dest.parse().map_err(|_| invalid())?; + Box::new(HttpSlateSender::new(url).map_err(|_| invalid())?) + } + "keybase" => Box::new(KeybaseChannel::new(dest.to_owned())?), + "self" => { + return Err(ErrorKind::WalletComms( + "No sender implementation for \"self\".".to_string(), + ) + .into()); + } + "file" => { + return Err(ErrorKind::WalletComms( + "File based transactions must be performed asynchronously.".to_string(), + ) + .into()); + } + _ => { + return Err(ErrorKind::WalletComms(format!( + "Wallet comm method \"{}\" does not exist.", + method + )) + .into()); + } + }) +} diff --git a/impls/src/adapters/null.rs b/impls/src/adapters/null.rs deleted file mode 100644 index 376133c07..000000000 --- a/impls/src/adapters/null.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2018 The Grin Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::config::WalletConfig; -/// Null Output 'plugin' implementation -use crate::libwallet::{Error, Slate}; -use crate::WalletCommAdapter; - -use std::collections::HashMap; - -#[derive(Clone)] -pub struct NullWalletCommAdapter {} - -impl NullWalletCommAdapter { - /// Create - pub fn new() -> Box { - Box::new(NullWalletCommAdapter {}) - } -} - -impl WalletCommAdapter for NullWalletCommAdapter { - fn supports_sync(&self) -> bool { - true - } - - fn send_tx_sync(&self, _dest: &str, slate: &Slate) -> Result { - Ok(slate.clone()) - } - - fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), Error> { - Ok(()) - } - - fn receive_tx_async(&self, _params: &str) -> Result { - unimplemented!(); - } - - fn listen( - &self, - _params: HashMap, - _config: WalletConfig, - _passphrase: &str, - _account: &str, - _node_api_secret: Option, - ) -> Result<(), Error> { - unimplemented!(); - } -} diff --git a/impls/src/adapters/server_auth.rs b/impls/src/adapters/server_auth.rs new file mode 100644 index 000000000..9195fa66c --- /dev/null +++ b/impls/src/adapters/server_auth.rs @@ -0,0 +1,85 @@ +//! Https SlateSender that checks to make sure the public key presented by the remote server +//! matches an expected public key. Provides server authentication and encryption without +//! relying on Public Key Infastructure. + +// use crate::foreign_rpc_client; +use crate::libwallet::{Error, ErrorKind, Slate}; +use crate::SlateSender; +use serde_json::{json, Value}; +use url::Url; + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct PublicKey(pub [u8; 32]); + +/// Address to and public key of server running the V2 jsonrpc foreign api on https +pub struct AuthenticatedHttpsSlateSender { + remote_foreign_url: Url, + remote_pk: PublicKey, +} + +impl AuthenticatedHttpsSlateSender { + /// Create, return Error if url scheme is not https + fn new(url: Url, public_key: PublicKey) -> Result { + if url.scheme() == "https" { + Ok(AuthenticatedHttpsSlateSender { + remote_foreign_url: url + .join("/v2/foreign") + .expect("/v2/foreign is an invalid url path"), + remote_pk: public_key, + }) + } else { + Err(NotHttps) + } + } + + fn post_authenticated(&self, body: &Value) -> Result { + // create TLSConfig with custom cert validation logic. That custom logic should only + // assert the cert is valid if the servers public key == self.remote_pk + // check out https://github.com/bddap/tryquinn for a starting point + + // make an https post to server using said TLSConfig + } +} + +impl SlateSender for AuthenticatedHttpsSlateSender { + fn send_tx(&self, slate: &Slate) -> Result { + // create jsonrpc request maually + // not using generated client helpers as adding grin_wallet_api to impls/Cargo.toml creates a + // dependency cycle + let req = json!({ + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [slate, null, null] + }); + + // send post request to self.remote_foreign_url using custom tls config + let res = self.post_authenticated(&req)?; + + // parse and return result + let res: Value = serde_json::from_str(&res).map_err(|e| ErrorKind::SlateDeser)?; + if res["error"] != json!(null) { + Err(ErrorKind::ClientCallback(format!( + "Posting transaction slate: Error: {}, Message: {}", + res["error"]["code"], res["error"]["message"] + )) + .into()) + } else { + Slate::deserialize_upgrade( + &serde_json::to_string(&res["result"]["Ok"]) + .expect("error serializing json value to string"), + ) + .map_err(|_| ErrorKind::SlateDeser.into()) + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct NotHttps; + +impl Into for NotHttps { + fn into(self) -> Error { + let err_str = format!("url scheme must be https",); + ErrorKind::GenericError(err_str).into() + } +} diff --git a/impls/src/lib.rs b/impls/src/lib.rs index ee846562a..2c68681dd 100644 --- a/impls/src/lib.rs +++ b/impls/src/lib.rs @@ -39,8 +39,8 @@ mod seed; pub mod test_framework; pub use crate::adapters::{ - FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, NullWalletCommAdapter, - WalletCommAdapter, + create_sender, HttpSlateSender, KeybaseAllChannels, KeybaseChannel, PathToSlate, SlateGetter, + SlatePutter, SlateReceiver, SlateSender, }; pub use crate::backends::{wallet_db_exists, LMDBBackend}; pub use crate::error::{Error, ErrorKind}; diff --git a/impls/src/test_framework/testclient.rs b/impls/src/test_framework/testclient.rs index d23f139bb..5f496a0de 100644 --- a/impls/src/test_framework/testclient.rs +++ b/impls/src/test_framework/testclient.rs @@ -19,19 +19,18 @@ use crate::api; use crate::chain::types::NoopAdapter; use crate::chain::Chain; -use crate::config::WalletConfig; use crate::core::core::verifier_cache::LruVerifierCache; use crate::core::core::Transaction; use crate::core::global::{set_mining_mode, ChainTypes}; use crate::core::{pow, ser}; use crate::keychain::Keychain; +use crate::libwallet; use crate::libwallet::api_impl::foreign; use crate::libwallet::{NodeClient, NodeVersionInfo, Slate, TxWrapper, WalletInst}; use crate::util; use crate::util::secp::pedersen; use crate::util::secp::pedersen::Commitment; use crate::util::{Mutex, RwLock}; -use crate::{libwallet, WalletCommAdapter}; use failure::ResultExt; use serde_json; use std::collections::HashMap; @@ -344,55 +343,6 @@ impl LocalWalletClient { } } -impl WalletCommAdapter for LocalWalletClient { - fn supports_sync(&self) -> bool { - true - } - - /// Send the slate to a listening wallet instance - fn send_tx_sync(&self, dest: &str, slate: &Slate) -> Result { - let m = WalletProxyMessage { - sender_id: self.id.clone(), - dest: dest.to_owned(), - method: "send_tx_slate".to_owned(), - body: serde_json::to_string(slate).unwrap(), - }; - { - let p = self.proxy_tx.lock(); - p.send(m).context(libwallet::ErrorKind::ClientCallback( - "Send TX Slate".to_owned(), - ))?; - } - let r = self.rx.lock(); - let m = r.recv().unwrap(); - trace!("Received send_tx_slate response: {:?}", m.clone()); - Ok( - serde_json::from_str(&m.body).context(libwallet::ErrorKind::ClientCallback( - "Parsing send_tx_slate response".to_owned(), - ))?, - ) - } - - fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), libwallet::Error> { - unimplemented!(); - } - - fn receive_tx_async(&self, _params: &str) -> Result { - unimplemented!(); - } - - fn listen( - &self, - _params: HashMap, - _config: WalletConfig, - _passphrase: &str, - _account: &str, - _node_api_secret: Option, - ) -> Result<(), libwallet::Error> { - unimplemented!(); - } -} - impl NodeClient for LocalWalletClient { fn node_url(&self) -> &str { "node" diff --git a/src/bin/cmd/wallet_args.rs b/src/bin/cmd/wallet_args.rs index 3a12f6e90..b6551d1b5 100644 --- a/src/bin/cmd/wallet_args.rs +++ b/src/bin/cmd/wallet_args.rs @@ -22,8 +22,11 @@ use grin_wallet_config::WalletConfig; use grin_wallet_controller::command; use grin_wallet_controller::{Error, ErrorKind}; use grin_wallet_impls::{instantiate_wallet, WalletSeed}; +use grin_wallet_impls::{PathToSlate, SlateGetter as _}; +use grin_wallet_libwallet::Slate; use grin_wallet_libwallet::{IssueInvoiceTxArgs, NodeClient, WalletInst}; use grin_wallet_util::grin_core as core; +use grin_wallet_util::grin_core::core::amount_to_hr_string; use grin_wallet_util::grin_keychain as keychain; use linefeed::terminal::Signal; use linefeed::{Interface, ReadResult}; @@ -31,14 +34,6 @@ use rpassword; use std::path::Path; use std::sync::Arc; -// shut up test compilation warnings -#[cfg(not(test))] -use grin_wallet_impls::FileWalletCommAdapter; -#[cfg(not(test))] -use grin_wallet_libwallet::Slate; -#[cfg(not(test))] -use grin_wallet_util::grin_core::core::amount_to_hr_string; - // define what to do on argument error macro_rules! arg_parse { ( $r:expr ) => { @@ -148,7 +143,6 @@ fn prompt_recovery_phrase() -> Result { Ok(phrase) } -#[cfg(not(test))] fn prompt_pay_invoice(slate: &Slate, method: &str, dest: &str) -> Result { let interface = Arc::new(Interface::new("pay")?); let amount = amount_to_hr_string(slate.amount, false); @@ -621,18 +615,17 @@ pub fn parse_process_invoice_args( // file input only let tx_file = parse_required(args, "input")?; - // Now we need to prompt the user whether they want to do this, - // which requires reading the slate - #[cfg(not(test))] - let adapter = FileWalletCommAdapter::new(); - #[cfg(not(test))] - let slate = match adapter.receive_tx_async(&tx_file) { - Ok(s) => s, - Err(e) => return Err(ParseError::ArgumentError(format!("{}", e))), - }; + if cfg!(not(test)) { + // Now we need to prompt the user whether they want to do this, + // which requires reading the slate - #[cfg(not(test))] // don't prompt during automated testing - prompt_pay_invoice(&slate, method, dest)?; + let slate = match PathToSlate((&tx_file).into()).get_tx() { + Ok(s) => s, + Err(e) => return Err(ParseError::ArgumentError(format!("{}", e))), + }; + + prompt_pay_invoice(&slate, method, dest)?; + } Ok(command::ProcessInvoiceArgs { message: message,