diff --git a/controller/src/command.rs b/controller/src/command.rs index 374e66c0..346db7e3 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -35,7 +35,7 @@ use crate::impls::{ LMDBBackend, NullWalletCommAdapter, }; use crate::impls::{HTTPNodeClient, WalletSeed}; -use crate::libwallet::{InitTxArgs, NodeClient, WalletInst}; +use crate::libwallet::{InitTxArgs, IssueInvoiceTxArgs, NodeClient, WalletInst}; use crate::{controller, display}; /// Arguments common to all wallet commands @@ -377,13 +377,45 @@ pub fn finalize( ) -> Result<(), Error> { let adapter = FileWalletCommAdapter::new(); let mut slate = adapter.receive_tx_async(&args.input)?; - controller::owner_single_use(wallet.clone(), |api| { - if let Err(e) = api.verify_slate_messages(&slate) { - error!("Error validating participant messages: {}", e); - return Err(e); + // Rather than duplicating the entire command, we'll just + // try to determine what kind of finalization this is + // based on the slate contents + // for now, we can tell this is an invoice transaction + // if the receipient (participant 1) hasn't completed sigs + let part_data = slate.participant_with_id(1); + let is_invoice = { + match part_data { + None => { + error!("Expected slate participant data missing"); + return Err(ErrorKind::ArgumentError( + "Expected Slate participant data missing".into(), + ))?; + } + Some(p) => !p.is_complete(), } - slate = api.finalize_tx(&mut slate).expect("Finalize failed"); + }; + if is_invoice { + controller::foreign_single_use(wallet.clone(), |api| { + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } + slate = api.finalize_invoice_tx(&mut slate)?; + Ok(()) + })?; + } else { + controller::owner_single_use(wallet.clone(), |api| { + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } + slate = api.finalize_tx(&mut slate)?; + Ok(()) + })?; + } + + controller::owner_single_use(wallet.clone(), |api| { let result = api.post_tx(&slate.tx, args.fluff); match result { Ok(_) => { @@ -396,9 +428,130 @@ pub fn finalize( } } })?; + Ok(()) } +/// Issue Invoice Args +pub struct IssueInvoiceArgs { + /// output file + pub dest: String, + /// issue invoice tx args + pub issue_args: IssueInvoiceTxArgs, +} + +pub fn issue_invoice_tx( + wallet: Arc>>, + args: IssueInvoiceArgs, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let slate = api.issue_invoice_tx(args.issue_args)?; + let mut tx_file = File::create(args.dest.clone())?; + tx_file.write_all(json::to_string(&slate).unwrap().as_bytes())?; + tx_file.sync_all()?; + Ok(()) + })?; + Ok(()) +} + +/// Arguments for the process_invoice command +pub struct ProcessInvoiceArgs { + pub message: Option, + pub minimum_confirmations: u64, + pub selection_strategy: String, + pub method: String, + pub dest: String, + pub max_outputs: usize, + pub target_slate_version: Option, + pub input: String, + pub estimate_selection_strategies: bool, +} + +/// Process invoice +pub fn process_invoice( + wallet: Arc>>, + args: ProcessInvoiceArgs, + dark_scheme: bool, +) -> Result<(), Error> { + let adapter = FileWalletCommAdapter::new(); + let slate = adapter.receive_tx_async(&args.input)?; + controller::owner_single_use(wallet.clone(), |api| { + if args.estimate_selection_strategies { + let strategies = vec!["smallest", "all"] + .into_iter() + .map(|strategy| { + let init_args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: 1u32, + selection_strategy_is_use_all: strategy == "all", + estimate_only: Some(true), + ..Default::default() + }; + let slate = api.init_send_tx(init_args).unwrap(); + (strategy, slate.amount, slate.fee) + }) + .collect(); + display::estimate(slate.amount, strategies, dark_scheme); + } else { + let init_args = InitTxArgs { + src_acct_name: None, + amount: 0, + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: 1u32, + selection_strategy_is_use_all: args.selection_strategy == "all", + message: args.message.clone(), + target_slate_version: args.target_slate_version, + send_args: None, + ..Default::default() + }; + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } + let result = api.process_invoice_tx(&slate, init_args); + let mut slate = match result { + Ok(s) => { + info!( + "Invoice processed: {} grin to {} (strategy '{}')", + core::amount_to_hr_string(slate.amount, false), + args.dest, + args.selection_strategy, + ); + s + } + Err(e) => { + info!("Tx not created: {}", e); + return Err(e); + } + }; + let adapter = match args.method.as_str() { + "http" => HTTPWalletCommAdapter::new(), + "file" => FileWalletCommAdapter::new(), + "self" => NullWalletCommAdapter::new(), + _ => NullWalletCommAdapter::new(), + }; + if adapter.supports_sync() { + slate = adapter.send_tx_sync(&args.dest, &slate)?; + api.tx_lock_outputs(&slate)?; + if args.method == "self" { + controller::foreign_single_use(wallet, |api| { + slate = api.finalize_invoice_tx(&slate)?; + Ok(()) + })?; + } + } else { + adapter.send_tx_async(&args.dest, &slate)?; + api.tx_lock_outputs(&slate)?; + } + } + Ok(()) + })?; + Ok(()) +} /// Info command args pub struct InfoArgs { pub minimum_confirmations: u64, diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs index 62482ba8..faf61253 100644 --- a/controller/tests/invoice.rs +++ b/controller/tests/invoice.rs @@ -124,6 +124,7 @@ fn invoice_tx_impl(test_dir: &str) -> Result<(), libwallet::Error> { ..Default::default() }; slate = api.process_invoice_tx(&slate, args)?; + api.tx_lock_outputs(&slate)?; Ok(()) })?; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 12c7883d..fbb6358d 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -330,9 +330,6 @@ where batch.commit()?; } - // Always lock the context for now - selection::lock_tx_context(&mut *w, slate, &context)?; - tx::update_message(&mut *w, &mut ret_slate)?; Ok(ret_slate) } diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 83283d57..1a40d3b7 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -27,14 +27,14 @@ use crate::grin_core::core::verifier_cache::LruVerifierCache; use crate::grin_core::libtx::{aggsig, build, secp_ser, tx_fee}; use crate::grin_core::map_vec; use crate::grin_keychain::{BlindSum, BlindingFactor, Keychain}; -use crate::grin_util::secp; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::Signature; -use crate::grin_util::RwLock; +use crate::grin_util::{self, secp, RwLock}; use failure::ResultExt; use rand::rngs::mock::StepRng; use rand::thread_rng; use serde_json; +use std::fmt; use std::sync::Arc; use uuid::Uuid; @@ -113,6 +113,36 @@ impl ParticipantMessageData { } } +impl fmt::Display for ParticipantMessageData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "")?; + write!(f, "Participant ID {} ", self.id)?; + if self.id == 0 { + writeln!(f, "(Sender)")?; + } else { + writeln!(f, "(Recipient)")?; + } + writeln!(f, "---------------------")?; + let static_secp = grin_util::static_secp_instance(); + let static_secp = static_secp.lock(); + writeln!( + f, + "Public Key: {}", + &grin_util::to_hex(self.public_key.serialize_vec(&static_secp, true).to_vec()) + )?; + let message = match self.message.clone() { + None => "None".to_owned(), + Some(m) => m, + }; + writeln!(f, "Message: {}", message)?; + let message_sig = match self.message_sig.clone() { + None => "None".to_owned(), + Some(m) => grin_util::to_hex(m.to_raw_data().to_vec()), + }; + writeln!(f, "Message Signature: {}", message_sig) + } +} + /// A 'Slate' is passed around to all parties to build up all of the public /// transaction data needed to create a finalized transaction. Callers can pass /// the slate around by whatever means they choose, (but we can provide some @@ -344,7 +374,6 @@ impl Slate { /// Creates the final signature, callable by either the sender or recipient /// (after phase 3: sender confirmation) - /// TODO: Only callable by receiver at the moment pub fn finalize(&mut self, keychain: &K) -> Result<(), Error> where K: Keychain, @@ -353,6 +382,16 @@ impl Slate { self.finalize_transaction(keychain, &final_sig) } + /// Return the participant with the given id + pub fn participant_with_id(&self, id: usize) -> Option { + for p in self.participant_data.iter() { + if p.id as usize == id { + return Some(p.clone()); + } + } + None + } + /// Return the sum of public nonces fn pub_nonce_sum(&self, secp: &secp::Secp256k1) -> Result { let pub_nonces = self diff --git a/libwallet/src/slate_versions/v2.rs b/libwallet/src/slate_versions/v2.rs index 1de35dac..62e563e7 100644 --- a/libwallet/src/slate_versions/v2.rs +++ b/libwallet/src/slate_versions/v2.rs @@ -29,7 +29,7 @@ //! * TxKernel fields serialized as hex strings instead of arrays: //! commit //! signature -//! * version_info field removed +//! * version field removed //! * VersionCompatInfo struct created with fields and added to beginning of struct //! version: u16 //! orig_verion: u16, diff --git a/src/bin/cmd/wallet_args.rs b/src/bin/cmd/wallet_args.rs index 79c276ab..cfba47a2 100644 --- a/src/bin/cmd/wallet_args.rs +++ b/src/bin/cmd/wallet_args.rs @@ -21,9 +21,10 @@ use failure::Fail; use grin_wallet_config::WalletConfig; use grin_wallet_controller::command; use grin_wallet_controller::{Error, ErrorKind}; -use grin_wallet_impls::{instantiate_wallet, WalletSeed}; -use grin_wallet_libwallet::{NodeClient, WalletInst}; +use grin_wallet_impls::{instantiate_wallet, FileWalletCommAdapter, WalletSeed}; +use grin_wallet_libwallet::{IssueInvoiceTxArgs, NodeClient, Slate, WalletInst}; use grin_wallet_util::grin_core as core; +use grin_wallet_util::grin_core::core::amount_to_hr_string; use grin_wallet_util::grin_keychain as keychain; use linefeed::terminal::Signal; use linefeed::{Interface, ReadResult}; @@ -140,6 +141,62 @@ fn prompt_recovery_phrase() -> Result { Ok(phrase) } +fn prompt_pay_invoice(slate: &Slate, method: &str, 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 method == "http" { + println!("* The resulting transaction will IMMEDIATELY be sent to the wallet listening at: '{}'.", dest); + } else { + println!("* The resulting transaction will be saved to the file '{}', which you can manually send back to the invoice creator.", dest); + } + println!(); + println!("The invoice slate's participant info is:"); + for m in slate.participant_messages().messages { + println!("{}", m); + } + 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( @@ -457,6 +514,140 @@ pub fn parse_finalize_args(args: &ArgMatches) -> Result 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)); + } + }; + // message + let message = match args.is_present("message") { + true => Some(args.value_of("message").unwrap().to_owned()), + false => None, + }; + // 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 (output file) + let dest = parse_required(args, "dest")?; + Ok(command::IssueInvoiceArgs { + dest: dest.into(), + issue_args: IssueInvoiceTxArgs { + dest_acct_name: None, + amount, + message, + target_slate_version, + }, + }) +} + +pub fn parse_process_invoice_args( + args: &ArgMatches, +) -> Result { + // TODO: display and prompt for confirmation of what we're doing + // message + let message = match args.is_present("message") { + true => Some(args.value_of("message").unwrap().to_owned()), + false => None, + }; + + // 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"); + + // method + let method = parse_required(args, "method")?; + + // dest + let dest = { + if method == "self" { + match args.value_of("dest") { + Some(d) => d, + None => "default", + } + } else { + if !estimate_selection_strategies { + parse_required(args, "dest")? + } else { + "" + } + } + }; + if !estimate_selection_strategies + && method == "http" + && !dest.starts_with("http://") + && !dest.starts_with("https://") + { + let msg = format!( + "HTTP Destination should start with http://: or https://: {}", + dest, + ); + return Err(ParseError::ArgumentError(msg)); + } + + // 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, + } + }; + + // file input only + let tx_file = parse_required(args, "input")?; + + // Now we need to prompt the user whether they want to do this, + // which requires reading the slate + let adapter = FileWalletCommAdapter::new(); + let slate = match adapter.receive_tx_async(&tx_file) { + Ok(s) => s, + Err(e) => return Err(ParseError::ArgumentError(format!("{}", e))), + }; + + #[cfg(not(test))] // don't prompt during automated testing + prompt_pay_invoice(&slate, method, dest)?; + + Ok(command::ProcessInvoiceArgs { + message: message, + minimum_confirmations: min_c, + selection_strategy: selection_strategy.to_owned(), + estimate_selection_strategies, + method: method.to_owned(), + dest: dest.to_owned(), + max_outputs: max_outputs, + target_slate_version: target_slate_version, + input: tx_file.to_owned(), + }) +} + pub fn parse_info_args(args: &ArgMatches) -> Result { // minimum_confirmations let mc = parse_required(args, "minimum_confirmations")?; @@ -610,6 +801,18 @@ pub fn wallet_command( let a = arg_parse!(parse_finalize_args(&args)); command::finalize(inst_wallet(), a) } + ("invoice", Some(args)) => { + let a = arg_parse!(parse_issue_invoice_args(&args)); + command::issue_invoice_tx(inst_wallet(), a) + } + ("pay", Some(args)) => { + let a = arg_parse!(parse_process_invoice_args(&args)); + command::process_invoice( + inst_wallet(), + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } ("info", Some(args)) => { let a = arg_parse!(parse_info_args(&args)); command::info( diff --git a/src/bin/cmd/wallet_tests.rs b/src/bin/cmd/wallet_tests.rs index 7dd27a1b..69aa3460 100644 --- a/src/bin/cmd/wallet_tests.rs +++ b/src/bin/cmd/wallet_tests.rs @@ -480,6 +480,54 @@ mod wallet_tests { ]; 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(), 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)?; @@ -501,6 +549,12 @@ mod wallet_tests { 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)?; + // let logging finish thread::sleep(Duration::from_millis(200)); Ok(()) diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 01a5dd42..22e7d613 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -162,6 +162,74 @@ subcommands: help: Fluff the transaction (ignore Dandelion relay protocol) short: f long: fluff + - invoice: + about: Initialize an invoice transction. + args: + - amount: + help: Number of coins to invoice with optional fraction, e.g. 12.423 + index: 1 + - message: + help: Optional participant message to include + short: g + long: message + takes_value: true + - slate_version: + help: Target slate version to output/send to receiver + short: v + long: slate_version + takes_value: true + - dest: + help: Name of destination slate output file + short: d + long: dest + takes_value: true + - pay: + about: Spend coins to pay the provided invoice transaction + args: + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: c + long: min_conf + default_value: "10" + takes_value: true + - selection_strategy: + help: Coin/Output selection strategy. + short: s + long: selection + possible_values: + - all + - smallest + default_value: all + takes_value: true + - estimate_selection_strategies: + help: Estimates all possible Coin/Output selection strategies. + short: e + long: estimate-selection + - method: + help: Method for sending the processed invoice back to the invoice creator + short: m + long: method + possible_values: + - file + - http + - self + default_value: file + takes_value: true + - dest: + help: Send the transaction to the provided server (start with http://) or save as file. + short: d + long: dest + takes_value: true + - message: + help: Optional participant message to include + short: g + long: message + takes_value: true + - input: + help: Partial transaction to process, expects the invoicer's transaction file. + short: i + long: input + takes_value: true - outputs: about: Raw wallet output info (list of outputs) - txs: