Save transaction as part of TxLogEntry after transaction completion (#1473)

* save wallet transaction on transaction completion

* rustfmt

* add resend command

* rustfmt

* added unit tests + fixes for grin wallet repost

* add ability to dump transaction file contents

* rustfmt

* wallet doc update
This commit is contained in:
Yeastplume 2018-09-05 12:12:29 +01:00 committed by GitHub
parent 77765796ab
commit 75f0ea6dd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 284 additions and 5 deletions

View file

@ -270,6 +270,22 @@ Be sure to use this command with caution, as there are many edge cases and possi
the recipient of a transaction. For the time being please be 100% certain that the relevant transaction is never, ever going to be posted before the recipient of a transaction. For the time being please be 100% certain that the relevant transaction is never, ever going to be posted before
running `grin wallet cancel` running `grin wallet cancel`
##### repost
If you're the sender of a posted transaction that doesn't confirm on the chain (due to a fork or full transaction pool), you can repost the copy of it that grin automatically stores in your wallet data whenever a transaction is finalized. This doesn't need to communicate with the recipient again, it just re-posts a transaction created during a previous `send` attempt.
To do this, look up the transaction id using the `grin wallet txs` command, and using the id (say 3 in this example,) enter:
`grin wallet repost -i 3`
This will attempt to repost the transaction to the chain. Note this won't attempt to send if the transaction is already marked as 'confirmed' within the wallet.
You can also use the `repost` command to dump the transaction in a raw json format with the `-m` (duMp) switch, e.g:
`grin wallet repost -i 3 -m tx_3.json`
This will create a file called tx_3.json containing your raw transaction data. Note that this formatting in the file isn't yet very user-readable.
##### restore ##### restore
If for some reason the wallet cancel commands above don't work, or you need to restore from a backed up `wallet.seed` file and password, you can perform a full wallet restore. If for some reason the wallet cancel commands above don't work, or you need to restore from a backed up `wallet.seed` file and password, you can perform a full wallet restore.

View file

@ -343,6 +343,50 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
}; };
Ok(()) Ok(())
} }
("repost", Some(repost_args)) => {
let tx_id: u32 = match repost_args.value_of("id") {
None => {
error!(LOGGER, "Transaction of a completed but unconfirmed transaction required (specify with --id=[id])");
panic!();
}
Some(tx) => match tx.parse() {
Ok(t) => t,
Err(_) => {
panic!("Unable to parse argument 'id' as a number");
}
},
};
let dump_file = repost_args.value_of("dumpfile");
let fluff = repost_args.is_present("fluff");
match dump_file {
None => {
let result = api.post_stored_tx(tx_id, fluff);
match result {
Ok(_) => {
info!(LOGGER, "Reposted transaction at {}", tx_id);
Ok(())
}
Err(e) => {
error!(LOGGER, "Tranasction reposting failed: {}", e);
Err(e)
}
}
}
Some(f) => {
let result = api.dump_stored_tx(tx_id, f);
match result {
Ok(_) => {
warn!(LOGGER, "Dumped transaction data for tx {} to {}", tx_id, f);
Ok(())
}
Err(e) => {
error!(LOGGER, "Tranasction reposting failed: {}", e);
Err(e)
}
}
}
}
}
("cancel", Some(tx_args)) => { ("cancel", Some(tx_args)) => {
let tx_id = tx_args let tx_id = tx_args
.value_of("id") .value_of("id")

View file

@ -222,6 +222,11 @@ fn main() {
.help("Fluff the transaction (ignore Dandelion relay protocol)") .help("Fluff the transaction (ignore Dandelion relay protocol)")
.short("f") .short("f")
.long("fluff"))) .long("fluff")))
.arg(Arg::with_name("stored_tx")
.help("If present, use the previously stored Unconfirmed transaction with given id.")
.short("t")
.long("stored_tx")
.takes_value(true))
.subcommand(SubCommand::with_name("receive") .subcommand(SubCommand::with_name("receive")
.about("Processes a transaction file to accept a transfer from a sender.") .about("Processes a transaction file to accept a transfer from a sender.")
@ -268,6 +273,23 @@ fn main() {
.long("id") .long("id")
.takes_value(true))) .takes_value(true)))
.subcommand(SubCommand::with_name("repost")
.about("Reposts a stored, completed but unconfirmed transaction to the chain, or dumps it to a file")
.arg(Arg::with_name("id")
.help("Transaction ID Containing the stored completed transaction")
.short("i")
.long("id")
.takes_value(true))
.arg(Arg::with_name("dumpfile")
.help("File name to duMp the tranaction to instead of posting")
.short("m")
.long("dumpfile")
.takes_value(true))
.arg(Arg::with_name("fluff")
.help("Fluff the transaction (ignore Dandelion relay protocol)")
.short("f")
.long("fluff")))
.subcommand(SubCommand::with_name("cancel") .subcommand(SubCommand::with_name("cancel")
.about("Cancels an previously created transaction, freeing previously locked outputs for use again") .about("Cancels an previously created transaction, freeing previously locked outputs for use again")
.arg(Arg::with_name("id") .arg(Arg::with_name("id")

View file

@ -114,6 +114,7 @@ pub fn txs(
bMG->"Amount Debited", bMG->"Amount Debited",
bMG->"Fee", bMG->"Fee",
bMG->"Net Difference", bMG->"Net Difference",
bMG->"Tx Data",
]); ]);
for t in txs { for t in txs {
@ -145,6 +146,10 @@ pub fn txs(
core::amount_to_hr_string(t.amount_debited - t.amount_credited, true) core::amount_to_hr_string(t.amount_debited - t.amount_credited, true)
) )
}; };
let tx_data = match t.tx_hex {
Some(_) => format!("Exists"),
None => "None".to_owned(),
};
table.add_row(row![ table.add_row(row![
bFC->id, bFC->id,
bFC->entry_type, bFC->entry_type,
@ -158,6 +163,7 @@ pub fn txs(
bFR->amount_debited_str, bFR->amount_debited_str,
bFR->fee, bFR->fee,
bFY->net_diff, bFY->net_diff,
bFb->tx_data,
]); ]);
} }

View file

@ -25,6 +25,7 @@ use std::sync::{Arc, Mutex};
use serde_json as json; use serde_json as json;
use core::core::hash::Hashed; use core::core::hash::Hashed;
use core::core::Transaction;
use core::ser; use core::ser;
use keychain::Keychain; use keychain::Keychain;
use libtx::slate::Slate; use libtx::slate::Slate;
@ -174,9 +175,10 @@ where
}; };
tx::complete_tx(&mut **w, &mut slate_out, &context)?; tx::complete_tx(&mut **w, &mut slate_out, &context)?;
let tx_hex = util::to_hex(ser::ser_vec(&slate_out.tx).unwrap());
// lock our inputs // lock our inputs
lock_fn_out(&mut **w)?; lock_fn_out(&mut **w, &tx_hex)?;
w.close()?; w.close()?;
Ok(slate_out) Ok(slate_out)
} }
@ -214,8 +216,10 @@ where
batch.commit()?; batch.commit()?;
} }
let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap());
// lock our inputs // lock our inputs
lock_fn(&mut **w)?; lock_fn(&mut **w, &tx_hex)?;
w.close()?; w.close()?;
Ok(()) Ok(())
} }
@ -337,6 +341,80 @@ where
} }
} }
/// Writes stored transaction data to a given file
pub fn dump_stored_tx(&self, tx_id: u32, dest: &str) -> Result<(), Error> {
let (confirmed, tx_hex) = {
let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?;
let res = tx::retrieve_tx_hex(&mut **w, tx_id)?;
w.close()?;
res
};
if confirmed {
warn!(
LOGGER,
"api: dump_stored_tx: transaction at {} is already confirmed.", tx_id
);
}
if tx_hex.is_none() {
error!(
LOGGER,
"api: dump_stored_tx: completed transaction at {} does not exist.", tx_id
);
return Err(ErrorKind::TransactionBuildingNotCompleted(tx_id))?;
}
let tx_bin = util::from_hex(tx_hex.unwrap()).unwrap();
let tx = ser::deserialize::<Transaction>(&mut &tx_bin[..])?;
let mut tx_file = File::create(dest)?;
tx_file.write_all(json::to_string(&tx).unwrap().as_bytes())?;
tx_file.sync_all()?;
Ok(())
}
/// (Re)Posts a transaction that's already been stored to the chain
pub fn post_stored_tx(&self, tx_id: u32, fluff: bool) -> Result<(), Error> {
let client;
let (confirmed, tx_hex) = {
let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?;
client = w.client().clone();
let res = tx::retrieve_tx_hex(&mut **w, tx_id)?;
w.close()?;
res
};
if confirmed {
error!(
LOGGER,
"api: repost_tx: transaction at {} is confirmed. NOT resending.", tx_id
);
return Err(ErrorKind::TransactionAlreadyConfirmed)?;
}
if tx_hex.is_none() {
error!(
LOGGER,
"api: repost_tx: completed transaction at {} does not exist.", tx_id
);
return Err(ErrorKind::TransactionBuildingNotCompleted(tx_id))?;
}
let res = client.post_tx(
&TxWrapper {
tx_hex: tx_hex.unwrap(),
},
fluff,
);
if let Err(e) = res {
error!(LOGGER, "api: repost_tx: failed with error: {}", e);
Err(e)
} else {
debug!(
LOGGER,
"api: repost_tx: successfully posted tx at: {}, fluff? {}", tx_id, fluff
);
Ok(())
}
}
/// Attempt to restore contents of wallet /// Attempt to restore contents of wallet
pub fn restore(&mut self) -> Result<(), Error> { pub fn restore(&mut self) -> Result<(), Error> {
let mut w = self.wallet.lock().unwrap(); let mut w = self.wallet.lock().unwrap();

View file

@ -19,6 +19,7 @@ use std::io;
use failure::{Backtrace, Context, Fail}; use failure::{Backtrace, Context, Fail};
use core;
use core::core::transaction; use core::core::transaction;
use keychain; use keychain;
use libtx; use libtx;
@ -99,6 +100,10 @@ pub enum ErrorKind {
#[fail(display = "JSON format error")] #[fail(display = "JSON format error")]
Format, Format,
/// Other serialization errors
#[fail(display = "Ser/Deserialization error")]
Deser(core::ser::Error),
/// IO Error /// IO Error
#[fail(display = "I/O error")] #[fail(display = "I/O error")]
IO, IO,
@ -147,6 +152,14 @@ pub enum ErrorKind {
#[fail(display = "Cancellation Error: {}", _0)] #[fail(display = "Cancellation Error: {}", _0)]
TransactionCancellationError(&'static str), TransactionCancellationError(&'static str),
/// Attempt to repost a transaction that's already confirmed
#[fail(display = "Transaction already confirmed error")]
TransactionAlreadyConfirmed,
/// Attempt to repost a transaction that's not completed and stored
#[fail(display = "Transaction building not completed: {}", _0)]
TransactionBuildingNotCompleted(u32),
/// Other /// Other
#[fail(display = "Generic error: {}", _0)] #[fail(display = "Generic error: {}", _0)]
GenericError(String), GenericError(String),
@ -218,3 +231,11 @@ impl From<transaction::Error> for Error {
} }
} }
} }
impl From<core::ser::Error> for Error {
fn from(error: core::ser::Error) -> Error {
Error {
inner: Context::new(ErrorKind::Deser(error)),
}
}
}

View file

@ -37,7 +37,14 @@ pub fn build_send_tx_slate<T: ?Sized, C, K>(
max_outputs: usize, max_outputs: usize,
change_outputs: usize, change_outputs: usize,
selection_strategy_is_use_all: bool, selection_strategy_is_use_all: bool,
) -> Result<(Slate, Context, impl FnOnce(&mut T) -> Result<(), Error>), Error> ) -> Result<
(
Slate,
Context,
impl FnOnce(&mut T, &str) -> Result<(), Error>,
),
Error,
>
where where
T: WalletBackend<C, K>, T: WalletBackend<C, K>,
C: WalletClient, C: WalletClient,
@ -90,12 +97,13 @@ where
// Return a closure to acquire wallet lock and lock the coins being spent // Return a closure to acquire wallet lock and lock the coins being spent
// so we avoid accidental double spend attempt. // so we avoid accidental double spend attempt.
let update_sender_wallet_fn = move |wallet: &mut T| { let update_sender_wallet_fn = move |wallet: &mut T, tx_hex: &str| {
let mut batch = wallet.batch()?; let mut batch = wallet.batch()?;
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); t.fee = Some(fee);
t.tx_hex = Some(tx_hex.to_owned());
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

@ -64,7 +64,14 @@ pub fn create_send_tx<T: ?Sized, C, K>(
max_outputs: usize, max_outputs: usize,
num_change_outputs: usize, num_change_outputs: usize,
selection_strategy_is_use_all: bool, selection_strategy_is_use_all: bool,
) -> Result<(Slate, Context, impl FnOnce(&mut T) -> Result<(), Error>), Error> ) -> Result<
(
Slate,
Context,
impl FnOnce(&mut T, &str) -> Result<(), Error>,
),
Error,
>
where where
T: WalletBackend<C, K>, T: WalletBackend<C, K>,
C: WalletClient, C: WalletClient,
@ -154,6 +161,25 @@ where
Ok(()) Ok(())
} }
/// Retrieve the associated stored finalised hex Transaction for a given transaction Id
/// as well as whether it's been confirmed
pub fn retrieve_tx_hex<T: ?Sized, C, K>(
wallet: &mut T,
tx_id: u32,
) -> Result<(bool, Option<String>), 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();
Ok((tx.confirmed, tx.tx_hex))
}
/// 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

@ -586,6 +586,8 @@ pub struct TxLogEntry {
pub amount_debited: u64, pub amount_debited: u64,
/// Fee /// Fee
pub fee: Option<u64>, pub fee: Option<u64>,
/// The transaction json itself, stored for reference or resending
pub tx_hex: Option<String>,
} }
impl ser::Writeable for TxLogEntry { impl ser::Writeable for TxLogEntry {
@ -616,6 +618,7 @@ impl TxLogEntry {
num_inputs: 0, num_inputs: 0,
num_outputs: 0, num_outputs: 0,
fee: None, fee: None,
tx_hex: None,
} }
} }

View file

@ -247,6 +247,61 @@ fn basic_transaction_api(
Ok(()) Ok(())
})?; })?;
// Send another transaction, but don't post to chain immediately and use
// the stored transaction instead
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
// note this will increment the block count as part of the transaction "Posting"
slate = sender_api.issue_send_tx(
amount * 2, // amount
2, // minimum confirmations
"wallet2", // dest
500, // max outputs
1, // num change outputs
true, // select all outputs
)?;
Ok(())
})?;
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
let (refreshed, _wallet1_info) = sender_api.retrieve_summary_info(true)?;
assert!(refreshed);
let (_, txs) = sender_api.retrieve_txs(true, None)?;
// find the transaction
let tx = txs
.iter()
.find(|t| t.tx_slate_id == Some(slate.id))
.unwrap();
sender_api.post_stored_tx(tx.id, false)?;
let (_, wallet1_info) = sender_api.retrieve_summary_info(true)?;
// should be mined now
assert_eq!(
wallet1_info.total,
amount * wallet1_info.last_confirmed_height - amount * 3
);
Ok(())
})?;
// mine a few more blocks
let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
// check wallet2 has stored transaction
wallet::controller::owner_single_use(wallet2.clone(), |api| {
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true)?;
assert!(wallet2_refreshed);
assert_eq!(wallet2_info.amount_currently_spendable, amount * 3);
// check tx log entry is confirmed
let (refreshed, txs) = api.retrieve_txs(true, None)?;
assert!(refreshed);
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
assert!(tx.is_some());
let tx = tx.unwrap();
assert!(tx.confirmed);
assert!(tx.confirmation_ts.is_some());
Ok(())
})?;
// let logging finish // let logging finish
thread::sleep(Duration::from_millis(200)); thread::sleep(Duration::from_millis(200));
Ok(()) Ok(())