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
This commit is contained in:
Yeastplume 2018-11-08 09:46:09 +00:00 committed by GitHub
parent e9c50ccb56
commit ee19cfcf86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 315 additions and 10 deletions

View file

@ -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. 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: 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 ```sh
[host]$ grin wallet send -d "transaction" -m file 60.00 [host]$ grin wallet send -d "transaction" -m file 60.00

View file

@ -267,9 +267,20 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
let method = send_args.value_of("method").ok_or_else(|| { let method = send_args.value_of("method").ok_or_else(|| {
ErrorKind::GenericError("Payment method required".to_string()) ErrorKind::GenericError("Payment method required".to_string())
})?; })?;
let dest = send_args.value_of("dest").ok_or_else(|| { let dest = {
ErrorKind::GenericError("Destination wallet address required".to_string()) 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 let change_outputs = send_args
.value_of("change_outputs") .value_of("change_outputs")
.ok_or_else(|| ErrorKind::GenericError("Change outputs required".to_string())) .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 dest
)).into()); )).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" { } else if method == "file" {
api.send_tx( api.send_tx(
true, true,

View file

@ -235,7 +235,7 @@ fn real_main() -> i32 {
.help("Method for sending this transaction.") .help("Method for sending this transaction.")
.short("m") .short("m")
.long("method") .long("method")
.possible_values(&["http", "file"]) .possible_values(&["http", "file", "self"])
.default_value("http") .default_value("http")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("dest") .arg(Arg::with_name("dest")

View file

@ -181,6 +181,7 @@ where
num_change_outputs, num_change_outputs,
selection_strategy_is_use_all, selection_strategy_is_use_all,
&parent_key_id, &parent_key_id,
false,
)?; )?;
lock_fn_out = lock_fn; lock_fn_out = lock_fn;
@ -204,6 +205,50 @@ where
Ok(slate_out) 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<Slate, Error> {
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 /// 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). /// receiver in whichever way they see fit (aka carrier pigeon mode).
pub fn send_tx( pub fn send_tx(
@ -228,6 +273,7 @@ where
num_change_outputs, num_change_outputs,
selection_strategy_is_use_all, selection_strategy_is_use_all,
&parent_key_id, &parent_key_id,
false,
)?; )?;
if write_to_disk { if write_to_disk {
let mut pub_tx = File::create(dest)?; let mut pub_tx = File::create(dest)?;
@ -507,8 +553,12 @@ where
let parent_key_id = wallet.parent_key_id(); let parent_key_id = wallet.parent_key_id();
// create an output using the amount in the slate // create an output using the amount in the slate
let (_, mut context, receiver_create_fn) = let (_, mut context, receiver_create_fn) = selection::build_recipient_output_with_slate(
selection::build_recipient_output_with_slate(&mut **wallet, &mut slate, parent_key_id)?; &mut **wallet,
&mut slate,
parent_key_id,
false,
)?;
// fill public keys // fill public keys
let _ = slate.fill_round_1( let _ = slate.fill_round_1(
@ -535,7 +585,7 @@ where
let mut w = self.wallet.lock(); let mut w = self.wallet.lock();
w.open_with_credentials()?; w.open_with_credentials()?;
let parent_key_id = w.parent_key_id(); 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()?; w.close()?;
if let Err(e) = res { if let Err(e) = res {

View file

@ -36,6 +36,7 @@ pub fn build_send_tx_slate<T: ?Sized, C, K>(
change_outputs: usize, change_outputs: usize,
selection_strategy_is_use_all: bool, selection_strategy_is_use_all: bool,
parent_key_id: Identifier, parent_key_id: Identifier,
is_self: bool,
) -> Result< ) -> Result<
( (
Slate, Slate,
@ -98,6 +99,9 @@ where
let mut batch = wallet.batch()?; let mut batch = wallet.batch()?;
let log_id = batch.next_tx_log_id(&parent_key_id)?; let log_id = batch.next_tx_log_id(&parent_key_id)?;
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_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.tx_slate_id = Some(slate_id);
t.fee = Some(fee); t.fee = Some(fee);
t.tx_hex = Some(tx_hex.to_owned()); t.tx_hex = Some(tx_hex.to_owned());
@ -144,6 +148,7 @@ pub fn build_recipient_output_with_slate<T: ?Sized, C, K>(
wallet: &mut T, wallet: &mut T,
slate: &mut Slate, slate: &mut Slate,
parent_key_id: Identifier, parent_key_id: Identifier,
is_self: bool,
) -> Result< ) -> Result<
( (
Identifier, Identifier,
@ -185,6 +190,9 @@ where
let mut batch = wallet.batch()?; let mut batch = wallet.batch()?;
let log_id = batch.next_tx_log_id(&parent_key_id)?; let log_id = batch.next_tx_log_id(&parent_key_id)?;
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_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.tx_slate_id = Some(slate_id);
t.amount_credited = amount; t.amount_credited = amount;
t.num_outputs = 1; t.num_outputs = 1;

View file

@ -32,6 +32,7 @@ pub fn receive_tx<T: ?Sized, C, K>(
wallet: &mut T, wallet: &mut T,
slate: &mut Slate, slate: &mut Slate,
parent_key_id: &Identifier, parent_key_id: &Identifier,
is_self: bool,
) -> Result<(), Error> ) -> Result<(), Error>
where where
T: WalletBackend<C, K>, T: WalletBackend<C, K>,
@ -39,8 +40,12 @@ where
K: Keychain, K: Keychain,
{ {
// create an output using the amount in the slate // create an output using the amount in the slate
let (_, mut context, receiver_create_fn) = let (_, mut context, receiver_create_fn) = selection::build_recipient_output_with_slate(
selection::build_recipient_output_with_slate(wallet, slate, parent_key_id.clone())?; wallet,
slate,
parent_key_id.clone(),
is_self,
)?;
// fill public keys // fill public keys
let _ = slate.fill_round_1( let _ = slate.fill_round_1(
@ -69,6 +74,7 @@ pub fn create_send_tx<T: ?Sized, C, K>(
num_change_outputs: usize, num_change_outputs: usize,
selection_strategy_is_use_all: bool, selection_strategy_is_use_all: bool,
parent_key_id: &Identifier, parent_key_id: &Identifier,
is_self: bool,
) -> Result< ) -> Result<
( (
Slate, Slate,
@ -107,6 +113,7 @@ where
num_change_outputs, num_change_outputs,
selection_strategy_is_use_all, selection_strategy_is_use_all,
parent_key_id.clone(), parent_key_id.clone(),
is_self,
)?; )?;
// Generate a kernel offset and subtract from our context's secret key. Store // Generate a kernel offset and subtract from our context's secret key. Store

View file

@ -546,6 +546,10 @@ pub enum TxLogEntryType {
TxReceived, TxReceived,
/// Inputs locked + change outputs when a transaction is created /// Inputs locked + change outputs when a transaction is created
TxSent, TxSent,
/// As above, but self-transaction
TxReceivedSelf,
/// As Above
TxSentSelf,
/// Received transaction that was rolled back by user /// Received transaction that was rolled back by user
TxReceivedCancelled, TxReceivedCancelled,
/// Sent transaction that was rolled back by user /// Sent transaction that was rolled back by user
@ -558,6 +562,8 @@ impl fmt::Display for TxLogEntryType {
TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed \nCoinbase"), TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed \nCoinbase"),
TxLogEntryType::TxReceived => write!(f, "Received Tx"), TxLogEntryType::TxReceived => write!(f, "Received Tx"),
TxLogEntryType::TxSent => write!(f, "Sent 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::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"),
TxLogEntryType::TxSentCancelled => write!(f, "Send Tx\n- Cancelled"), TxLogEntryType::TxSentCancelled => write!(f, "Send Tx\n- Cancelled"),
} }

162
wallet/tests/self_send.rs Normal file
View file

@ -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<LocalWalletClient, ExtKeychain> = 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());
}
}