From ee19cfcf86e3abb0145206a8eba342579f6dee71 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Thu, 8 Nov 2018 09:46:09 +0000 Subject: [PATCH] Wallet self-send (#1939) * add self send function to wallet * rustfmt * add destination to self send method * update usage doc * doc clarification * remove localhost send restriction --- doc/wallet/usage.md | 16 +- src/bin/cmd/wallet.rs | 64 +++++++- src/bin/grin.rs | 2 +- wallet/src/libwallet/api.rs | 56 ++++++- wallet/src/libwallet/internal/selection.rs | 8 + wallet/src/libwallet/internal/tx.rs | 11 +- wallet/src/libwallet/types.rs | 6 + wallet/tests/self_send.rs | 162 +++++++++++++++++++++ 8 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 wallet/tests/self_send.rs diff --git a/doc/wallet/usage.md b/doc/wallet/usage.md index e3e483120..37ae6d539 100644 --- a/doc/wallet/usage.md +++ b/doc/wallet/usage.md @@ -177,9 +177,23 @@ It's important to understand exactly what happens during a send command, so at a Outputs in your wallet will appear as unconfirmed or locked until the transaction hits the chain and is mined and validated. +You can also create a transaction entirely within your own wallet by specifying the method 'self'. Using the 'self' method, you can send yourself money in a single command (for testing purposes,) or distribute funds between accounts within your wallet without having to run a listener or manipulate files. For instance, to send funds from your wallet's 'default' account to an account called 'account1', use: + +```sh +[host]$ grin wallet send -m self -d "account1" 60 +``` + +or, to send between accounts, use the -a flag to specify the source account: + +```sh +[host]$ grin wallet -a "my_source_account" send -m self -d "my_dest_account" 60 +``` + +When sending to self, the transaction will be created and posted to the chain in the same operation. + 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: +* `-m` 'Method', which can be 'http', 'file' or 'self' (described above). If 'http' is specified (default), the transaction will be sent to the IP address which follows the `-d` flag. If 'file' is specified, 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 finalized by the sender using the `grin wallet finalize -i filename.response` command. To create a partial transaction file, use: ```sh [host]$ grin wallet send -d "transaction" -m file 60.00 diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index f7114bea2..c981ed89f 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -267,9 +267,20 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i let method = send_args.value_of("method").ok_or_else(|| { ErrorKind::GenericError("Payment method required".to_string()) })?; - let dest = send_args.value_of("dest").ok_or_else(|| { - ErrorKind::GenericError("Destination wallet address required".to_string()) - })?; + let dest = { + if method == "self" { + match send_args.value_of("dest") { + Some(d) => d, + None => "default", + } + } else { + send_args.value_of("dest").ok_or_else(|| { + ErrorKind::GenericError( + "Destination wallet address required".to_string(), + ) + })? + } + }; let change_outputs = send_args .value_of("change_outputs") .ok_or_else(|| ErrorKind::GenericError("Change outputs required".to_string())) @@ -335,6 +346,53 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i dest )).into()); } + } else if method == "self" { + let result = api.issue_self_tx( + amount, + minimum_confirmations, + max_outputs, + change_outputs, + selection_strategy == "all", + account, + dest, + ); + let slate = match result { + Ok(s) => { + info!( + "Tx created: {} grin to self, source acct: {} dest_acct: {} (strategy '{}')", + core::amount_to_hr_string(amount, false), + account, + dest, + selection_strategy, + ); + s + } + Err(e) => { + error!("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!("Backtrace: {}", e.backtrace().unwrap()); + } + }; + return Err(e); + } + }; + let result = api.post_tx(&slate, fluff); + match result { + Ok(_) => { + info!("Tx sent",); + Ok(()) + } + Err(e) => { + error!("Tx not sent: {}", e); + Err(e) + } + } } else if method == "file" { api.send_tx( true, diff --git a/src/bin/grin.rs b/src/bin/grin.rs index dbce54485..8679f7f07 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -235,7 +235,7 @@ fn real_main() -> i32 { .help("Method for sending this transaction.") .short("m") .long("method") - .possible_values(&["http", "file"]) + .possible_values(&["http", "file", "self"]) .default_value("http") .takes_value(true)) .arg(Arg::with_name("dest") diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index 6077e78c3..0a2728ce3 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -181,6 +181,7 @@ where num_change_outputs, selection_strategy_is_use_all, &parent_key_id, + false, )?; lock_fn_out = lock_fn; @@ -204,6 +205,50 @@ where Ok(slate_out) } + /// Issues a send transaction to the same wallet, without needing communication + /// good for consolidating outputs, or can be extended to split outputs to multiple + /// accounts + pub fn issue_self_tx( + &mut self, + amount: u64, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + src_acct_name: &str, + dest_acct_name: &str, + ) -> Result { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let orig_parent_key_id = w.parent_key_id(); + w.set_parent_key_id_by_name(src_acct_name)?; + let parent_key_id = w.parent_key_id(); + + let (mut slate, context, lock_fn) = tx::create_send_tx( + &mut **w, + amount, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + true, + )?; + + w.set_parent_key_id_by_name(dest_acct_name)?; + let parent_key_id = w.parent_key_id(); + tx::receive_tx(&mut **w, &mut slate, &parent_key_id, true)?; + + tx::complete_tx(&mut **w, &mut slate, &context)?; + let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap()); + + // lock our inputs + lock_fn(&mut **w, &tx_hex)?; + w.set_parent_key_id(orig_parent_key_id); + w.close()?; + Ok(slate) + } + /// 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 send_tx( @@ -228,6 +273,7 @@ where num_change_outputs, selection_strategy_is_use_all, &parent_key_id, + false, )?; if write_to_disk { let mut pub_tx = File::create(dest)?; @@ -507,8 +553,12 @@ where let parent_key_id = wallet.parent_key_id(); // 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, parent_key_id)?; + let (_, mut context, receiver_create_fn) = selection::build_recipient_output_with_slate( + &mut **wallet, + &mut slate, + parent_key_id, + false, + )?; // fill public keys let _ = slate.fill_round_1( @@ -535,7 +585,7 @@ where let mut w = self.wallet.lock(); w.open_with_credentials()?; let parent_key_id = w.parent_key_id(); - let res = tx::receive_tx(&mut **w, slate, &parent_key_id); + let res = tx::receive_tx(&mut **w, slate, &parent_key_id, false); w.close()?; if let Err(e) = res { diff --git a/wallet/src/libwallet/internal/selection.rs b/wallet/src/libwallet/internal/selection.rs index 523684489..c118fc50a 100644 --- a/wallet/src/libwallet/internal/selection.rs +++ b/wallet/src/libwallet/internal/selection.rs @@ -36,6 +36,7 @@ pub fn build_send_tx_slate( change_outputs: usize, selection_strategy_is_use_all: bool, parent_key_id: Identifier, + is_self: bool, ) -> Result< ( Slate, @@ -98,6 +99,9 @@ where let mut batch = wallet.batch()?; let log_id = batch.next_tx_log_id(&parent_key_id)?; let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id); + if is_self { + t.tx_type = TxLogEntryType::TxSentSelf; + } t.tx_slate_id = Some(slate_id); t.fee = Some(fee); t.tx_hex = Some(tx_hex.to_owned()); @@ -144,6 +148,7 @@ pub fn build_recipient_output_with_slate( wallet: &mut T, slate: &mut Slate, parent_key_id: Identifier, + is_self: bool, ) -> Result< ( Identifier, @@ -185,6 +190,9 @@ where let mut batch = wallet.batch()?; let log_id = batch.next_tx_log_id(&parent_key_id)?; let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id); + if is_self { + t.tx_type = TxLogEntryType::TxReceivedSelf; + } t.tx_slate_id = Some(slate_id); t.amount_credited = amount; t.num_outputs = 1; diff --git a/wallet/src/libwallet/internal/tx.rs b/wallet/src/libwallet/internal/tx.rs index 58e454fb7..23e419a45 100644 --- a/wallet/src/libwallet/internal/tx.rs +++ b/wallet/src/libwallet/internal/tx.rs @@ -32,6 +32,7 @@ pub fn receive_tx( wallet: &mut T, slate: &mut Slate, parent_key_id: &Identifier, + is_self: bool, ) -> Result<(), Error> where T: WalletBackend, @@ -39,8 +40,12 @@ where K: Keychain, { // create an output using the amount in the slate - let (_, mut context, receiver_create_fn) = - selection::build_recipient_output_with_slate(wallet, slate, parent_key_id.clone())?; + let (_, mut context, receiver_create_fn) = selection::build_recipient_output_with_slate( + wallet, + slate, + parent_key_id.clone(), + is_self, + )?; // fill public keys let _ = slate.fill_round_1( @@ -69,6 +74,7 @@ pub fn create_send_tx( num_change_outputs: usize, selection_strategy_is_use_all: bool, parent_key_id: &Identifier, + is_self: bool, ) -> Result< ( Slate, @@ -107,6 +113,7 @@ where num_change_outputs, selection_strategy_is_use_all, parent_key_id.clone(), + is_self, )?; // Generate a kernel offset and subtract from our context's secret key. Store diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 915ce651e..3203fc86c 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -546,6 +546,10 @@ pub enum TxLogEntryType { TxReceived, /// Inputs locked + change outputs when a transaction is created TxSent, + /// As above, but self-transaction + TxReceivedSelf, + /// As Above + TxSentSelf, /// Received transaction that was rolled back by user TxReceivedCancelled, /// Sent transaction that was rolled back by user @@ -558,6 +562,8 @@ impl fmt::Display for TxLogEntryType { TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed \nCoinbase"), TxLogEntryType::TxReceived => write!(f, "Received Tx"), TxLogEntryType::TxSent => write!(f, "Sent Tx"), + TxLogEntryType::TxReceivedSelf => write!(f, "Received Tx (Self)"), + TxLogEntryType::TxSentSelf => write!(f, "Sent Tx (Self)"), TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"), TxLogEntryType::TxSentCancelled => write!(f, "Send Tx\n- Cancelled"), } diff --git a/wallet/tests/self_send.rs b/wallet/tests/self_send.rs new file mode 100644 index 000000000..b2ece0d4d --- /dev/null +++ b/wallet/tests/self_send.rs @@ -0,0 +1,162 @@ +// Copyright 2018 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. + +//! Test a wallet sending to self +extern crate grin_chain as chain; +extern crate grin_core as core; +extern crate grin_keychain as keychain; +extern crate grin_store as store; +extern crate grin_util as util; +extern crate grin_wallet as wallet; +extern crate rand; +#[macro_use] +extern crate log; +extern crate chrono; +extern crate serde; +extern crate uuid; + +mod common; +use common::testclient::{LocalWalletClient, WalletProxy}; + +use std::fs; +use std::thread; +use std::time::Duration; + +use core::global; +use core::global::ChainTypes; +use keychain::ExtKeychain; +use wallet::libwallet; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// self send impl +fn self_send_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = common::create_wallet(&format!("{}/wallet1", test_dir), client.clone()); + wallet_proxy.add_wallet("wallet1", client.get_send_instance(), wallet1.clone()); + + // define recipient wallet, add to proxy + let wallet2 = common::create_wallet(&format!("{}/wallet2", test_dir), client.clone()); + let client = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + wallet_proxy.add_wallet("wallet2", client.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.new_account_path("mining")?; + api.new_account_path("listener")?; + Ok(()) + })?; + + // add account to wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), |api| { + api.new_account_path("listener")?; + Ok(()) + })?; + + // Default wallet 2 to listen on that account + { + let mut w = wallet2.lock(); + w.set_parent_key_id_by_name("listener")?; + } + + // Get some mining done + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let slate = api.issue_self_tx( + reward * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + "mining", + "listener", + )?; + api.post_tx(&slate, false)?; //mines a block + bh += 1; + Ok(()) + })?; + + let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'listener' account + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("listener")?; + } + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, 2 * reward); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_stress() { + let test_dir = "test_output/self_send"; + if let Err(e) = self_send_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +}