From 17bbb71a698ad9bdf927a7211618ef7810f816a6 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 24 Oct 2019 17:11:56 -0700 Subject: [PATCH 1/8] Add sync status command --- lib/src/commands.rs | 29 +++++++++++++++++++++++ lib/src/lightclient.rs | 54 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index eb0f270..b25f369 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -32,6 +32,34 @@ impl Command for SyncCommand { } } + +struct SyncStatusCommand {} +impl Command for SyncStatusCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Get the sync status of the wallet"); + h.push("Usage:"); + h.push("syncstatus"); + h.push(""); + + h.join("\n") + } + + fn short_help(&self) -> String { + "Get the sync status of the wallet".to_string() + } + + fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { + let status = lightclient.do_scan_status(); + match status.is_syncing { + false => object!{ "syncing" => "false" }, + true => object!{ "syncing" => "true", + "synced_blocks" => status.synced_blocks, + "total_blocks" => status.total_blocks } + }.pretty(2) + } +} + struct RescanCommand {} impl Command for RescanCommand { fn help(&self) -> String { @@ -691,6 +719,7 @@ pub fn get_commands() -> Box>> { let mut map: HashMap> = HashMap::new(); map.insert("sync".to_string(), Box::new(SyncCommand{})); + map.insert("syncstatus".to_string(), Box::new(SyncStatusCommand{})); map.insert("rescan".to_string(), Box::new(RescanCommand{})); map.insert("help".to_string(), Box::new(HelpCommand{})); map.insert("balance".to_string(), Box::new(BalanceCommand{})); diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 0be799d..04d3364 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -31,6 +31,22 @@ pub const DEFAULT_SERVER: &str = "https://lightd-main.zecwallet.co:443"; pub const WALLET_NAME: &str = "zecwallet-light-wallet.dat"; pub const LOGFILE_NAME: &str = "zecwallet-light-wallet.debug.log"; +#[derive(Clone, Debug)] +pub struct WalletStatus { + pub is_syncing: bool, + pub total_blocks: u64, + pub synced_blocks: u64, +} + +impl WalletStatus { + pub fn new() -> Self { + WalletStatus { + is_syncing: false, + total_blocks: 0, + synced_blocks: 0 + } + } +} #[derive(Clone, Debug)] pub struct LightClientConfig { @@ -203,6 +219,7 @@ pub struct LightClient { pub sapling_spend : Vec, sync_lock : Mutex<()>, + sync_status : Arc>, // The current syncing status of the Wallet. } impl LightClient { @@ -235,6 +252,7 @@ impl LightClient { sapling_output : vec![], sapling_spend : vec![], sync_lock : Mutex::new(()), + sync_status : Arc::new(RwLock::new(WalletStatus::new())), }; l.set_wallet_initial_state(0); @@ -260,6 +278,7 @@ impl LightClient { sapling_output : vec![], sapling_spend : vec![], sync_lock : Mutex::new(()), + sync_status : Arc::new(RwLock::new(WalletStatus::new())), }; l.set_wallet_initial_state(latest_block); @@ -283,6 +302,7 @@ impl LightClient { sapling_output : vec![], sapling_spend : vec![], sync_lock : Mutex::new(()), + sync_status : Arc::new(RwLock::new(WalletStatus::new())), }; println!("Setting birthday to {}", birthday); @@ -310,6 +330,7 @@ impl LightClient { sapling_output : vec![], sapling_spend : vec![], sync_lock : Mutex::new(()), + sync_status : Arc::new(RwLock::new(WalletStatus::new())), }; lc.read_sapling_params(); @@ -735,6 +756,11 @@ impl LightClient { response } + /// Return the syncing status of the wallet + pub fn do_scan_status(&self) -> WalletStatus { + self.sync_status.read().unwrap().clone() + } + pub fn do_sync(&self, print_updates: bool) -> String { // We can only do one sync at a time because we sync blocks in serial order // If we allow multiple syncs, they'll all get jumbled up. @@ -750,10 +776,12 @@ impl LightClient { // This will hold the latest block fetched from the RPC let latest_block_height = Arc::new(AtomicU64::new(0)); let lbh = latest_block_height.clone(); - fetch_latest_block(&self.get_server_uri(), self.config.no_cert_verification, move |block: BlockId| { + fetch_latest_block(&self.get_server_uri(), self.config.no_cert_verification, + move |block: BlockId| { lbh.store(block.height, Ordering::SeqCst); }); let latest_block = latest_block_height.load(Ordering::SeqCst); + if latest_block < last_scanned_height { let w = format!("Server's latest block({}) is behind ours({})", latest_block, last_scanned_height); @@ -772,6 +800,13 @@ impl LightClient { return "".to_string(); } + { + let mut status = self.sync_status.write().unwrap(); + status.is_syncing = true; + status.synced_blocks = last_scanned_height; + status.total_blocks = latest_block; + } + // Count how many bytes we've downloaded let bytes_downloaded = Arc::new(AtomicUsize::new(0)); @@ -795,11 +830,18 @@ impl LightClient { info!("Start height is {}", start_height); // Show updates only if we're syncing a lot of blocks - if print_updates && end_height - start_height > 100 { + if print_updates && (latest_block - start_height) > 100 { print!("Syncing {}/{}\r", start_height, latest_block); io::stdout().flush().ok().expect("Could not flush stdout"); } + { + let mut status = self.sync_status.write().unwrap(); + status.is_syncing = true; + status.synced_blocks = start_height; + status.total_blocks = latest_block; + } + // Fetch compact blocks info!("Fetching blocks {}-{}", start_height, end_height); @@ -909,7 +951,13 @@ impl LightClient { info!("Synced to {}, Downloaded {} kB", latest_block, bytes_downloaded.load(Ordering::SeqCst) / 1024); responses.push(format!("Synced to {}, Downloaded {} kB", latest_block, bytes_downloaded.load(Ordering::SeqCst) / 1024)); - + { + let mut status = self.sync_status.write().unwrap(); + status.is_syncing = false; + status.synced_blocks = latest_block; + status.total_blocks = latest_block; + } + // Get the Raw transaction for all the wallet transactions // We need to first copy over the Txids from the wallet struct, because From 865a72442db4ee1862fa776514da4a4a1c935b94 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 24 Oct 2019 18:26:25 -0700 Subject: [PATCH 2/8] Add syncstatus command --- cli/src/main.rs | 13 +++++-- lib/src/commands.rs | 77 ++++++++++++++++++++++++------------------ lib/src/lightclient.rs | 19 ++++++----- 3 files changed, 66 insertions(+), 43 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 48574ef..78fa96e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -221,7 +221,12 @@ fn startup(server: http::Uri, dangerous: bool, seed: Option, birthday: u if first_sync { let update = lightclient.do_sync(true); if print_updates { - println!("{}", update); + match update { + Ok(j) => { + println!("{}", j.pretty(2)); + }, + Err(e) => println!("{}", e) + } } } @@ -329,7 +334,11 @@ fn command_loop(lightclient: Arc) -> (Sender<(String, Vec)> Err(_) => { // Timeout. Do a sync to keep the wallet up-to-date. False to whether to print updates on the console info!("Timeout, doing a sync"); - lc.do_sync(false); + match lc.do_sync(false) { + Ok(_) => {}, + Err(e) => {error!("{}", e)} + } + } } } diff --git a/lib/src/commands.rs b/lib/src/commands.rs index b25f369..6f24e40 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -28,7 +28,10 @@ impl Command for SyncCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - lightclient.do_sync(true) + match lightclient.do_sync(true) { + Ok(j) => j.pretty(2), + Err(e) => e + } } } @@ -79,7 +82,10 @@ impl Command for RescanCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - lightclient.do_rescan() + match lightclient.do_rescan() { + Ok(j) => j.pretty(2), + Err(e) => e + } } } @@ -144,9 +150,7 @@ impl Command for InfoCommand { "Get the lightwalletd server's info".to_string() } - fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - lightclient.do_sync(true); - + fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { lightclient.do_info() } } @@ -169,9 +173,10 @@ impl Command for BalanceCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - lightclient.do_sync(true); - - format!("{}", lightclient.do_balance().pretty(2)) + match lightclient.do_sync(true) { + Ok(_) => format!("{}", lightclient.do_balance().pretty(2)), + Err(e) => e + } } } @@ -434,7 +439,7 @@ impl Command for SendCommand { } // Check for a single argument that can be parsed as JSON - if args.len() == 1 { + let send_args = if args.len() == 1 { // Sometimes on the command line, people use "'" for the quotes, which json::parse doesn't // understand. So replace it with double-quotes let arg_list = args[0].replace("'", "\""); @@ -455,20 +460,14 @@ impl Command for SendCommand { if !j.has_key("address") || !j.has_key("amount") { Err(format!("Need 'address' and 'amount'\n")) } else { - Ok((j["address"].as_str().unwrap(), j["amount"].as_u64().unwrap(), j["memo"].as_str().map(|s| s.to_string()))) + Ok((j["address"].as_str().unwrap().to_string().clone(), j["amount"].as_u64().unwrap(), j["memo"].as_str().map(|s| s.to_string().clone()))) } - }).collect::)>, String>>(); + }).collect::)>, String>>(); - let send_args = match maybe_send_args { - Ok(a) => a, + match maybe_send_args { + Ok(a) => a.clone(), Err(s) => { return format!("Error: {}\n{}", s, self.help()); } - }; - - lightclient.do_sync(true); - match lightclient.do_send(send_args) { - Ok(txid) => { object!{ "txid" => txid } }, - Err(e) => { object!{ "error" => e } } - }.pretty(2) + } } else if args.len() == 2 || args.len() == 3 { // Make sure we can parse the amount let value = match args[1].parse::() { @@ -480,13 +479,21 @@ impl Command for SendCommand { let memo = if args.len() == 3 { Some(args[2].to_string()) } else {None}; - lightclient.do_sync(true); - match lightclient.do_send(vec!((args[0], value, memo))) { - Ok(txid) => { object!{ "txid" => txid } }, - Err(e) => { object!{ "error" => e } } - }.pretty(2) + vec![(args[0].to_string(), value, memo)] } else { - self.help() + return self.help() + }; + + match lightclient.do_sync(true) { + Ok(_) => { + // Convert to the right format. String -> &str. + let tos = send_args.iter().map(|(a, v, m)| (a.as_str(), *v, m.clone()) ).collect::>(); + match lightclient.do_send(tos) { + Ok(txid) => { object!{ "txid" => txid } }, + Err(e) => { object!{ "error" => e } } + }.pretty(2) + }, + Err(e) => e } } } @@ -568,9 +575,12 @@ impl Command for TransactionsCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - lightclient.do_sync(true); - - format!("{}", lightclient.do_list_transactions().pretty(2)) + match lightclient.do_sync(true) { + Ok(_) => { + format!("{}", lightclient.do_list_transactions().pretty(2)) + }, + Err(e) => e + } } } @@ -662,9 +672,12 @@ impl Command for NotesCommand { false }; - lightclient.do_sync(true); - - format!("{}", lightclient.do_list_notes(all_notes).pretty(2)) + match lightclient.do_sync(true) { + Ok(_) => { + format!("{}", lightclient.do_list_notes(all_notes).pretty(2)) + }, + Err(e) => e + } } } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 04d3364..f2f659c 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -741,7 +741,7 @@ impl LightClient { Ok(array![new_address]) } - pub fn do_rescan(&self) -> String { + pub fn do_rescan(&self) -> Result { info!("Rescan starting"); // First, clear the state from the wallet self.wallet.read().unwrap().clear_blocks(); @@ -761,7 +761,7 @@ impl LightClient { self.sync_status.read().unwrap().clone() } - pub fn do_sync(&self, print_updates: bool) -> String { + pub fn do_sync(&self, print_updates: bool) -> Result { // We can only do one sync at a time because we sync blocks in serial order // If we allow multiple syncs, they'll all get jumbled up. let _lock = self.sync_lock.lock().unwrap(); @@ -786,7 +786,7 @@ impl LightClient { if latest_block < last_scanned_height { let w = format!("Server's latest block({}) is behind ours({})", latest_block, last_scanned_height); warn!("{}", w); - return w; + return Err(w); } info!("Latest block is {}", latest_block); @@ -797,7 +797,7 @@ impl LightClient { // If there's nothing to scan, just return if last_scanned_height == latest_block { info!("Nothing to sync, returning"); - return "".to_string(); + return Ok(object!{ "result" => "success" }) } { @@ -893,7 +893,7 @@ impl LightClient { // Make sure we're not re-orging too much! if total_reorg > (crate::lightwallet::MAX_REORG - 1) as u64 { error!("Reorg has now exceeded {} blocks!", crate::lightwallet::MAX_REORG); - return format!("Reorg has exceeded {} blocks. Aborting.", crate::lightwallet::MAX_REORG); + return Err(format!("Reorg has exceeded {} blocks. Aborting.", crate::lightwallet::MAX_REORG)); } if invalid_height > 0 { @@ -947,10 +947,7 @@ impl LightClient { println!(""); // New line to finish up the updates } - let mut responses = vec![]; - info!("Synced to {}, Downloaded {} kB", latest_block, bytes_downloaded.load(Ordering::SeqCst) / 1024); - responses.push(format!("Synced to {}, Downloaded {} kB", latest_block, bytes_downloaded.load(Ordering::SeqCst) / 1024)); { let mut status = self.sync_status.write().unwrap(); status.is_syncing = false; @@ -988,7 +985,11 @@ impl LightClient { }); }; - responses.join("\n") + Ok(object!{ + "result" => "success", + "latest_block" => latest_block, + "downloaded_bytes" => bytes_downloaded.load(Ordering::SeqCst) + }) } pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> Result { From 3a76d9260794d290e258acd384dd987ba7eec9fa Mon Sep 17 00:00:00 2001 From: Za Wilcox Date: Fri, 25 Oct 2019 18:23:39 -0600 Subject: [PATCH 3/8] factor logic out of main.rs into lib.rs (#9) --- cli/src/lib.rs | 295 +++++++++++++++++++++++++++++++++++++ cli/src/main.rs | 294 +++--------------------------------- lib/src/lib.rs | 1 - lib/src/startup_helpers.rs | 20 --- 4 files changed, 313 insertions(+), 297 deletions(-) create mode 100644 cli/src/lib.rs delete mode 100644 lib/src/startup_helpers.rs diff --git a/cli/src/lib.rs b/cli/src/lib.rs new file mode 100644 index 0000000..a2dec3d --- /dev/null +++ b/cli/src/lib.rs @@ -0,0 +1,295 @@ +use std::io::{self, Error, ErrorKind}; +use std::sync::Arc; +use std::sync::mpsc::{channel, Sender, Receiver}; + +use log::{info, error, LevelFilter}; +use log4rs::append::rolling_file::RollingFileAppender; +use log4rs::encode::pattern::PatternEncoder; +use log4rs::config::{Appender, Config, Root}; +use log4rs::filter::threshold::ThresholdFilter; +use log4rs::append::rolling_file::policy::compound::{ + CompoundPolicy, + trigger::size::SizeTrigger, + roll::fixed_window::FixedWindowRoller, +}; + + +use zecwalletlitelib::{commands, + lightclient::{LightClient, LightClientConfig}, +}; + +#[macro_export] +macro_rules! configure_clapapp { + ( $freshapp: expr ) => { + $freshapp.version("1.0.0") + .arg(Arg::with_name("dangerous") + .long("dangerous") + .help("Disable server TLS certificate verification. Use this if you're running a local lightwalletd with a self-signed certificate. WARNING: This is dangerous, don't use it with a server that is not your own.") + .takes_value(false)) + .arg(Arg::with_name("nosync") + .help("By default, zecwallet-cli will sync the wallet at startup. Pass --nosync to prevent the automatic sync at startup.") + .long("nosync") + .short("n") + .takes_value(false)) + .arg(Arg::with_name("recover") + .long("recover") + .help("Attempt to recover the seed from the wallet") + .takes_value(false)) + .arg(Arg::with_name("seed") + .short("s") + .long("seed") + .value_name("seed_phrase") + .help("Create a new wallet with the given 24-word seed phrase. Will fail if wallet already exists") + .takes_value(true)) + .arg(Arg::with_name("birthday") + .long("birthday") + .value_name("birthday") + .help("Specify wallet birthday when restoring from seed. This is the earlist block height where the wallet has a transaction.") + .takes_value(true)) + .arg(Arg::with_name("server") + .long("server") + .value_name("server") + .help("Lightwalletd server to connect to.") + .takes_value(true) + .default_value(lightclient::DEFAULT_SERVER)) + .arg(Arg::with_name("COMMAND") + .help("Command to execute. If a command is not specified, zecwallet-cli will start in interactive mode.") + .required(false) + .index(1)) + .arg(Arg::with_name("PARAMS") + .help("Params to execute command with. Run the 'help' command to get usage help.") + .required(false) + .multiple(true)) + }; +} + +/// This function is only tested against Linux. +pub fn report_permission_error() { + let user = std::env::var("USER").expect( + "Unexpected error reading value of $USER!"); + let home = std::env::var("HOME").expect( + "Unexpected error reading value of $HOME!"); + let current_executable = std::env::current_exe() + .expect("Unexpected error reporting executable path!"); + eprintln!("USER: {}", user); + eprintln!("HOME: {}", home); + eprintln!("Executable: {}", current_executable.display()); + if home == "/" { + eprintln!("User {} must have permission to write to '{}.zcash/' .", + user, + home); + } else { + eprintln!("User {} must have permission to write to '{}/.zcash/' .", + user, + home); + } +} + +/// Build the Logging config +pub fn get_log_config(config: &LightClientConfig) -> io::Result { + let window_size = 3; // log0, log1, log2 + let fixed_window_roller = + FixedWindowRoller::builder().build("zecwallet-light-wallet-log{}",window_size).unwrap(); + let size_limit = 5 * 1024 * 1024; // 5MB as max log file size to roll + let size_trigger = SizeTrigger::new(size_limit); + let compound_policy = CompoundPolicy::new(Box::new(size_trigger),Box::new(fixed_window_roller)); + + Config::builder() + .appender( + Appender::builder() + .filter(Box::new(ThresholdFilter::new(LevelFilter::Info))) + .build( + "logfile", + Box::new( + RollingFileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{d} {l}::{m}{n}"))) + .build(config.get_log_path(), Box::new(compound_policy))?, + ), + ), + ) + .build( + Root::builder() + .appender("logfile") + .build(LevelFilter::Debug), + ) + .map_err(|e|Error::new(ErrorKind::Other, format!("{}", e))) +} + + +pub fn startup(server: http::Uri, dangerous: bool, seed: Option, birthday: u64, first_sync: bool, print_updates: bool) + -> io::Result<(Sender<(String, Vec)>, Receiver)> { + // Try to get the configuration + let (config, latest_block_height) = LightClientConfig::create(server.clone(), dangerous)?; + + // Configure logging first. + let log_config = get_log_config(&config)?; + log4rs::init_config(log_config).map_err(|e| { + std::io::Error::new(ErrorKind::Other, e) + })?; + + let lightclient = match seed { + Some(phrase) => Arc::new(LightClient::new_from_phrase(phrase, &config, birthday)?), + None => { + if config.wallet_exists() { + Arc::new(LightClient::read_from_disk(&config)?) + } else { + println!("Creating a new wallet"); + Arc::new(LightClient::new(&config, latest_block_height)?) + } + } + }; + + // Print startup Messages + info!(""); // Blank line + info!("Starting Zecwallet-CLI"); + info!("Light Client config {:?}", config); + + if print_updates { + println!("Lightclient connecting to {}", config.server); + } + + // At startup, run a sync. + if first_sync { + let update = lightclient.do_sync(true); + if print_updates { + match update { + Ok(j) => { + println!("{}", j.pretty(2)); + }, + Err(e) => println!("{}", e) + } + } + } + + // Start the command loop + let (command_tx, resp_rx) = command_loop(lightclient.clone()); + + Ok((command_tx, resp_rx)) +} + +pub fn start_interactive(command_tx: Sender<(String, Vec)>, resp_rx: Receiver) { + // `()` can be used when no completer is required + let mut rl = rustyline::Editor::<()>::new(); + + println!("Ready!"); + + let send_command = |cmd: String, args: Vec| -> String { + command_tx.send((cmd.clone(), args)).unwrap(); + match resp_rx.recv() { + Ok(s) => s, + Err(e) => { + let e = format!("Error executing command {}: {}", cmd, e); + eprintln!("{}", e); + error!("{}", e); + return "".to_string() + } + } + }; + + let info = &send_command("info".to_string(), vec![]); + let chain_name = json::parse(info).unwrap()["chain_name"].as_str().unwrap().to_string(); + + loop { + // Read the height first + let height = json::parse(&send_command("height".to_string(), vec![])).unwrap()["height"].as_i64().unwrap(); + + let readline = rl.readline(&format!("({}) Block:{} (type 'help') >> ", + chain_name, height)); + match readline { + Ok(line) => { + rl.add_history_entry(line.as_str()); + // Parse command line arguments + let mut cmd_args = match shellwords::split(&line) { + Ok(args) => args, + Err(_) => { + println!("Mismatched Quotes"); + continue; + } + }; + + if cmd_args.is_empty() { + continue; + } + + let cmd = cmd_args.remove(0); + let args: Vec = cmd_args; + + println!("{}", send_command(cmd, args)); + + // Special check for Quit command. + if line == "quit" { + break; + } + }, + Err(rustyline::error::ReadlineError::Interrupted) => { + println!("CTRL-C"); + info!("CTRL-C"); + println!("{}", send_command("save".to_string(), vec![])); + break + }, + Err(rustyline::error::ReadlineError::Eof) => { + println!("CTRL-D"); + info!("CTRL-D"); + println!("{}", send_command("save".to_string(), vec![])); + break + }, + Err(err) => { + println!("Error: {:?}", err); + break + } + } + } +} + + +pub fn command_loop(lightclient: Arc) -> (Sender<(String, Vec)>, Receiver) { + let (command_tx, command_rx) = channel::<(String, Vec)>(); + let (resp_tx, resp_rx) = channel::(); + + let lc = lightclient.clone(); + std::thread::spawn(move || { + loop { + match command_rx.recv_timeout(std::time::Duration::from_secs(5 * 60)) { + Ok((cmd, args)) => { + let args = args.iter().map(|s| s.as_ref()).collect(); + + let cmd_response = commands::do_user_command(&cmd, &args, lc.as_ref()); + resp_tx.send(cmd_response).unwrap(); + + if cmd == "quit" { + info!("Quit"); + break; + } + }, + Err(_) => { + // Timeout. Do a sync to keep the wallet up-to-date. False to whether to print updates on the console + info!("Timeout, doing a sync"); + match lc.do_sync(false) { + Ok(_) => {}, + Err(e) => {error!("{}", e)} + } + } + } + } + }); + + (command_tx, resp_rx) +} + +pub fn attempt_recover_seed() { + // Create a Light Client Config in an attempt to recover the file. + let config = LightClientConfig { + server: "0.0.0.0:0".parse().unwrap(), + chain_name: "main".to_string(), + sapling_activation_height: 0, + consensus_branch_id: "000000".to_string(), + anchor_offset: 0, + no_cert_verification: false, + data_dir: None, + }; + + match LightClient::attempt_recover_seed(&config) { + Ok(seed) => println!("Recovered seed: '{}'", seed), + Err(e) => eprintln!("Failed to recover seed. Error: {}", e) + }; +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 78fa96e..0177d5e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,116 +1,20 @@ -use std::io::{self, Error, ErrorKind}; -use std::sync::Arc; -use std::sync::mpsc::{channel, Sender, Receiver}; - -use zecwalletlitelib::{commands, startup_helpers, - lightclient::{self, LightClient, LightClientConfig}, -}; - -use log::{info, error, LevelFilter}; -use log4rs::append::rolling_file::RollingFileAppender; -use log4rs::encode::pattern::PatternEncoder; -use log4rs::config::{Appender, Config, Root}; -use log4rs::filter::threshold::ThresholdFilter; -use log4rs::append::rolling_file::policy::compound::{ - CompoundPolicy, - trigger::size::SizeTrigger, - roll::fixed_window::FixedWindowRoller, -}; - - - -/// Build the Logging config -fn get_log_config(config: &LightClientConfig) -> io::Result { - let window_size = 3; // log0, log1, log2 - let fixed_window_roller = - FixedWindowRoller::builder().build("zecwallet-light-wallet-log{}",window_size).unwrap(); - let size_limit = 5 * 1024 * 1024; // 5MB as max log file size to roll - let size_trigger = SizeTrigger::new(size_limit); - let compound_policy = CompoundPolicy::new(Box::new(size_trigger),Box::new(fixed_window_roller)); - - Config::builder() - .appender( - Appender::builder() - .filter(Box::new(ThresholdFilter::new(LevelFilter::Info))) - .build( - "logfile", - Box::new( - RollingFileAppender::builder() - .encoder(Box::new(PatternEncoder::new("{d} {l}::{m}{n}"))) - .build(config.get_log_path(), Box::new(compound_policy))?, - ), - ), - ) - .build( - Root::builder() - .appender("logfile") - .build(LevelFilter::Debug), - ) - .map_err(|e|Error::new(ErrorKind::Other, format!("{}", e))) -} - +use zecwalletlitelib::lightclient::{self, LightClientConfig}; +use zecwallet_cli::{configure_clapapp, + report_permission_error, + startup, + start_interactive, + attempt_recover_seed}; +use log::error; pub fn main() { // Get command line arguments - use clap::{Arg, App}; - let matches = App::new("Zecwallet CLI") - .version("1.1.0") - .arg(Arg::with_name("seed") - .short("s") - .long("seed") - .value_name("seed_phrase") - .help("Create a new wallet with the given 24-word seed phrase. Will fail if wallet already exists") - .takes_value(true)) - .arg(Arg::with_name("birthday") - .long("birthday") - .value_name("birthday") - .help("Specify wallet birthday when restoring from seed. This is the earlist block height where the wallet has a transaction.") - .takes_value(true)) - .arg(Arg::with_name("server") - .long("server") - .value_name("server") - .help("Lightwalletd server to connect to.") - .takes_value(true) - .default_value(lightclient::DEFAULT_SERVER)) - .arg(Arg::with_name("dangerous") - .long("dangerous") - .help("Disable server TLS certificate verification. Use this if you're running a local lightwalletd with a self-signed certificate. WARNING: This is dangerous, don't use it with a server that is not your own.") - .takes_value(false)) - .arg(Arg::with_name("recover") - .long("recover") - .help("Attempt to recover the seed from the wallet") - .takes_value(false)) - .arg(Arg::with_name("nosync") - .help("By default, zecwallet-cli will sync the wallet at startup. Pass --nosync to prevent the automatic sync at startup.") - .long("nosync") - .short("n") - .takes_value(false)) - .arg(Arg::with_name("COMMAND") - .help("Command to execute. If a command is not specified, zecwallet-cli will start in interactive mode.") - .required(false) - .index(1)) - .arg(Arg::with_name("PARAMS") - .help("Params to execute command with. Run the 'help' command to get usage help.") - .required(false) - .multiple(true)) - .get_matches(); - + use clap::{App, Arg}; + let fresh_app = App::new("Zecwallet CLI"); + let configured_app = configure_clapapp!(fresh_app); + let matches = configured_app.get_matches(); if matches.is_present("recover") { // Create a Light Client Config in an attempt to recover the file. - let config = LightClientConfig { - server: "0.0.0.0:0".parse().unwrap(), - chain_name: "main".to_string(), - sapling_activation_height: 0, - consensus_branch_id: "000000".to_string(), - anchor_offset: 0, - no_cert_verification: false, - data_dir: None, - }; - - match LightClient::attempt_recover_seed(&config) { - Ok(seed) => println!("Recovered seed: '{}'", seed), - Err(e) => eprintln!("Failed to recover seed. Error: {}", e) - }; + attempt_recover_seed(); return; } @@ -152,12 +56,12 @@ pub fn main() { Err(e) => { eprintln!("Error during startup: {}", e); error!("Error during startup: {}", e); - match e.raw_os_error() { - Some(13) => { - startup_helpers::report_permission_error(); - }, - _ => {} - } + if cfg!(target_os = "unix" ) { + match e.raw_os_error() { + Some(13) => report_permission_error(), + _ => {}, + } + }; return; } }; @@ -184,165 +88,3 @@ pub fn main() { resp_rx.recv().unwrap(); } } - -fn startup(server: http::Uri, dangerous: bool, seed: Option, birthday: u64, first_sync: bool, print_updates: bool) - -> io::Result<(Sender<(String, Vec)>, Receiver)> { - // Try to get the configuration - let (config, latest_block_height) = LightClientConfig::create(server.clone(), dangerous)?; - - // Configure logging first. - let log_config = get_log_config(&config)?; - log4rs::init_config(log_config).map_err(|e| { - std::io::Error::new(ErrorKind::Other, e) - })?; - - let lightclient = match seed { - Some(phrase) => Arc::new(LightClient::new_from_phrase(phrase, &config, birthday)?), - None => { - if config.wallet_exists() { - Arc::new(LightClient::read_from_disk(&config)?) - } else { - println!("Creating a new wallet"); - Arc::new(LightClient::new(&config, latest_block_height)?) - } - } - }; - - // Print startup Messages - info!(""); // Blank line - info!("Starting Zecwallet-CLI"); - info!("Light Client config {:?}", config); - - if print_updates { - println!("Lightclient connecting to {}", config.server); - } - - // At startup, run a sync. - if first_sync { - let update = lightclient.do_sync(true); - if print_updates { - match update { - Ok(j) => { - println!("{}", j.pretty(2)); - }, - Err(e) => println!("{}", e) - } - } - } - - // Start the command loop - let (command_tx, resp_rx) = command_loop(lightclient.clone()); - - Ok((command_tx, resp_rx)) -} - - -fn start_interactive(command_tx: Sender<(String, Vec)>, resp_rx: Receiver) { - // `()` can be used when no completer is required - let mut rl = rustyline::Editor::<()>::new(); - - println!("Ready!"); - - let send_command = |cmd: String, args: Vec| -> String { - command_tx.send((cmd.clone(), args)).unwrap(); - match resp_rx.recv() { - Ok(s) => s, - Err(e) => { - let e = format!("Error executing command {}: {}", cmd, e); - eprintln!("{}", e); - error!("{}", e); - return "".to_string() - } - } - }; - - let info = &send_command("info".to_string(), vec![]); - let chain_name = json::parse(info).unwrap()["chain_name"].as_str().unwrap().to_string(); - - loop { - // Read the height first - let height = json::parse(&send_command("height".to_string(), vec![])).unwrap()["height"].as_i64().unwrap(); - - let readline = rl.readline(&format!("({}) Block:{} (type 'help') >> ", - chain_name, height)); - match readline { - Ok(line) => { - rl.add_history_entry(line.as_str()); - // Parse command line arguments - let mut cmd_args = match shellwords::split(&line) { - Ok(args) => args, - Err(_) => { - println!("Mismatched Quotes"); - continue; - } - }; - - if cmd_args.is_empty() { - continue; - } - - let cmd = cmd_args.remove(0); - let args: Vec = cmd_args; - - println!("{}", send_command(cmd, args)); - - // Special check for Quit command. - if line == "quit" { - break; - } - }, - Err(rustyline::error::ReadlineError::Interrupted) => { - println!("CTRL-C"); - info!("CTRL-C"); - println!("{}", send_command("save".to_string(), vec![])); - break - }, - Err(rustyline::error::ReadlineError::Eof) => { - println!("CTRL-D"); - info!("CTRL-D"); - println!("{}", send_command("save".to_string(), vec![])); - break - }, - Err(err) => { - println!("Error: {:?}", err); - break - } - } - } -} - - -fn command_loop(lightclient: Arc) -> (Sender<(String, Vec)>, Receiver) { - let (command_tx, command_rx) = channel::<(String, Vec)>(); - let (resp_tx, resp_rx) = channel::(); - - let lc = lightclient.clone(); - std::thread::spawn(move || { - loop { - match command_rx.recv_timeout(std::time::Duration::from_secs(5 * 60)) { - Ok((cmd, args)) => { - let args = args.iter().map(|s| s.as_ref()).collect(); - - let cmd_response = commands::do_user_command(&cmd, &args, lc.as_ref()); - resp_tx.send(cmd_response).unwrap(); - - if cmd == "quit" { - info!("Quit"); - break; - } - }, - Err(_) => { - // Timeout. Do a sync to keep the wallet up-to-date. False to whether to print updates on the console - info!("Timeout, doing a sync"); - match lc.do_sync(false) { - Ok(_) => {}, - Err(e) => {error!("{}", e)} - } - - } - } - } - }); - - (command_tx, resp_rx) -} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 13672b7..3e4fe27 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,7 +1,6 @@ #[macro_use] extern crate rust_embed; -pub mod startup_helpers; pub mod lightclient; pub mod grpcconnector; pub mod lightwallet; diff --git a/lib/src/startup_helpers.rs b/lib/src/startup_helpers.rs deleted file mode 100644 index 0b8e4bd..0000000 --- a/lib/src/startup_helpers.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub fn report_permission_error() { - let user = std::env::var("USER").expect( - "Unexpected error reading value of $USER!"); - let home = std::env::var("HOME").expect( - "Unexpected error reading value of $HOME!"); - let current_executable = std::env::current_exe() - .expect("Unexpected error reporting executable path!"); - eprintln!("USER: {}", user); - eprintln!("HOME: {}", home); - eprintln!("Executable: {}", current_executable.display()); - if home == "/" { - eprintln!("User {} must have permission to write to '{}.zcash/' .", - user, - home); - } else { - eprintln!("User {} must have permission to write to '{}/.zcash/' .", - user, - home); - } -} From 2cbdab37fa18bc29acb6d0890b5f30dbaeecedda Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 25 Oct 2019 17:36:54 -0700 Subject: [PATCH 4/8] Make commands case insensitive --- lib/src/commands.rs | 31 ++++++++++++++++++++++++++++++- lib/src/lightclient.rs | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 6f24e40..da91bde 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -757,8 +757,37 @@ pub fn get_commands() -> Box>> { } pub fn do_user_command(cmd: &str, args: &Vec<&str>, lightclient: &LightClient) -> String { - match get_commands().get(cmd) { + match get_commands().get(&cmd.to_ascii_lowercase()) { Some(cmd) => cmd.exec(args, lightclient), None => format!("Unknown command : {}. Type 'help' for a list of commands", cmd) } } + + + + +#[cfg(test)] +pub mod tests { + use lazy_static::lazy_static; + use super::do_user_command; + use crate::lightclient::{LightClient}; + + lazy_static!{ + static ref TEST_SEED: String = "youth strong sweet gorilla hammer unhappy congress stamp left stereo riot salute road tag clean toilet artefact fork certain leopard entire civil degree wonder".to_string(); + } + + #[test] + pub fn test_command_caseinsensitive() { + let lc = LightClient::unconnected(TEST_SEED.to_string(), None).unwrap(); + + assert_eq!(do_user_command("addresses", &vec![], &lc), + do_user_command("AddReSSeS", &vec![], &lc)); + assert_eq!(do_user_command("addresses", &vec![], &lc), + do_user_command("Addresses", &vec![], &lc)); + } + + #[test] + pub fn test_nosync_commands() { + // The following commands should run + } +} diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index f2f659c..fc8fea2 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -244,7 +244,7 @@ impl LightClient { /// Method to create a test-only version of the LightClient #[allow(dead_code)] - fn unconnected(seed_phrase: String, dir: Option) -> io::Result { + pub fn unconnected(seed_phrase: String, dir: Option) -> io::Result { let config = LightClientConfig::create_unconnected("test".to_string(), dir); let mut l = LightClient { wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), &config, 0)?)), From 020994549d761d6c0a87a05f3e9c40c7a5d01c59 Mon Sep 17 00:00:00 2001 From: DenioD Date: Sat, 26 Oct 2019 21:48:41 +0200 Subject: [PATCH 5/8] fix hush config path --- lib/src/lightclient.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 3d76a22..7016e2b 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -103,7 +103,7 @@ impl LightClientConfig { zcash_data_location.push("HUSH3"); } else { zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!"); - zcash_data_location.push(".komodo/HUSH3/"); + zcash_data_location.push(".komodo/HUSH3"); }; match &self.chain_name[..] { From 9e710e0bfdc6a6cb850d8bced02c15311d356122 Mon Sep 17 00:00:00 2001 From: DenioD Date: Sun, 27 Oct 2019 21:06:17 +0100 Subject: [PATCH 6/8] change wallet.dat and .log path to ./silentdragon --- README.md | 2 +- cli/src/lib.rs | 4 ++-- lib/src/lightclient.rs | 8 ++++---- lib/src/lightwallet/startup_helpers.rs | 4 ++-- lib/src/startup_helpers.rs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1b46e78..80d8e55 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Run `silentdragonlite-cli help` to see a list of all commands. ## Notes: * If you want to run your own server, please see [SilentDragonLite-cli lightwalletd](https://github.com/MyHush/lightwalletd), and then run `./silentdragonlite-cli --server http://127.0.0.1:9069`. You might also need to pass `--dangerous` if you are using a self-signed TLS certificate. -* The log file is in `~/.komodo/HUSH3/silentdragonlite-cli.debug.log`. Wallet is stored in `~/.komodo/HUSH3/silentdragonlite-cli.dat` +* The log file is in `~/.silentdragonlite/silentdragonlite-cli.debug.log`. Wallet is stored in `~/.silentdragonlite/silentdragonlite-cli.dat` ### Note Management silentdragonlite does automatic note and utxo management, which means it doesn't allow you to manually select which address to send outgoing transactions from. It follows these principles: diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 4c31111..b7406dd 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -75,11 +75,11 @@ pub fn report_permission_error() { eprintln!("HOME: {}", home); eprintln!("Executable: {}", current_executable.display()); if home == "/" { - eprintln!("User {} must have permission to write to '{}.komodo/' .", + eprintln!("User {} must have permission to write to '{}.silentdragonlite/' .", user, home); } else { - eprintln!("User {} must have permission to write to '{}/.komodo/HUSH3/' .", + eprintln!("User {} must have permission to write to '{}/.silentdragonlite/' .", user, home); } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 7016e2b..832c3ba 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -28,8 +28,8 @@ use crate::ANCHOR_OFFSET; mod checkpoints; pub const DEFAULT_SERVER: &str = "https://"; -pub const WALLET_NAME: &str = "silentdragonlite-cli-wallet.dat"; -pub const LOGFILE_NAME: &str = "silentdragonlite-cli-wallet.debug.log"; +pub const WALLET_NAME: &str = "silentdragonlite-wallet.dat"; +pub const LOGFILE_NAME: &str = "silentdragonlite-wallet.debug.log"; #[derive(Clone, Debug)] pub struct WalletStatus { @@ -100,10 +100,10 @@ impl LightClientConfig { } else { if cfg!(target_os="macos") || cfg!(target_os="windows") { zcash_data_location = dirs::data_dir().expect("Couldn't determine app data directory!"); - zcash_data_location.push("HUSH3"); + zcash_data_location.push("silentdragonlite"); } else { zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!"); - zcash_data_location.push(".komodo/HUSH3"); + zcash_data_location.push("/.silentdragonlite"); }; match &self.chain_name[..] { diff --git a/lib/src/lightwallet/startup_helpers.rs b/lib/src/lightwallet/startup_helpers.rs index 0b5ac59..6b47f46 100644 --- a/lib/src/lightwallet/startup_helpers.rs +++ b/lib/src/lightwallet/startup_helpers.rs @@ -9,11 +9,11 @@ pub fn report_permission_error() { eprintln!("HOME: {}", home); eprintln!("Executable: {}", current_executable.display()); if home == "/" { - eprintln!("User {} must have permission to write to '{}.komodo/HUSH3/' .", + eprintln!("User {} must have permission to write to '{}.silentdragonlite/' .", user, home); } else { - eprintln!("User {} must have permission to write to '{}/.komodo/HUSH3/ .", + eprintln!("User {} must have permission to write to '{}/.silentdragonlite/ .", user, home); } diff --git a/lib/src/startup_helpers.rs b/lib/src/startup_helpers.rs index c597c5d..44fd061 100644 --- a/lib/src/startup_helpers.rs +++ b/lib/src/startup_helpers.rs @@ -9,11 +9,11 @@ pub fn report_permission_error() { eprintln!("HOME: {}", home); eprintln!("Executable: {}", current_executable.display()); if home == "/" { - eprintln!("User {} must have permission to write to '{}.komodo/HUSH3/' .", + eprintln!("User {} must have permission to write to '{}.silentdragonlite/' .", user, home); } else { - eprintln!("User {} must have permission to write to '{}/.komodo/HUSH3/ .", + eprintln!("User {} must have permission to write to '{}/.silentdragonlite/ .", user, home); } From ccd079443faba4b0c9ad100a7ab68fbd369826b2 Mon Sep 17 00:00:00 2001 From: DenioD Date: Sun, 27 Oct 2019 21:28:27 +0100 Subject: [PATCH 7/8] fix missspell in path --- lib/src/lightclient.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 832c3ba..58d402a 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -103,7 +103,7 @@ impl LightClientConfig { zcash_data_location.push("silentdragonlite"); } else { zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!"); - zcash_data_location.push("/.silentdragonlite"); + zcash_data_location.push("/.silentdragonlite/"); }; match &self.chain_name[..] { From 127b8533cd3b20f454853f9da372215a72d469e7 Mon Sep 17 00:00:00 2001 From: DenioD Date: Sun, 27 Oct 2019 21:42:57 +0100 Subject: [PATCH 8/8] fix --- cli/src/lib.rs | 4 ++-- lib/src/lightclient.rs | 2 +- lib/src/lightwallet/startup_helpers.rs | 4 ++-- lib/src/startup_helpers.rs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index b7406dd..d4b9943 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -75,11 +75,11 @@ pub fn report_permission_error() { eprintln!("HOME: {}", home); eprintln!("Executable: {}", current_executable.display()); if home == "/" { - eprintln!("User {} must have permission to write to '{}.silentdragonlite/' .", + eprintln!("User {} must have permission to write to '{}silentdragonlite/' .", user, home); } else { - eprintln!("User {} must have permission to write to '{}/.silentdragonlite/' .", + eprintln!("User {} must have permission to write to '{}/silentdragonlite/' .", user, home); } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 58d402a..0c52baa 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -103,7 +103,7 @@ impl LightClientConfig { zcash_data_location.push("silentdragonlite"); } else { zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!"); - zcash_data_location.push("/.silentdragonlite/"); + zcash_data_location.push("silentdragonlite/"); }; match &self.chain_name[..] { diff --git a/lib/src/lightwallet/startup_helpers.rs b/lib/src/lightwallet/startup_helpers.rs index 6b47f46..d8b7cf2 100644 --- a/lib/src/lightwallet/startup_helpers.rs +++ b/lib/src/lightwallet/startup_helpers.rs @@ -9,11 +9,11 @@ pub fn report_permission_error() { eprintln!("HOME: {}", home); eprintln!("Executable: {}", current_executable.display()); if home == "/" { - eprintln!("User {} must have permission to write to '{}.silentdragonlite/' .", + eprintln!("User {} must have permission to write to '{}silentdragonlite/' .", user, home); } else { - eprintln!("User {} must have permission to write to '{}/.silentdragonlite/ .", + eprintln!("User {} must have permission to write to '{}/silentdragonlite/ .", user, home); } diff --git a/lib/src/startup_helpers.rs b/lib/src/startup_helpers.rs index 44fd061..a3cbaf7 100644 --- a/lib/src/startup_helpers.rs +++ b/lib/src/startup_helpers.rs @@ -9,11 +9,11 @@ pub fn report_permission_error() { eprintln!("HOME: {}", home); eprintln!("Executable: {}", current_executable.display()); if home == "/" { - eprintln!("User {} must have permission to write to '{}.silentdragonlite/' .", + eprintln!("User {} must have permission to write to '{}silentdragonlite/' .", user, home); } else { - eprintln!("User {} must have permission to write to '{}/.silentdragonlite/ .", + eprintln!("User {} must have permission to write to '{}/silentdragonlite/ .", user, home); }