From 806aa985d72b24e6dda7f64808edf09b8ee0f37c Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Wed, 23 Nov 2022 15:36:42 +0000 Subject: [PATCH] added transaction filtering implementation, addition of internal libwallet test to be filled out --- controller/tests/tx_list.rs | 197 ++++++++++++++++++++++++++++++ libwallet/src/api_impl/types.rs | 26 ++-- libwallet/src/internal/updater.rs | 173 +++++++++++++++++++++++++- 3 files changed, 378 insertions(+), 18 deletions(-) create mode 100644 controller/tests/tx_list.rs diff --git a/controller/tests/tx_list.rs b/controller/tests/tx_list.rs new file mode 100644 index 00000000..a227bdaa --- /dev/null +++ b/controller/tests/tx_list.rs @@ -0,0 +1,197 @@ +// 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 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 tx_results = api.retrieve_txs(mask, true, None, None, None)?.1; + 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(()) + }, + )?; + } + } + + // 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/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index 2cc81867..ce7221af 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -178,19 +178,19 @@ pub enum RetrieveTxQuerySortField { /// Retrieve Transaction List Pagination Arguments #[derive(Clone, Serialize, Deserialize)] pub struct RetrieveTxQueryArgs { - /// Retrieve transactions with an id lower than the given, inclusive - /// If None, consider items from the latest transaction and earlier - pub before_id_inc: Option, - /// Retrieve tranactions with an id higher than the given, inclusive + /// Retrieve transactions with an id higher than or equal to the given /// If None, consider items from the first transaction and later - pub after_id_inc: Option, + pub min_id_inc: 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_inc: 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 include cancelled transactions in the returned set - pub include_cancelled: Option, - /// whether to only consider non-cancelled, outstanding transactions + /// 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, @@ -209,17 +209,17 @@ pub struct RetrieveTxQueryArgs { /// Field within the tranasction list on which to sort /// defaults to ID if not present pub sort_field: Option, - /// Sort order, defaults to DESC if not present (most recent is first) + /// Sort order, defaults to ASC if not present (earliest is first) pub sort_order: Option, } impl Default for RetrieveTxQueryArgs { fn default() -> Self { Self { - before_id_inc: None, - after_id_inc: None, + min_id_inc: None, + max_id_inc: None, limit: None, - include_cancelled: Some(true), + exclude_cancelled: Some(false), include_outstanding_only: Some(false), include_confirmed_only: Some(false), min_amount_inc: None, @@ -229,7 +229,7 @@ impl Default for RetrieveTxQueryArgs { min_confirmed_timestamp_inc: None, max_confirmed_timestamp_inc: None, sort_field: Some(RetrieveTxQuerySortField::Id), - sort_order: Some(RetrieveTxQuerySortOrder::Desc), + sort_order: Some(RetrieveTxQuerySortOrder::Asc), } } } diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs index eb93ee3a..ff8db662 100644 --- a/libwallet/src/internal/updater.rs +++ b/libwallet/src/internal/updater.rs @@ -25,7 +25,6 @@ use crate::grin_core::global; use crate::grin_core::libtx::proof::ProofBuilder; use crate::grin_core::libtx::reward; use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; -use crate::grin_util as util; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; use crate::grin_util::static_secp_instance; @@ -33,7 +32,11 @@ use crate::internal::keys; use crate::types::{ NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo, }; -use crate::{BlockFees, CbData, OutputCommitMapping, RetrieveTxQueryArgs}; +use crate::{grin_util as util, Amount}; +use crate::{ + BlockFees, CbData, OutputCommitMapping, RetrieveTxQueryArgs, RetrieveTxQuerySortField, + RetrieveTxQuerySortOrder, +}; /// Retrieve all of the outputs (doesn't attempt to update from node) pub fn retrieve_outputs<'a, T: ?Sized, C, K>( @@ -88,6 +91,166 @@ 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 mut 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 + && (tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxReverted) + } 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.min_id_inc { + tx_entry.id >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_id_inc { + tx_entry.id <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_amount_inc { + v >= tx_entry.amount_credited - tx_entry.amount_debited + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_amount_inc { + v <= tx_entry.amount_credited - tx_entry.amount_debited + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_creation_timestamp_inc { + tx_entry.creation_ts >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp_inc { + tx_entry.creation_ts <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp_inc { + 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_inc { + if let Some(t) = tx_entry.confirmation_ts { + t <= v + } else { + true + } + } else { + true + } + }), + ); + + // Apply limit if requested + if let Some(l) = query_args.limit { + txs_iter = Box::new(txs_iter.take(l as usize)); + } + + 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| tx.amount_credited - 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(), + _ => {} + } + } + + 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>( @@ -106,7 +269,9 @@ where let mut txs: Vec = vec![]; // Adding in new tranasction list query logic. If `tx_id` or `tx_slate_id` // is provided, then `query_args` is ignored and old logic is followed. - if tx_id.is_some() || tx_slate_id.is_some() { + 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| { @@ -135,8 +300,6 @@ where }) .collect(); txs.sort_by_key(|tx| tx.creation_ts); - } else { - // TODO: Call Query Filter Function } Ok(txs) }