From d1b484259b7f7979eb204922a974fbd71a5e3a8b Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Fri, 7 Dec 2018 15:06:28 +0000 Subject: [PATCH] Wallet command line automated testing (#2097) * wallet test infrastructure * rustfmt * successful wallet create * rustfmt * start of command line tests * rustfmt --- Cargo.lock | 1 + src/bin/cmd/wallet.rs | 132 ++-------------- wallet/Cargo.toml | 3 +- wallet/src/adapters/http.rs | 8 +- wallet/src/adapters/keybase.rs | 8 +- wallet/src/command.rs | 54 +++++-- wallet/src/command_args.rs | 211 ++++++++++++++++++++----- wallet/src/error.rs | 4 + wallet/src/lib.rs | 7 +- wallet/src/libwallet/types.rs | 6 + wallet/src/node_clients/http.rs | 8 + wallet/tests/command_line.rs | 245 ++++++++++++++++++++++++++++++ wallet/tests/common/testclient.rs | 3 +- 13 files changed, 500 insertions(+), 190 deletions(-) create mode 100644 wallet/tests/command_line.rs diff --git a/Cargo.lock b/Cargo.lock index 67c94ca57..f436cd524 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,6 +956,7 @@ dependencies = [ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "grin_api 0.4.2", "grin_chain 0.4.2", + "grin_config 0.4.2", "grin_core 0.4.2", "grin_keychain 0.4.2", "grin_store 0.4.2", diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index 0f87b8d64..9b6ee079a 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -14,27 +14,11 @@ use clap::ArgMatches; use std::path::PathBuf; -use std::thread; -use std::time::Duration; use config::GlobalWalletConfig; -use core::global; -use grin_wallet::{self, command, command_args, WalletConfig, WalletSeed}; +use grin_wallet::{self, command_args, HTTPNodeClient, WalletConfig, WalletSeed}; use servers::start_webwallet_server; -// define what to do on argument error -macro_rules! arg_parse { - ( $r:expr ) => { - match $r { - Ok(res) => res, - Err(e) => { - println!("{}", e); - return 0; - } - } - }; -} - pub fn _init_wallet_seed(wallet_config: WalletConfig, password: &str) { if let Err(_) = WalletSeed::from_file(&wallet_config, password) { WalletSeed::init_file(&wallet_config, 32, password) @@ -55,115 +39,17 @@ pub fn seed_exists(wallet_config: WalletConfig) -> bool { pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i32 { // just get defaults from the global config - let mut wallet_config = config.members.unwrap().wallet; + let wallet_config = config.members.unwrap().wallet; - if let Some(t) = wallet_config.chain_type.clone() { - global::set_mining_mode(t); - } - - if wallet_args.is_present("external") { - wallet_config.api_listen_interface = "0.0.0.0".to_string(); - } - - if let Some(dir) = wallet_args.value_of("dir") { - wallet_config.data_file_dir = dir.to_string().clone(); - } - - if let Some(sa) = wallet_args.value_of("api_server_address") { - wallet_config.check_node_api_http_addr = sa.to_string().clone(); - } - - let global_wallet_args = arg_parse!(command_args::parse_global_args( - &wallet_config, - &wallet_args - )); - - // closure to instantiate wallet as needed by each subcommand - let inst_wallet = || { - let res = command_args::inst_wallet(wallet_config.clone(), &global_wallet_args); - res.unwrap_or_else(|e| { - println!("{}", e); - std::process::exit(0); - }) + // web wallet http server must be started from here + let _ = match wallet_args.subcommand() { + ("web", Some(_)) => start_webwallet_server(), + _ => {} }; - let res = match wallet_args.subcommand() { - ("init", Some(args)) => { - let a = arg_parse!(command_args::parse_init_args(&wallet_config, &args)); - command::init(&global_wallet_args, a) - } - ("recover", Some(args)) => { - let a = arg_parse!(command_args::parse_recover_args(&global_wallet_args, &args)); - command::recover(&wallet_config, a) - } - ("listen", Some(args)) => { - let mut c = wallet_config.clone(); - let mut g = global_wallet_args.clone(); - let a = arg_parse!(command_args::parse_listen_args(&mut c, &mut g, &args)); - command::listen(&wallet_config, &a, &g) - } - ("owner_api", Some(_)) => { - let mut g = global_wallet_args.clone(); - g.tls_conf = None; - command::owner_api(inst_wallet(), &g) - } - ("web", Some(_)) => { - start_webwallet_server(); - command::owner_api(inst_wallet(), &global_wallet_args) - } - ("account", Some(args)) => { - let a = arg_parse!(command_args::parse_account_args(&args)); - command::account(inst_wallet(), a) - } - ("send", Some(args)) => { - let a = arg_parse!(command_args::parse_send_args(&args)); - command::send(inst_wallet(), a) - } - ("receive", Some(args)) => { - let a = arg_parse!(command_args::parse_receive_args(&args)); - command::receive(inst_wallet(), &global_wallet_args, a) - } - ("finalize", Some(args)) => { - let a = arg_parse!(command_args::parse_finalize_args(&args)); - command::finalize(inst_wallet(), a) - } - ("info", Some(args)) => { - let a = arg_parse!(command_args::parse_info_args(&args)); - command::info( - inst_wallet(), - &global_wallet_args, - a, - wallet_config.dark_background_color_scheme.unwrap_or(true), - ) - } - ("outputs", Some(_)) => command::outputs( - inst_wallet(), - &global_wallet_args, - wallet_config.dark_background_color_scheme.unwrap_or(true), - ), - ("txs", Some(args)) => { - let a = arg_parse!(command_args::parse_txs_args(&args)); - command::txs( - inst_wallet(), - &global_wallet_args, - a, - wallet_config.dark_background_color_scheme.unwrap_or(true), - ) - } - ("repost", Some(args)) => { - let a = arg_parse!(command_args::parse_repost_args(&args)); - command::repost(inst_wallet(), a) - } - ("cancel", Some(args)) => { - let a = arg_parse!(command_args::parse_cancel_args(&args)); - command::cancel(inst_wallet(), a) - } - ("restore", Some(_)) => command::restore(inst_wallet()), - _ => { - println!("Unknown wallet command, use 'grin help wallet' for details"); - return 0; - } - }; + let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + let res = command_args::wallet_command(wallet_args, wallet_config, node_client); + // we need to give log output a chance to catch up before exiting thread::sleep(Duration::from_millis(100)); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index bfb4c8344..fe290042f 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -10,7 +10,7 @@ workspace = '..' [dependencies] blake2-rfc = "0.2" -clap = "2.31" +clap = { version = "2.31", features = ["yaml"] } rpassword = "2.0.0" byteorder = "1" failure = "0.1" @@ -41,3 +41,4 @@ grin_util = { path = "../util", version = "0.4.2" } [dev-dependencies] grin_chain = { path = "../chain", version = "0.4.2" } grin_store = { path = "../store", version = "0.4.2" } +grin_config = { path = "../config", version = "0.4.2" } diff --git a/wallet/src/adapters/http.rs b/wallet/src/adapters/http.rs index 824095c11..90a646b0a 100644 --- a/wallet/src/adapters/http.rs +++ b/wallet/src/adapters/http.rs @@ -20,7 +20,7 @@ use api; use controller; use core::libtx::slate::Slate; use libwallet::{Error, ErrorKind}; -use {instantiate_wallet, WalletCommAdapter, WalletConfig}; +use {instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig}; #[derive(Clone)] pub struct HTTPWalletCommAdapter {} @@ -70,9 +70,9 @@ impl WalletCommAdapter for HTTPWalletCommAdapter { account: &str, node_api_secret: Option, ) -> Result<(), Error> { - let wallet = - instantiate_wallet(config.clone(), passphrase, account, node_api_secret.clone()) - .context(ErrorKind::WalletSeedDecryption)?; + let node_client = HTTPNodeClient::new(&config.check_node_api_http_addr, node_api_secret); + let wallet = instantiate_wallet(config.clone(), node_client, passphrase, account) + .context(ErrorKind::WalletSeedDecryption)?; let listen_addr = params.get("api_listen_addr").unwrap(); let tls_conf = match params.get("certificate") { Some(s) => Some(api::TLSConfig::new( diff --git a/wallet/src/adapters/keybase.rs b/wallet/src/adapters/keybase.rs index b46966c23..58cf39a71 100644 --- a/wallet/src/adapters/keybase.rs +++ b/wallet/src/adapters/keybase.rs @@ -25,7 +25,7 @@ use std::process::{Command, Stdio}; use std::str::from_utf8; use std::thread::sleep; use std::time::{Duration, Instant}; -use {instantiate_wallet, WalletCommAdapter, WalletConfig}; +use {instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig}; const TTL: u16 = 60; // TODO: Pass this as a parameter const SLEEP_DURATION: Duration = Duration::from_millis(5000); @@ -219,9 +219,9 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter { account: &str, node_api_secret: Option, ) -> Result<(), Error> { - let wallet = - instantiate_wallet(config.clone(), passphrase, account, node_api_secret.clone()) - .context(ErrorKind::WalletSeedDecryption)?; + let node_client = HTTPNodeClient::new(&config.check_node_api_http_addr, node_api_secret); + let wallet = instantiate_wallet(config.clone(), node_client, passphrase, account) + .context(ErrorKind::WalletSeedDecryption)?; println!("Listening for messages via keybase chat..."); loop { diff --git a/wallet/src/command.rs b/wallet/src/command.rs index b315aaab7..44a52d1de 100644 --- a/wallet/src/command.rs +++ b/wallet/src/command.rs @@ -32,11 +32,9 @@ use error::{Error, ErrorKind}; use {controller, display, HTTPNodeClient, WalletConfig, WalletInst, WalletSeed}; use { FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, LMDBBackend, - NullWalletCommAdapter, + NodeClient, NullWalletCommAdapter, }; -pub type WalletRef = Arc>>; - /// Arguments common to all wallet commands #[derive(Clone)] pub struct GlobalArgs { @@ -131,7 +129,10 @@ pub fn listen(config: &WalletConfig, args: &ListenArgs, g_args: &GlobalArgs) -> Ok(()) } -pub fn owner_api(wallet: WalletRef, g_args: &GlobalArgs) -> Result<(), Error> { +pub fn owner_api( + wallet: Arc>>, + g_args: &GlobalArgs, +) -> Result<(), Error> { let res = controller::owner_listener( wallet, "127.0.0.1:13420", @@ -149,7 +150,10 @@ pub struct AccountArgs { pub create: Option, } -pub fn account(wallet: WalletRef, args: AccountArgs) -> Result<(), Error> { +pub fn account( + wallet: Arc>>, + args: AccountArgs, +) -> Result<(), Error> { if args.create.is_none() { let res = controller::owner_single_use(wallet, |api| { let acct_mappings = api.accounts()?; @@ -192,7 +196,10 @@ pub struct SendArgs { pub max_outputs: usize, } -pub fn send(wallet: WalletRef, args: SendArgs) -> Result<(), Error> { +pub fn send( + wallet: Arc>>, + args: SendArgs, +) -> Result<(), Error> { controller::owner_single_use(wallet.clone(), |api| { let result = api.initiate_tx( None, @@ -267,7 +274,11 @@ pub struct ReceiveArgs { pub message: Option, } -pub fn receive(wallet: WalletRef, g_args: &GlobalArgs, args: ReceiveArgs) -> Result<(), Error> { +pub fn receive( + wallet: Arc>>, + g_args: &GlobalArgs, + args: ReceiveArgs, +) -> Result<(), Error> { let adapter = FileWalletCommAdapter::new(); let mut slate = adapter.receive_tx_async(&args.input)?; controller::foreign_single_use(wallet, |api| { @@ -289,7 +300,10 @@ pub struct FinalizeArgs { pub fluff: bool, } -pub fn finalize(wallet: WalletRef, args: FinalizeArgs) -> Result<(), Error> { +pub fn finalize( + wallet: Arc>>, + args: FinalizeArgs, +) -> Result<(), Error> { let adapter = FileWalletCommAdapter::new(); let mut slate = adapter.receive_tx_async(&args.input)?; controller::owner_single_use(wallet.clone(), |api| { @@ -320,7 +334,7 @@ pub struct InfoArgs { } pub fn info( - wallet: WalletRef, + wallet: Arc>>, g_args: &GlobalArgs, args: InfoArgs, dark_scheme: bool, @@ -334,7 +348,11 @@ pub fn info( Ok(()) } -pub fn outputs(wallet: WalletRef, g_args: &GlobalArgs, dark_scheme: bool) -> Result<(), Error> { +pub fn outputs( + wallet: Arc>>, + g_args: &GlobalArgs, + dark_scheme: bool, +) -> Result<(), Error> { controller::owner_single_use(wallet.clone(), |api| { let (height, _) = api.node_height()?; let (validated, outputs) = api.retrieve_outputs(g_args.show_spent, true, None)?; @@ -350,7 +368,7 @@ pub struct TxsArgs { } pub fn txs( - wallet: WalletRef, + wallet: Arc>>, g_args: &GlobalArgs, args: TxsArgs, dark_scheme: bool, @@ -385,7 +403,10 @@ pub struct RepostArgs { pub fluff: bool, } -pub fn repost(wallet: WalletRef, args: RepostArgs) -> Result<(), Error> { +pub fn repost( + wallet: Arc>>, + args: RepostArgs, +) -> Result<(), Error> { controller::owner_single_use(wallet.clone(), |api| { let (_, txs) = api.retrieve_txs(true, Some(args.id), None)?; let stored_tx = txs[0].get_stored_tx(); @@ -428,7 +449,10 @@ pub struct CancelArgs { pub tx_id_string: String, } -pub fn cancel(wallet: WalletRef, args: CancelArgs) -> Result<(), Error> { +pub fn cancel( + wallet: Arc>>, + args: CancelArgs, +) -> Result<(), Error> { controller::owner_single_use(wallet.clone(), |api| { let result = api.cancel_tx(args.tx_id, args.tx_slate_id); match result { @@ -445,7 +469,9 @@ pub fn cancel(wallet: WalletRef, args: CancelArgs) -> Result<(), Error> { Ok(()) } -pub fn restore(wallet: WalletRef) -> Result<(), Error> { +pub fn restore( + wallet: Arc>>, +) -> Result<(), Error> { controller::owner_single_use(wallet.clone(), |api| { let result = api.restore(); match result { diff --git a/wallet/src/command_args.rs b/wallet/src/command_args.rs index adb0cc95b..3a540c410 100644 --- a/wallet/src/command_args.rs +++ b/wallet/src/command_args.rs @@ -14,19 +14,36 @@ /// Argument parsing and error handling for wallet commands use clap::ArgMatches; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use util::Mutex; + use failure::Fail; use api::TLSConfig; -use core::core; +use core; +use keychain; use std::path::Path; use util::file::get_first_line; -use ErrorKind; -use {command, instantiate_wallet, WalletConfig, WalletSeed}; +use {command, instantiate_wallet, NodeClient, WalletConfig, WalletInst, WalletSeed}; +use {Error, ErrorKind}; +// define what to do on argument error +macro_rules! arg_parse { + ( $r:expr ) => { + match $r { + Ok(res) => res, + Err(e) => { + return Err(ErrorKind::ArgumentError(format!("{}", e)).into()); + } + } + }; +} /// Simple error definition, just so we can return errors from all commands /// and let the caller figure out what to do #[derive(Clone, Eq, PartialEq, Debug, Fail)] -pub enum Error { +pub enum ParseError { #[fail(display = "Invalid Arguments: {}", _0)] ArgumentError(String), } @@ -63,14 +80,10 @@ fn prompt_password_confirm() -> String { pub fn inst_wallet( config: WalletConfig, g_args: &command::GlobalArgs, -) -> Result { + node_client: impl NodeClient + 'static, +) -> Result>>, ParseError> { let passphrase = prompt_password(&g_args.password); - let res = instantiate_wallet( - config.clone(), - &passphrase, - &g_args.account, - g_args.node_api_secret.clone(), - ); + let res = instantiate_wallet(config.clone(), node_client, &passphrase, &g_args.account); match res { Ok(p) => Ok(p), Err(e) => { @@ -82,31 +95,31 @@ pub fn inst_wallet( _ => format!("Error instantiating wallet: {}", e), } }; - Err(Error::ArgumentError(msg)) + Err(ParseError::ArgumentError(msg)) } } } // parses a required value, or throws error with message otherwise -fn parse_required<'a>(args: &'a ArgMatches, name: &str) -> Result<&'a str, Error> { +fn parse_required<'a>(args: &'a ArgMatches, name: &str) -> Result<&'a str, ParseError> { let arg = args.value_of(name); match arg { Some(ar) => Ok(ar), None => { let msg = format!("Value for argument '{}' is required in this context", name,); - Err(Error::ArgumentError(msg)) + Err(ParseError::ArgumentError(msg)) } } } // parses a number, or throws error with message otherwise -fn parse_u64(arg: &str, name: &str) -> Result { +fn parse_u64(arg: &str, name: &str) -> Result { let val = arg.parse::(); match val { Ok(v) => Ok(v), Err(e) => { let msg = format!("Could not parse {} as a whole number. e={}", name, e); - Err(Error::ArgumentError(msg)) + Err(ParseError::ArgumentError(msg)) } } } @@ -114,7 +127,7 @@ fn parse_u64(arg: &str, name: &str) -> Result { pub fn parse_global_args( config: &WalletConfig, args: &ArgMatches, -) -> Result { +) -> Result { let account = parse_required(args, "account")?; let mut show_spent = false; if args.is_present("show_spent") { @@ -133,7 +146,7 @@ pub fn parse_global_args( Some(k) => k, None => { let msg = format!("Private key for certificate is not set"); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } }; Some(TLSConfig::new(file, key)) @@ -151,18 +164,22 @@ pub fn parse_global_args( pub fn parse_init_args( config: &WalletConfig, + g_args: &command::GlobalArgs, args: &ArgMatches, -) -> Result { +) -> Result { if let Err(e) = WalletSeed::seed_file_exists(config) { let msg = format!("Not creating wallet - {}", e.inner); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } let list_length = match args.is_present("short_wordlist") { false => 32, true => 16, }; println!("Please enter a password for your new wallet"); - let password = prompt_password_confirm(); + let password = match g_args.password.clone() { + Some(p) => p, + None => prompt_password_confirm(), + }; Ok(command::InitArgs { list_length: list_length, password: password, @@ -173,14 +190,14 @@ pub fn parse_init_args( pub fn parse_recover_args( g_args: &command::GlobalArgs, args: &ArgMatches, -) -> Result { +) -> Result { let (passphrase, recovery_phrase) = { match args.value_of("recovery_phrase") { None => (prompt_password(&g_args.password), None), Some(l) => { if WalletSeed::from_mnemonic(l).is_err() { let msg = format!("Recovery word phrase is invalid"); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } println!("Please provide a new password for the recovered wallet"); (prompt_password_confirm(), Some(l.to_owned())) @@ -197,7 +214,7 @@ pub fn parse_listen_args( config: &mut WalletConfig, g_args: &mut command::GlobalArgs, args: &ArgMatches, -) -> Result { +) -> Result { // listen args let pass = match g_args.password.clone() { Some(p) => Some(p.to_owned()), @@ -213,7 +230,7 @@ pub fn parse_listen_args( }) } -pub fn parse_account_args(account_args: &ArgMatches) -> Result { +pub fn parse_account_args(account_args: &ArgMatches) -> Result { let create = match account_args.value_of("create") { None => None, Some(s) => Some(s.to_owned()), @@ -221,10 +238,10 @@ pub fn parse_account_args(account_args: &ArgMatches) -> Result Result { +pub fn parse_send_args(args: &ArgMatches) -> Result { // amount let amount = parse_required(args, "amount")?; - let amount = core::amount_from_hr_string(amount); + let amount = core::core::amount_from_hr_string(amount); let amount = match amount { Ok(a) => a, Err(e) => { @@ -232,7 +249,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result { "Could not parse amount as a number with optional decimal point. e={}", e ); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } }; @@ -268,7 +285,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result { "HTTP Destination should start with http://: or https://: {}", dest, ); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } // change_outputs @@ -294,7 +311,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result { }) } -pub fn parse_receive_args(receive_args: &ArgMatches) -> Result { +pub fn parse_receive_args(receive_args: &ArgMatches) -> Result { // message let message = match receive_args.is_present("message") { true => Some(receive_args.value_of("message").unwrap().to_owned()), @@ -307,7 +324,7 @@ pub fn parse_receive_args(receive_args: &ArgMatches) -> Result Result Result { +pub fn parse_finalize_args(args: &ArgMatches) -> Result { let fluff = args.is_present("fluff"); let tx_file = parse_required(args, "input")?; if !Path::new(&tx_file).is_file() { let msg = format!("File {} not found.", tx_file); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } Ok(command::FinalizeArgs { input: tx_file.to_owned(), @@ -330,7 +347,7 @@ pub fn parse_finalize_args(args: &ArgMatches) -> Result Result { +pub fn parse_info_args(args: &ArgMatches) -> Result { // minimum_confirmations let mc = parse_required(args, "minimum_confirmations")?; let mc = parse_u64(mc, "minimum_confirmations")?; @@ -339,7 +356,7 @@ pub fn parse_info_args(args: &ArgMatches) -> Result { }) } -pub fn parse_txs_args(args: &ArgMatches) -> Result { +pub fn parse_txs_args(args: &ArgMatches) -> Result { let tx_id = match args.value_of("id") { None => None, Some(tx) => Some(parse_u64(tx, "id")? as u32), @@ -347,7 +364,7 @@ pub fn parse_txs_args(args: &ArgMatches) -> Result { Ok(command::TxsArgs { id: tx_id }) } -pub fn parse_repost_args(args: &ArgMatches) -> Result { +pub fn parse_repost_args(args: &ArgMatches) -> Result { let tx_id = match args.value_of("id") { None => None, Some(tx) => Some(parse_u64(tx, "id")? as u32), @@ -366,7 +383,7 @@ pub fn parse_repost_args(args: &ArgMatches) -> Result Result { +pub fn parse_cancel_args(args: &ArgMatches) -> Result { let mut tx_id_string = ""; let tx_id = match args.value_of("id") { None => None, @@ -381,13 +398,13 @@ pub fn parse_cancel_args(args: &ArgMatches) -> Result { let msg = format!("Could not parse txid parameter. e={}", e); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } }, }; if (tx_id.is_none() && tx_slate_id.is_none()) || (tx_id.is_some() && tx_slate_id.is_some()) { let msg = format!("'id' (-i) or 'txid' (-t) argument is required."); - return Err(Error::ArgumentError(msg)); + return Err(ParseError::ArgumentError(msg)); } Ok(command::CancelArgs { tx_id: tx_id, @@ -395,3 +412,119 @@ pub fn parse_cancel_args(args: &ArgMatches) -> Result Result { + if let Some(t) = wallet_config.chain_type.clone() { + core::global::set_mining_mode(t); + } + + if wallet_args.is_present("external") { + wallet_config.api_listen_interface = "0.0.0.0".to_string(); + } + + if let Some(dir) = wallet_args.value_of("dir") { + wallet_config.data_file_dir = dir.to_string().clone(); + } + + if let Some(sa) = wallet_args.value_of("api_server_address") { + wallet_config.check_node_api_http_addr = sa.to_string().clone(); + } + + let global_wallet_args = arg_parse!(parse_global_args(&wallet_config, &wallet_args)); + + node_client.set_node_url(&wallet_config.check_node_api_http_addr); + node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone()); + + // closure to instantiate wallet as needed by each subcommand + let inst_wallet = || { + let res = inst_wallet(wallet_config.clone(), &global_wallet_args, node_client); + res.unwrap_or_else(|e| { + println!("{}", e); + std::process::exit(0); + }) + }; + + let res = match wallet_args.subcommand() { + ("init", Some(args)) => { + let a = arg_parse!(parse_init_args(&wallet_config, &global_wallet_args, &args)); + command::init(&global_wallet_args, a) + } + ("recover", Some(args)) => { + let a = arg_parse!(parse_recover_args(&global_wallet_args, &args)); + command::recover(&wallet_config, a) + } + ("listen", Some(args)) => { + let mut c = wallet_config.clone(); + let mut g = global_wallet_args.clone(); + let a = arg_parse!(parse_listen_args(&mut c, &mut g, &args)); + command::listen(&wallet_config, &a, &g) + } + ("owner_api", Some(_)) => { + let mut g = global_wallet_args.clone(); + g.tls_conf = None; + command::owner_api(inst_wallet(), &g) + } + ("web", Some(_)) => command::owner_api(inst_wallet(), &global_wallet_args), + ("account", Some(args)) => { + let a = arg_parse!(parse_account_args(&args)); + command::account(inst_wallet(), a) + } + ("send", Some(args)) => { + let a = arg_parse!(parse_send_args(&args)); + command::send(inst_wallet(), a) + } + ("receive", Some(args)) => { + let a = arg_parse!(parse_receive_args(&args)); + command::receive(inst_wallet(), &global_wallet_args, a) + } + ("finalize", Some(args)) => { + let a = arg_parse!(parse_finalize_args(&args)); + command::finalize(inst_wallet(), a) + } + ("info", Some(args)) => { + let a = arg_parse!(parse_info_args(&args)); + command::info( + inst_wallet(), + &global_wallet_args, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("outputs", Some(_)) => command::outputs( + inst_wallet(), + &global_wallet_args, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ), + ("txs", Some(args)) => { + let a = arg_parse!(parse_txs_args(&args)); + command::txs( + inst_wallet(), + &global_wallet_args, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("repost", Some(args)) => { + let a = arg_parse!(parse_repost_args(&args)); + command::repost(inst_wallet(), a) + } + ("cancel", Some(args)) => { + let a = arg_parse!(parse_cancel_args(&args)); + command::cancel(inst_wallet(), a) + } + ("restore", Some(_)) => command::restore(inst_wallet()), + _ => { + let msg = format!("Unknown wallet command, use 'grin help wallet' for details"); + return Err(ErrorKind::ArgumentError(msg).into()); + } + }; + if let Err(e) = res { + Err(e) + } else { + Ok(wallet_args.subcommand().0.to_owned()) + } +} diff --git a/wallet/src/error.rs b/wallet/src/error.rs index b2c7bce9c..49774f029 100644 --- a/wallet/src/error.rs +++ b/wallet/src/error.rs @@ -96,6 +96,10 @@ pub enum ErrorKind { #[fail(display = "BIP39 Mnemonic (word list) Error")] Mnemonic, + /// Command line argument error + #[fail(display = "{}", _0)] + ArgumentError(String), + /// Other #[fail(display = "Generic error: {}", _0)] GenericError(String), diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 2f03988a6..2a3fc433a 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -76,14 +76,13 @@ use util::Mutex; /// Helper to create an instance of the LMDB wallet pub fn instantiate_wallet( wallet_config: WalletConfig, + node_client: impl NodeClient + 'static, passphrase: &str, account: &str, - node_api_secret: Option, -) -> Result>>, Error> { +) -> Result>>, Error> { // First test decryption, so we can abort early if we have the wrong password let _ = WalletSeed::from_file(&wallet_config, passphrase)?; - let client_n = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, node_api_secret); - let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, client_n)?; + let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, node_client)?; db_wallet.set_parent_key_id_by_name(account)?; info!("Using LMDB Backend for wallet"); Ok(Arc::new(Mutex::new(db_wallet))) diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 1fa5e56cb..4d2435e4a 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -188,9 +188,15 @@ pub trait NodeClient: Sync + Send + Clone { /// Return the URL of the check node fn node_url(&self) -> &str; + /// Set the node URL + fn set_node_url(&mut self, node_url: &str); + /// Return the node api secret fn node_api_secret(&self) -> Option; + /// Change the API secret + fn set_node_api_secret(&mut self, node_api_secret: Option); + /// Posts a transaction to a grin node fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), Error>; diff --git a/wallet/src/node_clients/http.rs b/wallet/src/node_clients/http.rs index 1225b73fe..117194e17 100644 --- a/wallet/src/node_clients/http.rs +++ b/wallet/src/node_clients/http.rs @@ -52,6 +52,14 @@ impl NodeClient for HTTPNodeClient { self.node_api_secret.clone() } + fn set_node_url(&mut self, node_url: &str) { + self.node_url = node_url.to_owned(); + } + + fn set_node_api_secret(&mut self, node_api_secret: Option) { + self.node_api_secret = node_api_secret; + } + /// Posts a transaction to a grin node fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), libwallet::Error> { let url; diff --git a/wallet/tests/command_line.rs b/wallet/tests/command_line.rs new file mode 100644 index 000000000..69d130d4b --- /dev/null +++ b/wallet/tests/command_line.rs @@ -0,0 +1,245 @@ +// 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. + +//! Test wallet command line works as expected +extern crate grin_chain as chain; +extern crate grin_config as config; +extern crate grin_core as core; +extern crate grin_keychain as keychain; +extern crate grin_store as store; +extern crate grin_util as util; +extern crate grin_wallet as wallet; +extern crate rand; +#[macro_use] +extern crate log; +extern crate chrono; +extern crate serde; +extern crate uuid; +#[macro_use] +extern crate clap; + +mod common; +use common::testclient::{LocalWalletClient, WalletProxy}; + +use clap::{App, ArgMatches}; +use std::thread; +use std::time::Duration; +use std::{env, fs}; + +use config::GlobalWalletConfig; +use core::global; +use core::global::ChainTypes; +use keychain::ExtKeychain; +use wallet::{command_args, WalletConfig}; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// Create a wallet config file in the given current directory +pub fn config_command_wallet(dir_name: &str, wallet_name: &str) -> Result<(), wallet::Error> { + let mut current_dir; + let mut default_config = GlobalWalletConfig::default(); + current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + current_dir.push(dir_name); + current_dir.push(wallet_name); + let _ = fs::create_dir_all(current_dir.clone()); + let mut config_file_name = current_dir.clone(); + config_file_name.push("grin-wallet.toml"); + if config_file_name.exists() { + return Err(wallet::ErrorKind::ArgumentError( + "grin-wallet.toml already exists in the target directory. Please remove it first" + .to_owned(), + ))?; + } + default_config.update_paths(¤t_dir); + default_config + .write_to_file(config_file_name.to_str().unwrap()) + .unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + + println!( + "File {} configured and created", + config_file_name.to_str().unwrap(), + ); + Ok(()) +} + +/// Handles setup and detection of paths for wallet +pub fn initial_setup_wallet(dir_name: &str, wallet_name: &str) -> WalletConfig { + let mut current_dir; + current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + current_dir.push(dir_name); + current_dir.push(wallet_name); + let _ = fs::create_dir_all(current_dir.clone()); + let mut config_file_name = current_dir.clone(); + config_file_name.push("grin-wallet.toml"); + GlobalWalletConfig::new(config_file_name.to_str().unwrap()) + .unwrap() + .members + .unwrap() + .wallet +} + +fn get_wallet_subcommand<'a>( + wallet_dir: &str, + wallet_name: &str, + args: ArgMatches<'a>, +) -> ArgMatches<'a> { + match args.subcommand() { + ("wallet", Some(wallet_args)) => { + // wallet init command should spit out its config file then continue + // (if desired) + if let ("init", Some(init_args)) = wallet_args.subcommand() { + if init_args.is_present("here") { + let _ = config_command_wallet(wallet_dir, wallet_name); + } + } + wallet_args.to_owned() + } + _ => ArgMatches::new(), + } +} + +fn execute_command( + app: &App, + test_dir: &str, + wallet_name: &str, + client: &LocalWalletClient, + arg_vec: Vec<&str>, +) -> Result { + let args = app.clone().get_matches_from(arg_vec); + let args = get_wallet_subcommand(test_dir, wallet_name, args.clone()); + let config = initial_setup_wallet(test_dir, wallet_name); + command_args::wallet_command(&args, config.clone(), client.clone()) +} + +/// self send impl +fn command_line_test_impl(test_dir: &str) -> Result<(), wallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + + // load app yaml. If it don't exist, just say so and exit + let yml = load_yaml!("../../src/bin/grin.yml"); + let app = App::from_yaml(yml); + + // wallet init + let arg_vec = vec!["grin", "wallet", "-p", "password", "init", "-h"]; + // should create new wallet file + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + // trying to init twice - should fail + assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).is_err()); + + // add wallet to proxy + let wallet1 = common::create_wallet(&format!("{}/wallet1", test_dir), client1.clone()); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + // Create wallet 2 + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let wallet2 = common::create_wallet(&format!("{}/wallet2", test_dir), client2.clone()); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Create some accounts in wallet 1 + let arg_vec = vec![ + "grin", "wallet", "-p", "password", "account", "-c", "mining", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Create some accounts in wallet 2 + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + // already exists + assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "account", + "-c", + "account_2", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // let's see those accounts + let arg_vec = vec!["grin", "wallet", "-p", "password", "account"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // Mine a bit into wallet 1 so we have something to send + //let mut bh = 10u64; + //let chain = wallet_proxy.chain.clone(); + //let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + + // let's see those accounts + let arg_vec = vec!["grin", "wallet", "-p", "password", "account"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // Start wallet 1's listener, collect some coinbase outputs + let _arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "listen"]; + //execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_command_line() { + let test_dir = "test_output/command_line"; + if let Err(e) = command_line_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/wallet/tests/common/testclient.rs b/wallet/tests/common/testclient.rs index 56d37eee3..d277c9274 100644 --- a/wallet/tests/common/testclient.rs +++ b/wallet/tests/common/testclient.rs @@ -393,7 +393,8 @@ impl NodeClient for LocalWalletClient { fn node_api_secret(&self) -> Option { None } - + fn set_node_url(&mut self, _node_url: &str) {} + fn set_node_api_secret(&mut self, _node_api_secret: Option) {} /// Posts a transaction to a grin node /// In this case it will create a new block with award rewarded to fn post_tx(&self, tx: &TxWrapper, _fluff: bool) -> Result<(), libwallet::Error> {