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/controller/tests/tx_list_filter.rs b/controller/tests/tx_list_filter.rs index a227bdaa..4a8676f3 100644 --- a/controller/tests/tx_list_filter.rs +++ b/controller/tests/tx_list_filter.rs @@ -22,6 +22,7 @@ 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}; @@ -54,10 +55,150 @@ fn test_wallet_tx_filtering( mask: Option<&SecretKey>, ) -> Result<(), libwallet::Error> { wallet::controller::owner_single_use(Some(wallet.clone()), mask, None, |api, _m| { - let tx_results = api.retrieve_txs(mask, true, None, None, None)?.1; - for entry in tx_results.iter() { + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_id_inc = 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_inc = Some(5); + tx_query_args.max_id_inc = 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_inc = Some(5); + tx_query_args.max_id_inc = 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_inc = Some(5); + tx_query_args.max_id_inc = 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_inc = Some(5); + tx_query_args.max_id_inc = 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_inc = Some(5); + tx_query_args.max_id_inc = 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_inc = 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_inc = Some(60_000_000_000 - 59_963_300_000); + tx_query_args.max_amount_inc = 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_inc = 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(()) @@ -176,6 +317,28 @@ fn build_chain_for_tx_filtering( } } + // 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)?; 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/types.rs b/libwallet/src/api_impl/types.rs index ce7221af..213700e1 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -194,6 +194,14 @@ pub struct RetrieveTxQueryArgs { 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 pub min_amount_inc: Option, /// higher bound on the total amount (amount_credited - amount_debited), inclusive @@ -222,6 +230,10 @@ impl Default for RetrieveTxQueryArgs { 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_inc: None, max_amount_inc: None, min_creation_timestamp_inc: None, diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs index ff8db662..e9aea59d 100644 --- a/libwallet/src/internal/updater.rs +++ b/libwallet/src/internal/updater.rs @@ -38,6 +38,8 @@ use crate::{ 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>( wallet: &mut T, @@ -121,9 +123,6 @@ where if let Some(v) = query_args.include_outstanding_only { if v { !tx_entry.confirmed - && (tx_entry.tx_type == TxLogEntryType::TxReceived - || tx_entry.tx_type == TxLogEntryType::TxSent - || tx_entry.tx_type == TxLogEntryType::TxReverted) } else { true } @@ -142,6 +141,52 @@ where 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_inc { tx_entry.id >= v @@ -158,14 +203,34 @@ where }) .filter(|tx_entry| { if let Some(v) = query_args.min_amount_inc { - v >= tx_entry.amount_credited - tx_entry.amount_debited + 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_inc { - v <= tx_entry.amount_credited - tx_entry.amount_debited + 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 } @@ -228,7 +293,15 @@ where return_txs.sort_by_key(|tx| tx.confirmation_ts); } RetrieveTxQuerySortField::TotalAmount => { - return_txs.sort_by_key(|tx| tx.amount_credited - tx.amount_debited); + 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);