diff --git a/.gitignore b/.gitignore index e11f667cd..78d8a7cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,4 @@ target grin.log wallet.seed test_output -wallet* .idea/ diff --git a/Cargo.lock b/Cargo.lock index d7b17eac4..95f575b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -765,6 +765,7 @@ dependencies = [ "built 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "easy-jsonrpc 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "grin_wallet_api 2.1.0-beta.1", @@ -778,6 +779,10 @@ dependencies = [ "prettytable-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "rpassword 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e26ada10b..9ea7f1975 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,10 @@ grin_wallet_util = { path = "./util", version = "2.1.0-beta.1" } [build-dependencies] built = "0.3" + +[dev-dependencies] +url = "1.7.2" +serde = "1" +serde_derive = "1" +serde_json = "1" +easy-jsonrpc = "0.5.1" diff --git a/controller/src/controller.rs b/controller/src/controller.rs index 18c73a905..3e7705ac2 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -32,7 +32,7 @@ use serde_json; use std::net::SocketAddr; use std::sync::Arc; -use crate::apiwallet::{Foreign, ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc}; +use crate::apiwallet::{Foreign, ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, OwnerRpcS}; use easy_jsonrpc; use easy_jsonrpc::{Handler, MaybeReply}; @@ -126,8 +126,6 @@ where C: NodeClient + 'static, K: Keychain + 'static, { - let api_handler_v2 = OwnerAPIHandlerV2::new(wallet.clone()); - let mut router = Router::new(); if api_secret.is_some() { let api_basic_auth = @@ -139,10 +137,18 @@ where router.add_middleware(basic_auth_middleware); } + let api_handler_v2 = OwnerAPIHandlerV2::new(wallet.clone()); + + let api_handler_v3 = OwnerAPIHandlerV3::new(wallet.clone()); + router .add_route("/v2/owner", Arc::new(api_handler_v2)) .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; + router + .add_route("/v3/owner", Arc::new(api_handler_v3)) + .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; + // If so configured, add the foreign API to the same port if owner_api_include_foreign.unwrap_or(false) { warn!("Starting HTTP Foreign API on Owner server at {}.", addr); @@ -277,6 +283,79 @@ where } } +/// V3 API Handler/Wrapper for owner functions, which include a secure +/// mode + lifecycle functions +pub struct OwnerAPIHandlerV3 +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Wallet instance + pub wallet: Arc + 'static>>>, +} + +impl OwnerAPIHandlerV3 +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Create a new owner API handler for GET methods + pub fn new( + wallet: Arc + 'static>>>, + ) -> OwnerAPIHandlerV3 { + OwnerAPIHandlerV3 { wallet } + } + + fn call_api( + &self, + req: Request, + api: Owner<'static, L, C, K>, + ) -> Box + Send> { + Box::new(parse_body(req).and_then(move |val: serde_json::Value| { + let owner_api_s = &api as &dyn OwnerRpcS; + match owner_api_s.handle_request(val) { + MaybeReply::Reply(r) => ok(r), + MaybeReply::DontReply => { + // Since it's http, we need to return something. We return [] because jsonrpc + // clients will parse it as an empty batch response. + ok(serde_json::json!([])) + } + } + })) + } + + fn handle_post_request(&self, req: Request) -> WalletResponseFuture { + let api = Owner::new(self.wallet.clone()); + Box::new( + self.call_api(req, api) + .and_then(|resp| ok(json_response_pretty(&resp))), + ) + } +} + +impl api::Handler for OwnerAPIHandlerV3 +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + fn post(&self, req: Request) -> ResponseFuture { + Box::new( + self.handle_post_request(req) + .and_then(|r| ok(r)) + .or_else(|e| { + error!("Request Error: {:?}", e); + ok(create_error_response(e)) + }), + ) + } + + fn options(&self, _req: Request) -> ResponseFuture { + Box::new(ok(create_ok_response("{}"))) + } +} /// V2 API Handler/Wrapper for foreign functions pub struct ForeignAPIHandlerV2 where diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index f18da3b2d..2b0694bbb 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -84,7 +84,7 @@ where let mut validated = false; if refresh_from_node { - validated = update_outputs(w, keychain_mask, false); + validated = update_outputs(w, keychain_mask, false)?; } Ok(( @@ -116,7 +116,7 @@ where let mut validated = false; if refresh_from_node { - validated = update_outputs(w, keychain_mask, false); + validated = update_outputs(w, keychain_mask, false)?; } Ok(( @@ -141,7 +141,7 @@ where let mut validated = false; if refresh_from_node { - validated = update_outputs(w, keychain_mask, false); + validated = update_outputs(w, keychain_mask, false)?; } let wallet_info = updater::retrieve_info(&mut *w, &parent_key_id, minimum_confirmations)?; @@ -418,7 +418,7 @@ where K: Keychain + 'a, { let parent_key_id = w.parent_key_id(); - if !update_outputs(w, keychain_mask, false) { + if !update_outputs(w, keychain_mask, false)? { return Err(ErrorKind::TransactionCancellationError( "Can't contact running Grin node. Not Cancelling.", ))?; @@ -489,7 +489,7 @@ where C: NodeClient + 'a, K: Keychain + 'a, { - update_outputs(w, keychain_mask, true); + update_outputs(w, keychain_mask, true)?; w.check_repair(keychain_mask, delete_unconfirmed) } @@ -528,7 +528,7 @@ fn update_outputs<'a, T: ?Sized, C, K>( w: &mut T, keychain_mask: Option<&SecretKey>, update_all: bool, -) -> bool +) -> Result where T: WalletBackend<'a, C, K>, C: NodeClient + 'a, @@ -536,7 +536,12 @@ where { let parent_key_id = w.parent_key_id(); match updater::refresh_outputs(&mut *w, keychain_mask, &parent_key_id, update_all) { - Ok(_) => true, - Err(_) => false, + Ok(_) => Ok(true), + Err(e) => { + if let ErrorKind::InvalidKeychainMask = e.kind() { + return Err(e); + } + Ok(false) + } } } diff --git a/src/bin/cmd/wallet_tests.rs b/src/bin/cmd/wallet_tests.rs deleted file mode 100644 index 1f12a3db8..000000000 --- a/src/bin/cmd/wallet_tests.rs +++ /dev/null @@ -1,651 +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. - -//! Test wallet command line works as expected -#[cfg(test)] -mod wallet_tests { - use clap; - use grin_wallet_util::grin_util as util; - - use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; - - use clap::{App, ArgMatches}; - use std::path::PathBuf; - use std::sync::Arc; - use std::thread; - use std::time::Duration; - use std::{env, fs}; - use util::{Mutex, ZeroingString}; - - use grin_wallet_config::{GlobalWalletConfig, WalletConfig, GRIN_WALLET_DIR}; - use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; - use grin_wallet_libwallet::WalletInst; - use grin_wallet_util::grin_core::global::{self, ChainTypes}; - use grin_wallet_util::grin_keychain::ExtKeychain; - use util::secp::key::SecretKey; - - use super::super::wallet_args; - - 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<(), grin_wallet_controller::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(grin_wallet_controller::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() { - ("init", Some(init_args)) => { - // wallet init command should spit out its config file then continue - // (if desired) - if init_args.is_present("here") { - let _ = config_command_wallet(wallet_dir, wallet_name); - } - init_args.to_owned() - } - _ => ArgMatches::new(), - } - } - // - // Helper to create an instance of the LMDB wallet - fn instantiate_wallet( - mut wallet_config: WalletConfig, - node_client: LocalWalletClient, - passphrase: &str, - account: &str, - ) -> Result< - ( - Arc< - Mutex< - Box< - WalletInst< - 'static, - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, - LocalWalletClient, - ExtKeychain, - >, - >, - >, - >, - Option, - ), - grin_wallet_controller::Error, - > { - wallet_config.chain_type = None; - let mut wallet = Box::new(DefaultWalletImpl::::new(node_client).unwrap()) - as Box< - WalletInst< - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, - LocalWalletClient, - ExtKeychain, - >, - >; - let lc = wallet.lc_provider().unwrap(); - // legacy hack to avoid the need for changes in existing grin-wallet.toml files - // remove `wallet_data` from end of path as - // new lifecycle provider assumes grin_wallet.toml is in root of data directory - let mut top_level_wallet_dir = PathBuf::from(wallet_config.clone().data_file_dir); - if top_level_wallet_dir.ends_with(GRIN_WALLET_DIR) { - top_level_wallet_dir.pop(); - wallet_config.data_file_dir = top_level_wallet_dir.to_str().unwrap().into(); - } - lc.set_wallet_directory(&wallet_config.data_file_dir); - let keychain_mask = lc - .open_wallet(None, ZeroingString::from(passphrase), true, false) - .unwrap(); - let wallet_inst = lc.wallet_inst()?; - wallet_inst.set_parent_key_id_by_name(account)?; - Ok((Arc::new(Mutex::new(wallet)), keychain_mask)) - } - - 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 _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); - let mut config = initial_setup_wallet(test_dir, wallet_name); - //unset chain type so it doesn't get reset - config.chain_type = None; - wallet_args::wallet_command(&args, config.clone(), client.clone()) - } - - /// command line tests - fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::Error> { - setup(test_dir); - // Create a new proxy to simulate server and wallet responses - let mut wallet_proxy: WalletProxy< - DefaultLCProvider, - LocalWalletClient, - ExtKeychain, - > = WalletProxy::new(test_dir); - let chain = wallet_proxy.chain.clone(); - - // load app yaml. If it don't exist, just say so and exit - let yml = load_yaml!("../grin-wallet.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()); - let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); - - // add wallet to proxy - //let wallet1 = test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone()); - let config1 = initial_setup_wallet(test_dir, "wallet1"); - let (wallet1, mask1_i) = - instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; - wallet_proxy.add_wallet( - "wallet1", - client1.get_send_instance(), - wallet1.clone(), - mask1_i.clone(), - ); - - // Create wallet 2 - let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); - execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; - - let config2 = initial_setup_wallet(test_dir, "wallet2"); - let (wallet2, mask2_i) = - instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?; - wallet_proxy.add_wallet( - "wallet2", - client2.get_send_instance(), - wallet2.clone(), - mask2_i.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)?; - - // 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 - // (TODO: Be able to stop listeners so we can test this better) - let (wallet1, mask1_i) = - instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; - let mask1 = (&mask1_i).as_ref(); - grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - api.set_active_account(m, "mining")?; - Ok(()) - })?; - - let mut bh = 10u64; - let _ = test_framework::award_blocks_to_wallet( - &chain, - wallet1.clone(), - mask1, - bh as usize, - false, - ); - - let very_long_message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ - This part should all be truncated"; - - // Update info and check - let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "info"]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - // try a file exchange - let file_name = format!("{}/tx1.part_tx", test_dir); - let response_file_name = format!("{}/tx1.part_tx.response", test_dir); - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "send", - "-m", - "file", - "-d", - &file_name, - "-g", - very_long_message, - "10", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "account_1", - "receive", - "-i", - &file_name, - "-g", - "Thanks, Yeast!", - ]; - execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; - - // shouldn't be allowed to receive twice - assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); - - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "finalize", - "-i", - &response_file_name, - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - bh += 1; - - let (wallet1, mask1_i) = - instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; - let mask1 = (&mask1_i).as_ref(); - - // Check our transaction log, should have 10 entries - grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - api.set_active_account(m, "mining")?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; - assert!(refreshed); - assert_eq!(txs.len(), bh as usize); - Ok(()) - })?; - - let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false); - bh += 10; - - // update info for each - let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "info"]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "account_1", "info"]; - execute_command(&app, test_dir, "wallet2", &client1, arg_vec)?; - - // check results in wallet 2 - let (wallet2, mask2_i) = - instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?; - let mask2 = (&mask2_i).as_ref(); - - grin_wallet_controller::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { - api.set_active_account(m, "account_1")?; - let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; - assert_eq!(wallet1_info.last_confirmed_height, bh); - assert_eq!(wallet1_info.amount_currently_spendable, 10_000_000_000); - Ok(()) - })?; - - // Self-send to same account, using smallest strategy - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "send", - "-m", - "file", - "-d", - &file_name, - "-g", - "Love, Yeast, Smallest", - "-s", - "smallest", - "10", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "receive", - "-i", - &file_name, - "-g", - "Thanks, Yeast!", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; - - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "finalize", - "-i", - &response_file_name, - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - bh += 1; - - // Check our transaction log, should have bh entries + one for the self receive - let (wallet1, mask1_i) = - instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; - let mask1 = (&mask1_i).as_ref(); - - grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - api.set_active_account(m, "mining")?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; - assert!(refreshed); - assert_eq!(txs.len(), bh as usize + 1); - Ok(()) - })?; - - // Try using the self-send method, splitting up outputs for the fun of it - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "send", - "-m", - "self", - "-d", - "mining", - "-g", - "Self love", - "-o", - "3", - "-s", - "smallest", - "10", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - bh += 1; - - // Check our transaction log, should have bh entries + 2 for the self receives - let (wallet1, mask1_i) = - instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; - let mask1 = (&mask1_i).as_ref(); - - grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { - api.set_active_account(m, "mining")?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; - assert!(refreshed); - assert_eq!(txs.len(), bh as usize + 2); - Ok(()) - })?; - - // Another file exchange, don't send, but unlock with repair command - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "send", - "-m", - "file", - "-d", - &file_name, - "-g", - "Ain't sending", - "10", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - let arg_vec = vec!["grin-wallet", "-p", "password", "check", "-d"]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - // Another file exchange, cancel this time - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "send", - "-m", - "file", - "-d", - &file_name, - "-g", - "Ain't sending 2", - "10", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "cancel", - "-i", - "26", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - // issue an invoice tx, wallet 2 - let file_name = format!("{}/invoice.slate", test_dir); - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "invoice", - "-d", - &file_name, - "-g", - "Please give me your precious grins. Love, Yeast", - "65", - ]; - execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; - let output_file_name = format!("{}/invoice.slate.paid", test_dir); - - // now pay the invoice tx, wallet 1 - let arg_vec = vec![ - "grin-wallet", - "-a", - "mining", - "-p", - "password", - "pay", - "-i", - &file_name, - "-d", - &output_file_name, - "-g", - "Here you go", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - // and finalize, wallet 2 - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "finalize", - "-i", - &output_file_name, - ]; - execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; - - // bit more mining - let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); - //bh += 5; - - // txs and outputs (mostly spit out for a visual in test logs) - let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "txs"]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - // message output (mostly spit out for a visual in test logs) - let arg_vec = vec![ - "grin-wallet", - "-p", - "password", - "-a", - "mining", - "txs", - "-i", - "10", - ]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - // txs and outputs (mostly spit out for a visual in test logs) - let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "outputs"]; - execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; - - let arg_vec = vec!["grin-wallet", "-p", "password", "txs"]; - execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; - - let arg_vec = vec!["grin-wallet", "-p", "password", "outputs"]; - execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; - - // get tx output via -tx parameter - let mut tx_id = "".to_string(); - grin_wallet_controller::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { - api.set_active_account(m, "default")?; - let (_, txs) = api.retrieve_txs(m, true, None, None)?; - let some_tx_id = txs[0].tx_slate_id.clone(); - assert!(some_tx_id.is_some()); - tx_id = some_tx_id.unwrap().to_hyphenated().to_string().clone(); - Ok(()) - })?; - let arg_vec = vec!["grin-wallet", "-p", "password", "txs", "-t", &tx_id[..]]; - execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; - - // let logging finish - thread::sleep(Duration::from_millis(200)); - Ok(()) - } - - #[test] - fn wallet_command_line() { - let test_dir = "target/test_output/command_line"; - if let Err(e) = command_line_test_impl(test_dir) { - panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); - } - } -} diff --git a/src/bin/grin-wallet.rs b/src/bin/grin-wallet.rs index 843890745..531833b29 100644 --- a/src/bin/grin-wallet.rs +++ b/src/bin/grin-wallet.rs @@ -23,12 +23,11 @@ use crate::core::global; use crate::util::init_logger; use clap::App; use grin_wallet_config as config; -use grin_wallet_util::grin_api as api; use grin_wallet_util::grin_core as core; use grin_wallet_util::grin_util as util; use std::env; -mod cmd; +use grin_wallet::cmd; // include build information pub mod built_info { diff --git a/src/bin/cmd/mod.rs b/src/cmd/mod.rs similarity index 94% rename from src/bin/cmd/mod.rs rename to src/cmd/mod.rs index f5f6f04f4..150a07fef 100644 --- a/src/bin/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -13,7 +13,6 @@ // limitations under the License. mod wallet; -mod wallet_args; -mod wallet_tests; +pub mod wallet_args; pub use self::wallet::wallet_command; diff --git a/src/bin/cmd/wallet.rs b/src/cmd/wallet.rs similarity index 99% rename from src/bin/cmd/wallet.rs rename to src/cmd/wallet.rs index 05e81bf12..7ce082a84 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/cmd/wallet.rs @@ -51,7 +51,7 @@ pub fn wallet_command(wallet_args: &ArgMatches<'_>, config: GlobalWalletConfig) } // ... if node isn't available, allow offline functions - let res = wallet_args::wallet_command(wallet_args, wallet_config, node_client); + let res = wallet_args::wallet_command(wallet_args, wallet_config, node_client, false); // we need to give log output a chance to catch up before exiting thread::sleep(Duration::from_millis(100)); diff --git a/src/bin/cmd/wallet_args.rs b/src/cmd/wallet_args.rs similarity index 99% rename from src/bin/cmd/wallet_args.rs rename to src/cmd/wallet_args.rs index 8424a8a3d..35083a225 100644 --- a/src/bin/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -582,6 +582,7 @@ pub fn parse_issue_invoice_args( pub fn parse_process_invoice_args( args: &ArgMatches, + prompt: bool, ) -> Result { // TODO: display and prompt for confirmation of what we're doing // message @@ -636,7 +637,7 @@ pub fn parse_process_invoice_args( // file input only let tx_file = parse_required(args, "input")?; - if cfg!(not(test)) { + if prompt { // Now we need to prompt the user whether they want to do this, // which requires reading the slate @@ -754,6 +755,7 @@ pub fn wallet_command( wallet_args: &ArgMatches, mut wallet_config: WalletConfig, mut node_client: C, + test_mode: bool, ) -> Result where C: NodeClient + 'static + Clone, @@ -815,7 +817,7 @@ where let mask = lc.open_wallet( None, prompt_password(&global_wallet_args.password), - true, + false, false, )?; if let Some(account) = wallet_args.value_of("account") { @@ -854,6 +856,7 @@ where ("owner_api", Some(_)) => { let mut g = global_wallet_args.clone(); g.tls_conf = None; + print!("mask: {:?}", keychain_mask); command::owner_api(wallet, keychain_mask, &wallet_config, &g) } ("web", Some(_)) => { @@ -885,7 +888,7 @@ where command::issue_invoice_tx(wallet, km, a) } ("pay", Some(args)) => { - let a = arg_parse!(parse_process_invoice_args(&args)); + let a = arg_parse!(parse_process_invoice_args(&args, !test_mode)); command::process_invoice( wallet, km, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..ac6bf6747 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,18 @@ +// Copyright 2019 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 grin_wallet_config as config; +use grin_wallet_util::grin_api as api; +use grin_wallet_util::grin_util as util; + +pub mod cmd; diff --git a/tests/cmd_line_basic.rs b/tests/cmd_line_basic.rs new file mode 100644 index 000000000..b9a4f39c5 --- /dev/null +++ b/tests/cmd_line_basic.rs @@ -0,0 +1,494 @@ +// Copyright 2019 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 +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_wallet_impls::DefaultLCProvider; +use grin_wallet_util::grin_keychain::ExtKeychain; + +mod common; +use common::{execute_command, initial_setup_wallet, instantiate_wallet, setup}; + +/// command line tests +fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // load app yaml. If it don't exist, just say so and exit + let yml = load_yaml!("../src/bin/grin-wallet.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()); + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + + // add wallet to proxy + //let wallet1 = test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone()); + let config1 = initial_setup_wallet(test_dir, "wallet1"); + let (wallet1, mask1_i) = + instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet1.clone(), + mask1_i.clone(), + ); + + // Create wallet 2 + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let config2 = initial_setup_wallet(test_dir, "wallet2"); + let (wallet2, mask2_i) = + instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?; + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2_i.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)?; + + // 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 + // (TODO: Be able to stop listeners so we can test this better) + let (wallet1, mask1_i) = + instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + let mask1 = (&mask1_i).as_ref(); + grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + api.set_active_account(m, "mining")?; + Ok(()) + })?; + + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let very_long_message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + This part should all be truncated"; + + // Update info and check + let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "info"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // try a file exchange + let file_name = format!("{}/tx1.part_tx", test_dir); + let response_file_name = format!("{}/tx1.part_tx.response", test_dir); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + very_long_message, + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "account_1", + "receive", + "-i", + &file_name, + "-g", + "Thanks, Yeast!", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + // shouldn't be allowed to receive twice + assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "finalize", + "-i", + &response_file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + let (wallet1, mask1_i) = + instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + let mask1 = (&mask1_i).as_ref(); + + // Check our transaction log, should have 10 entries + grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + api.set_active_account(m, "mining")?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize); + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false); + bh += 10; + + // update info for each + let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "info"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "account_1", "info"]; + execute_command(&app, test_dir, "wallet2", &client1, arg_vec)?; + + // check results in wallet 2 + let (wallet2, mask2_i) = + instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?; + let mask2 = (&mask2_i).as_ref(); + + grin_wallet_controller::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { + api.set_active_account(m, "account_1")?; + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.amount_currently_spendable, 10_000_000_000); + Ok(()) + })?; + + // Self-send to same account, using smallest strategy + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + "Love, Yeast, Smallest", + "-s", + "smallest", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "receive", + "-i", + &file_name, + "-g", + "Thanks, Yeast!", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "finalize", + "-i", + &response_file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + // Check our transaction log, should have bh entries + one for the self receive + let (wallet1, mask1_i) = + instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + let mask1 = (&mask1_i).as_ref(); + + grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + api.set_active_account(m, "mining")?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize + 1); + Ok(()) + })?; + + // Try using the self-send method, splitting up outputs for the fun of it + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "self", + "-d", + "mining", + "-g", + "Self love", + "-o", + "3", + "-s", + "smallest", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + // Check our transaction log, should have bh entries + 2 for the self receives + let (wallet1, mask1_i) = + instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + let mask1 = (&mask1_i).as_ref(); + + grin_wallet_controller::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { + api.set_active_account(m, "mining")?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize + 2); + Ok(()) + })?; + + // Another file exchange, don't send, but unlock with repair command + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + "Ain't sending", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password", "check", "-d"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Another file exchange, cancel this time + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + "Ain't sending 2", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "cancel", + "-i", + "26", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // issue an invoice tx, wallet 2 + let file_name = format!("{}/invoice.slate", test_dir); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "invoice", + "-d", + &file_name, + "-g", + "Please give me your precious grins. Love, Yeast", + "65", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + let output_file_name = format!("{}/invoice.slate.paid", test_dir); + + // now pay the invoice tx, wallet 1 + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password", + "pay", + "-i", + &file_name, + "-d", + &output_file_name, + "-g", + "Here you go", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // and finalize, wallet 2 + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "finalize", + "-i", + &output_file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // bit more mining + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + //bh += 5; + + // txs and outputs (mostly spit out for a visual in test logs) + let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // message output (mostly spit out for a visual in test logs) + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "-a", + "mining", + "txs", + "-i", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // txs and outputs (mostly spit out for a visual in test logs) + let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "outputs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password", "txs"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password", "outputs"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // get tx output via -tx parameter + let mut tx_id = "".to_string(); + grin_wallet_controller::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { + api.set_active_account(m, "default")?; + let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let some_tx_id = txs[0].tx_slate_id.clone(); + assert!(some_tx_id.is_some()); + tx_id = some_tx_id.unwrap().to_hyphenated().to_string().clone(); + Ok(()) + })?; + let arg_vec = vec!["grin-wallet", "-p", "password", "txs", "-t", &tx_id[..]]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_command_line() { + let test_dir = "target/test_output/command_line"; + if let Err(e) = command_line_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 000000000..fa20960a3 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,297 @@ +// Copyright 2019 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. + +//! Common functions for wallet integration tests +extern crate grin_wallet; + +use grin_wallet_impls::test_framework::LocalWalletClient; +use grin_wallet_util::grin_util as util; + +use clap::{App, ArgMatches}; +use std::path::PathBuf; +use std::sync::Arc; +use std::{env, fs}; +use util::{Mutex, ZeroingString}; + +use grin_wallet_config::{GlobalWalletConfig, WalletConfig, GRIN_WALLET_DIR}; +use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; +use grin_wallet_libwallet::{WalletInfo, WalletInst}; +use grin_wallet_util::grin_core::global::{self, ChainTypes}; +use grin_wallet_util::grin_keychain::ExtKeychain; +use util::secp::key::SecretKey; + +use grin_wallet::cmd::wallet_args; +use grin_wallet_util::grin_api as api; + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::thread; +use std::time::Duration; +use url::Url; + +// Set up 2 wallets and launch the test proxy behind them +#[macro_export] +macro_rules! setup_proxy { + ($test_dir: expr, $chain: ident, $wallet1: ident, $client1: ident, $mask1: ident, $wallet2: ident, $client2: ident, $mask2: ident) => { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new($test_dir); + let $chain = wallet_proxy.chain.clone(); + + // load app yaml. If it don't exist, just say so and exit + let yml = load_yaml!("../src/bin/grin-wallet.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())?; + + // add wallet to proxy + let config1 = initial_setup_wallet($test_dir, "wallet1"); + //config1.owner_api_listen_port = Some(13420); + let ($wallet1, mask1_i) = + instantiate_wallet(config1.clone(), $client1.clone(), "password", "default")?; + let $mask1 = (&mask1_i).as_ref(); + wallet_proxy.add_wallet( + "wallet1", + $client1.get_send_instance(), + $wallet1.clone(), + mask1_i.clone(), + ); + + // Create wallet 2, which will run a listener + let $client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, $test_dir, "wallet2", &$client2, arg_vec.clone())?; + + let config2 = initial_setup_wallet($test_dir, "wallet2"); + //config2.api_listen_port = 23415; + let ($wallet2, mask2_i) = + instantiate_wallet(config2.clone(), $client2.clone(), "password", "default")?; + let $mask2 = (&mask2_i).as_ref(); + wallet_proxy.add_wallet( + "wallet2", + $client2.get_send_instance(), + $wallet2.clone(), + mask2_i.clone(), + ); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + }; +} + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +pub 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<(), grin_wallet_controller::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(grin_wallet_controller::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() { + ("init", Some(init_args)) => { + // wallet init command should spit out its config file then continue + // (if desired) + if init_args.is_present("here") { + let _ = config_command_wallet(wallet_dir, wallet_name); + } + init_args.to_owned() + } + _ => ArgMatches::new(), + } +} +// +// Helper to create an instance of the LMDB wallet +pub fn instantiate_wallet( + mut wallet_config: WalletConfig, + node_client: LocalWalletClient, + passphrase: &str, + account: &str, +) -> Result< + ( + Arc< + Mutex< + Box< + WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + Option, + ), + grin_wallet_controller::Error, +> { + wallet_config.chain_type = None; + let mut wallet = Box::new(DefaultWalletImpl::::new(node_client).unwrap()) + as Box< + WalletInst< + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet.lc_provider().unwrap(); + // legacy hack to avoid the need for changes in existing grin-wallet.toml files + // remove `wallet_data` from end of path as + // new lifecycle provider assumes grin_wallet.toml is in root of data directory + let mut top_level_wallet_dir = PathBuf::from(wallet_config.clone().data_file_dir); + if top_level_wallet_dir.ends_with(GRIN_WALLET_DIR) { + top_level_wallet_dir.pop(); + wallet_config.data_file_dir = top_level_wallet_dir.to_str().unwrap().into(); + } + lc.set_wallet_directory(&wallet_config.data_file_dir); + let keychain_mask = lc + .open_wallet(None, ZeroingString::from(passphrase), true, false) + .unwrap(); + let wallet_inst = lc.wallet_inst()?; + wallet_inst.set_parent_key_id_by_name(account)?; + Ok((Arc::new(Mutex::new(wallet)), keychain_mask)) +} + +pub 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 _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); + let mut config = initial_setup_wallet(test_dir, wallet_name); + //unset chain type so it doesn't get reset + config.chain_type = None; + wallet_args::wallet_command(&args, config.clone(), client.clone(), true) +} + +pub fn post(url: &Url, api_secret: Option, input: &IN) -> Result +where + IN: Serialize, +{ + // 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) +} + +pub fn send_request( + id: u64, + dest: &str, + req: &str, +) -> Result, api::Error> +where + OUT: DeserializeOwned, +{ + let url = Url::parse(dest).unwrap(); + let req: Value = serde_json::from_str(req).unwrap(); + let res: String = post(&url, None, &req).map_err(|e| { + let err_string = format!("{}", e); + println!("{}", err_string); + thread::sleep(Duration::from_millis(200)); + e + })?; + let res = serde_json::from_str(&res).unwrap(); + let res = easy_jsonrpc::Response::from_json_response(res).unwrap(); + let res = res.outputs.get(&id).unwrap().clone().unwrap(); + if res["Err"] != json!(null) { + Ok(Err(WalletAPIReturnError { + message: res["Err"].as_str().unwrap().to_owned(), + })) + } else { + // deserialize result into expected type + let value: OUT = serde_json::from_value(res["Ok"].clone()).unwrap(); + Ok(Ok(value)) + } +} + +// Types to make working with json responses easier +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WalletAPIReturnError { + message: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RetrieveSummaryInfoResp(pub bool, pub WalletInfo); diff --git a/tests/data/v3_reqs/open_wallet.req.json b/tests/data/v3_reqs/open_wallet.req.json new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/v3_reqs/retrieve_info.req.json b/tests/data/v3_reqs/retrieve_info.req.json new file mode 100644 index 000000000..a70f745d3 --- /dev/null +++ b/tests/data/v3_reqs/retrieve_info.req.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "method": "retrieve_summary_info", + "params": { + "token": null, + "refresh_from_node": true, + "minimum_confirmations": 1 + }, + "id": 1 +} diff --git a/tests/owner_v3.rs b/tests/owner_v3.rs new file mode 100644 index 000000000..f5f1db84a --- /dev/null +++ b/tests/owner_v3.rs @@ -0,0 +1,76 @@ +// Copyright 2019 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. + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_wallet_impls::DefaultLCProvider; +use grin_wallet_util::grin_keychain::ExtKeychain; + +#[macro_use] +mod common; +use common::RetrieveSummaryInfoResp; +use common::{execute_command, initial_setup_wallet, instantiate_wallet, send_request, setup}; + +#[test] +fn owner_v3() -> Result<(), grin_wallet_controller::Error> { + let test_dir = "target/test_output/owner_v3"; + setup(test_dir); + + setup_proxy!(test_dir, chain, wallet1, client1, mask1, wallet2, client2, _mask2); + + // add some blocks manually + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // run the owner listener on wallet 1 + let arg_vec = vec!["grin-wallet", "-p", "password", "owner_api"]; + // Set running + thread::spawn(move || { + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).unwrap(); + }); + + // run the foreign listener for wallet 2 + let arg_vec = vec!["grin-wallet", "-p", "password", "listen"]; + // Set owner listener running + thread::spawn(move || { + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone()).unwrap(); + }); + + thread::sleep(Duration::from_millis(200)); + + // Send simple retrieve_info request to owner listener + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request(1, "http://127.0.0.1:3420/v3/owner", req)?; + assert!(res.is_ok()); + let value: RetrieveSummaryInfoResp = res.unwrap(); + assert_eq!(value.1.amount_currently_spendable, 420000000000); + println!("Response: {:?}", value); + Ok(()) +}