use std::io::{self}; use std::sync::Arc; use std::sync::mpsc::{channel, Sender, Receiver}; use log::{info, error}; use silentdragonlitelib::{commands, lightclient::{LightClient, LightClientConfig}, }; #[macro_export] macro_rules! configure_clapapp { ( $freshapp: expr ) => { $freshapp.version("1.3.2") .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, Silentdragonlite-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("password") .long("password") .help("When recovering seed, specify a password for the encrypted wallet") .takes_value(true)) .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, Silentdragonlite-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 '{}.silentdragonlite/' .", user, home); } else { eprintln!("User {} must have permission to write to '{}/.silentdragonlite/' .", user, home); } } 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)?; 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)?) } } }; // Initialize logging lightclient.init_logging()?; // Print startup Messages info!(""); // Blank line info!("Starting Silentdragonlite-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!["false".to_string()])).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(password: Option) { // 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, password) { Ok(seed) => println!("Recovered seed: '{}'", seed), Err(e) => eprintln!("Failed to recover seed. Error: {}", e) }; }