mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-21 03:21:08 +03:00
added transaction cancellation feature (#1280)
This commit is contained in:
parent
4a5a41fb30
commit
80f82eecc1
11 changed files with 398 additions and 32 deletions
|
@ -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| {
|
||||
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: {:?}",
|
||||
"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 {
|
||||
|
|
|
@ -83,7 +83,12 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec<OutputData>) -> Re
|
|||
}
|
||||
|
||||
/// 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);
|
||||
println!();
|
||||
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->"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<TxLogEntry>) -> 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<TxLogEntry>) -> 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<TxLogEntry>) -> 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! \
|
||||
|
|
|
@ -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<u32>,
|
||||
) -> Result<(bool, Vec<OutputData>), 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<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();
|
||||
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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<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
|
||||
pub fn issue_burn_tx<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
|
|
|
@ -37,6 +37,7 @@ use util::{self, LOGGER};
|
|||
pub fn retrieve_outputs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
show_spent: bool,
|
||||
tx_id: Option<u32>,
|
||||
) -> Result<Vec<OutputData>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
|
@ -56,19 +57,38 @@ where
|
|||
}
|
||||
})
|
||||
.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);
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
/// Retrieve all of the transaction entries
|
||||
pub fn retrieve_txs<T: ?Sized, C, K>(wallet: &mut T) -> Result<Vec<TxLogEntry>, Error>
|
||||
/// Retrieve all of the transaction entries, or a particular entry
|
||||
pub fn retrieve_txs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
tx_id: Option<u32>,
|
||||
) -> Result<Vec<TxLogEntry>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// 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);
|
||||
Ok(txs)
|
||||
}
|
||||
|
@ -108,6 +128,40 @@ where
|
|||
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
|
||||
pub fn apply_api_outputs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
|
|
|
@ -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<u64>,
|
||||
}
|
||||
|
||||
impl ser::Writeable for TxLogEntry {
|
||||
|
@ -509,6 +517,7 @@ impl TxLogEntry {
|
|||
amount_debited: 0,
|
||||
num_inputs: 0,
|
||||
num_outputs: 0,
|
||||
fee: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -238,7 +238,11 @@ where
|
|||
//let mut api_outputs: HashMap<pedersen::Commitment, String> = HashMap::new();
|
||||
let mut outputs: Vec<api::Output> = 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 {
|
||||
|
|
|
@ -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<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]
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue