mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-21 11:31: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)"))
|
.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 {
|
||||||
|
|
|
@ -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! \
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue