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.
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

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(|| {
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,

View file

@ -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")

View file

@ -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<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
/// 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 {

View file

@ -36,6 +36,7 @@ pub fn build_send_tx_slate<T: ?Sized, C, K>(
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<T: ?Sized, C, K>(
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;

View file

@ -32,6 +32,7 @@ pub fn receive_tx<T: ?Sized, C, K>(
wallet: &mut T,
slate: &mut Slate,
parent_key_id: &Identifier,
is_self: bool,
) -> Result<(), Error>
where
T: WalletBackend<C, K>,
@ -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<T: ?Sized, C, K>(
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

View file

@ -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"),
}

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());
}
}