mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-21 03:21:08 +03:00
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:
parent
77765796ab
commit
75f0ea6dd3
10 changed files with 284 additions and 5 deletions
|
@ -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
|
||||
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
|
||||
|
||||
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.
|
||||
|
|
|
@ -343,6 +343,50 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
|
|||
};
|
||||
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)) => {
|
||||
let tx_id = tx_args
|
||||
.value_of("id")
|
||||
|
|
|
@ -222,6 +222,11 @@ fn main() {
|
|||
.help("Fluff the transaction (ignore Dandelion relay protocol)")
|
||||
.short("f")
|
||||
.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")
|
||||
.about("Processes a transaction file to accept a transfer from a sender.")
|
||||
|
@ -268,6 +273,23 @@ fn main() {
|
|||
.long("id")
|
||||
.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")
|
||||
.about("Cancels an previously created transaction, freeing previously locked outputs for use again")
|
||||
.arg(Arg::with_name("id")
|
||||
|
|
|
@ -114,6 +114,7 @@ pub fn txs(
|
|||
bMG->"Amount Debited",
|
||||
bMG->"Fee",
|
||||
bMG->"Net Difference",
|
||||
bMG->"Tx Data",
|
||||
]);
|
||||
|
||||
for t in txs {
|
||||
|
@ -145,6 +146,10 @@ pub fn txs(
|
|||
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![
|
||||
bFC->id,
|
||||
bFC->entry_type,
|
||||
|
@ -158,6 +163,7 @@ pub fn txs(
|
|||
bFR->amount_debited_str,
|
||||
bFR->fee,
|
||||
bFY->net_diff,
|
||||
bFb->tx_data,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ use std::sync::{Arc, Mutex};
|
|||
use serde_json as json;
|
||||
|
||||
use core::core::hash::Hashed;
|
||||
use core::core::Transaction;
|
||||
use core::ser;
|
||||
use keychain::Keychain;
|
||||
use libtx::slate::Slate;
|
||||
|
@ -174,9 +175,10 @@ where
|
|||
};
|
||||
|
||||
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_fn_out(&mut **w)?;
|
||||
lock_fn_out(&mut **w, &tx_hex)?;
|
||||
w.close()?;
|
||||
Ok(slate_out)
|
||||
}
|
||||
|
@ -214,8 +216,10 @@ where
|
|||
batch.commit()?;
|
||||
}
|
||||
|
||||
let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap());
|
||||
|
||||
// lock our inputs
|
||||
lock_fn(&mut **w)?;
|
||||
lock_fn(&mut **w, &tx_hex)?;
|
||||
w.close()?;
|
||||
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
|
||||
pub fn restore(&mut self) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
|
|
|
@ -19,6 +19,7 @@ use std::io;
|
|||
|
||||
use failure::{Backtrace, Context, Fail};
|
||||
|
||||
use core;
|
||||
use core::core::transaction;
|
||||
use keychain;
|
||||
use libtx;
|
||||
|
@ -99,6 +100,10 @@ pub enum ErrorKind {
|
|||
#[fail(display = "JSON format error")]
|
||||
Format,
|
||||
|
||||
/// Other serialization errors
|
||||
#[fail(display = "Ser/Deserialization error")]
|
||||
Deser(core::ser::Error),
|
||||
|
||||
/// IO Error
|
||||
#[fail(display = "I/O error")]
|
||||
IO,
|
||||
|
@ -147,6 +152,14 @@ pub enum ErrorKind {
|
|||
#[fail(display = "Cancellation Error: {}", _0)]
|
||||
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
|
||||
#[fail(display = "Generic error: {}", _0)]
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,14 @@ pub fn build_send_tx_slate<T: ?Sized, C, K>(
|
|||
max_outputs: usize,
|
||||
change_outputs: usize,
|
||||
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
|
||||
T: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
|
@ -90,12 +97,13 @@ where
|
|||
|
||||
// Return a closure to acquire wallet lock and lock the coins being spent
|
||||
// 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 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);
|
||||
t.tx_hex = Some(tx_hex.to_owned());
|
||||
let mut amount_debited = 0;
|
||||
t.num_inputs = lock_inputs.len();
|
||||
for id in lock_inputs {
|
||||
|
|
|
@ -64,7 +64,14 @@ pub fn create_send_tx<T: ?Sized, C, K>(
|
|||
max_outputs: usize,
|
||||
num_change_outputs: usize,
|
||||
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
|
||||
T: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
|
@ -154,6 +161,25 @@ where
|
|||
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
|
||||
pub fn issue_burn_tx<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
|
|
|
@ -586,6 +586,8 @@ pub struct TxLogEntry {
|
|||
pub amount_debited: u64,
|
||||
/// Fee
|
||||
pub fee: Option<u64>,
|
||||
/// The transaction json itself, stored for reference or resending
|
||||
pub tx_hex: Option<String>,
|
||||
}
|
||||
|
||||
impl ser::Writeable for TxLogEntry {
|
||||
|
@ -616,6 +618,7 @@ impl TxLogEntry {
|
|||
num_inputs: 0,
|
||||
num_outputs: 0,
|
||||
fee: None,
|
||||
tx_hex: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -247,6 +247,61 @@ fn basic_transaction_api(
|
|||
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
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in a new issue