added transaction cancellation feature (#1280)

This commit is contained in:
Yeastplume 2018-07-20 15:13:37 +01:00 committed by GitHub
parent 4a5a41fb30
commit 80f82eecc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 398 additions and 32 deletions

View file

@ -292,7 +292,20 @@ fn main() {
.about("raw wallet output info (list of outputs)")) .about("raw wallet output info (list of outputs)"))
.subcommand(SubCommand::with_name("txs") .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") .subcommand(SubCommand::with_name("info")
.about("basic wallet contents summary")) .about("basic wallet contents summary"))
@ -754,7 +767,7 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
} }
("outputs", Some(_)) => { ("outputs", Some(_)) => {
let (height, _) = api.node_height()?; 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 = let _res =
wallet::display::outputs(height, validated, outputs).unwrap_or_else(|e| { wallet::display::outputs(height, validated, outputs).unwrap_or_else(|e| {
panic!( panic!(
@ -764,17 +777,55 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
}); });
Ok(()) 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 (height, _) = api.node_height()?;
let (validated, txs) = api.retrieve_txs(true)?; let (validated, txs) = api.retrieve_txs(true, tx_id)?;
let _res = wallet::display::txs(height, validated, txs).unwrap_or_else(|e| { let include_status = !tx_id.is_some();
let _res = wallet::display::txs(height, validated, txs, include_status)
.unwrap_or_else(|e| {
panic!( panic!(
"Error getting wallet outputs: {:?} Config: {:?}", "Error getting wallet outputs: {} Config: {:?}",
e, wallet_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(()) 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(_)) => { ("restore", Some(_)) => {
let result = api.restore(); let result = api.restore();
match result { match result {

View file

@ -83,7 +83,12 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec<OutputData>) -> Re
} }
/// Display transaction log in a pretty way /// Display transaction log in a pretty way
pub fn txs(cur_height: u64, validated: bool, txs: Vec<TxLogEntry>) -> Result<(), Error> { pub fn txs(
cur_height: u64,
validated: bool,
txs: Vec<TxLogEntry>,
include_status: bool,
) -> Result<(), Error> {
let title = format!("Transaction Log - Block Height: {}", cur_height); let title = format!("Transaction Log - Block Height: {}", cur_height);
println!(); println!();
let mut t = term::stdout().unwrap(); let mut t = term::stdout().unwrap();
@ -104,6 +109,8 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec<TxLogEntry>) -> Result<(),
bMG->"Num. Outputs", bMG->"Num. Outputs",
bMG->"Amount Credited", bMG->"Amount Credited",
bMG->"Amount Debited", bMG->"Amount Debited",
bMG->"Fee",
bMG->"Net Difference",
]); ]);
for t in txs { for t in txs {
@ -119,10 +126,22 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec<TxLogEntry>) -> Result<(),
None => "None".to_owned(), None => "None".to_owned(),
}; };
let confirmed = format!("{}", t.confirmed); let confirmed = format!("{}", t.confirmed);
let amount_credited = format!("{}", t.amount_credited);
let num_inputs = format!("{}", t.num_inputs); let num_inputs = format!("{}", t.num_inputs);
let num_outputs = format!("{}", t.num_outputs); 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![ table.add_row(row![
bFC->id, bFC->id,
bFC->entry_type, bFC->entry_type,
@ -132,8 +151,10 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec<TxLogEntry>) -> Result<(),
bFB->confirmation_ts, bFB->confirmation_ts,
bFC->num_inputs, bFC->num_inputs,
bFC->num_outputs, bFC->num_outputs,
bFG->amount_credited, bFG->amount_credited_str,
bFR->amount_debited, bFR->amount_debited_str,
bFR->fee,
bFY->net_diff,
]); ]);
} }
@ -141,7 +162,7 @@ pub fn txs(cur_height: u64, validated: bool, txs: Vec<TxLogEntry>) -> Result<(),
table.printstd(); table.printstd();
println!(); println!();
if !validated { if !validated && include_status {
println!( println!(
"\nWARNING: Wallet failed to verify data. \ "\nWARNING: Wallet failed to verify data. \
The above is from local cache and possibly invalid! \ The above is from local cache and possibly invalid! \

View file

@ -27,7 +27,7 @@ use libwallet::internal::{tx, updater};
use libwallet::types::{ use libwallet::types::{
BlockFees, CbData, OutputData, TxLogEntry, TxWrapper, WalletBackend, WalletClient, WalletInfo, BlockFees, CbData, OutputData, TxLogEntry, TxWrapper, WalletBackend, WalletClient, WalletInfo,
}; };
use libwallet::Error; use libwallet::{Error, ErrorKind};
use util::{self, LOGGER}; use util::{self, LOGGER};
/// Wrapper around internal API functions, containing a reference to /// Wrapper around internal API functions, containing a reference to
@ -62,10 +62,12 @@ where
/// Attempt to update and retrieve outputs /// Attempt to update and retrieve outputs
/// Return (whether the outputs were validated against a node, OutputData) /// 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( pub fn retrieve_outputs(
&self, &self,
include_spent: bool, include_spent: bool,
refresh_from_node: bool, refresh_from_node: bool,
tx_id: Option<u32>,
) -> Result<(bool, Vec<OutputData>), Error> { ) -> Result<(bool, Vec<OutputData>), Error> {
let mut w = self.wallet.lock().unwrap(); let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?; w.open_with_credentials()?;
@ -77,7 +79,7 @@ where
let res = Ok(( let res = Ok((
validated, validated,
updater::retrieve_outputs(&mut **w, include_spent)?, updater::retrieve_outputs(&mut **w, include_spent, tx_id)?,
)); ));
w.close()?; w.close()?;
@ -86,7 +88,11 @@ where
/// Attempt to update outputs and retrieve tranasactions /// Attempt to update outputs and retrieve tranasactions
/// Return (whether the outputs were validated against a node, OutputData) /// Return (whether the outputs were validated against a node, OutputData)
pub fn retrieve_txs(&self, refresh_from_node: bool) -> Result<(bool, Vec<TxLogEntry>), Error> { pub fn retrieve_txs(
&self,
refresh_from_node: bool,
tx_id: Option<u32>,
) -> Result<(bool, Vec<TxLogEntry>), Error> {
let mut w = self.wallet.lock().unwrap(); let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?; w.open_with_credentials()?;
@ -95,7 +101,7 @@ where
validated = self.update_outputs(&mut w); 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()?; w.close()?;
res res
@ -167,6 +173,24 @@ where
Ok(slate_out) 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 /// Issue a burn TX
pub fn issue_burn_tx( pub fn issue_burn_tx(
&mut self, &mut self,
@ -217,7 +241,7 @@ where
Ok((height, true)) Ok((height, true))
} }
Err(_) => { 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() { let height = match outputs.1.iter().map(|out| out.height).max() {
Some(height) => height, Some(height) => height,
None => 0, None => 0,

View file

@ -163,7 +163,7 @@ where
update_from_node = true; update_from_node = true;
} }
} }
api.retrieve_outputs(false, update_from_node) api.retrieve_outputs(false, update_from_node, None)
} }
fn retrieve_summary_info( fn retrieve_summary_info(

View file

@ -132,6 +132,18 @@ pub enum ErrorKind {
#[fail(display = "Wallet seed doesn't exist error")] #[fail(display = "Wallet seed doesn't exist error")]
WalletSeedDoesntExist, 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 /// Other
#[fail(display = "Generic error: {}", _0)] #[fail(display = "Generic error: {}", _0)]
GenericError(String), GenericError(String),

View file

@ -99,6 +99,7 @@ where
let log_id = batch.next_tx_log_id(root_key_id.clone())?; let log_id = batch.next_tx_log_id(root_key_id.clone())?;
let mut t = TxLogEntry::new(TxLogEntryType::TxSent, log_id); let mut t = TxLogEntry::new(TxLogEntryType::TxSent, log_id);
t.tx_slate_id = Some(slate_id); t.tx_slate_id = Some(slate_id);
t.fee = Some(fee);
let mut amount_debited = 0; let mut amount_debited = 0;
t.num_inputs = lock_inputs.len(); t.num_inputs = lock_inputs.len();
for id in lock_inputs { for id in lock_inputs {

View file

@ -19,7 +19,7 @@ use keychain::{Identifier, Keychain};
use libtx::slate::Slate; use libtx::slate::Slate;
use libtx::{build, tx_fee}; use libtx::{build, tx_fee};
use libwallet::internal::{selection, sigcontext, updater}; use libwallet::internal::{selection, sigcontext, updater};
use libwallet::types::{WalletBackend, WalletClient}; use libwallet::types::{TxLogEntryType, WalletBackend, WalletClient};
use libwallet::{Error, ErrorKind}; use libwallet::{Error, ErrorKind};
use util::LOGGER; use util::LOGGER;
@ -131,6 +131,30 @@ where
Ok(()) Ok(())
} }
/// Rollback outputs associated with a transaction in the wallet
pub fn cancel_tx<T: ?Sized, C, K>(wallet: &mut T, tx_id: u32) -> Result<(), Error>
where
T: WalletBackend<C, K>,
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 /// Issue a burn tx
pub fn issue_burn_tx<T: ?Sized, C, K>( pub fn issue_burn_tx<T: ?Sized, C, K>(
wallet: &mut T, wallet: &mut T,

View file

@ -37,6 +37,7 @@ use util::{self, LOGGER};
pub fn retrieve_outputs<T: ?Sized, C, K>( pub fn retrieve_outputs<T: ?Sized, C, K>(
wallet: &mut T, wallet: &mut T,
show_spent: bool, show_spent: bool,
tx_id: Option<u32>,
) -> Result<Vec<OutputData>, Error> ) -> Result<Vec<OutputData>, Error>
where where
T: WalletBackend<C, K>, T: WalletBackend<C, K>,
@ -56,19 +57,38 @@ where
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// 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::<Vec<_>>();
}
outputs.sort_by_key(|out| out.n_child); outputs.sort_by_key(|out| out.n_child);
Ok(outputs) Ok(outputs)
} }
/// Retrieve all of the transaction entries /// Retrieve all of the transaction entries, or a particular entry
pub fn retrieve_txs<T: ?Sized, C, K>(wallet: &mut T) -> Result<Vec<TxLogEntry>, Error> pub fn retrieve_txs<T: ?Sized, C, K>(
wallet: &mut T,
tx_id: Option<u32>,
) -> Result<Vec<TxLogEntry>, Error>
where where
T: WalletBackend<C, K>, T: WalletBackend<C, K>,
C: WalletClient, C: WalletClient,
K: Keychain, K: Keychain,
{ {
// just read the wallet here, no need for a write lock // just read the wallet here, no need for a write lock
let mut txs = wallet.tx_log_iter().collect::<Vec<_>>(); 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::<Vec<_>>()
};
txs.sort_by_key(|tx| tx.creation_ts); txs.sort_by_key(|tx| tx.creation_ts);
Ok(txs) Ok(txs)
} }
@ -108,6 +128,40 @@ where
Ok(wallet_outputs) Ok(wallet_outputs)
} }
/// Cancel transaction and associated outputs
pub fn cancel_tx_and_outputs<T: ?Sized, C, K>(
wallet: &mut T,
tx: TxLogEntry,
outputs: Vec<OutputData>,
) -> Result<(), libwallet::Error>
where
T: WalletBackend<C, K>,
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 /// Apply refreshed API output data to the wallet
pub fn apply_api_outputs<T: ?Sized, C, K>( pub fn apply_api_outputs<T: ?Sized, C, K>(
wallet: &mut T, wallet: &mut T,

View file

@ -431,7 +431,7 @@ impl Default for WalletDetails {
} }
/// Types of transactions that can be contained within a TXLog entry /// 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 { pub enum TxLogEntryType {
/// A coinbase transaction becomes confirmed /// A coinbase transaction becomes confirmed
ConfirmedCoinbase, ConfirmedCoinbase,
@ -439,6 +439,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,
/// Received transaction that was rolled back by user
TxReceivedCancelled,
/// Sent transaction that was rolled back by user
TxSentCancelled,
} }
impl fmt::Display for TxLogEntryType { impl fmt::Display for TxLogEntryType {
@ -447,6 +451,8 @@ impl fmt::Display for TxLogEntryType {
TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed Coinbase"), TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed Coinbase"),
TxLogEntryType::TxReceived => write!(f, "Recieved Tx"), TxLogEntryType::TxReceived => write!(f, "Recieved Tx"),
TxLogEntryType::TxSent => write!(f, "Sent 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, pub amount_credited: u64,
/// Amount debited via this transaction /// Amount debited via this transaction
pub amount_debited: u64, pub amount_debited: u64,
/// Fee
pub fee: Option<u64>,
} }
impl ser::Writeable for TxLogEntry { impl ser::Writeable for TxLogEntry {
@ -509,6 +517,7 @@ impl TxLogEntry {
amount_debited: 0, amount_debited: 0,
num_inputs: 0, num_inputs: 0,
num_outputs: 0, num_outputs: 0,
fee: None,
} }
} }

View file

@ -238,7 +238,11 @@ where
//let mut api_outputs: HashMap<pedersen::Commitment, String> = HashMap::new(); //let mut api_outputs: HashMap<pedersen::Commitment, String> = HashMap::new();
let mut outputs: Vec<api::Output> = vec![]; let mut outputs: Vec<api::Output> = vec![];
for o in split { 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 commit = Commitment::from_vec(c);
let out = common::get_output_local(&self.chain.clone(), &commit); let out = common::get_output_local(&self.chain.clone(), &commit);
if let Some(o) = out { if let Some(o) = out {

View file

@ -38,6 +38,7 @@ use keychain::ExtKeychain;
use util::LOGGER; use util::LOGGER;
use wallet::libtx::slate::Slate; use wallet::libtx::slate::Slate;
use wallet::libwallet; use wallet::libwallet;
use wallet::libwallet::types::OutputStatus;
fn clean_output_dir(test_dir: &str) { fn clean_output_dir(test_dir: &str) {
let _ = fs::remove_dir_all(test_dir); let _ = fs::remove_dir_all(test_dir);
@ -130,7 +131,7 @@ fn basic_transaction_api(
// Check transaction log for wallet 1 // Check transaction log for wallet 1
wallet::controller::owner_single_use(wallet1.clone(), |api| { wallet::controller::owner_single_use(wallet1.clone(), |api| {
let (_, wallet1_info) = api.retrieve_summary_info(true)?; 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); assert!(refreshed);
let fee = wallet::libtx::tx_fee( let fee = wallet::libtx::tx_fee(
wallet1_info.last_confirmed_height as usize - cm as usize, wallet1_info.last_confirmed_height as usize - cm as usize,
@ -144,12 +145,13 @@ fn basic_transaction_api(
assert!(!tx.confirmed); assert!(!tx.confirmed);
assert!(tx.confirmation_ts.is_none()); assert!(tx.confirmation_ts.is_none());
assert_eq!(tx.amount_debited - tx.amount_credited, fee + amount); assert_eq!(tx.amount_debited - tx.amount_credited, fee + amount);
assert_eq!(Some(fee), tx.fee);
Ok(()) Ok(())
})?; })?;
// Check transaction log for wallet 2 // Check transaction log for wallet 2
wallet::controller::owner_single_use(wallet2.clone(), |api| { 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); assert!(refreshed);
// we should have a transaction entry for this slate // we should have a transaction entry for this slate
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); 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!(tx.confirmation_ts.is_none());
assert_eq!(amount, tx.amount_credited); assert_eq!(amount, tx.amount_credited);
assert_eq!(0, tx.amount_debited); assert_eq!(0, tx.amount_debited);
assert_eq!(None, tx.fee);
Ok(()) Ok(())
})?; })?;
@ -195,7 +198,7 @@ fn basic_transaction_api(
assert_eq!(wallet1_info.amount_immature, cm * reward + fee); assert_eq!(wallet1_info.amount_immature, cm * reward + fee);
// check tx log entry is confirmed // check tx log entry is confirmed
let (refreshed, txs) = api.retrieve_txs(true)?; let (refreshed, txs) = api.retrieve_txs(true, None)?;
assert!(refreshed); assert!(refreshed);
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
assert!(tx.is_some()); assert!(tx.is_some());
@ -231,7 +234,7 @@ fn basic_transaction_api(
assert_eq!(wallet2_info.amount_currently_spendable, amount); assert_eq!(wallet2_info.amount_currently_spendable, amount);
// check tx log entry is confirmed // check tx log entry is confirmed
let (refreshed, txs) = api.retrieve_txs(true)?; let (refreshed, txs) = api.retrieve_txs(true, None)?;
assert!(refreshed); assert!(refreshed);
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
assert!(tx.is_some()); assert!(tx.is_some());
@ -246,6 +249,161 @@ fn basic_transaction_api(
Ok(()) 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<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(),
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] #[ignore]
#[test] #[test]
fn file_wallet_basic_transaction_api() { fn file_wallet_basic_transaction_api() {
@ -260,3 +418,11 @@ fn db_wallet_basic_transaction_api() {
println!("Libwallet Error: {}", e); 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);
}
}