mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 08:51:08 +03:00
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:
parent
e9c50ccb56
commit
ee19cfcf86
8 changed files with 315 additions and 10 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
162
wallet/tests/self_send.rs
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue