diff --git a/doc/wallet/usage.md b/doc/wallet/usage.md index 74a25e95f..a7151fff0 100644 --- a/doc/wallet/usage.md +++ b/doc/wallet/usage.md @@ -144,6 +144,12 @@ Outputs in your wallet will appear as unconfirmed or locked until the transactio Other flags here are: +* `-m` 'Method', which can be 'http' or 'file'. In the first case, the transaction will be sent to the IP address which follows the `-d` flag. In the second case, Grin wallet will generate a partial transaction file under the file name specified in the `-d` flag. This file needs to be signed by the recipient using the `grin wallet receive -i filename` command and finalize by the sender using the `grin wallet finalize -i filename.response` command. To create a partial transaction file, use: + + ``` +[host]$ grin wallet send -d "transaction" -m file 60.00 +``` + * `-s` 'Selection strategy', which can be 'all' or 'smallest'. Since it's advantageous for outputs to be removed from the Grin chain, the default strategy for selecting inputs in Step 1 above is to use as many outputs as possible to consolidate your balance into a couple of outputs. This also drastically reduces your wallet size, so everyone wins. The downside is that the entire contents of diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index 03f6f1461..477d0275b 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use serde_json as json; +use std::fs::File; +use std::io::Read; use std::path::PathBuf; /// Wallet commands processing use std::process::exit; @@ -166,7 +169,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { wallet_config.clone(), passphrase, ))); - let res = controller::owner_single_use(wallet, |api| { + let res = controller::owner_single_use(wallet.clone(), |api| { match wallet_args.subcommand() { ("send", Some(send_args)) => { let amount = send_args @@ -182,6 +185,9 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { let selection_strategy = send_args .value_of("selection_strategy") .expect("Selection strategy required"); + let method = send_args + .value_of("method") + .expect("Payment method required"); let dest = send_args .value_of("dest") .expect("Destination wallet address required"); @@ -192,54 +198,63 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { .expect("Failed to parse number of change outputs."); let fluff = send_args.is_present("fluff"); let max_outputs = 500; - if dest.starts_with("http") { - let result = api.issue_send_tx( - amount, - minimum_confirmations, - dest, - max_outputs, - change_outputs, - selection_strategy == "all", - ); - let slate = match result { - Ok(s) => { - info!( - LOGGER, - "Tx created: {} grin to {} (strategy '{}')", - core::amount_to_hr_string(amount, false), - dest, - selection_strategy, - ); - s - } - Err(e) => { - error!(LOGGER, "Tx not created: {:?}", e); - match e.kind() { - // user errors, don't backtrace - libwallet::ErrorKind::NotEnoughFunds { .. } => {} - libwallet::ErrorKind::FeeDispute { .. } => {} - libwallet::ErrorKind::FeeExceedsAmount { .. } => {} - _ => { - // otherwise give full dump - error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap()); - } - }; - panic!(); - } - }; - let result = api.post_tx(&slate, fluff); - match result { - Ok(_) => { - info!(LOGGER, "Tx sent",); - Ok(()) - } - Err(e) => { - error!(LOGGER, "Tx not sent: {:?}", e); - Err(e) + if method == "http" { + if dest.starts_with("http://") { + let result = api.issue_send_tx( + amount, + minimum_confirmations, + dest, + max_outputs, + change_outputs, + selection_strategy == "all", + ); + let slate = match result { + Ok(s) => { + info!( + LOGGER, + "Tx created: {} grin to {} (strategy '{}')", + core::amount_to_hr_string(amount, false), + dest, + selection_strategy, + ); + s + } + Err(e) => { + error!(LOGGER, "Tx not created: {:?}", e); + match e.kind() { + // user errors, don't backtrace + libwallet::ErrorKind::NotEnoughFunds { .. } => {} + libwallet::ErrorKind::FeeDispute { .. } => {} + libwallet::ErrorKind::FeeExceedsAmount { .. } => {} + _ => { + // otherwise give full dump + error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap()); + } + }; + panic!(); + } + }; + let result = api.post_tx(&slate, fluff); + match result { + Ok(_) => { + info!(LOGGER, "Tx sent",); + Ok(()) + } + Err(e) => { + error!(LOGGER, "Tx not sent: {:?}", e); + Err(e) + } } + } else { + error!( + LOGGER, + "HTTP Destination should start with http://: {}", dest + ); + panic!(); } - } else { - api.file_send_tx( + } else if method == "file" { + api.send_tx( + true, amount, minimum_confirmations, dest, @@ -248,21 +263,36 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { selection_strategy == "all", ).expect("Send failed"); Ok(()) + } else { + error!(LOGGER, "unsupported payment method: {}", method); + panic!(); } } ("receive", Some(send_args)) => { - let tx_file = send_args - .value_of("input") - .expect("Transaction file required"); - api.file_receive_tx(tx_file).expect("Receive failed"); - Ok(()) + let mut receive_result: Result<(), grin_wallet::libwallet::Error> = Ok(()); + let res = controller::foreign_single_use(wallet, |api| { + let tx_file = send_args + .value_of("input") + .expect("Transaction file required"); + receive_result = api.file_receive_tx(tx_file); + Ok(()) + }); + if res.is_err() { + exit(1); + } + receive_result } ("finalize", Some(send_args)) => { let fluff = send_args.is_present("fluff"); let tx_file = send_args .value_of("input") .expect("Receiver's transaction file required"); - let slate = api.file_finalize_tx(tx_file).expect("Finalize failed"); + let mut pub_tx_f = File::open(tx_file)?; + let mut content = String::new(); + pub_tx_f.read_to_string(&mut content)?; + let mut slate: grin_wallet::libtx::slate::Slate = json::from_str(&content) + .map_err(|_| grin_wallet::libwallet::ErrorKind::Format)?; + let _ = api.finalize_tx(&mut slate).expect("Finalize failed"); let result = api.post_tx(&slate, fluff); match result { @@ -377,7 +407,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { } } Some(f) => { - let result = api.dump_stored_tx(tx_id, f); + let result = api.dump_stored_tx(tx_id, true, f); match result { Ok(_) => { warn!(LOGGER, "Dumped transaction data for tx {} to {}", tx_id, f); diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 7702d5ea3..0c33d16ba 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -213,6 +213,13 @@ fn main() { .long("change_outputs") .default_value("1") .takes_value(true)) + .arg(Arg::with_name("method") + .help("Method for sending this transaction.") + .short("m") + .long("method") + .possible_values(&["http", "file"]) + .default_value("http") + .takes_value(true)) .arg(Arg::with_name("dest") .help("Send the transaction to the provided server (start with http://) or save as file.") .short("d") diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index 44be3ea56..1935f2257 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -185,15 +185,16 @@ where /// Write a transaction to send to file so a user can transmit it to the /// receiver in whichever way they see fit (aka carrier pigeon mode). - pub fn file_send_tx( + pub fn send_tx( &mut self, + write_to_disk: bool, amount: u64, minimum_confirmations: u64, dest: &str, max_outputs: usize, num_change_outputs: usize, selection_strategy_is_use_all: bool, - ) -> Result<(), Error> { + ) -> Result { let mut w = self.wallet.lock().unwrap(); w.open_with_credentials()?; @@ -205,10 +206,11 @@ where num_change_outputs, selection_strategy_is_use_all, )?; - - let mut pub_tx = File::create(dest)?; - pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?; - pub_tx.sync_all()?; + if write_to_disk { + let mut pub_tx = File::create(dest)?; + pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?; + pub_tx.sync_all()?; + } { let mut batch = w.batch()?; @@ -221,60 +223,19 @@ where // lock our inputs lock_fn(&mut **w, &tx_hex)?; w.close()?; - Ok(()) - } - - /// A sender provided a transaction file with appropriate public keys and - /// metadata. Complete the receivers' end of it to generate another file - /// to send back. - pub fn file_receive_tx(&mut self, source: &str) -> Result<(), Error> { - let mut pub_tx_f = File::open(source)?; - let mut content = String::new(); - pub_tx_f.read_to_string(&mut content)?; - let mut slate: Slate = json::from_str(&content).map_err(|_| ErrorKind::Format)?; - - let mut wallet = self.wallet.lock().unwrap(); - wallet.open_with_credentials()?; - - // create an output using the amount in the slate - let (_, mut context, receiver_create_fn) = - selection::build_recipient_output_with_slate(&mut **wallet, &mut slate)?; - - // fill public keys - let _ = slate.fill_round_1( - wallet.keychain(), - &mut context.sec_key, - &context.sec_nonce, - 1, - )?; - - // perform partial sig - let _ = slate.fill_round_2(wallet.keychain(), &context.sec_key, &context.sec_nonce, 1)?; - - // save to file - let mut pub_tx = File::create(source.to_owned() + ".response")?; - pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?; - - // Save output in wallet - let _ = receiver_create_fn(&mut wallet); - Ok(()) + Ok(slate) } /// Sender finalization of the transaction. Takes the file returned by the /// sender as well as the private file generate on the first send step. /// Builds the complete transaction and sends it to a grin node for /// propagation. - pub fn file_finalize_tx(&mut self, receiver_file: &str) -> Result { - let mut pub_tx_f = File::open(receiver_file)?; - let mut content = String::new(); - pub_tx_f.read_to_string(&mut content)?; - let mut slate: Slate = json::from_str(&content).map_err(|_| ErrorKind::Format)?; - + pub fn finalize_tx(&mut self, slate: &mut Slate) -> Result<(), Error> { let mut w = self.wallet.lock().unwrap(); w.open_with_credentials()?; let context = w.get_private_context(slate.id.as_bytes())?; - tx::complete_tx(&mut **w, &mut slate, &context)?; + tx::complete_tx(&mut **w, slate, &context)?; { let mut batch = w.batch()?; batch.delete_private_context(slate.id.as_bytes())?; @@ -282,7 +243,7 @@ where } w.close()?; - Ok(slate) + Ok(()) } /// Roll back a transaction and all associated outputs with a given @@ -342,7 +303,12 @@ where } /// Writes stored transaction data to a given file - pub fn dump_stored_tx(&self, tx_id: u32, dest: &str) -> Result<(), Error> { + pub fn dump_stored_tx( + &self, + tx_id: u32, + write_to_disk: bool, + dest: &str, + ) -> Result { let (confirmed, tx_hex) = { let mut w = self.wallet.lock().unwrap(); w.open_with_credentials()?; @@ -365,10 +331,12 @@ where } let tx_bin = util::from_hex(tx_hex.unwrap()).unwrap(); let tx = ser::deserialize::(&mut &tx_bin[..])?; - let mut tx_file = File::create(dest)?; - tx_file.write_all(json::to_string(&tx).unwrap().as_bytes())?; - tx_file.sync_all()?; - Ok(()) + if write_to_disk { + let mut tx_file = File::create(dest)?; + tx_file.write_all(json::to_string(&tx).unwrap().as_bytes())?; + tx_file.sync_all()?; + } + Ok(tx) } /// (Re)Posts a transaction that's already been stored to the chain @@ -498,6 +466,42 @@ where res } + /// A sender provided a transaction file with appropriate public keys and + /// metadata. Complete the receivers' end of it to generate another file + /// to send back. + pub fn file_receive_tx(&mut self, source: &str) -> Result<(), Error> { + let mut pub_tx_f = File::open(source)?; + let mut content = String::new(); + pub_tx_f.read_to_string(&mut content)?; + let mut slate: Slate = json::from_str(&content).map_err(|_| ErrorKind::Format)?; + + let mut wallet = self.wallet.lock().unwrap(); + wallet.open_with_credentials()?; + + // create an output using the amount in the slate + let (_, mut context, receiver_create_fn) = + selection::build_recipient_output_with_slate(&mut **wallet, &mut slate)?; + + // fill public keys + let _ = slate.fill_round_1( + wallet.keychain(), + &mut context.sec_key, + &context.sec_nonce, + 1, + )?; + + // perform partial sig + let _ = slate.fill_round_2(wallet.keychain(), &context.sec_key, &context.sec_nonce, 1)?; + + // save to file + let mut pub_tx = File::create(source.to_owned() + ".response")?; + pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?; + + // Save output in wallet + let _ = receiver_create_fn(&mut wallet); + Ok(()) + } + /// Receive a transaction from a sender pub fn receive_tx(&mut self, slate: &mut Slate) -> Result<(), Error> { let mut w = self.wallet.lock().unwrap(); diff --git a/wallet/src/libwallet/controller.rs b/wallet/src/libwallet/controller.rs index 459fdcfac..513317fa2 100644 --- a/wallet/src/libwallet/controller.rs +++ b/wallet/src/libwallet/controller.rs @@ -28,6 +28,7 @@ use hyper::{Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json; +use core::core::Transaction; use keychain::Keychain; use libtx::slate::Slate; use libwallet::api::{APIForeign, APIOwner}; @@ -229,6 +230,35 @@ where api.retrieve_txs(update_from_node, id) } + fn dump_stored_tx( + &self, + req: &Request, + api: APIOwner, + ) -> Result { + let params = parse_params(req); + if let Some(id_string) = params.get("id") { + match id_string[0].parse() { + Ok(id) => match api.dump_stored_tx(id, false, "") { + Ok(tx) => Ok(tx), + Err(e) => { + error!(LOGGER, "dump_stored_tx: failed with error: {}", e); + Err(e) + } + }, + Err(e) => { + error!(LOGGER, "dump_stored_tx: could not parse id: {}", e); + Err(ErrorKind::TransactionDumpError( + "dump_stored_tx: cannot dump transaction. Could not parse id in request.", + ).into()) + } + } + } else { + Err(ErrorKind::TransactionDumpError( + "dump_stored_tx: Cannot dump transaction. Missing id param in request.", + ).into()) + } + } + fn retrieve_summary_info( &self, req: &Request, @@ -260,6 +290,7 @@ where "retrieve_summary_info" => json_response(&self.retrieve_summary_info(req, api)?), "node_height" => json_response(&self.node_height(req, api)?), "retrieve_txs" => json_response(&self.retrieve_txs(req, api)?), + "dump_stored_tx" => json_response(&self.dump_stored_tx(req, api)?), _ => response(StatusCode::BAD_REQUEST, ""), }) } @@ -270,17 +301,77 @@ where mut api: APIOwner, ) -> Box + Send> { Box::new(parse_body(req).and_then(move |args: SendTXArgs| { - api.issue_send_tx( - args.amount, - args.minimum_confirmations, - &args.dest, - args.max_outputs, - args.num_change_outputs, - args.selection_strategy_is_use_all, - ) + if args.method == "http" { + api.issue_send_tx( + args.amount, + args.minimum_confirmations, + &args.dest, + args.max_outputs, + args.num_change_outputs, + args.selection_strategy_is_use_all, + ) + } else if args.method == "file" { + api.send_tx( + false, + args.amount, + args.minimum_confirmations, + &args.dest, + args.max_outputs, + args.num_change_outputs, + args.selection_strategy_is_use_all, + ) + } else { + error!(LOGGER, "unsupported payment method: {}", args.method); + return Err(ErrorKind::ClientCallback("unsupported payment method"))?; + } })) } + fn finalize_tx( + &self, + req: Request, + mut api: APIOwner, + ) -> Box + Send> { + Box::new( + parse_body(req).and_then(move |mut slate| match api.finalize_tx(&mut slate) { + Ok(_) => ok(slate.clone()), + Err(e) => { + error!(LOGGER, "finalize_tx: failed with error: {}", e); + err(e) + } + }), + ) + } + + fn cancel_tx( + &self, + req: Request, + mut api: APIOwner, + ) -> Box + Send> { + let params = parse_params(&req); + if let Some(id_string) = params.get("id") { + Box::new(match id_string[0].parse() { + Ok(id) => match api.cancel_tx(id) { + Ok(_) => ok(()), + Err(e) => { + error!(LOGGER, "finalize_tx: failed with error: {}", e); + err(e) + } + }, + Err(e) => { + error!(LOGGER, "finalize_tx: could not parse id: {}", e); + err(ErrorKind::TransactionCancellationError( + "finalize_tx: cannot cancel transaction. Could not parse id in request.", + ).into()) + } + }) + } else { + Box::new(err(ErrorKind::TransactionCancellationError( + "finalize_tx: Cannot cancel transaction. Missing id param in request.", + ).into())) + } + } + fn issue_burn_tx( &self, _req: Request, @@ -307,6 +398,14 @@ where self.issue_send_tx(req, api) .and_then(|slate| ok(json_response_pretty(&slate))), ), + "finalize_tx" => Box::new( + self.finalize_tx(req, api) + .and_then(|slate| ok(json_response_pretty(&slate))), + ), + "cancel_tx" => Box::new( + self.cancel_tx(req, api) + .and_then(|_| ok(response(StatusCode::OK, ""))), + ), "issue_burn_tx" => Box::new( self.issue_burn_tx(req, api) .and_then(|_| ok(response(StatusCode::OK, ""))), diff --git a/wallet/src/libwallet/error.rs b/wallet/src/libwallet/error.rs index 344dbc4bf..93735f192 100644 --- a/wallet/src/libwallet/error.rs +++ b/wallet/src/libwallet/error.rs @@ -152,6 +152,10 @@ pub enum ErrorKind { #[fail(display = "Cancellation Error: {}", _0)] TransactionCancellationError(&'static str), + /// Cancellation error + #[fail(display = "Tx dump Error: {}", _0)] + TransactionDumpError(&'static str), + /// Attempt to repost a transaction that's already confirmed #[fail(display = "Transaction already confirmed error")] TransactionAlreadyConfirmed, diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 8a4cc1221..614a04eee 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -642,6 +642,8 @@ pub struct SendTXArgs { pub amount: u64, /// minimum confirmations pub minimum_confirmations: u64, + /// payment method + pub method: String, /// destination url pub dest: String, /// Max number of outputs