diff --git a/doc/wallet/usage.md b/doc/wallet/usage.md index e4f94d549..74a25e95f 100644 --- a/doc/wallet/usage.md +++ b/doc/wallet/usage.md @@ -270,6 +270,22 @@ Be sure to use this command with caution, as there are many edge cases and possi the recipient of a transaction. For the time being please be 100% certain that the relevant transaction is never, ever going to be posted before running `grin wallet cancel` +##### repost + +If you're the sender of a posted transaction that doesn't confirm on the chain (due to a fork or full transaction pool), you can repost the copy of it that grin automatically stores in your wallet data whenever a transaction is finalized. This doesn't need to communicate with the recipient again, it just re-posts a transaction created during a previous `send` attempt. + +To do this, look up the transaction id using the `grin wallet txs` command, and using the id (say 3 in this example,) enter: + +`grin wallet repost -i 3` + +This will attempt to repost the transaction to the chain. Note this won't attempt to send if the transaction is already marked as 'confirmed' within the wallet. + +You can also use the `repost` command to dump the transaction in a raw json format with the `-m` (duMp) switch, e.g: + +`grin wallet repost -i 3 -m tx_3.json` + +This will create a file called tx_3.json containing your raw transaction data. Note that this formatting in the file isn't yet very user-readable. + ##### restore If for some reason the wallet cancel commands above don't work, or you need to restore from a backed up `wallet.seed` file and password, you can perform a full wallet restore. diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index 0444e819e..36aca992a 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -343,6 +343,50 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { }; Ok(()) } + ("repost", Some(repost_args)) => { + let tx_id: u32 = match repost_args.value_of("id") { + None => { + error!(LOGGER, "Transaction of a completed but unconfirmed transaction required (specify with --id=[id])"); + panic!(); + } + Some(tx) => match tx.parse() { + Ok(t) => t, + Err(_) => { + panic!("Unable to parse argument 'id' as a number"); + } + }, + }; + let dump_file = repost_args.value_of("dumpfile"); + let fluff = repost_args.is_present("fluff"); + match dump_file { + None => { + let result = api.post_stored_tx(tx_id, fluff); + match result { + Ok(_) => { + info!(LOGGER, "Reposted transaction at {}", tx_id); + Ok(()) + } + Err(e) => { + error!(LOGGER, "Tranasction reposting failed: {}", e); + Err(e) + } + } + } + Some(f) => { + let result = api.dump_stored_tx(tx_id, f); + match result { + Ok(_) => { + warn!(LOGGER, "Dumped transaction data for tx {} to {}", tx_id, f); + Ok(()) + } + Err(e) => { + error!(LOGGER, "Tranasction reposting failed: {}", e); + Err(e) + } + } + } + } + } ("cancel", Some(tx_args)) => { let tx_id = tx_args .value_of("id") diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 2bc216f1a..7702d5ea3 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -222,6 +222,11 @@ fn main() { .help("Fluff the transaction (ignore Dandelion relay protocol)") .short("f") .long("fluff"))) + .arg(Arg::with_name("stored_tx") + .help("If present, use the previously stored Unconfirmed transaction with given id.") + .short("t") + .long("stored_tx") + .takes_value(true)) .subcommand(SubCommand::with_name("receive") .about("Processes a transaction file to accept a transfer from a sender.") @@ -268,6 +273,23 @@ fn main() { .long("id") .takes_value(true))) + .subcommand(SubCommand::with_name("repost") + .about("Reposts a stored, completed but unconfirmed transaction to the chain, or dumps it to a file") + .arg(Arg::with_name("id") + .help("Transaction ID Containing the stored completed transaction") + .short("i") + .long("id") + .takes_value(true)) + .arg(Arg::with_name("dumpfile") + .help("File name to duMp the tranaction to instead of posting") + .short("m") + .long("dumpfile") + .takes_value(true)) + .arg(Arg::with_name("fluff") + .help("Fluff the transaction (ignore Dandelion relay protocol)") + .short("f") + .long("fluff"))) + .subcommand(SubCommand::with_name("cancel") .about("Cancels an previously created transaction, freeing previously locked outputs for use again") .arg(Arg::with_name("id") diff --git a/wallet/src/display.rs b/wallet/src/display.rs index 7205851a5..a7e0c6bff 100644 --- a/wallet/src/display.rs +++ b/wallet/src/display.rs @@ -114,6 +114,7 @@ pub fn txs( bMG->"Amount Debited", bMG->"Fee", bMG->"Net Difference", + bMG->"Tx Data", ]); for t in txs { @@ -145,6 +146,10 @@ pub fn txs( core::amount_to_hr_string(t.amount_debited - t.amount_credited, true) ) }; + let tx_data = match t.tx_hex { + Some(_) => format!("Exists"), + None => "None".to_owned(), + }; table.add_row(row![ bFC->id, bFC->entry_type, @@ -158,6 +163,7 @@ pub fn txs( bFR->amount_debited_str, bFR->fee, bFY->net_diff, + bFb->tx_data, ]); } diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index f5cc34860..44be3ea56 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -25,6 +25,7 @@ use std::sync::{Arc, Mutex}; use serde_json as json; use core::core::hash::Hashed; +use core::core::Transaction; use core::ser; use keychain::Keychain; use libtx::slate::Slate; @@ -174,9 +175,10 @@ where }; tx::complete_tx(&mut **w, &mut slate_out, &context)?; + let tx_hex = util::to_hex(ser::ser_vec(&slate_out.tx).unwrap()); // lock our inputs - lock_fn_out(&mut **w)?; + lock_fn_out(&mut **w, &tx_hex)?; w.close()?; Ok(slate_out) } @@ -214,8 +216,10 @@ where batch.commit()?; } + let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap()); + // lock our inputs - lock_fn(&mut **w)?; + lock_fn(&mut **w, &tx_hex)?; w.close()?; Ok(()) } @@ -337,6 +341,80 @@ where } } + /// Writes stored transaction data to a given file + pub fn dump_stored_tx(&self, tx_id: u32, dest: &str) -> Result<(), Error> { + let (confirmed, tx_hex) = { + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let res = tx::retrieve_tx_hex(&mut **w, tx_id)?; + w.close()?; + res + }; + if confirmed { + warn!( + LOGGER, + "api: dump_stored_tx: transaction at {} is already confirmed.", tx_id + ); + } + if tx_hex.is_none() { + error!( + LOGGER, + "api: dump_stored_tx: completed transaction at {} does not exist.", tx_id + ); + return Err(ErrorKind::TransactionBuildingNotCompleted(tx_id))?; + } + 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(()) + } + + /// (Re)Posts a transaction that's already been stored to the chain + pub fn post_stored_tx(&self, tx_id: u32, fluff: bool) -> Result<(), Error> { + let client; + let (confirmed, tx_hex) = { + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + client = w.client().clone(); + let res = tx::retrieve_tx_hex(&mut **w, tx_id)?; + w.close()?; + res + }; + if confirmed { + error!( + LOGGER, + "api: repost_tx: transaction at {} is confirmed. NOT resending.", tx_id + ); + return Err(ErrorKind::TransactionAlreadyConfirmed)?; + } + if tx_hex.is_none() { + error!( + LOGGER, + "api: repost_tx: completed transaction at {} does not exist.", tx_id + ); + return Err(ErrorKind::TransactionBuildingNotCompleted(tx_id))?; + } + + let res = client.post_tx( + &TxWrapper { + tx_hex: tx_hex.unwrap(), + }, + fluff, + ); + if let Err(e) = res { + error!(LOGGER, "api: repost_tx: failed with error: {}", e); + Err(e) + } else { + debug!( + LOGGER, + "api: repost_tx: successfully posted tx at: {}, fluff? {}", tx_id, fluff + ); + Ok(()) + } + } + /// Attempt to restore contents of wallet pub fn restore(&mut self) -> Result<(), Error> { let mut w = self.wallet.lock().unwrap(); diff --git a/wallet/src/libwallet/error.rs b/wallet/src/libwallet/error.rs index 877813b70..344dbc4bf 100644 --- a/wallet/src/libwallet/error.rs +++ b/wallet/src/libwallet/error.rs @@ -19,6 +19,7 @@ use std::io; use failure::{Backtrace, Context, Fail}; +use core; use core::core::transaction; use keychain; use libtx; @@ -99,6 +100,10 @@ pub enum ErrorKind { #[fail(display = "JSON format error")] Format, + /// Other serialization errors + #[fail(display = "Ser/Deserialization error")] + Deser(core::ser::Error), + /// IO Error #[fail(display = "I/O error")] IO, @@ -147,6 +152,14 @@ pub enum ErrorKind { #[fail(display = "Cancellation Error: {}", _0)] TransactionCancellationError(&'static str), + /// Attempt to repost a transaction that's already confirmed + #[fail(display = "Transaction already confirmed error")] + TransactionAlreadyConfirmed, + + /// Attempt to repost a transaction that's not completed and stored + #[fail(display = "Transaction building not completed: {}", _0)] + TransactionBuildingNotCompleted(u32), + /// Other #[fail(display = "Generic error: {}", _0)] GenericError(String), @@ -218,3 +231,11 @@ impl From for Error { } } } + +impl From for Error { + fn from(error: core::ser::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Deser(error)), + } + } +} diff --git a/wallet/src/libwallet/internal/selection.rs b/wallet/src/libwallet/internal/selection.rs index f073d6b37..07f252be2 100644 --- a/wallet/src/libwallet/internal/selection.rs +++ b/wallet/src/libwallet/internal/selection.rs @@ -37,7 +37,14 @@ pub fn build_send_tx_slate( max_outputs: usize, change_outputs: usize, selection_strategy_is_use_all: bool, -) -> Result<(Slate, Context, impl FnOnce(&mut T) -> Result<(), Error>), Error> +) -> Result< + ( + Slate, + Context, + impl FnOnce(&mut T, &str) -> Result<(), Error>, + ), + Error, +> where T: WalletBackend, C: WalletClient, @@ -90,12 +97,13 @@ where // Return a closure to acquire wallet lock and lock the coins being spent // so we avoid accidental double spend attempt. - let update_sender_wallet_fn = move |wallet: &mut T| { + let update_sender_wallet_fn = move |wallet: &mut T, tx_hex: &str| { let mut batch = wallet.batch()?; let log_id = batch.next_tx_log_id(root_key_id.clone())?; let mut t = TxLogEntry::new(TxLogEntryType::TxSent, log_id); t.tx_slate_id = Some(slate_id); t.fee = Some(fee); + t.tx_hex = Some(tx_hex.to_owned()); let mut amount_debited = 0; t.num_inputs = lock_inputs.len(); for id in lock_inputs { diff --git a/wallet/src/libwallet/internal/tx.rs b/wallet/src/libwallet/internal/tx.rs index a87d761c8..5fad584e8 100644 --- a/wallet/src/libwallet/internal/tx.rs +++ b/wallet/src/libwallet/internal/tx.rs @@ -64,7 +64,14 @@ pub fn create_send_tx( max_outputs: usize, num_change_outputs: usize, selection_strategy_is_use_all: bool, -) -> Result<(Slate, Context, impl FnOnce(&mut T) -> Result<(), Error>), Error> +) -> Result< + ( + Slate, + Context, + impl FnOnce(&mut T, &str) -> Result<(), Error>, + ), + Error, +> where T: WalletBackend, C: WalletClient, @@ -154,6 +161,25 @@ where Ok(()) } +/// Retrieve the associated stored finalised hex Transaction for a given transaction Id +/// as well as whether it's been confirmed +pub fn retrieve_tx_hex( + wallet: &mut T, + tx_id: u32, +) -> Result<(bool, Option), Error> +where + T: WalletBackend, + C: WalletClient, + K: Keychain, +{ + let tx_vec = updater::retrieve_txs(wallet, Some(tx_id))?; + if tx_vec.len() != 1 { + return Err(ErrorKind::TransactionDoesntExist(tx_id))?; + } + let tx = tx_vec[0].clone(); + Ok((tx.confirmed, tx.tx_hex)) +} + /// Issue a burn tx pub fn issue_burn_tx( wallet: &mut T, diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 6d1023869..8a4cc1221 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -586,6 +586,8 @@ pub struct TxLogEntry { pub amount_debited: u64, /// Fee pub fee: Option, + /// The transaction json itself, stored for reference or resending + pub tx_hex: Option, } impl ser::Writeable for TxLogEntry { @@ -616,6 +618,7 @@ impl TxLogEntry { num_inputs: 0, num_outputs: 0, fee: None, + tx_hex: None, } } diff --git a/wallet/tests/transaction.rs b/wallet/tests/transaction.rs index dc8b973c8..c20c29926 100644 --- a/wallet/tests/transaction.rs +++ b/wallet/tests/transaction.rs @@ -247,6 +247,61 @@ fn basic_transaction_api( Ok(()) })?; + // Send another transaction, but don't post to chain immediately and use + // the stored transaction instead + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + slate = sender_api.issue_send_tx( + amount * 2, // amount + 2, // minimum confirmations + "wallet2", // dest + 500, // max outputs + 1, // num change outputs + true, // select all outputs + )?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + let (refreshed, _wallet1_info) = sender_api.retrieve_summary_info(true)?; + assert!(refreshed); + let (_, txs) = sender_api.retrieve_txs(true, None)?; + + // find the transaction + let tx = txs + .iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + sender_api.post_stored_tx(tx.id, false)?; + let (_, wallet1_info) = sender_api.retrieve_summary_info(true)?; + // should be mined now + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount * 3 + ); + Ok(()) + })?; + + // mine a few more blocks + let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + + // check wallet2 has stored transaction + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, amount * 3); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(true, None)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + Ok(()) + })?; + // let logging finish thread::sleep(Duration::from_millis(200)); Ok(())