diff --git a/src/bin/grin.rs b/src/bin/grin.rs index c1871f942..2c3d1a7a8 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -292,7 +292,20 @@ fn main() { .about("raw wallet output info (list of outputs)")) .subcommand(SubCommand::with_name("txs") - .about("display list of transactions")) + .about("Display transaction information") + .arg(Arg::with_name("id") + .help("If specified, display transaction with given ID and all associated Inputs/Outputs") + .short("i") + .long("id") + .takes_value(true))) + + .subcommand(SubCommand::with_name("cancel") + .about("Cancels an previously created transaction, freeing previously locked outputs for use again") + .arg(Arg::with_name("id") + .help("The ID of the transaction to cancel") + .short("i") + .long("id") + .takes_value(true))) .subcommand(SubCommand::with_name("info") .about("basic wallet contents summary")) @@ -754,7 +767,7 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { } ("outputs", Some(_)) => { let (height, _) = api.node_height()?; - let (validated, outputs) = api.retrieve_outputs(show_spent, true)?; + let (validated, outputs) = api.retrieve_outputs(show_spent, true, None)?; let _res = wallet::display::outputs(height, validated, outputs).unwrap_or_else(|e| { panic!( @@ -764,17 +777,55 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { }); Ok(()) } - ("txs", Some(_)) => { + ("txs", Some(txs_args)) => { + let tx_id = match txs_args.value_of("id") { + None => None, + Some(tx) => match tx.parse() { + Ok(t) => Some(t), + Err(_) => panic!("Unable to parse argument 'id' as a number"), + }, + }; let (height, _) = api.node_height()?; - let (validated, txs) = api.retrieve_txs(true)?; - let _res = wallet::display::txs(height, validated, txs).unwrap_or_else(|e| { - panic!( - "Error getting wallet outputs: {:?} Config: {:?}", - e, wallet_config - ) - }); + let (validated, txs) = api.retrieve_txs(true, tx_id)?; + let include_status = !tx_id.is_some(); + let _res = wallet::display::txs(height, validated, txs, include_status) + .unwrap_or_else(|e| { + panic!( + "Error getting wallet outputs: {} Config: {:?}", + e, wallet_config + ) + }); + // if given a particular transaction id, also get and display associated + // inputs/outputs + if tx_id.is_some() { + let (_, outputs) = api.retrieve_outputs(true, false, tx_id)?; + let _res = + wallet::display::outputs(height, validated, outputs).unwrap_or_else(|e| { + panic!( + "Error getting wallet outputs: {} Config: {:?}", + e, wallet_config + ) + }); + }; Ok(()) } + ("cancel", Some(tx_args)) => { + let tx_id = tx_args + .value_of("id") + .expect("'id' argument (-i) is required."); + let tx_id = tx_id.parse().expect("Could not parse id parameter."); + let result = api.cancel_tx(tx_id); + match result { + Ok(_) => { + info!(LOGGER, "Transaction {} Cancelled", tx_id); + Ok(()) + } + Err(e) => { + error!(LOGGER, "TX Cancellation failed: {}", e); + Err(e) + } + } + } ("restore", Some(_)) => { let result = api.restore(); match result { diff --git a/wallet/src/display.rs b/wallet/src/display.rs index cc7d58202..44300e8de 100644 --- a/wallet/src/display.rs +++ b/wallet/src/display.rs @@ -83,7 +83,12 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec) -> Re } /// Display transaction log in a pretty way -pub fn txs(cur_height: u64, validated: bool, txs: Vec) -> Result<(), Error> { +pub fn txs( + cur_height: u64, + validated: bool, + txs: Vec, + include_status: bool, +) -> Result<(), Error> { let title = format!("Transaction Log - Block Height: {}", cur_height); println!(); let mut t = term::stdout().unwrap(); @@ -104,6 +109,8 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec) -> Result<(), bMG->"Num. Outputs", bMG->"Amount Credited", bMG->"Amount Debited", + bMG->"Fee", + bMG->"Net Difference", ]); for t in txs { @@ -119,10 +126,22 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec) -> Result<(), None => "None".to_owned(), }; let confirmed = format!("{}", t.confirmed); - let amount_credited = format!("{}", t.amount_credited); let num_inputs = format!("{}", t.num_inputs); let num_outputs = format!("{}", t.num_outputs); - let amount_debited = format!("{}", t.amount_debited); + let amount_debited_str = core::amount_to_hr_string(t.amount_debited); + let amount_credited_str = core::amount_to_hr_string(t.amount_credited); + let fee = match t.fee { + Some(f) => format!("{}", core::amount_to_hr_string(f)), + None => "None".to_owned(), + }; + let net_diff = if t.amount_credited >= t.amount_debited { + core::amount_to_hr_string(t.amount_credited - t.amount_debited) + } else { + format!( + "-{}", + core::amount_to_hr_string(t.amount_debited - t.amount_credited) + ) + }; table.add_row(row![ bFC->id, bFC->entry_type, @@ -132,8 +151,10 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec) -> Result<(), bFB->confirmation_ts, bFC->num_inputs, bFC->num_outputs, - bFG->amount_credited, - bFR->amount_debited, + bFG->amount_credited_str, + bFR->amount_debited_str, + bFR->fee, + bFY->net_diff, ]); } @@ -141,7 +162,7 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec) -> Result<(), table.printstd(); println!(); - if !validated { + if !validated && include_status { println!( "\nWARNING: Wallet failed to verify data. \ The above is from local cache and possibly invalid! \ diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index 28a2b19cc..b714b5c3b 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -27,7 +27,7 @@ use libwallet::internal::{tx, updater}; use libwallet::types::{ BlockFees, CbData, OutputData, TxLogEntry, TxWrapper, WalletBackend, WalletClient, WalletInfo, }; -use libwallet::Error; +use libwallet::{Error, ErrorKind}; use util::{self, LOGGER}; /// Wrapper around internal API functions, containing a reference to @@ -62,10 +62,12 @@ where /// Attempt to update and retrieve outputs /// Return (whether the outputs were validated against a node, OutputData) + /// if tx_id is some then only retrieve outputs for associated transaction pub fn retrieve_outputs( &self, include_spent: bool, refresh_from_node: bool, + tx_id: Option, ) -> Result<(bool, Vec), Error> { let mut w = self.wallet.lock().unwrap(); w.open_with_credentials()?; @@ -77,7 +79,7 @@ where let res = Ok(( validated, - updater::retrieve_outputs(&mut **w, include_spent)?, + updater::retrieve_outputs(&mut **w, include_spent, tx_id)?, )); w.close()?; @@ -86,7 +88,11 @@ where /// Attempt to update outputs and retrieve tranasactions /// Return (whether the outputs were validated against a node, OutputData) - pub fn retrieve_txs(&self, refresh_from_node: bool) -> Result<(bool, Vec), Error> { + pub fn retrieve_txs( + &self, + refresh_from_node: bool, + tx_id: Option, + ) -> Result<(bool, Vec), Error> { let mut w = self.wallet.lock().unwrap(); w.open_with_credentials()?; @@ -95,7 +101,7 @@ where validated = self.update_outputs(&mut w); } - let res = Ok((validated, updater::retrieve_txs(&mut **w)?)); + let res = Ok((validated, updater::retrieve_txs(&mut **w, tx_id)?)); w.close()?; res @@ -167,6 +173,24 @@ where Ok(slate_out) } + /// Roll back a transaction and all associated outputs with a given + /// transaction id This means delete all change outputs, (or recipient + /// output if you're recipient), and unlock all locked outputs associated + /// with the transaction used when a transaction is created but never + /// posted + pub fn cancel_tx(&mut self, tx_id: u32) -> Result<(), Error> { + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + if !self.update_outputs(&mut w) { + return Err(ErrorKind::TransactionCancellationError( + "Can't contact running Grin node. Not Cancelling.", + ))?; + } + tx::cancel_tx(&mut **w, tx_id)?; + w.close()?; + Ok(()) + } + /// Issue a burn TX pub fn issue_burn_tx( &mut self, @@ -217,7 +241,7 @@ where Ok((height, true)) } Err(_) => { - let outputs = self.retrieve_outputs(true, false)?; + let outputs = self.retrieve_outputs(true, false, None)?; let height = match outputs.1.iter().map(|out| out.height).max() { Some(height) => height, None => 0, diff --git a/wallet/src/libwallet/controller.rs b/wallet/src/libwallet/controller.rs index 6c8b6a732..5328ca3df 100644 --- a/wallet/src/libwallet/controller.rs +++ b/wallet/src/libwallet/controller.rs @@ -163,7 +163,7 @@ where update_from_node = true; } } - api.retrieve_outputs(false, update_from_node) + api.retrieve_outputs(false, update_from_node, None) } fn retrieve_summary_info( diff --git a/wallet/src/libwallet/error.rs b/wallet/src/libwallet/error.rs index 4309da538..3c9efc205 100644 --- a/wallet/src/libwallet/error.rs +++ b/wallet/src/libwallet/error.rs @@ -132,6 +132,18 @@ pub enum ErrorKind { #[fail(display = "Wallet seed doesn't exist error")] WalletSeedDoesntExist, + /// Transaction doesn't exist + #[fail(display = "Transaction {} doesn't exist", _0)] + TransactionDoesntExist(u32), + + /// Transaction already rolled back + #[fail(display = "Transaction {} cannot be cancelled", _0)] + TransactionNotCancellable(u32), + + /// Cancellation error + #[fail(display = "Cancellation Error: {}", _0)] + TransactionCancellationError(&'static str), + /// Other #[fail(display = "Generic error: {}", _0)] GenericError(String), diff --git a/wallet/src/libwallet/internal/selection.rs b/wallet/src/libwallet/internal/selection.rs index 4122a8d5e..701622688 100644 --- a/wallet/src/libwallet/internal/selection.rs +++ b/wallet/src/libwallet/internal/selection.rs @@ -99,6 +99,7 @@ where 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); 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 bdf1917c9..0a78f7f6e 100644 --- a/wallet/src/libwallet/internal/tx.rs +++ b/wallet/src/libwallet/internal/tx.rs @@ -19,7 +19,7 @@ use keychain::{Identifier, Keychain}; use libtx::slate::Slate; use libtx::{build, tx_fee}; use libwallet::internal::{selection, sigcontext, updater}; -use libwallet::types::{WalletBackend, WalletClient}; +use libwallet::types::{TxLogEntryType, WalletBackend, WalletClient}; use libwallet::{Error, ErrorKind}; use util::LOGGER; @@ -131,6 +131,30 @@ where Ok(()) } +/// Rollback outputs associated with a transaction in the wallet +pub fn cancel_tx(wallet: &mut T, tx_id: u32) -> Result<(), 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(); + if tx.tx_type != TxLogEntryType::TxSent && tx.tx_type != TxLogEntryType::TxReceived { + return Err(ErrorKind::TransactionNotCancellable(tx_id))?; + } + if tx.confirmed == true { + return Err(ErrorKind::TransactionNotCancellable(tx_id))?; + } + // get outputs associated with tx + let outputs = updater::retrieve_outputs(wallet, false, Some(tx_id))?; + updater::cancel_tx_and_outputs(wallet, tx, outputs)?; + Ok(()) +} + /// Issue a burn tx pub fn issue_burn_tx( wallet: &mut T, diff --git a/wallet/src/libwallet/internal/updater.rs b/wallet/src/libwallet/internal/updater.rs index b76eb1fcb..d8483e2d6 100644 --- a/wallet/src/libwallet/internal/updater.rs +++ b/wallet/src/libwallet/internal/updater.rs @@ -37,6 +37,7 @@ use util::{self, LOGGER}; pub fn retrieve_outputs( wallet: &mut T, show_spent: bool, + tx_id: Option, ) -> Result, Error> where T: WalletBackend, @@ -56,19 +57,38 @@ where } }) .collect::>(); + // only include outputs with a given tx_id if provided + if let Some(id) = tx_id { + outputs = outputs + .into_iter() + .filter(|out| out.tx_log_entry == Some(id)) + .collect::>(); + } outputs.sort_by_key(|out| out.n_child); Ok(outputs) } -/// Retrieve all of the transaction entries -pub fn retrieve_txs(wallet: &mut T) -> Result, Error> +/// Retrieve all of the transaction entries, or a particular entry +pub fn retrieve_txs( + wallet: &mut T, + tx_id: Option, +) -> Result, Error> where T: WalletBackend, C: WalletClient, K: Keychain, { // just read the wallet here, no need for a write lock - let mut txs = wallet.tx_log_iter().collect::>(); + let mut txs = if let Some(id) = tx_id { + let tx = wallet.tx_log_iter().find(|t| t.id == id); + if let Some(t) = tx { + vec![t] + } else { + vec![] + } + } else { + wallet.tx_log_iter().collect::>() + }; txs.sort_by_key(|tx| tx.creation_ts); Ok(txs) } @@ -108,6 +128,40 @@ where Ok(wallet_outputs) } +/// Cancel transaction and associated outputs +pub fn cancel_tx_and_outputs( + wallet: &mut T, + tx: TxLogEntry, + outputs: Vec, +) -> Result<(), libwallet::Error> +where + T: WalletBackend, + C: WalletClient, + K: Keychain, +{ + let mut batch = wallet.batch()?; + for mut o in outputs { + // unlock locked outputs + if o.status == OutputStatus::Unconfirmed { + batch.delete(&o.key_id)?; + } + if o.status == OutputStatus::Locked { + o.status = OutputStatus::Unconfirmed; + batch.save(o)?; + } + } + let mut tx = tx.clone(); + if tx.tx_type == TxLogEntryType::TxSent { + tx.tx_type = TxLogEntryType::TxSentCancelled; + } + if tx.tx_type == TxLogEntryType::TxReceived { + tx.tx_type = TxLogEntryType::TxReceivedCancelled; + } + batch.save_tx_log_entry(tx)?; + batch.commit()?; + Ok(()) +} + /// Apply refreshed API output data to the wallet pub fn apply_api_outputs( wallet: &mut T, diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 6d081f34a..ef76e956a 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -431,7 +431,7 @@ impl Default for WalletDetails { } /// Types of transactions that can be contained within a TXLog entry -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub enum TxLogEntryType { /// A coinbase transaction becomes confirmed ConfirmedCoinbase, @@ -439,6 +439,10 @@ pub enum TxLogEntryType { TxReceived, /// Inputs locked + change outputs when a transaction is created TxSent, + /// Received transaction that was rolled back by user + TxReceivedCancelled, + /// Sent transaction that was rolled back by user + TxSentCancelled, } impl fmt::Display for TxLogEntryType { @@ -447,6 +451,8 @@ impl fmt::Display for TxLogEntryType { TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed Coinbase"), TxLogEntryType::TxReceived => write!(f, "Recieved Tx"), TxLogEntryType::TxSent => write!(f, "Sent Tx"), + TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx - Cancelled"), + TxLogEntryType::TxSentCancelled => write!(f, "Send Tx - Cancelled"), } } } @@ -480,6 +486,8 @@ pub struct TxLogEntry { pub amount_credited: u64, /// Amount debited via this transaction pub amount_debited: u64, + /// Fee + pub fee: Option, } impl ser::Writeable for TxLogEntry { @@ -509,6 +517,7 @@ impl TxLogEntry { amount_debited: 0, num_inputs: 0, num_outputs: 0, + fee: None, } } diff --git a/wallet/tests/common/testclient.rs b/wallet/tests/common/testclient.rs index f63b5e632..b52c35c3e 100644 --- a/wallet/tests/common/testclient.rs +++ b/wallet/tests/common/testclient.rs @@ -238,7 +238,11 @@ where //let mut api_outputs: HashMap = HashMap::new(); let mut outputs: Vec = vec![]; for o in split { - let c = util::from_hex(String::from(o)).unwrap(); + let o_str = String::from(o); + if o_str.len() == 0 { + continue; + } + let c = util::from_hex(o_str).unwrap(); let commit = Commitment::from_vec(c); let out = common::get_output_local(&self.chain.clone(), &commit); if let Some(o) = out { diff --git a/wallet/tests/transaction.rs b/wallet/tests/transaction.rs index ec875a5e8..4be68e6d8 100644 --- a/wallet/tests/transaction.rs +++ b/wallet/tests/transaction.rs @@ -38,6 +38,7 @@ use keychain::ExtKeychain; use util::LOGGER; use wallet::libtx::slate::Slate; use wallet::libwallet; +use wallet::libwallet::types::OutputStatus; fn clean_output_dir(test_dir: &str) { let _ = fs::remove_dir_all(test_dir); @@ -130,7 +131,7 @@ fn basic_transaction_api( // Check transaction log for wallet 1 wallet::controller::owner_single_use(wallet1.clone(), |api| { let (_, wallet1_info) = api.retrieve_summary_info(true)?; - let (refreshed, txs) = api.retrieve_txs(true)?; + let (refreshed, txs) = api.retrieve_txs(true, None)?; assert!(refreshed); let fee = wallet::libtx::tx_fee( wallet1_info.last_confirmed_height as usize - cm as usize, @@ -144,12 +145,13 @@ fn basic_transaction_api( assert!(!tx.confirmed); assert!(tx.confirmation_ts.is_none()); assert_eq!(tx.amount_debited - tx.amount_credited, fee + amount); + assert_eq!(Some(fee), tx.fee); Ok(()) })?; // Check transaction log for wallet 2 wallet::controller::owner_single_use(wallet2.clone(), |api| { - let (refreshed, txs) = api.retrieve_txs(true)?; + let (refreshed, txs) = api.retrieve_txs(true, None)?; assert!(refreshed); // we should have a transaction entry for this slate let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); @@ -159,6 +161,7 @@ fn basic_transaction_api( assert!(tx.confirmation_ts.is_none()); assert_eq!(amount, tx.amount_credited); assert_eq!(0, tx.amount_debited); + assert_eq!(None, tx.fee); Ok(()) })?; @@ -195,7 +198,7 @@ fn basic_transaction_api( assert_eq!(wallet1_info.amount_immature, cm * reward + fee); // check tx log entry is confirmed - let (refreshed, txs) = api.retrieve_txs(true)?; + 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()); @@ -231,7 +234,7 @@ fn basic_transaction_api( assert_eq!(wallet2_info.amount_currently_spendable, amount); // check tx log entry is confirmed - let (refreshed, txs) = api.retrieve_txs(true)?; + 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()); @@ -246,6 +249,161 @@ fn basic_transaction_api( Ok(()) } +/// Test rolling back transactions and outputs when a transaction is never +/// posted to a chain +fn tx_rollback(test_dir: &str, backend_type: common::BackendType) -> 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(), + backend_type.clone(), + ); + wallet_proxy.add_wallet("wallet1", client.get_send_instance(), wallet1.clone()); + + // define recipient wallet, add to proxy + let client = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let wallet2 = common::create_wallet( + &format!("{}/wallet2", test_dir), + client.clone(), + backend_type.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!(LOGGER, "Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); + // mine a few blocks + let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 5); + + let amount = 30_000_000_000; + let mut slate = Slate::blank(1); + 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, // amount + 2, // minimum confirmations + "wallet2", // dest + 500, // max outputs + true, // select all outputs + )?; + Ok(()) + })?; + + // Check transaction log for wallet 1 + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (refreshed, _wallet1_info) = api.retrieve_summary_info(true)?; + assert!(refreshed); + let (_, txs) = api.retrieve_txs(true, None)?; + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let mut locked_count = 0; + let mut unconfirmed_count = 0; + // get the tx entry, check outputs are as expected + let (_, outputs) = api.retrieve_outputs(true, false, Some(tx.unwrap().id))?; + for o in outputs.clone() { + if o.status == OutputStatus::Locked { + locked_count = locked_count + 1; + } + if o.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(outputs.len(), 3); + assert_eq!(locked_count, 2); + assert_eq!(unconfirmed_count, 1); + + Ok(()) + })?; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (refreshed, txs) = api.retrieve_txs(true, None)?; + assert!(refreshed); + let mut unconfirmed_count = 0; + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + // get the tx entry, check outputs are as expected + let (_, outputs) = api.retrieve_outputs(true, false, Some(tx.unwrap().id))?; + for o in outputs.clone() { + if o.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(outputs.len(), 1); + assert_eq!(unconfirmed_count, 1); + let (refreshed, wallet2_info) = api.retrieve_summary_info(true)?; + assert!(refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + assert_eq!(wallet2_info.total, amount); + Ok(()) + })?; + + // wallet 1 is bold and doesn't ever post the transaction mine a few more blocks + let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 5); + + // Wallet 1 decides to roll back instead + wallet::controller::owner_single_use(wallet1.clone(), |api| { + // can't roll back coinbase + let res = api.cancel_tx(1); + assert!(res.is_err()); + let (_, txs) = api.retrieve_txs(true, None)?; + let tx = txs.iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + api.cancel_tx(tx.id)?; + let (refreshed, wallet1_info) = api.retrieve_summary_info(true)?; + assert!(refreshed); + // check all eligible inputs should be now be spendable + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward + ); + // can't roll back again + let res = api.cancel_tx(tx.id); + assert!(res.is_err()); + + Ok(()) + })?; + + // Wallet 2 rolls back + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (_, txs) = api.retrieve_txs(true, None)?; + let tx = txs.iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + api.cancel_tx(tx.id)?; + let (refreshed, wallet2_info) = api.retrieve_summary_info(true)?; + assert!(refreshed); + // check all eligible inputs should be now be spendable + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + assert_eq!(wallet2_info.total, 0,); + // can't roll back again + let res = api.cancel_tx(tx.id); + assert!(res.is_err()); + + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + #[ignore] #[test] fn file_wallet_basic_transaction_api() { @@ -260,3 +418,11 @@ fn db_wallet_basic_transaction_api() { println!("Libwallet Error: {}", e); } } + +#[test] +fn db_wallet_tx_rollback() { + let test_dir = "test_output/tx_rollback_db"; + if let Err(e) = tx_rollback(test_dir, common::BackendType::LMDBBackend) { + println!("Libwallet Error: {}", e); + } +}