diff --git a/api/src/owner.rs b/api/src/owner.rs index 732e6444..2eaf6aca 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -26,8 +26,8 @@ use crate::keychain::{Identifier, Keychain}; use crate::libwallet::api_impl::owner; use crate::libwallet::slate::Slate; use crate::libwallet::types::{ - AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, TxLogEntry, - WalletBackend, WalletInfo, + AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, + PaymentCommitMapping, TxLogEntry, WalletBackend, WalletInfo, }; use crate::libwallet::{Error, ErrorKind}; @@ -304,6 +304,56 @@ where res } + /// Returns a list of payment outputs from the active account in the wallet. + /// + /// # Arguments + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../grin_wallet_libwallet/types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain output information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// * `tx_id` - If `Some(i)`, only return the outputs associated with + /// the transaction log entry of id `i`. + /// + /// # Returns + /// * `(bool, Vec)` - A tuple: + /// * The first `bool` element indicates whether the data was successfully + /// refreshed from the node (note this may be false even if the `refresh_from_node` + /// argument was set to `true`. + /// * The second element contains a vector of + /// [PaymentCommitMapping](../grin_wallet_libwallet/types/struct.PaymentCommitMapping.html) + /// of which each element is a mapping between the wallet's internal + /// [PaymentData](../grin_wallet_libwallet/types/struct.Output.html) + /// and the Output commitment + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone()); + /// let update_from_node = true; + /// let tx_id = None; + /// + /// let result = api_owner.retrieve_payments(update_from_node, tx_id); + /// + /// if let Ok((was_updated, payment_mappings)) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_payments( + &self, + refresh_from_node: bool, + tx_id: Option, + ) -> Result<(bool, Vec), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let res = owner::retrieve_payments(&mut *w, refresh_from_node, tx_id); + w.close()?; + res + } + /// Returns a list of [Transaction Log Entries](../grin_wallet_libwallet/types/struct.TxLogEntry.html) /// from the active account in the wallet. /// diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 6d9be88b..8040e1ea 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -162,6 +162,7 @@ pub trait OwnerRpc { "mmr_index": null, "n_child": 0, "root_key_id": "0200000000000000000000000000000000", + "slate_id": null, "status": "Unspent", "tx_log_entry": 0, "value": "60000000000" @@ -178,6 +179,7 @@ pub trait OwnerRpc { "mmr_index": null, "n_child": 1, "root_key_id": "0200000000000000000000000000000000", + "slate_id": null, "status": "Unspent", "tx_log_entry": 1, "value": "60000000000" diff --git a/controller/src/command.rs b/controller/src/command.rs index fb33470e..a555b614 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -433,6 +433,20 @@ pub fn outputs( Ok(()) } +pub fn payments( + wallet: Arc>>, + g_args: &GlobalArgs, + dark_scheme: bool, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let res = api.node_height()?; + let (validated, outputs) = api.retrieve_payments(true, None)?; + display::payments(&g_args.account, res.height, validated, outputs, dark_scheme)?; + Ok(()) + })?; + Ok(()) +} + /// Txs command args pub struct TxsArgs { pub id: Option, @@ -462,8 +476,22 @@ pub fn txs( let (_, outputs) = api.retrieve_outputs(true, false, args.id)?; display::outputs(&g_args.account, res.height, validated, outputs, dark_scheme)?; // should only be one here, but just in case - for tx in txs { - display::tx_messages(&tx, dark_scheme)?; + for tx in &txs { + let (_, outputs) = api.retrieve_payments(true, tx.tx_slate_id)?; + if outputs.len() > 0 { + display::payments( + &g_args.account, + res.height, + validated, + outputs, + dark_scheme, + )?; + } + } + + // should only be one here, but just in case + for tx in &txs { + display::tx_messages(tx, dark_scheme)?; } }; Ok(()) diff --git a/controller/src/display.rs b/controller/src/display.rs index 461d0ec4..e941d148 100644 --- a/controller/src/display.rs +++ b/controller/src/display.rs @@ -15,7 +15,8 @@ use crate::core::core::{self, amount_to_hr_string}; use crate::core::global; use crate::libwallet::types::{ - AcctPathMapping, OutputCommitMapping, OutputStatus, TxLogEntry, WalletInfo, + AcctPathMapping, OutputCommitMapping, OutputStatus, PaymentCommitMapping, TxLogEntry, + WalletInfo, }; use crate::libwallet::Error; use crate::util; @@ -119,6 +120,89 @@ pub fn outputs( Ok(()) } +/// Display payments in a pretty way +pub fn payments( + account: &str, + cur_height: u64, + validated: bool, + outputs: Vec, + dark_background_color_scheme: bool, +) -> Result<(), Error> { + let title = format!( + "Wallet Payments - Account '{}' - Block Height: {}", + account, cur_height + ); + println!(); + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Output Commitment", + bMG->"Block Height", + bMG->"Locked Until", + bMG->"Status", + bMG->"# Confirms", + bMG->"Value", + bMG->"Shared Transaction Id" + ]); + + for payment in outputs { + let commit = format!("{}", util::to_hex(payment.commit.as_ref().to_vec())); + let out = payment.output; + + let height = format!("{}", out.height); + let lock_height = format!("{}", out.lock_height); + let status = format!("{}", out.status); + + let num_confirmations = format!("{}", out.num_confirmations(cur_height)); + let value = if out.value == 0 { + "unknown".to_owned() + } else { + format!("{}", core::amount_to_hr_string(out.value, false)) + }; + let slate_id = format!("{}", out.slate_id); + + if dark_background_color_scheme { + table.add_row(row![ + bFC->commit, + bFB->height, + bFB->lock_height, + bFR->status, + bFB->num_confirmations, + bFG->value, + bFC->slate_id, + ]); + } else { + table.add_row(row![ + bFD->commit, + bFB->height, + bFB->lock_height, + bFR->status, + bFB->num_confirmations, + bFG->value, + bFD->slate_id, + ]); + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + if !validated { + println!( + "\nWARNING: Wallet failed to verify data. \ + The above is from local cache and possibly invalid! \ + (is your `grin server` offline or broken?)" + ); + } + Ok(()) +} + /// Display transaction log in a pretty way pub fn txs( account: &str, diff --git a/controller/tests/transaction.rs b/controller/tests/transaction.rs index 640f6e20..f3d8cf33 100644 --- a/controller/tests/transaction.rs +++ b/controller/tests/transaction.rs @@ -430,6 +430,16 @@ fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> { assert_eq!(output_mappings.len(), 3); assert_eq!(locked_count, 2); assert_eq!(unconfirmed_count, 1); + // check the payments are as expected + unconfirmed_count = 0; + let (_, payments) = api.retrieve_payments(false, tx.unwrap().tx_slate_id)?; + for p in &payments { + if p.output.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(payments.len(), 1); + assert_eq!(unconfirmed_count, 1); Ok(()) })?; @@ -450,6 +460,16 @@ fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> { } assert_eq!(outputs.len(), 1); assert_eq!(unconfirmed_count, 1); + // check the payments are as expected: receiver don't have this. + unconfirmed_count = 0; + let (_, payments) = api.retrieve_payments(false, tx.unwrap().tx_slate_id)?; + for p in &payments { + if p.output.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(payments.len(), 0); + assert_eq!(unconfirmed_count, 0); let (refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; assert!(refreshed); assert_eq!(wallet2_info.amount_currently_spendable, 0,); diff --git a/impls/src/backends/lmdb.rs b/impls/src/backends/lmdb.rs index d939808e..a8a32b42 100644 --- a/impls/src/backends/lmdb.rs +++ b/impls/src/backends/lmdb.rs @@ -42,6 +42,8 @@ pub const DB_DIR: &'static str = "db"; pub const TX_SAVE_DIR: &'static str = "saved_txs"; const OUTPUT_PREFIX: u8 = 'o' as u8; +const PAYMENT_PREFIX: u8 = 'P' as u8; +const PAYMENT_COMMITS_PREFIX: u8 = 'Q' as u8; const DERIV_PREFIX: u8 = 'd' as u8; const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8; const PRIVATE_TX_CONTEXT_PREFIX: u8 = 'p' as u8; @@ -238,6 +240,20 @@ where Box::new(self.db.iter(&[OUTPUT_PREFIX]).unwrap()) } + fn get_payment_log_commits(&self, u: &Uuid) -> Result, Error> { + let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec()); + self.db.get_ser(&key).map_err(|e| e.into()) + } + + fn get_payment_log_entry(&self, commit: String) -> Result, Error> { + let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec()); + self.db.get_ser(&key).map_err(|e| e.into()) + } + + fn payment_log_iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.db.iter(&[PAYMENT_PREFIX]).unwrap()) + } + fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); self.db.get_ser(&key).map_err(|e| e.into()) @@ -380,7 +396,7 @@ where } fn save(&mut self, out: OutputData) -> Result<(), Error> { - // Save the output data to the db. + // Save the self output data to the db. { let key = match out.mmr_index { Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i), @@ -392,6 +408,27 @@ where Ok(()) } + fn save_payment_commits(&mut self, u: &Uuid, commits: PaymentCommits) -> Result<(), Error> { + // Save the payment commits list data to the db. + { + let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec()); + self.db.borrow().as_ref().unwrap().put_ser(&key, &commits)?; + } + + Ok(()) + } + + fn save_payment(&mut self, out: PaymentData) -> Result<(), Error> { + // Save the payment output data to the db. + { + let commit = out.commit.clone(); + let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec()); + self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?; + } + + Ok(()) + } + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { let key = match mmr_index { Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), @@ -404,6 +441,24 @@ where .map_err(|e| e.into()) } + fn get_payment_commits(&self, u: &Uuid) -> Result { + let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec()); + option_to_not_found( + self.db.borrow().as_ref().unwrap().get_ser(&key), + &format!("slate_id: {}", u.to_string()), + ) + .map_err(|e| e.into()) + } + + fn get_payment_log_entry(&self, commit: String) -> Result { + let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec()); + option_to_not_found( + self.db.borrow().as_ref().unwrap().get_ser(&key), + &format!("key: {:?}", commit), + ) + .map_err(|e| e.into()) + } + fn iter(&self) -> Box> { Box::new( self.db diff --git a/impls/src/lmdb_wallet.rs b/impls/src/lmdb_wallet.rs index 5a54bcad..2ee6a5bb 100644 --- a/impls/src/lmdb_wallet.rs +++ b/impls/src/lmdb_wallet.rs @@ -43,6 +43,8 @@ pub const DB_DIR: &'static str = "db"; pub const TX_SAVE_DIR: &'static str = "saved_txs"; const OUTPUT_PREFIX: u8 = 'o' as u8; +const PAYMENT_PREFIX: u8 = 'P' as u8; +const PAYMENT_COMMITS_PREFIX: u8 = 'Q' as u8; const DERIV_PREFIX: u8 = 'd' as u8; const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8; const PRIVATE_TX_CONTEXT_PREFIX: u8 = 'p' as u8; @@ -239,6 +241,20 @@ where Box::new(self.db.iter(&[OUTPUT_PREFIX]).unwrap().map(|o| o.1)) } + fn get_payment_log_commits(&self, u: &Uuid) -> Result, Error> { + let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec()); + self.db.get_ser(&key).map_err(|e| e.into()) + } + + fn get_payment_log_entry(&self, commit: String) -> Result, Error> { + let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec()); + self.db.get_ser(&key).map_err(|e| e.into()) + } + + fn payment_log_iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.db.iter(&[PAYMENT_PREFIX]).unwrap().map(|o| o.1)) + } + fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); self.db.get_ser(&key).map_err(|e| e.into()) @@ -386,7 +402,7 @@ where } fn save(&mut self, out: OutputData) -> Result<(), Error> { - // Save the output data to the db. + // Save the self output data to the db. { let key = match out.mmr_index { Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i), @@ -398,6 +414,27 @@ where Ok(()) } + fn save_payment_commits(&mut self, u: &Uuid, commits: PaymentCommits) -> Result<(), Error> { + // Save the payment commits list data to the db. + { + let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec()); + self.db.borrow().as_ref().unwrap().put_ser(&key, &commits)?; + } + + Ok(()) + } + + fn save_payment(&mut self, out: PaymentData) -> Result<(), Error> { + // Save the payment output data to the db. + { + let commit = out.commit.clone(); + let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec()); + self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?; + } + + Ok(()) + } + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { let key = match mmr_index { Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), @@ -410,6 +447,24 @@ where .map_err(|e| e.into()) } + fn get_payment_commits(&self, u: &Uuid) -> Result { + let key = to_key(PAYMENT_COMMITS_PREFIX, &mut u.as_bytes().to_vec()); + option_to_not_found( + self.db.borrow().as_ref().unwrap().get_ser(&key), + &format!("slate_id: {}", u.to_string()), + ) + .map_err(|e| e.into()) + } + + fn get_payment_log_entry(&self, commit: String) -> Result { + let key = to_key(PAYMENT_PREFIX, &mut commit.as_bytes().to_vec()); + option_to_not_found( + self.db.borrow().as_ref().unwrap().get_ser(&key), + &format!("key: {:?}", commit), + ) + .map_err(|e| e.into()) + } + fn iter(&self) -> Box> { Box::new( self.db diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 8b763fee..c93d801c 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -25,8 +25,8 @@ use crate::grin_keychain::{Identifier, Keychain}; use crate::internal::{keys, selection, tx, updater}; use crate::slate::Slate; use crate::types::{ - AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, TxLogEntry, - TxWrapper, WalletBackend, WalletInfo, + AcctPathMapping, InitTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, + PaymentCommitMapping, TxLogEntry, TxWrapper, WalletBackend, WalletInfo, }; use crate::{Error, ErrorKind}; @@ -83,10 +83,29 @@ where Ok(( validated, - updater::retrieve_outputs(&mut *w, include_spent, tx_id, Some(&parent_key_id))?, + updater::retrieve_outputs(&mut *w, include_spent, tx_id, None, Some(&parent_key_id))?, )) } +/// Returns a list of payment outputs from the active account in the wallet. +pub fn retrieve_payments( + w: &mut T, + refresh_from_node: bool, + tx_id: Option, +) -> Result<(bool, Vec), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let mut validated = false; + if refresh_from_node { + validated = update_outputs(w, false); + } + + Ok((validated, updater::retrieve_payments(w, tx_id)?)) +} + /// Retrieve txs pub fn retrieve_txs( w: &mut T, diff --git a/libwallet/src/internal/restore.rs b/libwallet/src/internal/restore.rs index 661d4286..06ccae8f 100644 --- a/libwallet/src/internal/restore.rs +++ b/libwallet/src/internal/restore.rs @@ -215,6 +215,7 @@ where lock_height: output.lock_height, is_coinbase: output.is_coinbase, tx_log_entry: Some(log_id), + slate_id: None, }); let max_child_index = found_parents.get(&parent_key_id).unwrap().clone(); @@ -283,7 +284,7 @@ where // Now, get all outputs owned by this wallet (regardless of account) let wallet_outputs = { - let res = updater::retrieve_outputs(&mut *wallet, true, None, None)?; + let res = updater::retrieve_outputs(&mut *wallet, true, None, None, None)?; res }; diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 38d33af7..18658623 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -155,6 +155,7 @@ where lock_height: 0, is_coinbase: false, tx_log_entry: Some(log_id), + slate_id: Some(slate_id.clone()), })?; } batch.save_tx_log_entry(t.clone(), &parent_key_id)?; @@ -224,6 +225,7 @@ where lock_height: 0, is_coinbase: false, tx_log_entry: Some(log_id), + slate_id: Some(slate_id), })?; batch.save_tx_log_entry(t, &parent_key_id)?; batch.commit()?; diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 52165a0f..ff22fb05 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -17,10 +17,14 @@ use uuid::Uuid; use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_util::secp::pedersen; +use crate::grin_util::to_hex; use crate::grin_util::Mutex; use crate::internal::{selection, updater}; use crate::slate::Slate; -use crate::types::{Context, NodeClient, TxLogEntryType, WalletBackend}; +use crate::types::{ + Context, NodeClient, OutputStatus, PaymentCommits, PaymentData, TxLogEntryType, WalletBackend, +}; use crate::{Error, ErrorKind}; /// static for incrementing test UUIDs @@ -220,6 +224,63 @@ where )?; // Final transaction can be built by anyone at this stage slate.finalize(wallet.keychain())?; + + let parent_key_id = Some(&context.parent_key_id); + + // Get the change output/s from database + let changes = updater::retrieve_outputs(wallet, false, None, Some(slate.id), parent_key_id)?; + let change_commits = changes + .iter() + .map(|oc| oc.commit.clone()) + .collect::>(); + + // Find the payment output/s + let mut outputs: Vec = Vec::new(); + for output in slate.tx.outputs() { + if !change_commits.contains(&output.commit) { + outputs.insert(0, output.commit.clone()); + } + } + + // sender save the payment output + let mut batch = wallet.batch()?; + batch.save_payment_commits( + &slate.id, + PaymentCommits { + commits: outputs + .iter() + .map(|c| to_hex(c.as_ref().to_vec())) + .collect::>(), + slate_id: slate.id, + }, + )?; + // todo: multiple receivers transaction + if outputs.len() > 1 { + for output in outputs { + let payment_output = to_hex(output.clone().as_ref().to_vec()); + batch.save_payment(PaymentData { + commit: payment_output, + value: 0, // '0' means unknown here, since '0' value is impossible for an output. + status: OutputStatus::Unconfirmed, + height: slate.height, + lock_height: 0, + slate_id: slate.id, + })?; + } + } else if outputs.len() == 1 { + let payment_output = to_hex(outputs.first().clone().unwrap().as_ref().to_vec()); + batch.save_payment(PaymentData { + commit: payment_output, + value: slate.amount, + status: OutputStatus::Unconfirmed, + height: slate.height, + lock_height: 0, + slate_id: slate.id, + })?; + } else { + warn!("complete_tx - no 'payment' output! is this a sending to self for test purpose?"); + } + batch.commit()?; Ok(()) } @@ -253,7 +314,7 @@ where return Err(ErrorKind::TransactionNotCancellable(tx_id_string))?; } // get outputs associated with tx - let res = updater::retrieve_outputs(wallet, false, Some(tx.id), Some(&parent_key_id))?; + let res = updater::retrieve_outputs(wallet, false, Some(tx.id), None, Some(&parent_key_id))?; let outputs = res.iter().map(|m| m.output.clone()).collect(); updater::cancel_tx_and_outputs(wallet, tx, outputs, parent_key_id)?; Ok(()) diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs index c4ec7853..040e77fe 100644 --- a/libwallet/src/internal/updater.rs +++ b/libwallet/src/internal/updater.rs @@ -28,15 +28,16 @@ use crate::grin_util as util; use crate::grin_util::secp::pedersen; use crate::internal::keys; use crate::types::{ - BlockFees, CbData, NodeClient, OutputCommitMapping, OutputData, OutputStatus, TxLogEntry, - TxLogEntryType, WalletBackend, WalletInfo, + BlockFees, CbData, NodeClient, OutputCommitMapping, OutputData, OutputStatus, + PaymentCommitMapping, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo, }; -/// Retrieve all of the outputs (doesn't attempt to update from node) +/// Retrieve all of the self outputs (doesn't attempt to update from node) pub fn retrieve_outputs( wallet: &mut T, show_spent: bool, tx_id: Option, + slate_id: Option, parent_key_id: Option<&Identifier>, ) -> Result, Error> where @@ -58,6 +59,14 @@ where .collect::>(); } + // only include outputs with a given slate_id if provided + if let Some(id) = slate_id { + outputs = outputs + .into_iter() + .filter(|out| out.slate_id == Some(id)) + .collect::>(); + } + if let Some(k) = parent_key_id { outputs = outputs .iter() @@ -82,6 +91,38 @@ where Ok(res) } +/// Retrieve all of the payment outputs (doesn't attempt to update from node) +pub fn retrieve_payments( + wallet: &mut T, + tx_id: Option, +) -> Result, Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // just read the wallet here, no need for a write lock + let mut outputs = wallet.payment_log_iter().collect::>(); + + // only include outputs with a given tx_id if provided + if let Some(id) = tx_id { + outputs = outputs + .into_iter() + .filter(|out| out.slate_id == id) + .collect::>(); + } + + let res = outputs + .into_iter() + .map(|output| { + let commit = + pedersen::Commitment::from_vec(util::from_hex(output.commit.clone()).unwrap()); + PaymentCommitMapping { output, commit } + }) + .collect(); + Ok(res) +} + /// Retrieve all of the transaction entries, or a particular entry /// if `parent_key_id` is set, only return entries from that key pub fn retrieve_txs( @@ -291,6 +332,21 @@ where t.confirmed = true; batch.save_tx_log_entry(t, &parent_key_id)?; } + + // if there's a related payment output being confirmed, refresh that payment log + if let Some(slate_id) = output.slate_id { + if let Ok(commits) = batch.get_payment_commits(&slate_id) { + for commit in commits.commits { + if let Ok(mut payment) = + batch.get_payment_log_entry(commit.clone()) + { + payment.height = o.1; + payment.mark_confirmed(); + batch.save_payment(payment)?; + } + } + } + } } output.height = o.1; output.mark_unspent(); @@ -413,6 +469,7 @@ where locked_total += out.value; } OutputStatus::Spent => {} + OutputStatus::Confirmed => {} } } @@ -490,6 +547,7 @@ where lock_height: lock_height, is_coinbase: true, tx_log_entry: None, + slate_id: None, })?; batch.commit()?; } diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 95cdeb36..81063de6 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -85,10 +85,19 @@ where /// return the parent path fn parent_key_id(&mut self) -> Identifier; - /// Iterate over all output data stored by the backend + /// Iterate over all self output data stored by the backend fn iter<'a>(&'a self) -> Box + 'a>; - /// Get output data by id + /// Get payment commits list by slate id + fn get_payment_log_commits(&self, u: &Uuid) -> Result, Error>; + + /// Get payment output data by commit + fn get_payment_log_entry(&self, commit: String) -> Result, Error>; + + /// Iterate over all payment output data stored by the backend + fn payment_log_iter<'a>(&'a self) -> Box + 'a>; + + /// Get self owned output data by id fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; /// Get an (Optional) tx log entry by uuid @@ -140,12 +149,24 @@ where /// Return the keychain being used fn keychain(&mut self) -> &mut K; - /// Add or update data about an output to the backend + /// Add or update data about a self owned output to the backend fn save(&mut self, out: OutputData) -> Result<(), Error>; - /// Gets output data by id + /// Add or update data about a payment output to the backend + fn save_payment(&mut self, out: PaymentData) -> Result<(), Error>; + + /// Add or update data about a payment commits list to the backend + fn save_payment_commits(&mut self, u: &Uuid, commits: PaymentCommits) -> Result<(), Error>; + + /// Gets self owned output data by id fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; + /// Gets payment commits list by slate id + fn get_payment_commits(&self, u: &Uuid) -> Result; + + /// Gets payment output data by commit + fn get_payment_log_entry(&self, commit: String) -> Result; + /// Iterate over all output data stored by the backend fn iter(&self) -> Box>; @@ -270,6 +291,8 @@ pub struct OutputData { pub is_coinbase: bool, /// Optional corresponding internal entry in tx entry log pub tx_log_entry: Option, + /// Unique transaction ID, selected by sender + pub slate_id: Option, } impl ser::Writeable for OutputData { @@ -346,6 +369,94 @@ impl OutputData { } } } + +/// Information about a payment output that's being tracked by the wallet. +/// It belongs to the receiver, and it's paid by this wallet. + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct PaymentData { + /// The actual commit + pub commit: String, + /// Value of the output + pub value: u64, + /// Current status of the output + pub status: OutputStatus, + /// Height of the output + pub height: u64, + /// Height we are locked until + pub lock_height: u64, + /// Unique transaction ID, selected by sender + pub slate_id: Uuid, +} + +impl ser::Writeable for PaymentData { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for PaymentData { + fn read(reader: &mut dyn ser::Reader) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +impl PaymentData { + /// How many confirmations has this output received? + /// If height == 0 then we are either Unconfirmed or the output was + /// cut-through + /// so we do not actually know how many confirmations this output had (and + /// never will). + pub fn num_confirmations(&self, current_height: u64) -> u64 { + if self.height > current_height { + return 0; + } + if self.status == OutputStatus::Unconfirmed { + 0 + } else { + // if an output has height n and we are at block n + // then we have a single confirmation (the block it originated in) + 1 + (current_height - self.height) + } + } + + /// Marks this output as confirmed if it was previously unconfirmed + pub fn mark_confirmed(&mut self) { + match self.status { + OutputStatus::Unconfirmed => self.status = OutputStatus::Confirmed, + _ => (), + } + } +} + +/// Information about the payment commit/s in one tx that's being tracked by the wallet. +/// They belong to the receiver/s, and they're paid by this wallet. +/// +/// Note: because lmdb can't have multiple keys to same value, we have to use this to find +/// the commit lists by the slate id, in case we support multiple receivers in one tx in the future. + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PaymentCommits { + /// The actual commit/s + pub commits: Vec, + /// Unique transaction ID, selected by sender + pub slate_id: Uuid, +} + +impl ser::Writeable for PaymentCommits { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for PaymentCommits { + fn read(reader: &mut dyn ser::Reader) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + /// Status of an output that's being tracked by the wallet. Can either be /// unconfirmed, spent, unspent, or locked (when it's been used to generate /// a transaction but we don't have confirmation that the transaction was @@ -360,6 +471,8 @@ pub enum OutputStatus { Locked, /// Spent Spent, + /// Confirmed + Confirmed, } impl fmt::Display for OutputStatus { @@ -369,6 +482,7 @@ impl fmt::Display for OutputStatus { OutputStatus::Unspent => write!(f, "Unspent"), OutputStatus::Locked => write!(f, "Locked"), OutputStatus::Spent => write!(f, "Spent"), + OutputStatus::Confirmed => write!(f, "Confirmed"), } } } @@ -853,6 +967,19 @@ pub struct OutputCommitMapping { pub commit: pedersen::Commitment, } +/// Map PaymentData to commits +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PaymentCommitMapping { + /// Payment Data + pub output: PaymentData, + /// The commit + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub commit: pedersen::Commitment, +} + /// Node height result #[derive(Serialize, Deserialize, Debug, Clone)] pub struct NodeHeightResult { diff --git a/src/bin/cmd/wallet_args.rs b/src/bin/cmd/wallet_args.rs index 145a9c2b..eca1b2b5 100644 --- a/src/bin/cmd/wallet_args.rs +++ b/src/bin/cmd/wallet_args.rs @@ -624,6 +624,14 @@ pub fn wallet_command( &global_wallet_args, wallet_config.dark_background_color_scheme.unwrap_or(true), ), + ("payments", Some(_)) => { + info!("payments command received"); + command::payments( + inst_wallet(), + &global_wallet_args, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } ("txs", Some(args)) => { let a = arg_parse!(parse_txs_args(&args)); command::txs( diff --git a/src/bin/cmd/wallet_tests.rs b/src/bin/cmd/wallet_tests.rs index db598e0e..f265471c 100644 --- a/src/bin/cmd/wallet_tests.rs +++ b/src/bin/cmd/wallet_tests.rs @@ -501,6 +501,10 @@ mod wallet_tests { let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "outputs"]; execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + // payments + let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "payments"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + // let logging finish thread::sleep(Duration::from_millis(200)); Ok(()) diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 01a5dd42..8157a77e 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -163,7 +163,9 @@ subcommands: short: f long: fluff - outputs: - about: Raw wallet output info (list of outputs) + about: Raw wallet self owned output info (list of outputs) + - payments: + about: Raw wallet payment output info (list of outputs) - txs: about: Display transaction information args: