// Copyright 2021 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. /// Argument parsing and error handling for wallet commands use self::core::consensus; use crate::api::TLSConfig; use crate::cli::command_loop; use crate::config::GRIN_WALLET_DIR; use crate::util::file::get_first_line; use crate::util::secp::key::SecretKey; use crate::util::{Mutex, ZeroingString}; use clap::ArgMatches; use grin_core as core; use grin_core::core::amount_to_hr_string; use grin_keychain as keychain; use grin_wallet_api::Owner; use grin_wallet_config::{TorConfig, WalletConfig}; use grin_wallet_controller::{command, Error}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; use grin_wallet_libwallet::{self, Slate, SlatepackAddress, SlatepackArmor}; use grin_wallet_libwallet::{IssueInvoiceTxArgs, NodeClient, WalletInst, WalletLCProvider}; use linefeed::terminal::Signal; use linefeed::{Interface, ReadResult}; use rpassword; use std::convert::TryFrom; use std::path::{Path, PathBuf}; use std::sync::Arc; // define what to do on argument error macro_rules! arg_parse { ( $r:expr ) => { match $r { Ok(res) => res, Err(e) => { return Err(Error::ArgumentError(format!("{}", e))); } } }; } /// Simple error definition, just so we can return errors from all commands /// and let the caller figure out what to do #[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] pub enum ParseError { #[error("Invalid Arguments: {0}")] ArgumentError(String), #[error("Parsing IO error: {0}")] IOError(String), #[error("Wallet configuration already exists: {0}")] WalletExists(String), #[error("User Cancelled")] CancelledError, } impl From for ParseError { fn from(e: std::io::Error) -> ParseError { ParseError::IOError(format!("{}", e)) } } fn prompt_password_stdout(prompt: &str) -> ZeroingString { ZeroingString::from(rpassword::prompt_password_stdout(prompt).unwrap()) } pub fn prompt_password(password: &Option) -> ZeroingString { match password { None => prompt_password_stdout("Password: "), Some(p) => p.clone(), } } fn prompt_password_confirm() -> ZeroingString { let mut first = ZeroingString::from("first"); let mut second = ZeroingString::from("second"); while first != second { first = prompt_password_stdout("Password: "); second = prompt_password_stdout("Confirm Password: "); } first } fn prompt_recovery_phrase( wallet: Arc>>>, ) -> Result where DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: keychain::Keychain + 'static, { let interface = Arc::new(Interface::new("recover")?); let mut phrase = ZeroingString::from(""); interface.set_report_signal(Signal::Interrupt, true); interface.set_prompt("phrase> ")?; loop { println!("Please enter your recovery phrase:"); let res = interface.read_line()?; match res { ReadResult::Eof => break, ReadResult::Signal(sig) => { if sig == Signal::Interrupt { interface.cancel_read_line()?; return Err(ParseError::CancelledError); } } ReadResult::Input(line) => { let mut w_lock = wallet.lock(); let p = w_lock.lc_provider().unwrap(); if p.validate_mnemonic(ZeroingString::from(line.clone())) .is_ok() { phrase = ZeroingString::from(line); break; } else { println!(); println!("Recovery word phrase is invalid."); println!(); interface.set_buffer(&line)?; } } } } Ok(phrase) } fn prompt_slatepack() -> Result { let interface = Arc::new(Interface::new("slatepack_input")?); let mut message = String::from(""); interface.set_report_signal(Signal::Interrupt, true); interface.set_prompt("")?; loop { println!("Please paste your encoded slatepack message:"); let res = interface.read_line()?; match res { ReadResult::Eof => break, ReadResult::Signal(sig) => { if sig == Signal::Interrupt { interface.cancel_read_line()?; return Err(ParseError::CancelledError); } } ReadResult::Input(line) => { if SlatepackArmor::decode(line.as_bytes()).is_ok() { message = line; break; } else { println!(); println!("Input is not a valid slatepack."); println!(); interface.set_buffer(&line)?; } } } } Ok(message) } fn prompt_pay_invoice(slate: &Slate, dest: &str) -> Result { let interface = Arc::new(Interface::new("pay")?); let amount = amount_to_hr_string(slate.amount, false); interface.set_report_signal(Signal::Interrupt, true); interface.set_prompt( "To proceed, type the exact amount of the invoice as displayed above (or Q/q to quit) > ", )?; println!(); println!( "This command will pay the amount specified in the invoice using your wallet's funds." ); println!("After you confirm, the following will occur: "); println!(); println!( "* {} of your wallet funds will be added to the transaction to pay this invoice.", amount ); if dest.len() > 0 { println!("* The wallet will IMMEDIATELY attempt to send the resulting transaction to the wallet listening at: '{}'.", dest); println!("* If other wallet is not listening, the resulting transaction will output as a slatepack which you can manually send back to the invoice creator."); } else { println!("* The resulting transaction will output as a slatepack which you can manually send back to the invoice creator."); } println!(); println!("Please review the above information carefully before proceeding"); println!(); loop { let res = interface.read_line()?; match res { ReadResult::Eof => return Ok(false), ReadResult::Signal(sig) => { if sig == Signal::Interrupt { interface.cancel_read_line()?; return Err(ParseError::CancelledError); } } ReadResult::Input(line) => { match line.trim() { "Q" | "q" => return Err(ParseError::CancelledError), result => { if result == amount { return Ok(true); } else { println!("Please enter exact amount of the invoice as shown above or Q to quit"); println!(); } } } } } } } // instantiate wallet (needed by most functions) pub fn inst_wallet( config: WalletConfig, node_client: C, ) -> Result>>>, ParseError> where DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: keychain::Keychain + 'static, { let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client.clone()).unwrap()) as Box>; let lc = wallet.lc_provider().unwrap(); let _ = lc.set_top_level_directory(&config.data_file_dir); Ok(Arc::new(Mutex::new(wallet))) } // parses a required value, or throws error with message otherwise fn parse_required<'a>(args: &'a ArgMatches, name: &str) -> Result<&'a str, ParseError> { let arg = args.value_of(name); match arg { Some(ar) => Ok(ar), None => { let msg = format!("Value for argument '{}' is required in this context", name,); Err(ParseError::ArgumentError(msg)) } } } // parses an optional value, throws error if value isn't provided fn parse_optional(args: &ArgMatches, name: &str) -> Result, ParseError> { if !args.is_present(name) { return Ok(None); } let arg = args.value_of(name); match arg { Some(ar) => Ok(Some(ar.into())), None => { let msg = format!("Value for argument '{}' is required in this context", name,); Err(ParseError::ArgumentError(msg)) } } } // parses a number, or throws error with message otherwise fn parse_u64(arg: &str, name: &str) -> Result { let val = arg.parse::(); match val { Ok(v) => Ok(v), Err(e) => { let msg = format!("Could not parse {} as a whole number. e={}", name, e); Err(ParseError::ArgumentError(msg)) } } } // As above, but optional fn parse_u64_or_none(arg: Option<&str>) -> Option { let val = match arg { Some(a) => a.parse::(), None => return None, }; match val { Ok(v) => Some(v), Err(_) => None, } } // Parse a value expressed in Grin (e.g. "1.5") into nanograms. fn parse_grin_amount(arg: &str, name: &str) -> Result { match arg.parse::() { Ok(g) => Ok((g * consensus::GRIN_BASE as f64) as u64), Err(e) => Err(ParseError::ArgumentError(format!( "Could not parse {} '{}' as a number. e={}", name, arg, e ))), } } pub fn parse_global_args( config: &WalletConfig, args: &ArgMatches, ) -> Result { let account = parse_required(args, "account")?; let mut show_spent = false; if args.is_present("show_spent") { show_spent = true; } let api_secret = get_first_line(config.api_secret_path.clone()); let node_api_secret = get_first_line(config.node_api_secret_path.clone()); let password = match args.value_of("pass") { None => None, Some(p) => Some(ZeroingString::from(p)), }; let tls_conf = match config.tls_certificate_file.clone() { None => None, Some(file) => { let key = match config.tls_certificate_key.clone() { Some(k) => k, None => { let msg = format!("Private key for certificate is not set"); return Err(ParseError::ArgumentError(msg)); } }; Some(TLSConfig::new(file, key)) } }; Ok(command::GlobalArgs { account: account.to_owned(), show_spent: show_spent, api_secret: api_secret, node_api_secret: node_api_secret, password: password, tls_conf: tls_conf, }) } pub fn parse_init_args( wallet: Arc>>>, config: &WalletConfig, g_args: &command::GlobalArgs, args: &ArgMatches, _test_mode: bool, ) -> Result where DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: keychain::Keychain + 'static, { let list_length = match args.is_present("short_wordlist") { false => 32, true => 16, }; let recovery_phrase = match args.is_present("recover") { true => Some(prompt_recovery_phrase(wallet)?), false => None, }; if recovery_phrase.is_some() { println!("Please provide a new password for the recovered wallet"); } else { println!("Please enter a password for your new wallet"); } let password = match g_args.password.clone() { Some(p) => p, None => prompt_password_confirm(), }; Ok(command::InitArgs { list_length: list_length, password: password, config: config.clone(), recovery_phrase: recovery_phrase, restore: false, }) } pub fn parse_recover_args( g_args: &command::GlobalArgs, ) -> Result where { let passphrase = prompt_password(&g_args.password); Ok(command::RecoverArgs { passphrase: passphrase, }) } pub fn parse_listen_args( config: &mut WalletConfig, tor_config: &mut TorConfig, args: &ArgMatches, ) -> Result { if let Some(port) = args.value_of("port") { config.api_listen_port = port.parse().unwrap(); } if let Some(bridge) = args.value_of("bridge") { tor_config.bridge.bridge_line = Some(bridge.into()); } if args.is_present("no_tor") { tor_config.use_tor_listener = false; } Ok(command::ListenArgs {}) } pub fn parse_owner_api_args( config: &mut WalletConfig, args: &ArgMatches, ) -> Result<(), ParseError> { if let Some(port) = args.value_of("port") { config.owner_api_listen_port = Some(port.parse().unwrap()); } if args.is_present("run_foreign") { config.owner_api_include_foreign = Some(true); } Ok(()) } pub fn parse_scan_rewind_hash_args( args: &ArgMatches, ) -> Result { let rewind_hash = parse_required(args, "rewind_hash")?; let start_height = parse_u64_or_none(args.value_of("start_height")); let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip")); if backwards_from_tip.is_some() && start_height.is_some() { let msg = format!("backwards_from tip and start_height cannot both be present"); return Err(ParseError::ArgumentError(msg)); } Ok(command::ViewWalletScanArgs { rewind_hash: rewind_hash.into(), start_height, backwards_from_tip, }) } pub fn parse_account_args(account_args: &ArgMatches) -> Result { let create = match account_args.value_of("create") { None => None, Some(s) => Some(s.to_owned()), }; Ok(command::AccountArgs { create: create }) } pub fn parse_send_args(args: &ArgMatches) -> Result { // amount let amount = parse_required(args, "amount")?; let (amount, spend_max) = if amount.eq_ignore_ascii_case("max") { (Ok(0), true) } else { (core::core::amount_from_hr_string(amount), false) }; let amount = match amount { Ok(a) => a, Err(e) => { let msg = format!( "Could not parse amount as a number with optional decimal point. e={}", e ); return Err(ParseError::ArgumentError(msg)); } }; let amount_includes_fee = args.is_present("amount_includes_fee") || spend_max; // minimum_confirmations let min_c = parse_required(args, "minimum_confirmations")?; let min_c = parse_u64(min_c, "minimum_confirmations")?; // selection_strategy let selection_strategy = parse_required(args, "selection_strategy")?; // estimate_selection_strategies let estimate_selection_strategies = args.is_present("estimate_selection_strategies"); let late_lock = args.is_present("late_lock"); // dest let dest = match args.value_of("dest") { Some(d) => d, None => "default", }; // change_outputs let change_outputs = parse_required(args, "change_outputs")?; let change_outputs = parse_u64(change_outputs, "change_outputs")? as usize; // fluff let fluff = args.is_present("fluff"); // ttl_blocks let ttl_blocks = parse_u64_or_none(args.value_of("ttl_blocks")); // max_outputs let max_outputs = 500; // target slate version to create/send let target_slate_version = { match args.is_present("slate_version") { true => { let v = parse_required(args, "slate_version")?; Some(parse_u64(v, "slate_version")? as u16) } false => None, } }; let payment_proof_address = { match args.is_present("no_payment_proof") { false => match SlatepackAddress::try_from(dest) { Ok(a) => Some(a), Err(_) => { if !estimate_selection_strategies { println!("No recipient Slatepack address or provided address invalid. No payment proof will be requested."); } None } }, true => None, } }; let outfile = parse_optional(args, "outfile")?; let bridge = match args.value_of("bridge") { Some(b) => Some(b.to_string()), None => None, }; let slatepack_qr = args.is_present("slatepack_qr"); Ok(command::SendArgs { amount: amount, amount_includes_fee: amount_includes_fee, use_max_amount: spend_max, minimum_confirmations: min_c, selection_strategy: selection_strategy.to_owned(), estimate_selection_strategies, late_lock, dest: dest.to_owned(), change_outputs: change_outputs, fluff: fluff, max_outputs: max_outputs, payment_proof_address, ttl_blocks, target_slate_version: target_slate_version, outfile, skip_tor: args.is_present("manual"), bridge: bridge, slatepack_qr: slatepack_qr, }) } pub fn parse_receive_args(args: &ArgMatches) -> Result { // input file let input_file = match args.is_present("input") { true => { let file = args.value_of("input").unwrap().to_owned(); // validate input if !Path::new(&file).is_file() { let msg = format!("File {} not found.", &file); return Err(ParseError::ArgumentError(msg)); } Some(file) } false => None, }; let mut input_slatepack_message = None; if input_file.is_none() { input_slatepack_message = Some(prompt_slatepack()?); } let outfile = parse_optional(args, "outfile")?; let bridge = parse_optional(args, "bridge")?; let slatepack_qr = args.is_present("slatepack_qr"); Ok(command::ReceiveArgs { input_file, input_slatepack_message, skip_tor: args.is_present("manual"), outfile, bridge, slatepack_qr: slatepack_qr, }) } pub fn parse_unpack_args(args: &ArgMatches) -> Result { // input file let input_file = match args.is_present("input") { true => { let file = args.value_of("input").unwrap().to_owned(); // validate input if !Path::new(&file).is_file() { let msg = format!("File {} not found.", &file); return Err(ParseError::ArgumentError(msg)); } Some(file) } false => None, }; let mut input_slatepack_message = None; if input_file.is_none() { input_slatepack_message = Some(prompt_slatepack()?); } let outfile = parse_optional(args, "outfile")?; let bridge = parse_optional(args, "bridge")?; let slatepack_qr = args.is_present("slatepack_qr"); Ok(command::ReceiveArgs { input_file, input_slatepack_message, skip_tor: args.is_present("manual"), outfile, bridge, slatepack_qr: slatepack_qr, }) } pub fn parse_finalize_args(args: &ArgMatches) -> Result { let fluff = args.is_present("fluff"); let nopost = args.is_present("nopost"); let input_file = match args.is_present("input") { true => { let file = args.value_of("input").unwrap().to_owned(); // validate input if !Path::new(&file).is_file() { let msg = format!("File {} not found.", &file); return Err(ParseError::ArgumentError(msg)); } Some(file) } false => None, }; let mut input_slatepack_message = None; if input_file.is_none() { input_slatepack_message = Some(prompt_slatepack()?); } let outfile = parse_optional(args, "outfile")?; let slatepack_qr = args.is_present("slatepack_qr"); Ok(command::FinalizeArgs { input_file, input_slatepack_message, fluff: fluff, nopost: nopost, outfile, slatepack_qr: slatepack_qr, }) } pub fn parse_issue_invoice_args( args: &ArgMatches, ) -> Result { let amount = parse_required(args, "amount")?; let amount = core::core::amount_from_hr_string(amount); let amount = match amount { Ok(a) => a, Err(e) => { let msg = format!( "Could not parse amount as a number with optional decimal point. e={}", e ); return Err(ParseError::ArgumentError(msg)); } }; // target slate version to create let target_slate_version = { match args.is_present("slate_version") { true => { let v = parse_required(args, "slate_version")?; Some(parse_u64(v, "slate_version")? as u16) } false => None, } }; // dest, for encryption let dest = match args.value_of("dest") { Some(d) => d, None => "default", }; let outfile = parse_optional(args, "outfile")?; let slatepack_qr = args.is_present("slatepack_qr"); Ok(command::IssueInvoiceArgs { dest: dest.into(), issue_args: IssueInvoiceTxArgs { dest_acct_name: None, amount, target_slate_version, }, outfile, slatepack_qr: slatepack_qr, }) } fn get_slate( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, args: &ArgMatches, ) -> Result<(Slate, Option), Error> where L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: keychain::Keychain + 'static, { let input_file = match args.is_present("input") { true => { let file = args.value_of("input").unwrap().to_owned(); // validate input if !Path::new(&file).is_file() { let msg = format!("File {} not found.", &file); return Err(Error::GenericError(msg)); } Some(file) } false => None, }; let mut input_slatepack_message = None; if input_file.is_none() { input_slatepack_message = Some(prompt_slatepack().map_err(|e| { let msg = format!("{}", e); Error::GenericError(msg) })?) } command::parse_slatepack( owner_api, keychain_mask, input_file, input_slatepack_message, ) } pub fn parse_process_invoice_args( args: &ArgMatches, prompt: bool, slate: Slate, ret_address: Option, ) -> Result { // minimum_confirmations let min_c = parse_required(args, "minimum_confirmations")?; let min_c = parse_u64(min_c, "minimum_confirmations")?; // selection_strategy let selection_strategy = parse_required(args, "selection_strategy")?; // estimate_selection_strategies let estimate_selection_strategies = args.is_present("estimate_selection_strategies"); // ttl_blocks let ttl_blocks = parse_u64_or_none(args.value_of("ttl_blocks")); // max_outputs let max_outputs = 500; if prompt { let dest = match ret_address.clone() { Some(a) => String::try_from(&a).unwrap(), None => String::from(""), }; // Now we need to prompt the user whether they want to do this, prompt_pay_invoice(&slate, &dest)?; } let outfile = parse_optional(args, "outfile")?; let bridge = parse_optional(args, "bridge")?; let slatepack_qr = args.is_present("slatepack_qr"); Ok(command::ProcessInvoiceArgs { minimum_confirmations: min_c, selection_strategy: selection_strategy.to_owned(), estimate_selection_strategies, ret_address, slate, max_outputs, ttl_blocks, skip_tor: args.is_present("manual"), outfile, bridge, slatepack_qr: slatepack_qr, }) } pub fn parse_info_args(args: &ArgMatches) -> Result { // minimum_confirmations let mc = parse_required(args, "minimum_confirmations")?; let mc = parse_u64(mc, "minimum_confirmations")?; Ok(command::InfoArgs { minimum_confirmations: mc, }) } pub fn parse_check_args(args: &ArgMatches) -> Result { let delete_unconfirmed = args.is_present("delete_unconfirmed"); let start_height = parse_u64_or_none(args.value_of("start_height")); let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip")); if backwards_from_tip.is_some() && start_height.is_some() { let msg = format!("backwards_from tip and start_height cannot both be present"); return Err(ParseError::ArgumentError(msg)); } Ok(command::CheckArgs { start_height, backwards_from_tip, delete_unconfirmed, }) } pub fn parse_txs_args(args: &ArgMatches) -> Result { let tx_id = match args.value_of("id") { None => None, Some(tx) => Some(parse_u64(tx, "id")? as u32), }; let tx_slate_id = match args.value_of("txid") { None => None, Some(tx) => match tx.parse() { Ok(t) => Some(t), Err(e) => { let msg = format!("Could not parse txid parameter. e={}", e); return Err(ParseError::ArgumentError(msg)); } }, }; if tx_id.is_some() && tx_slate_id.is_some() { let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided."); return Err(ParseError::ArgumentError(msg)); } let count = match args.value_of("count") { None => None, Some(c) => Some(parse_u64(c, "count")? as u32), }; Ok(command::TxsArgs { id: tx_id, tx_slate_id: tx_slate_id, count: count, }) } pub fn parse_post_args(args: &ArgMatches) -> Result { let fluff = args.is_present("fluff"); // input file let input_file = match args.is_present("input") { true => { let file = args.value_of("input").unwrap().to_owned(); // validate input if !Path::new(&file).is_file() { let msg = format!("File {} not found.", &file); return Err(ParseError::ArgumentError(msg)); } Some(file) } false => None, }; let mut input_slatepack_message = None; if input_file.is_none() { input_slatepack_message = Some(prompt_slatepack()?); } Ok(command::PostArgs { input_file, input_slatepack_message, fluff, }) } pub fn parse_repost_args(args: &ArgMatches) -> Result { let tx_id = match args.value_of("id") { None => None, Some(tx) => Some(parse_u64(tx, "id")? as u32), }; let fluff = args.is_present("fluff"); let dump_file = match args.value_of("dumpfile") { None => None, Some(d) => Some(d.to_owned()), }; Ok(command::RepostArgs { id: tx_id.unwrap(), dump_file: dump_file, fluff: fluff, }) } pub fn parse_cancel_args(args: &ArgMatches) -> Result { let mut tx_id_string = ""; let tx_id = match args.value_of("id") { None => None, Some(tx) => Some(parse_u64(tx, "id")? as u32), }; let tx_slate_id = match args.value_of("txid") { None => None, Some(tx) => match tx.parse() { Ok(t) => { tx_id_string = tx; Some(t) } Err(e) => { let msg = format!("Could not parse txid parameter. e={}", e); return Err(ParseError::ArgumentError(msg)); } }, }; if (tx_id.is_none() && tx_slate_id.is_none()) || (tx_id.is_some() && tx_slate_id.is_some()) { let msg = format!("'id' (-i) or 'txid' (-t) argument is required."); return Err(ParseError::ArgumentError(msg)); } Ok(command::CancelArgs { tx_id: tx_id, tx_slate_id: tx_slate_id, tx_id_string: tx_id_string.to_owned(), }) } pub fn parse_export_proof_args(args: &ArgMatches) -> Result { let output_file = parse_required(args, "output")?; let tx_id = match args.value_of("id") { None => None, Some(tx) => Some(parse_u64(tx, "id")? as u32), }; let tx_slate_id = match args.value_of("txid") { None => None, Some(tx) => match tx.parse() { Ok(t) => Some(t), Err(e) => { let msg = format!("Could not parse txid parameter. e={}", e); return Err(ParseError::ArgumentError(msg)); } }, }; if tx_id.is_some() && tx_slate_id.is_some() { let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided."); return Err(ParseError::ArgumentError(msg)); } if tx_id.is_none() && tx_slate_id.is_none() { let msg = format!("Either 'id' (-i) or 'txid' (-t) must be provided."); return Err(ParseError::ArgumentError(msg)); } Ok(command::ProofExportArgs { output_file: output_file.to_owned(), id: tx_id, tx_slate_id: tx_slate_id, }) } pub fn parse_verify_proof_args(args: &ArgMatches) -> Result { let input_file = parse_required(args, "input")?; Ok(command::ProofVerifyArgs { input_file: input_file.to_owned(), }) } // TODO: parse args pub fn parse_contract_new_args( args: &ArgMatches, account: &String, ) -> Result { // Optional: without --encrypt-for the contract slatepack is produced unencrypted. let counterparty_addr = args.value_of("encrypt-for").map(String::from); // TODO: Make sure the values are in some expected bounds. // TODO: How to deal with decimals and precision? we probably want users to express the value in Grin. // Parse receive and send params and convert them to nano grin let receive = match args.value_of("receive") { Some(g) => Some(parse_grin_amount(g, "receive")?), None => None, }; let send = match args.value_of("send") { Some(g) => Some(parse_grin_amount(g, "send")?), None => None, }; if receive.is_some() && send.is_some() { return Err(ParseError::ArgumentError(String::from( "You can only specify receive or send, not both.", ))); }; if receive.is_none() && send.is_none() { return Err(ParseError::ArgumentError(String::from( "You must specify either --receive or --send.", ))); }; // TODO: verify this is correct e.g. which values are passed here by default etc. let src_acct_name = Some(String::from(account)); let add_outputs = args.is_present("add-outputs"); let as_json = args.is_present("as-json"); let no_payjoin = args.is_present("no-payjoin"); let use_inputs = match args.value_of("use-inputs") { Some(v) => { if no_payjoin { return Err(ParseError::ArgumentError(String::from( "Can't use --no-payjoin with --use-inputs.", ))); } Some(String::from(v)) } None => { if no_payjoin { None } else { // Some("any") means pick 1 random input to contribute (payjoin) Some(String::from("any")) } } }; let make_outputs = match args.value_of("make-outputs") { Some(v) => { // Validate each comma-separated amount here so a bad value returns a // ParseError instead of panicking later in output_amounts(). for amt in v.split(',') { parse_grin_amount(amt, "make-outputs")?; } Some(String::from(v)) } None => None, }; let num_participants = match args.value_of("num-participants") { Some(v) => v.parse::().map_err(|e| { ParseError::ArgumentError(format!("Could not parse num-participants '{}'. e={}", v, e)) })?, None => 2, }; Ok(command::ContractNewArgs { counterparty_addr: counterparty_addr, receive: receive, send: send, src_acct_name: src_acct_name, num_participants: num_participants, as_json: as_json, add_outputs: add_outputs, use_inputs: use_inputs, make_outputs: make_outputs, // TODO: Future features below fee_rate: None, outfile: None, }) } // TODO: parse args pub fn parse_contract_setup_args( args: &ArgMatches, ) -> Result { // TODO: Make sure the values are in some expected bounds. // TODO: How to deal with decimals and precision? we probably want users to express the value in Grin. let counterparty_addr = match args.value_of("encrypt-for") { Some(v) => Some(String::from(v)), None => None, }; // Parse receive and send params and convert them to nano grin let receive = match args.value_of("receive") { Some(g) => Some(parse_grin_amount(g, "receive")?), None => None, }; let send = match args.value_of("send") { Some(g) => Some(parse_grin_amount(g, "send")?), None => None, }; if receive.is_some() && send.is_some() { return Err(ParseError::ArgumentError(String::from( "You can only specify receive or send, not both.", ))); }; let as_json = args.is_present("as-json"); let no_payjoin = args.is_present("no-payjoin"); let use_inputs = match args.value_of("use-inputs") { Some(v) => { if no_payjoin { return Err(ParseError::ArgumentError(String::from( "Can't use --no-payjoin with --use-inputs.", ))); } Some(String::from(v)) } None => { if no_payjoin { None } else { // Some("any") means pick 1 random input to contribute (payjoin) Some(String::from("any")) } } }; let make_outputs = match args.value_of("make-outputs") { Some(v) => { // Validate each comma-separated amount here so a bad value returns a // ParseError instead of panicking later in output_amounts(). for amt in v.split(',') { parse_grin_amount(amt, "make-outputs")?; } Some(String::from(v)) } None => None, }; // TODO: should we catch if the person calls "--receive=5" when it should be "--send=5"? // Perhaps we could detect this from the slate state e.g. S1 -> receive, I1 -> send? Ok(command::ContractSetupArgs { counterparty_addr: counterparty_addr, receive: receive, send: send, as_json: as_json, add_outputs: false, use_inputs: use_inputs, make_outputs: make_outputs, // TODO: Future features below fee_rate: None, outfile: None, }) } pub fn parse_contract_revoke_args( args: &ArgMatches, ) -> Result { let tx_id_str = parse_required(args, "tx-id")?; let tx_id = tx_id_str.parse::().map_err(|e| { ParseError::ArgumentError(format!("Could not parse tx-id '{}'. e={}", tx_id_str, e)) })?; Ok(command::ContractRevokeArgs { tx_id: tx_id }) } pub fn wallet_command( wallet_args: &ArgMatches, mut wallet_config: WalletConfig, tor_config: Option, mut node_client: C, test_mode: bool, wallet_inst_cb: F, ) -> Result where C: NodeClient + 'static + Clone, F: FnOnce( Arc< Mutex< Box< dyn WalletInst< 'static, DefaultLCProvider<'static, C, keychain::ExtKeychain>, C, keychain::ExtKeychain, >, >, >, >, ), { if let Some(dir) = wallet_args.value_of("top_level_dir") { wallet_config.data_file_dir = dir.to_string().clone(); } if let Some(sa) = wallet_args.value_of("api_server_address") { wallet_config.check_node_api_http_addr = sa.to_string().clone(); } let global_wallet_args = arg_parse!(parse_global_args(&wallet_config, &wallet_args)); node_client.set_node_url(&wallet_config.check_node_api_http_addr); node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone()); // legacy hack to avoid the need for changes in existing grin-wallet.toml files // remove `wallet_data` from end of path // 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(); } // for backwards compatibility: If tor config doesn't exist in the file, assume // the top level directory for data let tor_config = tor_config.unwrap_or_else(|| TorConfig { send_config_dir: wallet_config.data_file_dir.clone(), ..Default::default() }); // Instantiate wallet (doesn't open the wallet) let wallet = inst_wallet::, C, keychain::ExtKeychain>( wallet_config.clone(), node_client, ) .unwrap_or_else(|e| { println!("{}", e); std::process::exit(1); }); { let mut wallet_lock = wallet.lock(); let lc = wallet_lock.lc_provider().unwrap(); let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); } // provide wallet instance back to the caller (handy for testing with // local wallet proxy, etc) wallet_inst_cb(wallet.clone()); // don't open wallet for certain lifecycle commands let mut open_wallet = true; match wallet_args.subcommand() { ("init", Some(_)) => open_wallet = false, ("recover", _) => open_wallet = false, ("cli", _) => open_wallet = false, ("owner_api", _) => { // If wallet exists and password is present then open it. Otherwise, that's fine too. let mut wallet_lock = wallet.lock(); let lc = wallet_lock.lc_provider().unwrap(); open_wallet = wallet_args.is_present("pass") && lc.wallet_exists(None)?; } _ => {} } let keychain_mask = match open_wallet { true => { let mut wallet_lock = wallet.lock(); let lc = wallet_lock.lc_provider().unwrap(); let mask = lc.open_wallet( None, prompt_password(&global_wallet_args.password), false, false, )?; if let Some(account) = wallet_args.value_of("account") { let wallet_inst = lc.wallet_inst()?; wallet_inst.set_parent_key_id_by_name(account)?; } mask } false => None, }; let res = match wallet_args.subcommand() { ("cli", Some(_)) => command_loop( wallet, keychain_mask, &wallet_config, &tor_config, &global_wallet_args, test_mode, ), _ => { let mut owner_api = Owner::new(wallet, None); parse_and_execute( &mut owner_api, keychain_mask, &wallet_config, &tor_config, &global_wallet_args, &wallet_args, test_mode, false, ) } }; if let Err(e) = res { Err(e) } else { Ok(wallet_args.subcommand().0.to_owned()) } } pub fn parse_and_execute( owner_api: &mut Owner, keychain_mask: Option, wallet_config: &WalletConfig, tor_config: &TorConfig, global_wallet_args: &command::GlobalArgs, wallet_args: &ArgMatches, test_mode: bool, cli_mode: bool, ) -> Result<(), Error> where DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: keychain::Keychain + 'static, { let km = (&keychain_mask).as_ref(); if test_mode { owner_api.doctest_mode = true; owner_api.doctest_retain_tld = true; } match wallet_args.subcommand() { ("init", Some(args)) => { let a = arg_parse!(parse_init_args( owner_api.wallet_inst.clone(), wallet_config, global_wallet_args, &args, test_mode, )); command::init(owner_api, &global_wallet_args, a, test_mode) } ("recover", Some(_)) => { let a = arg_parse!(parse_recover_args(&global_wallet_args,)); command::recover(owner_api, a) } ("listen", Some(args)) => { let mut c = wallet_config.clone(); let mut t = tor_config.clone(); let a = arg_parse!(parse_listen_args(&mut c, &mut t, &args)); command::listen( owner_api, Arc::new(Mutex::new(keychain_mask)), &c, &t, &a, &global_wallet_args.clone(), cli_mode, test_mode, ) } ("owner_api", Some(args)) => { let mut c = wallet_config.clone(); let mut g = global_wallet_args.clone(); g.tls_conf = None; arg_parse!(parse_owner_api_args(&mut c, &args)); command::owner_api(owner_api, keychain_mask, &c, &tor_config, &g, test_mode) } ("web", Some(_)) => command::owner_api( owner_api, keychain_mask, wallet_config, tor_config, global_wallet_args, test_mode, ), ("rewind_hash", Some(_)) => command::rewind_hash(owner_api, km), ("scan_rewind_hash", Some(args)) => { let a = arg_parse!(parse_scan_rewind_hash_args(&args)); command::scan_rewind_hash( owner_api, a, wallet_config.dark_background_color_scheme.unwrap_or(true), ) } ("account", Some(args)) => { let a = arg_parse!(parse_account_args(&args)); command::account(owner_api, km, a) } ("send", Some(args)) => { let a = arg_parse!(parse_send_args(&args)); command::send( owner_api, km, Some(tor_config.clone()), a, wallet_config.dark_background_color_scheme.unwrap_or(true), test_mode, ) } ("receive", Some(args)) => { let a = arg_parse!(parse_receive_args(&args)); command::receive( owner_api, km, &global_wallet_args, a, Some(tor_config.clone()), test_mode, ) } ("unpack", Some(args)) => { let a = arg_parse!(parse_unpack_args(&args)); command::unpack(owner_api, km, a) } ("finalize", Some(args)) => { let a = arg_parse!(parse_finalize_args(&args)); command::finalize(owner_api, km, a) } ("invoice", Some(args)) => { let a = arg_parse!(parse_issue_invoice_args(&args)); command::issue_invoice_tx(owner_api, km, a) } ("pay", Some(args)) => { // get slate first let (slate, address) = get_slate(owner_api, km, args)?; let a = arg_parse!(parse_process_invoice_args( &args, !test_mode, slate, address )); command::process_invoice( owner_api, km, Some(tor_config.clone()), a, wallet_config.dark_background_color_scheme.unwrap_or(true), test_mode, ) } ("info", Some(args)) => { let a = arg_parse!(parse_info_args(&args)); command::info( owner_api, km, global_wallet_args, a, wallet_config.dark_background_color_scheme.unwrap_or(true), ) } ("outputs", Some(_)) => command::outputs( owner_api, km, &global_wallet_args, wallet_config.dark_background_color_scheme.unwrap_or(true), ), ("txs", Some(args)) => { let a = arg_parse!(parse_txs_args(&args)); command::txs( owner_api, km, &global_wallet_args, a, wallet_config.dark_background_color_scheme.unwrap_or(true), ) } ("post", Some(args)) => { let a = arg_parse!(parse_post_args(&args)); command::post(owner_api, km, a) } ("repost", Some(args)) => { let a = arg_parse!(parse_repost_args(&args)); command::repost(owner_api, km, a) } ("cancel", Some(args)) => { let a = arg_parse!(parse_cancel_args(&args)); command::cancel(owner_api, km, a) } ("export_proof", Some(args)) => { let a = arg_parse!(parse_export_proof_args(&args)); command::proof_export(owner_api, km, a) } ("verify_proof", Some(args)) => { let a = arg_parse!(parse_verify_proof_args(&args)); command::proof_verify(owner_api, km, a) } ("address", Some(_)) => command::address(owner_api, &global_wallet_args, km), ("scan", Some(args)) => { let a = arg_parse!(parse_check_args(&args)); command::scan(owner_api, km, a) } ("open", Some(_)) => { // for CLI mode only, should be handled externally Ok(()) } ("close", Some(_)) => { // for CLI mode only, should be handled externally Ok(()) } ("contract", Some(args)) => match args.subcommand() { ("new", Some(new_args)) => { let account = &global_wallet_args.account; let a = arg_parse!(parse_contract_new_args(&new_args, account)); command::contract_new(owner_api, km, a) } // ("setup", Some(setup_args)) => { // let a = arg_parse!(parse_contract_setup_args(&setup_args)); // command::contract_setup(owner_api, km, a) // } ("sign", Some(sign_args)) => { // Sign command takes setup_args so we use the same parser let setup_args = arg_parse!(parse_contract_setup_args(&sign_args)); let broadcast_tx = !sign_args.is_present("no-broadcast"); command::contract_sign(owner_api, km, setup_args, broadcast_tx) } ("view", Some(view_args)) => { Err(Error::ArgumentError(String::from("Not implemented")).into()) } ("revoke", Some(revoke_args)) => { let a = arg_parse!(parse_contract_revoke_args(&revoke_args)); command::contract_revoke(owner_api, km, a) } _ => Err(Error::ArgumentError(String::from("Unknown contract subcommand.")).into()), }, _ => { let msg = format!("Unknown wallet command, use 'grin-wallet help' for details"); return Err(Error::ArgumentError(msg)); } } }