diff --git a/Cargo.lock b/Cargo.lock index e00aab63..99f5740d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1599,6 +1599,7 @@ dependencies = [ "grin_wallet_util", "lazy_static", "log", + "num-bigint", "rand 0.6.5", "regex", "secrecy 0.6.0", diff --git a/api/src/owner.rs b/api/src/owner.rs index 12e54e04..ac80d787 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -16,6 +16,7 @@ use chrono::prelude::*; use ed25519_dalek::SecretKey as DalekSecretKey; +use grin_wallet_libwallet::RetrieveTxQueryArgs; use uuid::Uuid; use crate::config::{TorConfig, WalletConfig}; @@ -447,6 +448,9 @@ where /// the transaction log entry of id `i`. /// * `tx_slate_id` - If `Some(uuid)`, only return transactions associated with /// the given [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html) uuid. + /// * `tx_query_args` - If provided, use advanced query arguments as documented in + /// (../grin_wallet_libwallet/types.struct.RetrieveTxQueryArgs.html). If either + /// `tx_id` or `tx_slate_id` is provided in the same call, this argument is ignored /// /// # Returns /// * `(bool, Vec, tx_slate_id: Option, + tx_query_args: Option, ) -> Result<(bool, Vec), Error> { let tx = { let t = self.status_tx.lock(); @@ -496,6 +501,7 @@ where refresh_from_node, tx_id, tx_slate_id, + tx_query_args, )?; if self.doctest_mode { res.1 = res @@ -1161,7 +1167,7 @@ where /// let tx_slate_id = None; /// /// // Return all TxLogEntries - /// let result = api_owner.retrieve_txs(None, update_from_node, tx_id, tx_slate_id); + /// let result = api_owner.retrieve_txs(None, update_from_node, tx_id, tx_slate_id, None); /// /// if let Ok((was_updated, tx_log_entries)) = result { /// let stored_tx = api_owner.get_stored_tx(None, Some(tx_log_entries[0].id), None).unwrap(); diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 2591577f..23fc6b63 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -13,6 +13,7 @@ // limitations under the License. //! JSON-RPC Stub generation for the Owner API +use grin_wallet_libwallet::RetrieveTxQueryArgs; use uuid::Uuid; use crate::config::{TorConfig, WalletConfig}; @@ -215,6 +216,7 @@ pub trait OwnerRpc { # , 2, false, false, false, false); ``` */ + fn retrieve_outputs( &self, token: Token, @@ -309,6 +311,97 @@ pub trait OwnerRpc { tx_slate_id: Option, ) -> Result<(bool, Vec), Error>; + /** + Networked version of [Owner::retrieve_txs](struct.Owner.html#method.retrieve_txs), which passes only the `tx_query_args` + parameter. See (../grin_wallet_libwallet/types.struct.RetrieveTxQueryArgs.html) + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "query_txs", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "refresh_from_node": true, + "query": { + "min_id": 0, + "max_id": 100, + "min_amount": "0", + "max_amount": "60000000000", + "sort_field": "Id", + "sort_order": "Asc" + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [ + true, + [ + { + "amount_credited": "60000000000", + "amount_debited": "0", + "confirmation_ts": "2019-01-15T16:01:26Z", + "confirmed": true, + "creation_ts": "2019-01-15T16:01:26Z", + "fee": null, + "id": 0, + "kernel_excess": "0838e19c490038b10f051c9c190a9b1f96d59bbd242f5d3143f50630deb74342ed", + "kernel_lookup_min_height": 1, + "num_inputs": 0, + "num_outputs": 1, + "parent_key_id": "0200000000000000000000000000000000", + "stored_tx": null, + "ttl_cutoff_height": null, + "tx_slate_id": null, + "payment_proof": null, + "reverted_after": null, + "tx_type": "ConfirmedCoinbase" + }, + { + "amount_credited": "60000000000", + "amount_debited": "0", + "confirmation_ts": "2019-01-15T16:01:26Z", + "confirmed": true, + "creation_ts": "2019-01-15T16:01:26Z", + "fee": null, + "id": 1, + "kernel_excess": "08cd9d890c0b6a004f700aa5939a1ce0488fe2a11fa33cf096b50732ceab0be1df", + "kernel_lookup_min_height": 2, + "num_inputs": 0, + "num_outputs": 1, + "parent_key_id": "0200000000000000000000000000000000", + "stored_tx": null, + "ttl_cutoff_height": null, + "payment_proof": null, + "reverted_after": null, + "tx_slate_id": null, + "tx_type": "ConfirmedCoinbase" + } + ] + ] + } + } + # "# + # , 2, false, false, false, false); + ``` + + */ + + fn query_txs( + &self, + token: Token, + refresh_from_node: bool, + query: RetrieveTxQueryArgs, + ) -> Result<(bool, Vec), Error>; + /** Networked version of [Owner::retrieve_summary_info](struct.Owner.html#method.retrieve_summary_info). @@ -1919,6 +2012,23 @@ where refresh_from_node, tx_id, tx_slate_id, + None, + ) + } + + fn query_txs( + &self, + token: Token, + refresh_from_node: bool, + query: RetrieveTxQueryArgs, + ) -> Result<(bool, Vec), Error> { + Owner::retrieve_txs( + self, + (&token.keychain_mask).as_ref(), + refresh_from_node, + None, + None, + Some(query), ) } diff --git a/controller/src/command.rs b/controller/src/command.rs index 3c11bcd1..938eb829 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -1123,7 +1123,8 @@ where let updater_running = owner_api.updater_running.load(Ordering::Relaxed); controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { let res = api.node_height(m)?; - let (validated, txs) = api.retrieve_txs(m, true, args.id, args.tx_slate_id)?; + // Note advanced query args not currently supported by command line client + let (validated, txs) = api.retrieve_txs(m, true, args.id, args.tx_slate_id, None)?; let include_status = !args.id.is_some() && !args.tx_slate_id.is_some(); // If view count is specified, restrict the TX list to `txs.len() - count` let first_tx = args @@ -1235,7 +1236,7 @@ where } Some(s) => s, }; - let (_, txs) = api.retrieve_txs(m, true, Some(args.id), None)?; + let (_, txs) = api.retrieve_txs(m, true, Some(args.id), None, None)?; match args.dump_file { None => { if txs[0].confirmed { diff --git a/controller/tests/accounts.rs b/controller/tests/accounts.rs index 88cbf576..744a5e14 100644 --- a/controller/tests/accounts.rs +++ b/controller/tests/accounts.rs @@ -135,7 +135,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(wallet1_info.total, 5 * reward); assert_eq!(wallet1_info.amount_currently_spendable, (5 - cm) * reward); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 5); Ok(()) })?; @@ -159,7 +159,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(wallet1_info.total, 7 * reward); assert_eq!(wallet1_info.amount_currently_spendable, 7 * reward); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 7); Ok(()) })?; @@ -178,7 +178,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(wallet1_info.total, 0,); assert_eq!(wallet1_info.amount_currently_spendable, 0,); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 0); Ok(()) })?; @@ -210,7 +210,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; assert!(wallet1_refreshed); assert_eq!(wallet1_info.last_confirmed_height, 13); - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 9); Ok(()) })?; @@ -225,7 +225,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(wallet1_info.last_confirmed_height, 12); let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; assert_eq!(wallet1_info.last_confirmed_height, 13); - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; println!("{:?}", txs); assert_eq!(txs.len(), 5); Ok(()) @@ -236,7 +236,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; assert!(wallet2_refreshed); assert_eq!(wallet2_info.last_confirmed_height, 13); - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 1); Ok(()) })?; @@ -254,7 +254,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(wallet2_info.total, 0,); assert_eq!(wallet2_info.amount_currently_spendable, 0,); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 0); Ok(()) })?; diff --git a/controller/tests/check.rs b/controller/tests/check.rs index 2034acc3..634cdd22 100644 --- a/controller/tests/check.rs +++ b/controller/tests/check.rs @@ -119,7 +119,7 @@ fn scan_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(wallet1_info.total, bh * reward); assert_eq!(wallet1_info.amount_currently_spendable, (bh - cm) * reward); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; let (c, _) = libwallet::TxLogEntry::sum_confirmed(&txs); assert_eq!(wallet1_info.total, c); assert_eq!(txs.len(), bh as usize); @@ -150,7 +150,7 @@ fn scan_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // check we have a problem now wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; let (c, _) = libwallet::TxLogEntry::sum_confirmed(&txs); assert!(wallet1_info.total != c); Ok(()) diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs index 8faf8603..70511d32 100644 --- a/controller/tests/invoice.rs +++ b/controller/tests/invoice.rs @@ -146,7 +146,7 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Check transaction log for wallet 2 wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { let (_, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); assert!(txs.len() == 1); println!( @@ -161,7 +161,7 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // exists wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); assert_eq!(txs.len() as u64, bh + 1); println!( diff --git a/controller/tests/no_change.rs b/controller/tests/no_change.rs index b18fb25e..cefba915 100644 --- a/controller/tests/no_change.rs +++ b/controller/tests/no_change.rs @@ -104,7 +104,7 @@ fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Refresh and check transaction log for wallet 1 wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { - let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; assert!(refreshed); let tx = txs[0].clone(); println!("SIMPLE SEND - SENDING WALLET"); @@ -117,7 +117,7 @@ fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Refresh and check transaction log for wallet 2 wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { - let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; assert!(refreshed); let tx = txs[0].clone(); println!("SIMPLE SEND - RECEIVING WALLET"); @@ -170,7 +170,7 @@ fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // check wallet 2's version wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { - let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; assert!(refreshed); for tx in txs { stored_excess = tx.kernel_excess; @@ -184,7 +184,7 @@ fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Refresh and check transaction log for wallet 1 wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { - let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; assert!(refreshed); for tx in txs { println!("Wallet 1: {:?}", tx); diff --git a/controller/tests/payment_proofs.rs b/controller/tests/payment_proofs.rs index 92f919e3..edb416f3 100644 --- a/controller/tests/payment_proofs.rs +++ b/controller/tests/payment_proofs.rs @@ -116,7 +116,7 @@ fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err sender_api.tx_lock_outputs(m, &slate)?; // Ensure what's stored in TX log for payment proof is correct - let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; assert!(txs[0].payment_proof.is_some()); let pp = txs[0].clone().payment_proof.unwrap(); assert_eq!( diff --git a/controller/tests/repost.rs b/controller/tests/repost.rs index b0aef3a8..283164cf 100644 --- a/controller/tests/repost.rs +++ b/controller/tests/repost.rs @@ -154,7 +154,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // Now repost from cached wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { - let (_, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; println!("TXS[0]: {:?}", txs[0]); let stored_tx = api.get_stored_tx(m, None, Some(&txs[0].tx_slate_id.unwrap()))?; println!("Stored tx: {:?}", stored_tx); @@ -224,7 +224,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // Now repost from cached wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { - let (_, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; let stored_tx_slate = api.get_stored_tx(m, Some(txs[0].id), None)?.unwrap(); api.post_tx(m, &stored_tx_slate, false)?; bh += 1; diff --git a/controller/tests/revert.rs b/controller/tests/revert.rs index 57e73ad1..ea6c0b2a 100644 --- a/controller/tests/revert.rs +++ b/controller/tests/revert.rs @@ -133,7 +133,7 @@ fn revert( assert_eq!(info.amount_currently_spendable, (bh - cm) * reward); assert_eq!(info.amount_reverted, 0); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; let (c, _) = libwallet::TxLogEntry::sum_confirmed(&txs); assert_eq!(info.total, c); assert_eq!(txs.len(), bh as usize); @@ -148,7 +148,7 @@ fn revert( assert_eq!(info.amount_currently_spendable, 0); assert_eq!(info.amount_reverted, 0); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 0); Ok(()) })?; @@ -188,7 +188,7 @@ fn revert( assert_eq!(info.amount_currently_spendable, 0); assert_eq!(info.amount_reverted, 0); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 1); let tx = &txs[0]; assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceived); @@ -230,7 +230,7 @@ fn revert( assert_eq!(info.amount_currently_spendable, sent); assert_eq!(info.amount_reverted, 0); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 1); let tx = &txs[0]; assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceived); @@ -266,7 +266,7 @@ fn revert( assert_eq!(info.amount_currently_spendable, 0); assert_eq!(info.amount_reverted, sent); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 1); let tx = &txs[0]; assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReverted); @@ -300,7 +300,7 @@ fn revert_reconfirm_impl(test_dir: &'static str) -> Result<(), libwallet::Error> assert_eq!(info.amount_currently_spendable, sent); assert_eq!(info.amount_reverted, 0); // check tx log as well - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 1); let tx = &txs[0]; assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceived); @@ -329,7 +329,7 @@ fn revert_cancel_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(info.amount_currently_spendable, 0); assert_eq!(info.amount_reverted, sent); - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 1); let tx = &txs[0]; @@ -345,7 +345,7 @@ fn revert_cancel_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert_eq!(info.amount_reverted, 0); // Check updated tx log - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; assert_eq!(txs.len(), 1); let tx = &txs[0]; assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceivedCancelled); diff --git a/controller/tests/transaction.rs b/controller/tests/transaction.rs index 0293555b..fa34014b 100644 --- a/controller/tests/transaction.rs +++ b/controller/tests/transaction.rs @@ -145,7 +145,7 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> // Check transaction log for wallet 1 wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); let fee = core::libtx::tx_fee( wallet1_info.last_confirmed_height as usize - cm as usize, @@ -166,7 +166,7 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> // Check transaction log for wallet 2 wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); // we should have a transaction entry for this slate let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); @@ -211,7 +211,7 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> assert_eq!(wallet1_info.amount_immature, cm * reward + fee); // check tx log entry is confirmed - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); assert!(tx.is_some()); @@ -247,7 +247,7 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> assert_eq!(wallet2_info.amount_currently_spendable, amount); // check tx log entry is confirmed - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); assert!(tx.is_some()); @@ -315,7 +315,7 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { let (refreshed, _wallet1_info) = sender_api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); - let (_, txs) = sender_api.retrieve_txs(m, true, None, None)?; + let (_, txs) = sender_api.retrieve_txs(m, true, None, None, None)?; // find the transaction let tx = txs .iter() @@ -344,7 +344,7 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> assert_eq!(wallet2_info.amount_currently_spendable, amount * 3); // check tx log entry is confirmed - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); assert!(tx.is_some()); @@ -474,7 +474,7 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { wallet1_info.last_confirmed_height ); assert!(refreshed); - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, 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()); @@ -499,7 +499,7 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { // Check transaction log for wallet 2 wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); let mut unconfirmed_count = 0; let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); @@ -529,7 +529,7 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { // can't roll back coinbase let res = api.cancel_tx(m, Some(1), None); assert!(res.is_err()); - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; let tx = txs .iter() .find(|t| t.tx_slate_id == Some(slate.id)) @@ -556,7 +556,7 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { // Wallet 2 rolls back wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; let tx = txs .iter() .find(|t| t.tx_slate_id == Some(slate.id)) diff --git a/controller/tests/ttl_cutoff.rs b/controller/tests/ttl_cutoff.rs index 47c66e9c..ec4340bc 100644 --- a/controller/tests/ttl_cutoff.rs +++ b/controller/tests/ttl_cutoff.rs @@ -95,7 +95,7 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; sender_api.tx_lock_outputs(m, &slate)?; - let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; let tx = txs[0].clone(); assert_eq!(tx.ttl_cutoff_height, Some(12)); @@ -106,7 +106,7 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 2, false); wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { - let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; let tx = txs[0].clone(); assert_eq!(tx.ttl_cutoff_height, Some(12)); @@ -116,7 +116,7 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // Should also be gone in wallet 2, and output gone wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |sender_api, m| { - let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; let tx = txs[0].clone(); let outputs = sender_api.retrieve_outputs(m, false, true, None)?.1; assert_eq!(outputs.len(), 0); @@ -144,7 +144,7 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> sender_api.tx_lock_outputs(m, &slate_i)?; slate = slate_i; - let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; let tx = txs[0].clone(); assert_eq!(tx.ttl_cutoff_height, Some(14)); @@ -156,7 +156,7 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // Wallet 2 will need to have updated past the TTL wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |sender_api, m| { - let (_, _) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + let (_, _) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; Ok(()) })?; diff --git a/controller/tests/tx_list_filter.rs b/controller/tests/tx_list_filter.rs new file mode 100644 index 00000000..7a8dab03 --- /dev/null +++ b/controller/tests/tx_list_filter.rs @@ -0,0 +1,360 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests of advanced TX filtering + +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use libwallet::{RetrieveTxQueryArgs, RetrieveTxQuerySortField}; + +use self::libwallet::{InitTxArgs, Slate}; +use impls::test_framework::{self, LocalWalletClient}; +use std::sync::{atomic::Ordering, Arc}; +use std::thread; +use std::time::Duration; +use util::secp::key::SecretKey; +use util::Mutex; + +use self::keychain::ExtKeychain; +use self::libwallet::WalletInst; +use impls::DefaultLCProvider; + +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +fn test_wallet_tx_filtering( + wallet: Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + mask: Option<&SecretKey>, +) -> Result<(), libwallet::Error> { + wallet::controller::owner_single_use(Some(wallet.clone()), mask, None, |api, _m| { + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_id = Some(5); + + // Min ID + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results[0].id, 5); + assert_eq!(tx_results[tx_results.len() - 1].id, 33); + + // Max ID + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(20); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results[0].id, 5); + assert_eq!(tx_results[tx_results.len() - 1].id, 20); + + // Exclude 1 cancelled + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 28); + + // Exclude 1 cancelled, show confirmed only + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(true); + tx_query_args.include_confirmed_only = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 14); + + // show outstanding only (including cancelled) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(false); + tx_query_args.include_outstanding_only = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 15); + + // outstanding only and confirmed only should give empty set + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(false); + tx_query_args.include_outstanding_only = Some(true); + tx_query_args.include_confirmed_only = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 0); + + // include sent only + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_sent_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 15); + + // include received only (none in this set) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_received_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 0); + + // include reverted only (none in this set) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_reverted_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 0); + + // include coinbase only + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_coinbase_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 19); + + // Amounts + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_amount = Some(60_000_000_000 - 59_963_300_000); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 27); + + // amount, should see as above with coinbases excluded + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_amount = Some(60_000_000_000 - 59_963_300_000); + tx_query_args.max_amount = Some(60_000_000_000 - 1); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 8); + + // Amount - should only see coinbase (incoming) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_amount = Some(60_000_000_000); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 19); + + // sort order + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.sort_order = Some(libwallet::RetrieveTxQuerySortOrder::Desc); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + + assert_eq!(tx_results[0].id, 33); + assert_eq!(tx_results[tx_results.len() - 1].id, 0); + + // change sort field to amount desc, should have coinbases first + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.sort_order = Some(libwallet::RetrieveTxQuerySortOrder::Desc); + tx_query_args.sort_field = Some(RetrieveTxQuerySortField::TotalAmount); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results[0].amount_credited, 60_000_000_000); + + /*for entry in tx_results.iter() { + println!("{:?}", entry); + }*/ + + Ok(()) + })?; + Ok(()) +} + +/// Builds a wallet + chain with a few transactions, and return wallet for further testing +fn build_chain_for_tx_filtering( + test_dir: &'static str, + block_height: usize, +) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + debug!("Mask1: {:?}", mask1); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + debug!("Mask2: {:?}", mask2); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Stop the scanning updater threads because it extends the time needed to build the chain + // exponentially + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, _m| { + api.stop_updater()?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, _m| { + api.stop_updater()?; + Ok(()) + })?; + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // Start off with a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + + for i in 0..block_height { + let mut wallet_1_has_funds = false; + + // Check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + debug!( + "Wallet 1 spendable - {}", + wallet1_info.amount_currently_spendable + ); + if wallet1_info.amount_currently_spendable > reward { + wallet_1_has_funds = true; + } + Ok(()) + })?; + + if !wallet_1_has_funds { + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 1, false); + continue; + } + + // send a random tx + let num_txs = 1; + for _ in 0..num_txs { + let amount: u64 = i as u64 * 1_000_000; + let mut slate = Slate::blank(1, false); + debug!("Creating TX for {}", amount); + wallet::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + slate = sender_api.finalize_tx(m, &slate)?; + Ok(()) + }, + )?; + } + } + + // Cancel a tx for filtering testing + let amount: u64 = 1_000_000; + let mut slate = Slate::blank(1, false); + debug!("Creating TX for {}", amount); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + sender_api.cancel_tx(m, Some(33), None)?; + Ok(()) + })?; + + // Perform actual testing + test_wallet_tx_filtering(wallet1, mask1)?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_tx_filtering() { + let test_dir = "test_output/advanced_tx_filtering"; + clean_output_dir(test_dir); + setup(test_dir); + if let Err(e) = build_chain_for_tx_filtering(test_dir, 30) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index e4762260..05a16627 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -34,6 +34,7 @@ curve25519-dalek = "2.1" secrecy = "0.6" bech32 = "0.7" byteorder = "1.3" +num-bigint = "0.2" grin_wallet_util = { path = "../util", version = "5.2.0-alpha.1" } grin_wallet_config = { path = "../config", version = "5.2.0-alpha.1" } diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 7a21075c..be08950f 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -82,6 +82,7 @@ where &mut *w, None, Some(ret_slate.id), + None, Some(&parent_key_id), use_test_rng, )?; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 96cbfdc7..0ab87e47 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -33,8 +33,9 @@ use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, Walle use crate::Error; use crate::{ address, wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, - OutputCommitMapping, PaymentProof, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, - SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus, WalletInst, WalletLCProvider, + OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack, + SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus, + WalletInst, WalletLCProvider, }; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; @@ -313,6 +314,7 @@ pub fn retrieve_txs<'a, L, C, K>( refresh_from_node: bool, tx_id: Option, tx_slate_id: Option, + query_args: Option, ) -> Result<(bool, Vec), Error> where L: WalletLCProvider<'a, C, K>, @@ -332,7 +334,14 @@ where wallet_lock!(wallet_inst, w); let parent_key_id = w.parent_key_id(); - let txs = updater::retrieve_txs(&mut **w, tx_id, tx_slate_id, Some(&parent_key_id), false)?; + let txs = updater::retrieve_txs( + &mut **w, + tx_id, + tx_slate_id, + query_args, + Some(&parent_key_id), + false, + )?; Ok((validated, txs)) } @@ -403,6 +412,7 @@ where refresh_from_node, tx_id, tx_slate_id, + None, )?; if txs.1.len() != 1 { return Err(Error::PaymentProofRetrieval( @@ -661,6 +671,7 @@ where &mut *w, None, Some(ret_slate.id), + None, Some(&parent_key_id), use_test_rng, )?; @@ -1121,7 +1132,7 @@ where // Step 2: Update outstanding transactions with no change outputs by kernel let mut txs = { wallet_lock!(wallet_inst, w); - updater::retrieve_txs(&mut **w, None, None, Some(&parent_key_id), true)? + updater::retrieve_txs(&mut **w, None, None, None, Some(&parent_key_id), true)? }; result = update_txs_via_kernel(wallet_inst.clone(), keychain_mask, &mut txs)?; if !result { diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index f54102d7..f50bfc48 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -23,6 +23,7 @@ use crate::slate_versions::SlateVersion; use crate::types::OutputData; use crate::SlatepackAddress; +use chrono::prelude::*; use ed25519_dalek::Signature as DalekSignature; /// Type for storing amounts (in nanogrins). @@ -148,6 +149,107 @@ impl Default for IssueInvoiceTxArgs { } } +/// Sort tx retrieval order +#[derive(Clone, Serialize, Deserialize)] +pub enum RetrieveTxQuerySortOrder { + /// Ascending + Asc, + /// Descending + Desc, +} + +/// Valid sort fields for a transaction list retrieval query +#[derive(Clone, Serialize, Deserialize)] +pub enum RetrieveTxQuerySortField { + /// Transaction Id + Id, + /// Creation Timestamp + CreationTimestamp, + /// Confirmation Timestamp + ConfirmationTimestamp, + /// TotalAmount (AmountCredited-AmountDebited) + TotalAmount, + /// Amount Credited + AmountCredited, + /// Amount Debited + AmountDebited, +} + +/// Retrieve Transaction List Pagination Arguments +#[derive(Clone, Serialize, Deserialize)] +pub struct RetrieveTxQueryArgs { + /// Retrieve transactions with an id higher than or equal to the given + /// If None, consider items from the first transaction and later + pub min_id: Option, + /// Retrieve tranactions with an id less than or equal to the given + /// If None, consider items from the last transaction and earlier + pub max_id: Option, + /// The maximum number of transactions to return + /// if both `before_id_inc` and `after_id_inc` are supplied, this will apply + /// to the before and earlier set + pub limit: Option, + /// whether to exclude cancelled transactions in the returned set + pub exclude_cancelled: Option, + /// whether to only consider outstanding transactions + pub include_outstanding_only: Option, + /// whether to only consider confirmed-only transactions + pub include_confirmed_only: Option, + /// whether to only consider sent transactions + pub include_sent_only: Option, + /// whether to only consider received transactions + pub include_received_only: Option, + /// whether to only consider coinbase transactions + pub include_coinbase_only: Option, + /// whether to only consider reverted transactions + pub include_reverted_only: Option, + /// lower bound on the total amount (amount_credited - amount_debited), inclusive + #[serde(with = "secp_ser::opt_string_or_u64")] + #[serde(default)] + pub min_amount: Option, + /// higher bound on the total amount (amount_credited - amount_debited), inclusive + #[serde(with = "secp_ser::opt_string_or_u64")] + #[serde(default)] + pub max_amount: Option, + /// lower bound on the creation timestamp, inclusive + pub min_creation_timestamp: Option>, + /// higher bound on on the creation timestamp, inclusive + pub max_creation_timestamp: Option>, + /// lower bound on the confirmation timestamp, inclusive + pub min_confirmed_timestamp: Option>, + /// higher bound on the confirmation timestamp, inclusive + pub max_confirmed_timestamp: Option>, + /// Field within the tranasction list on which to sort + /// defaults to ID if not present + pub sort_field: Option, + /// Sort order, defaults to ASC if not present (earliest is first) + pub sort_order: Option, +} + +impl Default for RetrieveTxQueryArgs { + fn default() -> Self { + Self { + min_id: None, + max_id: None, + limit: None, + exclude_cancelled: Some(false), + include_outstanding_only: Some(false), + include_confirmed_only: Some(false), + include_sent_only: Some(false), + include_received_only: Some(false), + include_coinbase_only: Some(false), + include_reverted_only: Some(false), + min_amount: None, + max_amount: None, + min_creation_timestamp: None, + max_creation_timestamp: None, + min_confirmed_timestamp: None, + max_confirmed_timestamp: None, + sort_field: Some(RetrieveTxQuerySortField::Id), + sort_order: Some(RetrieveTxQuerySortOrder::Asc), + } + } +} + /// Fees in block to use for coinbase amount calculation #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BlockFees { diff --git a/libwallet/src/internal/scan.rs b/libwallet/src/internal/scan.rs index f8d9f7f1..129e99ca 100644 --- a/libwallet/src/internal/scan.rs +++ b/libwallet/src/internal/scan.rs @@ -378,6 +378,7 @@ where &mut **w, output.tx_log_entry, None, + None, Some(&parent_key_id), false, )?; diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index a4a40c3e..a5dde1aa 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -351,7 +351,14 @@ where } else if let Some(tx_slate_id) = tx_slate_id { tx_id_string = tx_slate_id.to_string(); } - let tx_vec = updater::retrieve_txs(wallet, tx_id, tx_slate_id, Some(&parent_key_id), false)?; + let tx_vec = updater::retrieve_txs( + wallet, + tx_id, + tx_slate_id, + None, + Some(&parent_key_id), + false, + )?; if tx_vec.len() != 1 { return Err(Error::TransactionDoesntExist(tx_id_string)); } @@ -390,7 +397,7 @@ where K: Keychain + 'a, { // finalize command - let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, false)?; + let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, None, false)?; let mut tx = None; // don't want to assume this is the right tx, in case of self-sending for t in tx_vec { @@ -511,7 +518,14 @@ where C: NodeClient + 'a, K: Keychain + 'a, { - let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), Some(parent_key_id), false)?; + let tx_vec = updater::retrieve_txs( + wallet, + None, + Some(slate.id), + None, + Some(parent_key_id), + false, + )?; if tx_vec.is_empty() { return Err(Error::PaymentProof( "TxLogEntry with original proof info not found (is account correct?)".to_owned(), diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs index da2209e3..a9958dec 100644 --- a/libwallet/src/internal/updater.rs +++ b/libwallet/src/internal/updater.rs @@ -33,7 +33,12 @@ use crate::internal::keys; use crate::types::{ NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo, }; -use crate::{BlockFees, CbData, OutputCommitMapping}; +use crate::{ + BlockFees, CbData, OutputCommitMapping, RetrieveTxQueryArgs, RetrieveTxQuerySortField, + RetrieveTxQuerySortOrder, +}; + +use num_bigint::BigInt; /// Retrieve all of the outputs (doesn't attempt to update from node) pub fn retrieve_outputs<'a, T: ?Sized, C, K>( @@ -88,12 +93,244 @@ where Ok(res) } +/// Apply advanced filtering to resultset from retrieve_txs below +pub fn apply_advanced_tx_list_filtering<'a, T: ?Sized, C, K>( + wallet: &mut T, + query_args: &RetrieveTxQueryArgs, +) -> Vec +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Apply simple bool, GTE or LTE fields + let txs_iter: Box> = Box::new( + wallet + .tx_log_iter() + .filter(|tx_entry| { + if let Some(v) = query_args.exclude_cancelled { + if v { + tx_entry.tx_type != TxLogEntryType::TxReceivedCancelled + && tx_entry.tx_type != TxLogEntryType::TxSentCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_outstanding_only { + if v { + !tx_entry.confirmed + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_confirmed_only { + if v { + tx_entry.confirmed + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_sent_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_received_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxReceivedCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_coinbase_only { + if v { + tx_entry.tx_type == TxLogEntryType::ConfirmedCoinbase + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_reverted_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReverted + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_id { + tx_entry.id >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_id { + tx_entry.id <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) + - BigInt::from(tx_entry.amount_credited) + >= BigInt::from(v) + } else { + BigInt::from(tx_entry.amount_credited) + - BigInt::from(tx_entry.amount_debited) + >= BigInt::from(v) + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) + - BigInt::from(tx_entry.amount_credited) + <= BigInt::from(v) + } else { + BigInt::from(tx_entry.amount_credited) + - BigInt::from(tx_entry.amount_debited) + <= BigInt::from(v) + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_creation_timestamp { + tx_entry.creation_ts >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + tx_entry.creation_ts <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t >= v + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t <= v + } else { + true + } + } else { + true + } + }), + ); + + let mut return_txs: Vec = txs_iter.collect(); + + // Now apply requested sorting + if let Some(ref s) = query_args.sort_field { + match s { + RetrieveTxQuerySortField::Id => { + return_txs.sort_by_key(|tx| tx.id); + } + RetrieveTxQuerySortField::CreationTimestamp => { + return_txs.sort_by_key(|tx| tx.creation_ts); + } + RetrieveTxQuerySortField::ConfirmationTimestamp => { + return_txs.sort_by_key(|tx| tx.confirmation_ts); + } + RetrieveTxQuerySortField::TotalAmount => { + return_txs.sort_by_key(|tx| { + if tx.tx_type == TxLogEntryType::TxSent + || tx.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx.amount_debited) - BigInt::from(tx.amount_credited) + } else { + BigInt::from(tx.amount_credited) - BigInt::from(tx.amount_debited) + } + }); + } + RetrieveTxQuerySortField::AmountCredited => { + return_txs.sort_by_key(|tx| tx.amount_credited); + } + RetrieveTxQuerySortField::AmountDebited => { + return_txs.sort_by_key(|tx| tx.amount_debited); + } + } + } else { + return_txs.sort_by_key(|tx| tx.id); + } + + if let Some(ref s) = query_args.sort_order { + match s { + RetrieveTxQuerySortOrder::Desc => return_txs.reverse(), + _ => {} + } + } + + // Apply limit if requested + if let Some(l) = query_args.limit { + return_txs = return_txs.into_iter().take(l as usize).collect() + } + + return_txs +} + /// 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<'a, T: ?Sized, C, K>( wallet: &mut T, tx_id: Option, tx_slate_id: Option, + query_args: Option, parent_key_id: Option<&Identifier>, outstanding_only: bool, ) -> Result, Error> @@ -102,34 +339,41 @@ where C: NodeClient + 'a, K: Keychain + 'a, { - let mut txs: Vec = wallet - .tx_log_iter() - .filter(|tx_entry| { - let f_pk = match parent_key_id { - Some(k) => tx_entry.parent_key_id == *k, - None => true, - }; - let f_tx_id = match tx_id { - Some(i) => tx_entry.id == i, - None => true, - }; - let f_txs = match tx_slate_id { - Some(t) => tx_entry.tx_slate_id == Some(t), - None => true, - }; - let f_outstanding = match outstanding_only { - true => { - !tx_entry.confirmed - && (tx_entry.tx_type == TxLogEntryType::TxReceived - || tx_entry.tx_type == TxLogEntryType::TxSent - || tx_entry.tx_type == TxLogEntryType::TxReverted) - } - false => true, - }; - f_pk && f_tx_id && f_txs && f_outstanding - }) - .collect(); - txs.sort_by_key(|tx| tx.creation_ts); + let mut txs; + // Adding in new transaction list query logic. If `tx_id` or `tx_slate_id` + // is provided, then `query_args` is ignored and old logic is followed. + if query_args.is_some() && tx_id.is_none() && tx_slate_id.is_none() { + txs = apply_advanced_tx_list_filtering(wallet, &query_args.unwrap()) + } else { + txs = wallet + .tx_log_iter() + .filter(|tx_entry| { + let f_pk = match parent_key_id { + Some(k) => tx_entry.parent_key_id == *k, + None => true, + }; + let f_tx_id = match tx_id { + Some(i) => tx_entry.id == i, + None => true, + }; + let f_txs = match tx_slate_id { + Some(t) => tx_entry.tx_slate_id == Some(t), + None => true, + }; + let f_outstanding = match outstanding_only { + true => { + !tx_entry.confirmed + && (tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxReverted) + } + false => true, + }; + f_pk && f_tx_id && f_txs && f_outstanding + }) + .collect(); + txs.sort_by_key(|tx| tx.creation_ts); + } Ok(txs) } @@ -171,7 +415,7 @@ where .filter(|x| x.root_key_id == *parent_key_id && x.status != OutputStatus::Spent) .collect(); - let tx_entries = retrieve_txs(wallet, None, None, Some(&parent_key_id), true)?; + let tx_entries = retrieve_txs(wallet, None, None, None, Some(&parent_key_id), true)?; // Only select outputs that are actually involved in an outstanding transaction let unspents = match update_all { diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 9da67f64..3c663a9c 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -65,7 +65,8 @@ pub use crate::slatepack::{ pub use api_impl::owner_updater::StatusMessage; pub use api_impl::types::{ Amount, BlockFees, BuiltOutput, InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs, - NodeHeightResult, OutputCommitMapping, PaymentProof, VersionInfo, + NodeHeightResult, OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, + RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, VersionInfo, }; pub use internal::scan::scan; pub use slate_versions::ser as dalek_ser; diff --git a/tests/cmd_line_basic.rs b/tests/cmd_line_basic.rs index 56ecf2d0..bce4a8d0 100644 --- a/tests/cmd_line_basic.rs +++ b/tests/cmd_line_basic.rs @@ -238,7 +238,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: None, |api, m| { api.set_active_account(m, "mining")?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); assert_eq!(txs.len(), bh as usize); for t in txs { @@ -436,7 +436,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: None, |api, m| { api.set_active_account(m, "mining")?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); assert_eq!(txs.len(), bh as usize); Ok(()) @@ -509,7 +509,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: None, |api, m| { api.set_active_account(m, "mining")?; - let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); assert_eq!(txs.len(), bh as usize + 1); Ok(()) @@ -634,7 +634,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: None, |api, m| { api.set_active_account(m, "default")?; - let (_, txs) = api.retrieve_txs(m, true, None, None)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; let some_tx_id = txs[0].tx_slate_id.clone(); assert!(some_tx_id.is_some()); tx_id = some_tx_id.unwrap().to_hyphenated().to_string().clone();