From e3810c1b20d1f74a469e8d99cba41880db4c9608 Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta <32920299+utkarshg6@users.noreply.github.com> Date: Tue, 24 Jan 2023 13:17:27 +0530 Subject: [PATCH] GH-611: Accountant Overlapping Scans (#176) * GH-611: add test to scan for payables in case flag is false * GH-611: add one more test and todos for Scanner * GH-611: change Scanner from a trait to struct * GH-611: use RefCell to eliminate the problem with mutable closure inside tools.rs * GH-611: remove redundant fields from tools.rs * GH-611: migrate flag from Accountant to Scanner * GH-611: fix test accountant_have_proper_defaulted_values * GH-611: rename items in tools.rs * GH-611: add functions to update flag for is_scan_running * GH-611: add a TODO for verifying that scanners are defaulted properly * GH-611: throw error when scan is already running. * GH-611: add a todo to handle the case when UI triggers a scan, but the scan is already running * GH-611: allow scan is running error logs to print the scan type * GH-611: write logs whenever a new scan is requested but scan is already running * GH-611: rename the module to scanners * GH-611: use timestamp instead of boolean flag for marking whether a scan is running * GH-611: change the logs to include timestamp in case scan is already running * GH-611: add todos for ending scans for PendingPayables * GH-611: use a RefCell to update the initiated_at variable * GH-611: add a test for testing whether scan has started * GH-611: use mark_as_started() in the scan() * GH-611: end the scan for payables at handle_sent_payable() * GH-611: allow blockchain bridge to send ReportTransactionReceipts with an empty vector * GH-611: disable starting and ending the scans to fix the tests in accountant/mod.rs * GH-611: remove compiler warnings * GH-611: scanners struct can be constructed with the respective scanners * GH-611: eliminate the BeginMessageWrapper * GH-611: modify PayableDAOMock * GH-611: modify PendingPayableDaoFactoryMock and ReceivableDaoFactoryMock * GH-611: supply DAOs for Scanner inside the tests of accountant/mod.rs * GH-611: introduce scanner mock * GH-611: modify ScannerMock and replace NullScanner with ScannerMock * GH-611: an attempt to migrate the contents of scan_for_payables() inside PayableScanner * GH-611: remove ctx and comment out code * GH-611: remove the commented out code * GH-611: add a todo!() inside payable_exceeded_threshold() to generate a successful build * GH-611: write test for payable_thresholds_real * GH-611: get rid of copy from structs in accountant.rs * GH-611: pull out payment_thresholds out of accountant_config and distribute a reference(counted) to individual scanners * GH-611: refactor tools for Payable Scanner * GH-611: migrate the payable scanner tools to the tools.rs * GH-611: Add a todo and comment out the test * GH-611: refactor tools.rs * GH-611: fix qualified_payables_and_summary() * GH-611: test drive the checks for whether a payable is qualified * GH-611: migrate test for testing debt extremes inside tools.rs * GH-611: refactor the investigate_debt_extremes() * GH-611: migrate tools for payable_scanner inside a different module * GH-611: add test for payable_scanner for initiating a scan * GH-611: add tests for pending payable initating a scan * GH-611: extend tests to assert for log messages * GH-611: migrate the scan_for_receivables to the begin_scan() * GH-611: add test for scanning for delinquency * GH-611: remove referenced counter from the payable_scanner_tools * GH-611: remove some import warnings * GH-611: generate timestamp inside begin_scan() * GH-611: modify BannedDaoFactoryMock * GH-611: fix test accountant_sends_report_accounts_payable_to_blockchain_bridge_when_qualified_payable_found * GH-611: fix the test accountant_sends_a_request_to_blockchain_bridge_to_scan_for_received_payments * GH-611: fix test scan_for_pending_payable_found_unresolved_pending_payable_and_urges_their_processing * GH-611: remove the code from accountant/mod.rs that has been moved to scanners.rs * GH-611: add ScannerError * GH-611: fix accountant_scans_after_startup * GH-611: refactor handlers for scan requests in accountant/mod.rs * GH-611: fix more tests inside accountant/mod.rs * GH-611: reorder dao in tests for Accountant and Scanner, and replace ScannerMock with NullScanner * GH-611: use the timestamp from the function parameter for scanners * GH-611: refactor the handlers for scan requests * GH-611: fix tests related to externally triggered scan * GH-611: fix test accountant_payable_scan_timer_triggers_periodical_scanning_for_payables * GH-611: fix test periodical_scanning_for_pending_payable_works * GH-611: fix periodical_scanning_for_receivables_and_delinquencies_works * GH-611: begin_scan() updates the timestamp * GH-611: remove unnecessary test * GH-611: throw error in case scan is already running * GH-611: handle error message ScanAlreadyRunning * GH-611: fix tests for when the scan is already running * GH-611: remove commented code and change the test name to periodical_scanning_for_payable_works * GH-611: remove warnings * GH-611: add timestamp as a paramneter in the function investigate_debt_extremes() * Test is passing * GH-611: remove the warnings * GH-611: refactor test scan_for_payable_message_triggers_payment_for_balances_over_the_curve and update recorder.rs * GH-611: decouple pending payable and payable daos inside test pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled * GH-611: provide correct DAOs to the Accountant after migrating handle_sent_payable() to end_scan() inside the PayableScanner * GH-611: fix tests in accountant to call scan_finished() directly * GH-611: migrate all code of handle_sent_payable() to scan_finished() for the PayableScanner * GH-611: throw errors when a problem happens while handling SentPayable message * GH-611: migrate seperate_early_errors to tools.rs finished * GH-611: remove commented out from accountant/mod.rs * GH-611: return an option of NodeToUiMessage from the scan_finished() * GH-611: format the error messages inside scanners.rs * GH-611: refactor scan_finished() of PayableScanner * GH-611: migration to scan_finished() for PendingPayableScanner in progress * GH-611: directly store fields of accountant config inside Accountant * GH-611: pull fields of accountant config outside * GH-611: refactor utility fn to build bootstrapper config with defaults * GH-611: comment out AccountantConfig * GH-611: migrate process_transaction_by_status() to the scanners.rs * GH-611: return errors instead of panicking inside PendingPayable Scanner * GH-611: pass payable dao inside pending payable scanner * GH-611: share FinancialStatistics with the PendingPayableScanner * GH-611: fix AccountantBuilder's default configuration * GH-611: supply PayableDAO for PendingPayableScanner inside tests * GH-611: fix the handler for the PendingPayable Scanner * GH-611: rename individual scanner in Scanners struct * GH-611: migrate test interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() to scanners.rs * GH-611: migrate test interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval * GH-611: migrate interpret_transaction_receipt_panics_at_undefined_status_code() to scanners.rs * GH-611: rename the panic message * GH-611: migrate test interpret_transaction_receipt_when_transaction_status_is_a_failure() to scanners.rs * GH-611: migrate handle_pending_tx_handles_none_returned_for_transaction_receipt() to scanners.rs * GH-611: migrate test for report transaction receipts message into scanners.rs * GH-611: remove unnecessary code from accountant.rs * GH-611: migrate some functions for PendingPayable Scanner to tools.rs * GH-611: reorder items inside accountant.rs * GH-611: migrate tests for CancelPendingTransactions inside scanners.rs * GH-611: remove the CancelFailedPendingTransaction message * GH-611: migrate tests for update_payable_fingerprint() * GH-611: migrate tests for confirming pending transactions to scanners.rs * GH-611: remove transaction confirmation tools * GH-611: migrate handling of ReceivedPayments message to the scan_finished() of scanners.rs * GH-611: use mark_as_started() to update the timestamp inside Scanners * GH-611: reanme ScannerError to BeginScanError * GH-611: replace Errors into panics inside scan_finished() of all the Scanners * GH-611: modify tests for Scanners to assert whether scan_finished() stops the scan * GH-611: add logging when scan has ended * GH-611: fix test for the periodical scanning of Payable Scanner * GH-611: use ScannerMock for testing periodical scanning for payables * GH-611: fix test for the periodical scanning of receivables and deliquencies * GH-611: fix test for periodical scanning for pending payables * GH-611: rename the fn name for stopping the system inside ScannerMock * GH-611: remove import warnings * GH-611: improve the implementation of ScannerMock * GH-611: end the scan in case begin_scan() returns an error of nothing to process * GH-611: refactor begin_scan() for Receivable Scanner * GH-611: remove an obsolete assert realted to threshold tools * GH-611: fix the test pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() * GH-611: refactor scanners.rs * GH-611: use default implementation of PaymentThresholds * GH-611: migrate the test utility functions of scanner.rs to accountant/test_utils.rs * GH-611: remove BannedDao and PaymentThresholds as a field of Accountant * GH-611: implement scan_finished() for ScannerMock * GH-611: remove multiple occurences of BannedDao inside tests of accountant/mod.rs * GH-611: rename scan_finished() to finish_scan() * GH-611: remove the file .idea/inspectionProfiles/Project_Default.xml from git tracking * remove rustup check + added rust version override * Update ci-matrix.yml * GH-611: use OffsetDateTime in the logger.rs * GH-611: reorder items in the src/accountant/mod.rs * GH-611: rename the file tools.rs to scanners_tools.rs * GH-611: remove redundant code * GH-611: reorder ReportTransactionReceipts * GH-611: add Eq to the PendingTransactionStatus * GH-611: remove warnings * GH-611: Trigger GitHub Actions * Rust version hotfix (#179) (#182) * remove rustup check + added rust version override * Update ci-matrix.yml * GH-611: fix handling the message with empty vector for PendingPayables * GH-611: clone migrator_config instead of using take() on Option * GH-611: fix test masq_erc20_contract_exists_on_ethereum_ropsten_integration * GH-611: consistently pass DAOs inside Accountant and Scanners * GH-611: Review 1 (#209) * GH-611: refactor the Accountant's constructor * GH-611: remove contract test for eth ropsten * GH-611: rename function names that handles scan requests * GH-611: rename message to scan_message * GH-611: remove the eprintln!() from the production code * GH-611: use response_skeleton to geneate logs in different severity * GH-611: remove code duplication in the handling of scan requests * GH-611: simplify the financial_statistics * GH-611: refactor test scan_request_from_ui_is_handled_in_case_the_scan_is_already_running() * GH-611: Add the ability to log complete tx hash * GH-611: refactor test accountant_receives_new_payments_to_the_receivables_dao() * GH-611: refactor accountant_scans_after_startup() * GH-611: minor test improments related to duration and begin_scan_params * GH-611: strengthen the test start_message_triggers_no_scans_in_suppress_mode() * GH-611: minor improvements in tests and renames * GH-611: remove duration from the test accountant_does_not_initiate_another_scan_in_case_it_receives_the_message_and_the_scanner_is_running() * GH-611: log full hash * GH-611: remove the contract test for ropsten * GH-611: rename make_scan_intervals_with_defaults() to default_scan_intervals() * GH-611: use a default implementation for the default scan intervals * GH-611: stop using a mutable reference of BootstrapperConfig to build Accountant * GH-611: rename test in blockchain_bridge.rs * GH-611: finish review changes for scanners.rs * GH-611: change the log when receivable scanner no new payments from the blockchain bridge * GH-611: change the error message when the scan_finished() is called but the timestamp is not found. * GH-611: working on refactoring the mark_as_ended() * GH-611: write a common function for updating the timestamp when a scan is ended. * GH-611: rename function names and add function names in the panic messages for the NullScanner * GH-611: strengthen the test scanners_struct_can_be_constructed_with_the_respective_scanners() * GH-611: improve test payable_scanner_can_initiate_a_scan() * GH-611: minor changes in the scanners.rs * GH-611: change the way we log NothingToProcess error * GH-611: strengthen the test receivable_scanner_scans_for_delinquencies() * GH-611: improve test for handle_none_status() * GH-611: remove the pub keyword from multiple functions inside the impl block of BeginScanError * GH-611: review changes for scanners_tools.rs * GH-611: use builder approach to build the scanners for tests * GH-611: remove the wrapper of migrator_config (risky) * GH-611: remove the wrapper from the when_pending_too_long (risky) * GH-611: put the mistakenly removed contract test back * GH-611: remove some unnecessary comments * GH-611: make the suppress_initial_scans flag just a boolean rather than wrapping * GH-611: minor remaining code changes * GH-611: Review 2 (#217) * GH-611: rename the function names again * GH-611: rename the field to when_pending_too_long_sec * GH-611: refactor test scan_request_from_ui_is_handled_in_case_the_scan_is_already_running * GH-611: use use_logs_containing inside the test periodical_scanning_for_receivables_and_delinquencies_works * GH-611: change the scan intervals back to their unique values * GH-611: improve timestamp_as_string function in scanners.rs * GH-611: rename the function to remove_timestamp_and_log * GH-611: migrate remove_timestamp_and_log to ScannerCommon * GH-611: change to exists_log_containing * GH-611: refactor handle_error() and remove log() inside BeginScanError * GH-611: use macro to remove code duplication in scanners.rs * GH-611: remove unnecessary modifications * GH-611: use the best practices of builder pattern for the individual scanner mocks * GH-611: refactor the constructor of Accountant * GH-611: rename the tests in scanners_tools.rs * GH-611: remove take() from the constructor of Accountant * GH-611: use just borrow for financial statistics * GH-611: remove the test that was testing panic * GH-611: remove assertions from the test start_message_triggers_no_scans_in_suppress_mode * GH 611: Review 3 (#220) * GH-611: remove the clone from the earning wallet iniside the Accountant's constructor * GH-611: rename the function to simply remove_timestamp() * GH-611: remove clone from the payment_thresholds * GH-611: change the test_name variable in tests for remove_timestamp * GH-611: rearrangement after the merge is practically done; 6 tests remains to be suspicious and will need to be examined for duplication or their utility. * GH-611: renaming file and modules to be more consistent (utils fits to file names around); also refactoring investigate_debt_extremes * GH-611: tests finally passing; next some refactoring * GH-611: todosudo chown -R bert:bert target are gone * GH-611: remove clippy warnings * GH-611: refactored AccountantBuilder to require fewer method calls for injecting individual mock DAOs * GH-611: the best I could do now for Utkarshe's review; let's consider that completed from my part * GH-611: bump the version from 0.7.0 to 0.7.1 * GH-611: change response_skeleton to response_skeleton_opt in ScanError * GH-611: send ScanError message to the Accountant when response_skeleton_opt is None * GH-611: end scan once a ScanError message is received * GH-611: assert on each case for handling scan error * GH-611: remove todos * GH-611: modify AccountantBuilder to accept logger * GH-611: rename the variable from is_scan_running to scan_started_at_opt * GH-611: remove unnecessary assertions from the helper fn assert_scan_error_is_handled_properly Co-authored-by: Dan Wiebe Co-authored-by: FinsaasGH <89403560+FinsaasGH@users.noreply.github.com> Co-authored-by: Bert --- .gitignore | 1 + automap/Cargo.lock | 2 +- dns_utility/Cargo.lock | 4 +- dns_utility/Cargo.toml | 2 +- masq/Cargo.toml | 2 +- masq_lib/Cargo.toml | 2 +- masq_lib/src/logger.rs | 2 +- multinode_integration_tests/Cargo.toml | 2 +- node/Cargo.lock | 8 +- node/Cargo.toml | 2 +- node/src/accountant/dao_utils.rs | 278 +- node/src/accountant/mod.rs | 3746 ++++------------- node/src/accountant/receivable_dao.rs | 19 +- node/src/accountant/scanners.rs | 2277 ++++++++++ node/src/accountant/scanners_utils.rs | 540 +++ node/src/accountant/test_utils.rs | 659 ++- node/src/accountant/tools.rs | 165 - node/src/actor_system_factory.rs | 64 +- node/src/blockchain/blockchain_bridge.rs | 170 +- node/src/bootstrapper.rs | 38 +- node/src/daemon/setup_reporter.rs | 23 +- node/src/database/dao_utils.rs | 0 .../unprivileged_parse_args_configuration.rs | 148 +- node/src/sub_lib/accountant.rs | 32 +- node/src/sub_lib/combined_parameters.rs | 17 +- node/src/test_utils/mod.rs | 32 +- port_exposer/Cargo.lock | 2 +- port_exposer/Cargo.toml | 2 +- 28 files changed, 4903 insertions(+), 3336 deletions(-) create mode 100644 node/src/accountant/scanners.rs create mode 100644 node/src/accountant/scanners_utils.rs delete mode 100644 node/src/accountant/tools.rs create mode 100644 node/src/database/dao_utils.rs diff --git a/.gitignore b/.gitignore index e252b6329..cfcb37e57 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /results/ **/*.rs.bk .idea/azure/ +.idea/inspectionProfiles/Project_Default.xml ### Node node_modules diff --git a/automap/Cargo.lock b/automap/Cargo.lock index 97ee98688..4c474a63d 100644 --- a/automap/Cargo.lock +++ b/automap/Cargo.lock @@ -908,7 +908,7 @@ dependencies = [ [[package]] name = "masq_lib" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix", "clap", diff --git a/dns_utility/Cargo.lock b/dns_utility/Cargo.lock index fbe59b66d..850141b1f 100644 --- a/dns_utility/Cargo.lock +++ b/dns_utility/Cargo.lock @@ -407,7 +407,7 @@ dependencies = [ [[package]] name = "dns_utility" -version = "0.7.0" +version = "0.7.1" dependencies = [ "core-foundation", "ipconfig 0.2.2", @@ -811,7 +811,7 @@ dependencies = [ [[package]] name = "masq_lib" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix", "clap", diff --git a/dns_utility/Cargo.toml b/dns_utility/Cargo.toml index 2df8c6820..21cbebca3 100644 --- a/dns_utility/Cargo.toml +++ b/dns_utility/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dns_utility" -version = "0.7.0" +version = "0.7.1" license = "GPL-3.0-only" authors = ["Dan Wiebe ", "MASQ"] copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved." diff --git a/masq/Cargo.toml b/masq/Cargo.toml index 9da81d27d..c796f6b7d 100644 --- a/masq/Cargo.toml +++ b/masq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masq" -version = "0.7.0" +version = "0.7.1" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved." diff --git a/masq_lib/Cargo.toml b/masq_lib/Cargo.toml index b871d2334..04eccbb2f 100644 --- a/masq_lib/Cargo.toml +++ b/masq_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masq_lib" -version = "0.7.0" +version = "0.7.1" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved." diff --git a/masq_lib/src/logger.rs b/masq_lib/src/logger.rs index a0eef154a..2d7066da3 100644 --- a/masq_lib/src/logger.rs +++ b/masq_lib/src/logger.rs @@ -22,7 +22,7 @@ use time::format_description::parse; use time::OffsetDateTime; const UI_MESSAGE_LOG_LEVEL: Level = Level::Info; -const TIME_FORMATTING_STRING: &str = +pub const TIME_FORMATTING_STRING: &str = "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"; lazy_static! { diff --git a/multinode_integration_tests/Cargo.toml b/multinode_integration_tests/Cargo.toml index 674220dad..b31f772e5 100644 --- a/multinode_integration_tests/Cargo.toml +++ b/multinode_integration_tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multinode_integration_tests" -version = "0.7.0" +version = "0.7.1" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved." diff --git a/node/Cargo.lock b/node/Cargo.lock index 86a081080..7d685bc6a 100644 --- a/node/Cargo.lock +++ b/node/Cargo.lock @@ -1827,7 +1827,7 @@ dependencies = [ [[package]] name = "masq" -version = "0.7.0" +version = "0.7.1" dependencies = [ "atty", "clap", @@ -1847,7 +1847,7 @@ dependencies = [ [[package]] name = "masq_lib" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix", "clap", @@ -2023,7 +2023,7 @@ dependencies = [ [[package]] name = "multinode_integration_tests" -version = "0.7.0" +version = "0.7.1" dependencies = [ "base64 0.13.0", "crossbeam-channel 0.5.1", @@ -2116,7 +2116,7 @@ dependencies = [ [[package]] name = "node" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix", "automap", diff --git a/node/Cargo.toml b/node/Cargo.toml index 44e752515..3f8d369f0 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node" -version = "0.7.0" +version = "0.7.1" license = "GPL-3.0-only" authors = ["Dan Wiebe ", "MASQ"] copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved." diff --git a/node/src/accountant/dao_utils.rs b/node/src/accountant/dao_utils.rs index 514a0306e..75b191841 100644 --- a/node/src/accountant/dao_utils.rs +++ b/node/src/accountant/dao_utils.rs @@ -3,18 +3,17 @@ use crate::accountant::big_int_processing::big_int_divider::BigIntDivider; use crate::accountant::payable_dao::PayableAccount; use crate::accountant::receivable_dao::ReceivableAccount; -use crate::accountant::{checked_conversion, sign_conversion}; +use crate::accountant::{checked_conversion, gwei_to_wei, sign_conversion}; use crate::database::connection_wrapper::ConnectionWrapper; use crate::database::db_initializer::{ connection_or_panic, DbInitializationConfig, DbInitializerReal, }; +use crate::sub_lib::accountant::PaymentThresholds; use masq_lib::constants::WEIS_OF_GWEI; use masq_lib::messages::{ RangeQuery, TopRecordsConfig, TopRecordsOrdering, UiPayableAccount, UiReceivableAccount, }; -use masq_lib::utils::ExpectValue; use rusqlite::{Row, ToSql}; -use std::cell::RefCell; use std::fmt::Display; use std::iter::FlatMap; use std::path::{Path, PathBuf}; @@ -42,14 +41,14 @@ pub fn from_time_t(time_t: i64) -> SystemTime { pub struct DaoFactoryReal { pub data_directory: PathBuf, - pub init_config: RefCell>, + pub init_config: DbInitializationConfig, } impl DaoFactoryReal { pub fn new(data_directory: &Path, init_config: DbInitializationConfig) -> Self { Self { data_directory: data_directory.to_path_buf(), - init_config: RefCell::new(Some(init_config)), + init_config, } } @@ -57,7 +56,7 @@ impl DaoFactoryReal { connection_or_panic( &DbInitializerReal::default(), &self.data_directory, - self.init_config.take().expectv("Db init config"), + self.init_config.clone(), ) } } @@ -343,15 +342,81 @@ pub fn sum_i128_values_from_table( .sum() } +pub struct ThresholdUtils {} + +impl ThresholdUtils { + pub fn slope(payment_thresholds: &PaymentThresholds) -> i128 { + /* + Slope is an integer, rather than a float, to improve performance. Since there are + computations that divide by the slope, it cannot be allowed to be zero; but since it's + an integer, it can't get any closer to zero than -1. + + If the numerator of this computation is less than the denominator, the slope will be + calculated as 0; therefore, .permanent_debt_allowed_gwei must be less than + .debt_threshold_gwei, so that the numerator will be no greater than -10^9 (-gwei_to_wei(1)), + and the denominator must be less than or equal to 10^9. + + These restrictions do not seem over-strict, since having .permanent_debt_allowed greater + than or equal to .debt_threshold_gwei would result in chaos, and setting + .threshold_interval_sec over 10^9 would mean continuing to declare debts delinquent after + more than 31 years. + + If payment_thresholds are ever configurable by the user, these validations should be done + on the values before they are accepted. + */ + + (gwei_to_wei::(payment_thresholds.permanent_debt_allowed_gwei) + - gwei_to_wei::(payment_thresholds.debt_threshold_gwei)) + / payment_thresholds.threshold_interval_sec as i128 + } + + pub fn calculate_finite_debt_limit_by_age( + payment_thresholds: &PaymentThresholds, + debt_age_s: u64, + ) -> u128 { + if Self::qualifies_for_permanent_debt_limit(debt_age_s, payment_thresholds) { + return gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei); + }; + let m = ThresholdUtils::slope(payment_thresholds); + let b = ThresholdUtils::compute_theoretical_interception_with_y_axis( + m, + payment_thresholds.maturity_threshold_sec as i128, + gwei_to_wei(payment_thresholds.debt_threshold_gwei), + ); + let y = m * debt_age_s as i128 + b; + y as u128 + } + + fn compute_theoretical_interception_with_y_axis( + m: i128, //is negative + maturity_threshold_sec: i128, + debt_threshold_wei: i128, + ) -> i128 { + debt_threshold_wei - (maturity_threshold_sec * m) + } + + fn qualifies_for_permanent_debt_limit( + debt_age_s: u64, + payment_thresholds: &PaymentThresholds, + ) -> bool { + debt_age_s + > (payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec) + } +} + #[cfg(test)] mod tests { use super::*; use crate::database::connection_wrapper::ConnectionWrapperReal; + use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::test_utils::make_wallet; + use masq_lib::constants::MASQ_TOTAL_SUPPLY; use masq_lib::messages::TopRecordsOrdering::Balance; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::types::{ToSqlOutput, Value}; use rusqlite::{Connection, OpenFlags}; + use std::collections::HashMap; use std::time::UNIX_EPOCH; #[test] @@ -545,4 +610,205 @@ mod tests { let _ = iterator.vigilant_flatten().collect::>(); } + + fn gap_tester(payment_thresholds: &PaymentThresholds) -> (u64, u64) { + let mut counts_of_unique_elements: HashMap = HashMap::new(); + (1_u64..20) + .map(|to_add| { + ThresholdUtils::calculate_finite_debt_limit_by_age( + &payment_thresholds, + 1500 + to_add, + ) as u64 + }) + .for_each(|point_height| { + counts_of_unique_elements + .entry(point_height) + .and_modify(|q| *q += 1) + .or_insert(1); + }); + + let mut heights_and_counts = counts_of_unique_elements.drain().collect::>(); + heights_and_counts.sort_by_key(|(height, _)| (u64::MAX - height)); + let mut counts_of_groups_of_the_same_size: HashMap = HashMap::new(); + let mut previous_height = + ThresholdUtils::calculate_finite_debt_limit_by_age(&payment_thresholds, 1500) as u64; + heights_and_counts + .into_iter() + .for_each(|(point_height, unique_count)| { + let height_change = if point_height <= previous_height { + previous_height - point_height + } else { + panic!("unexpected trend; previously: {previous_height}, now: {point_height}") + }; + counts_of_groups_of_the_same_size + .entry(unique_count) + .and_modify(|(_height_change, occurrence_so_far)| *occurrence_so_far += 1) + .or_insert((height_change, 1)); + previous_height = point_height; + }); + + let mut sortable = counts_of_groups_of_the_same_size + .drain() + .collect::>(); + sortable.sort_by_key(|(_key, (_height_change, occurrence))| *occurrence); + + let (number_of_seconds_detected, (height_change, occurrence)) = + sortable.last().expect("no values to analyze"); + //checking if the sample of undistorted results (consist size groups) has enough weight compared to 20 tries from the beginning + if number_of_seconds_detected * occurrence >= 15 { + (*number_of_seconds_detected as u64, *height_change) + } else { + panic!("couldn't provide a relevant amount of data for the analysis") + } + } + + fn assert_on_height_granularity_with_advancing_time( + description_of_given_pt: &str, + payment_thresholds: &PaymentThresholds, + expected_height_change_wei: u64, + ) { + let (seconds_needed_for_smallest_change_in_height, absolute_height_change_wei) = + gap_tester(&payment_thresholds); + + assert_eq!( + seconds_needed_for_smallest_change_in_height, + 1, + "while testing {} we expected that these thresholds: {:?} will require only 1 s until \ + we see the height change but computed {} s instead", + description_of_given_pt, + payment_thresholds, + seconds_needed_for_smallest_change_in_height + ); + assert_eq!( + absolute_height_change_wei, + expected_height_change_wei, + "while testing {} we expected that these thresholds: {:?} will cause a height change \ + of {} wei as a result of advancement in time by {} s but the true result is {}", + description_of_given_pt, + payment_thresholds, + expected_height_change_wei, + seconds_needed_for_smallest_change_in_height, + absolute_height_change_wei + ) + } + + #[test] + fn testing_granularity_calculate_sloped_threshold_by_time() { + let payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 1000, + payment_grace_period_sec: 0, + permanent_debt_allowed_gwei: 100, + debt_threshold_gwei: 10_000, + threshold_interval_sec: 10_000, + unban_below_gwei: 100, + }; + + assert_on_height_granularity_with_advancing_time( + "135° slope", + &payment_thresholds, + 990_000_000, + ); + + let payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 1000, + payment_grace_period_sec: 0, + permanent_debt_allowed_gwei: 100, + debt_threshold_gwei: 3_420, + threshold_interval_sec: 10_000, + unban_below_gwei: 100, + }; + + assert_on_height_granularity_with_advancing_time( + "160° slope", + &payment_thresholds, + 332_000_000, + ); + + let payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 1000, + payment_grace_period_sec: 0, + permanent_debt_allowed_gwei: 100, + debt_threshold_gwei: 875, + threshold_interval_sec: 10_000, + unban_below_gwei: 100, + }; + + assert_on_height_granularity_with_advancing_time( + "175° slope", + &payment_thresholds, + 77_500_000, + ); + } + + #[test] + fn checking_chosen_values_for_the_payment_thresholds_defaults_on_height_values_granularity() { + let payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; + + assert_on_height_granularity_with_advancing_time( + "default thresholds", + &payment_thresholds, + 23_148_148_148_148, + ); + } + + #[test] + fn slope_has_loose_enough_limitations_to_allow_work_with_number_bigger_than_masq_token_max_supply( + ) { + //max masq token supply by August 2022: 37,500,000 + let payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 20, + payment_grace_period_sec: 33, + permanent_debt_allowed_gwei: 1, + debt_threshold_gwei: MASQ_TOTAL_SUPPLY * WEIS_OF_GWEI as u64, + threshold_interval_sec: 1, + unban_below_gwei: 0, + }; + + let slope = ThresholdUtils::slope(&payment_thresholds); + + assert_eq!(slope, -37499999999999999000000000); + let check = { + let y_interception = ThresholdUtils::compute_theoretical_interception_with_y_axis( + slope, + payment_thresholds.maturity_threshold_sec as i128, + gwei_to_wei(payment_thresholds.debt_threshold_gwei), + ); + slope * (payment_thresholds.maturity_threshold_sec + 1) as i128 + y_interception + }; + assert_eq!(check, WEIS_OF_GWEI) + } + + #[test] + fn slope_after_its_end_turns_into_permanent_debt_allowed() { + let payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 1000, + payment_grace_period_sec: 444, + permanent_debt_allowed_gwei: 44, + debt_threshold_gwei: 8888, + threshold_interval_sec: 11111, + unban_below_gwei: 0, + }; + + let right_at_the_end = ThresholdUtils::calculate_finite_debt_limit_by_age( + &payment_thresholds, + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec + + 1, + ); + let a_certain_distance_further = ThresholdUtils::calculate_finite_debt_limit_by_age( + &payment_thresholds, + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec + + 1234, + ); + + assert_eq!( + right_at_the_end, + gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei) + ); + assert_eq!( + a_certain_distance_further, + gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei) + ) + } } diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 618566357..163bdf264 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -6,17 +6,19 @@ pub mod financials; pub mod payable_dao; pub mod pending_payable_dao; pub mod receivable_dao; -pub mod tools; +pub mod scanners; +pub mod scanners_utils; #[cfg(test)] pub mod test_utils; use core::fmt::Debug; use masq_lib::constants::{SCAN_ERROR, WEIS_OF_GWEI}; +use std::cell::{Ref, RefCell}; use masq_lib::messages::{ QueryResults, ScanType, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, - UiScanRequest, UiScanResponse, + UiScanRequest, }; use masq_lib::ui_gateway::{MessageBody, MessagePath}; @@ -26,23 +28,21 @@ use crate::accountant::dao_utils::{ use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, }; -use crate::accountant::payable_dao::{Payable, PayableAccount, PayableDaoError, PayableDaoFactory}; -use crate::accountant::pending_payable_dao::{PendingPayableDao, PendingPayableDaoFactory}; -use crate::accountant::receivable_dao::{ - ReceivableAccount, ReceivableDaoError, ReceivableDaoFactory, -}; -use crate::accountant::tools::accountant_tools::{Scanner, Scanners, TransactionConfirmationTools}; -use crate::banned_dao::{BannedDao, BannedDaoFactory}; +use crate::accountant::payable_dao::{Payable, PayableDaoError}; +use crate::accountant::pending_payable_dao::PendingPayableDao; +use crate::accountant::receivable_dao::ReceivableDaoError; +use crate::accountant::scanners::{NotifyLaterForScanners, Scanners}; use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::{BlockchainError, BlockchainTransaction}; use crate::bootstrapper::BootstrapperConfig; use crate::database::db_initializer::DbInitializationConfig; -use crate::sub_lib::accountant::{AccountantConfig, FinancialStatistics, PaymentThresholds}; -use crate::sub_lib::accountant::{AccountantSubs, ReportServicesConsumedMessage}; +use crate::sub_lib::accountant::DaoFactories; +use crate::sub_lib::accountant::FinancialStatistics; +use crate::sub_lib::accountant::ReportExitServiceProvidedMessage; +use crate::sub_lib::accountant::ReportRoutingServiceProvidedMessage; +use crate::sub_lib::accountant::ReportServicesConsumedMessage; +use crate::sub_lib::accountant::{AccountantSubs, ScanIntervals}; use crate::sub_lib::accountant::{MessageIdGenerator, MessageIdGeneratorReal}; -use crate::sub_lib::accountant::{ - ReportExitServiceProvidedMessage, ReportRoutingServiceProvidedMessage, -}; use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; use crate::sub_lib::peer_actors::{BindMessage, StartMessage}; use crate::sub_lib::utils::{handle_ui_crash_request, NODE_MAILBOX_CAPACITY}; @@ -54,25 +54,23 @@ use actix::Context; use actix::Handler; use actix::Message; use actix::Recipient; -use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; use masq_lib::messages::UiFinancialsResponse; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::ui_gateway::MessageTarget::ClientId; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; -use masq_lib::utils::{plus, ExpectValue}; +use masq_lib::utils::ExpectValue; use payable_dao::PayableDao; use receivable_dao::ReceivableDao; use std::any::type_name; #[cfg(test)] -use std::any::Any; use std::default::Default; use std::fmt::Display; use std::ops::{Div, Mul}; use std::path::Path; -use std::time::{Duration, SystemTime}; -use thousands::Separable; +use std::rc::Rc; +use std::time::SystemTime; use web3::types::{TransactionReceipt, H256}; pub const CRASH_KEY: &str = "ACCOUNTANT"; @@ -80,23 +78,23 @@ pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours pub struct Accountant { - config: AccountantConfig, + scan_intervals: ScanIntervals, + suppress_initial_scans: bool, consuming_wallet: Option, - earning_wallet: Wallet, + earning_wallet: Rc, payable_dao: Box, receivable_dao: Box, pending_payable_dao: Box, - banned_dao: Box, crashable: bool, scanners: Scanners, - confirmation_tools: TransactionConfirmationTools, - financial_statistics: FinancialStatistics, - report_accounts_payable_sub: Option>, + notify_later: NotifyLaterForScanners, + financial_statistics: Rc>, + report_accounts_payable_sub_opt: Option>, retrieve_transactions_sub: Option>, + request_transaction_receipts_subs_opt: Option>, report_new_payments_sub: Option>, report_sent_payments_sub: Option>, ui_message_sub: Option>, - payable_threshold_gauge: Box, message_id_generator: Box, logger: Logger, } @@ -150,7 +148,7 @@ pub struct ScanForPendingPayables { #[derive(Debug, Clone, Message, PartialEq, Eq)] pub struct ScanError { pub scan_type: ScanType, - pub response_skeleton: ResponseSkeleton, + pub response_skeleton_opt: Option, pub msg: String, } @@ -167,7 +165,7 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, _msg: StartMessage, ctx: &mut Self::Context) -> Self::Result { - if self.config.suppress_initial_scans { + if self.suppress_initial_scans { info!( &self.logger, "Started with --scans off; declining to begin database and blockchain scans" @@ -194,7 +192,13 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { - self.handle_received_payments(msg); + if let Some(node_to_ui_msg) = self.scanners.receivable.finish_scan(msg, &self.logger) { + self.ui_message_sub + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } } } @@ -202,7 +206,13 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: SentPayables, _ctx: &mut Self::Context) -> Self::Result { - self.handle_sent_payables(msg); + if let Some(node_to_ui_msg) = self.scanners.payable.finish_scan(msg, &self.logger) { + self.ui_message_sub + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } } } @@ -210,11 +220,14 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_scan_message( - self.scanners.payables.as_ref(), - msg.response_skeleton_opt, + self.handle_request_of_scan_for_payable(msg.response_skeleton_opt); + let _ = self.notify_later.scan_for_payable.notify_later( + ScanForPayables { + response_skeleton_opt: None, + }, + self.scan_intervals.payable_scan_interval, ctx, - ) + ); } } @@ -222,11 +235,14 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_scan_message( - self.scanners.pending_payables.as_ref(), - msg.response_skeleton_opt, + self.handle_request_of_scan_for_pending_payable(msg.response_skeleton_opt); + let _ = self.notify_later.scan_for_pending_payable.notify_later( + ScanForPendingPayables { + response_skeleton_opt: None, // because scheduled scans don't respond + }, + self.scan_intervals.pending_payable_scan_interval, ctx, - ) + ); } } @@ -234,11 +250,14 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - self.handle_scan_message( - self.scanners.receivables.as_ref(), - msg.response_skeleton_opt, + self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); + let _ = self.notify_later.scan_for_receivable.notify_later( + ScanForReceivables { + response_skeleton_opt: None, // because scheduled scans don't respond + }, + self.scan_intervals.receivable_scan_interval, ctx, - ) + ); } } @@ -247,26 +266,39 @@ impl Handler for Accountant { fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); - let error_msg = NodeToUiMessage { - target: ClientId(scan_error.response_skeleton.client_id), - body: MessageBody { - opcode: "scan".to_string(), - path: MessagePath::Conversation(scan_error.response_skeleton.context_id), - payload: Err(( - SCAN_ERROR, - format!( - "{:?} scan failed: '{}'", - scan_error.scan_type, scan_error.msg - ), - )), - }, + match scan_error.scan_type { + ScanType::Payables => { + self.scanners.payable.mark_as_ended(&self.logger); + } + ScanType::PendingPayables => { + self.scanners.pending_payable.mark_as_ended(&self.logger); + } + ScanType::Receivables => { + self.scanners.receivable.mark_as_ended(&self.logger); + } }; - error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); - self.ui_message_sub - .as_ref() - .expect("UIGateway not bound") - .try_send(error_msg) - .expect("UiGateway is dead"); + if let Some(response_skeleton) = scan_error.response_skeleton_opt { + let error_msg = NodeToUiMessage { + target: ClientId(response_skeleton.client_id), + body: MessageBody { + opcode: "scan".to_string(), + path: MessagePath::Conversation(response_skeleton.context_id), + payload: Err(( + SCAN_ERROR, + format!( + "{:?} scan failed: '{}'", + scan_error.scan_type, scan_error.msg + ), + )), + }, + }; + error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); + self.ui_message_sub + .as_ref() + .expect("UIGateway not bound") + .try_send(error_msg) + .expect("UiGateway is dead"); + } } } @@ -331,57 +363,17 @@ pub struct ReportTransactionReceipts { impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { - debug!( - self.logger, - "Processing receipts for {} transactions", - msg.fingerprints_with_receipts.len() - ); - let statuses = self.handle_pending_transaction_with_its_receipt(&msg); - self.process_transactions_by_their_status(statuses, ctx); - if let Some(response_skeleton) = &msg.response_skeleton_opt { + fn handle(&mut self, msg: ReportTransactionReceipts, _ctx: &mut Self::Context) -> Self::Result { + if let Some(node_to_ui_msg) = self.scanners.pending_payable.finish_scan(msg, &self.logger) { self.ui_message_sub .as_ref() - .expect("UIGateway not bound") - .try_send(NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) .expect("UIGateway is dead"); } } } -#[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct CancelFailedPendingTransaction { - pub id: PendingPayableId, -} - -impl Handler for Accountant { - type Result = (); - - fn handle( - &mut self, - msg: CancelFailedPendingTransaction, - _ctx: &mut Self::Context, - ) -> Self::Result { - self.handle_cancel_pending_transaction(msg) - } -} - -#[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ConfirmPendingTransaction { - pub pending_payable_fingerprint: PendingPayableFingerprint, -} - -impl Handler for Accountant { - type Result = (); - - fn handle(&mut self, msg: ConfirmPendingTransaction, _ctx: &mut Self::Context) -> Self::Result { - self.handle_confirm_pending_transaction(msg) - } -} - impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: PendingPayableFingerprint, _ctx: &mut Self::Context) -> Self::Result { @@ -404,7 +396,7 @@ impl Handler for Accountant { client_id, context_id, }, - ); + ) } else { handle_ui_crash_request(msg, &self.logger, self.crashable, CRASH_KEY) } @@ -412,35 +404,41 @@ impl Handler for Accountant { } impl Accountant { - pub fn new( - config: &BootstrapperConfig, - payable_dao_factory: Box, - receivable_dao_factory: Box, - pending_payable_dao_factory: Box, - banned_dao_factory: Box, - ) -> Accountant { + pub fn new(config: BootstrapperConfig, dao_factories: DaoFactories) -> Accountant { + let payment_thresholds = config.payment_thresholds_opt.expectv("Payment thresholds"); + let scan_intervals = config.scan_intervals_opt.expectv("Scan Intervals"); + let earning_wallet = Rc::new(config.earning_wallet); + let financial_statistics = Rc::new(RefCell::new(FinancialStatistics::default())); + let payable_dao = dao_factories.payable_dao_factory.make(); + let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); + let receivable_dao = dao_factories.receivable_dao_factory.make(); + let scanners = Scanners::new( + dao_factories, + Rc::new(payment_thresholds), + Rc::clone(&earning_wallet), + config.when_pending_too_long_sec, + Rc::clone(&financial_statistics), + ); + Accountant { - config: *config - .accountant_config_opt - .as_ref() - .expectv("Accountant config"), + scan_intervals, + suppress_initial_scans: config.suppress_initial_scans, consuming_wallet: config.consuming_wallet_opt.clone(), - earning_wallet: config.earning_wallet.clone(), - payable_dao: payable_dao_factory.make(), - receivable_dao: receivable_dao_factory.make(), - pending_payable_dao: pending_payable_dao_factory.make(), - banned_dao: banned_dao_factory.make(), + earning_wallet: Rc::clone(&earning_wallet), + payable_dao, + receivable_dao, + pending_payable_dao, + scanners, crashable: config.crash_point == CrashPoint::Message, - scanners: Scanners::default(), - financial_statistics: FinancialStatistics::default(), - report_accounts_payable_sub: None, + notify_later: NotifyLaterForScanners::default(), + financial_statistics: Rc::clone(&financial_statistics), + report_accounts_payable_sub_opt: None, retrieve_transactions_sub: None, + request_transaction_receipts_subs_opt: None, report_new_payments_sub: None, report_sent_payments_sub: None, ui_message_sub: None, - confirmation_tools: TransactionConfirmationTools::default(), message_id_generator: Box::new(MessageIdGeneratorReal::default()), - payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), logger: Logger::new("Accountant"), } } @@ -465,161 +463,6 @@ impl Accountant { DaoFactoryReal::new(data_directory, DbInitializationConfig::panic_on_migration()) } - fn handle_scan_message( - &self, - scanner: &dyn Scanner, - response_skeleton_opt: Option, - ctx: &mut Context, - ) { - scanner.scan(self, response_skeleton_opt); - scanner.notify_later_assertable(self, ctx) - } - - fn scan_for_payables(&self, response_skeleton_opt: Option) { - info!(self.logger, "Scanning for payables"); - - let all_non_pending_payables = self.payable_dao.non_pending_payables(); - debug!( - self.logger, - "{}", - Self::investigate_debt_extremes(&all_non_pending_payables) - ); - let qualified_payables = all_non_pending_payables - .into_iter() - .filter(|account| self.should_pay(account)) - .collect::>(); - info!( - self.logger, - "Chose {} qualified debts to pay", - qualified_payables.len() - ); - self.payables_debug_summary(&qualified_payables); - if !qualified_payables.is_empty() { - self.report_accounts_payable_sub - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(ReportAccountsPayable { - accounts: qualified_payables, - response_skeleton_opt, - }) - .expect("BlockchainBridge is dead") - } - } - - fn scan_for_delinquencies(&self) { - info!(self.logger, "Scanning for delinquencies"); - let now = SystemTime::now(); - self.receivable_dao - .new_delinquencies(now, &self.config.payment_thresholds) - .into_iter() - .for_each(|account| { - self.banned_dao.ban(&account.wallet); - let (balance, age) = Self::balance_and_age(&account); - info!( - self.logger, - "Wallet {} (balance: {} MASQ, age: {} sec) banned for delinquency", - account.wallet, - balance, - age.as_secs() - ) - }); - self.receivable_dao - .paid_delinquencies(&self.config.payment_thresholds) - .into_iter() - .for_each(|account| { - self.banned_dao.unban(&account.wallet); - let (balance, age) = Self::balance_and_age(&account); - info!( - self.logger, - "Wallet {} (balance: {} MASQ, age: {} sec) is no longer delinquent: unbanned", - account.wallet, - balance, - age.as_secs() - ) - }); - } - - fn scan_for_received_payments(&self, response_skeleton_opt: Option) { - info!( - self.logger, - "Scanning for receivables to {}", self.earning_wallet - ); - self.retrieve_transactions_sub - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(RetrieveTransactions { - recipient: self.earning_wallet.clone(), - response_skeleton_opt, - }) - .expect("BlockchainBridge is dead"); - } - - fn scan_for_pending_payable(&self, response_skeleton_opt: Option) { - info!(self.logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_fingerprints(); - if filtered_pending_payable.is_empty() { - debug!(self.logger, "No pending payable found during last scan") - } else { - debug!( - self.logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - self.confirmation_tools - .request_transaction_receipts_subs_opt - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(RequestTransactionReceipts { - pending_payable: filtered_pending_payable, - response_skeleton_opt, - }) - .expect("BlockchainBridge is dead"); - } - } - - fn balance_and_age(account: &ReceivableAccount) -> (String, Duration) { - let balance = format!("{}", account.balance_wei / WEIS_OF_GWEI); - let age = account - .last_received_timestamp - .elapsed() - .unwrap_or_else(|_| Duration::new(0, 0)); - (balance, age) - } - - fn should_pay(&self, payable: &PayableAccount) -> bool { - self.payable_exceeded_threshold(payable).is_some() - } - - fn payable_exceeded_threshold(&self, payable: &PayableAccount) -> Option { - let debt_age = SystemTime::now() - .duration_since(payable.last_paid_timestamp) - .expect("Internal error") - .as_secs(); - - if self.payable_threshold_gauge.is_innocent_age( - debt_age, - self.config.payment_thresholds.maturity_threshold_sec as u64, - ) { - return None; - } - - if self.payable_threshold_gauge.is_innocent_balance( - payable.balance_wei, - gwei_to_wei(self.config.payment_thresholds.permanent_debt_allowed_gwei), - ) { - return None; - } - - let threshold = self - .payable_threshold_gauge - .calculate_payout_threshold_in_gwei(&self.config.payment_thresholds, debt_age); - if payable.balance_wei > threshold { - Some(threshold) - } else { - None - } - } - fn record_service_provided( &self, service_rate: u64, @@ -696,100 +539,15 @@ impl Accountant { } } - //for debugging only - fn investigate_debt_extremes(all_non_pending_payables: &[PayableAccount]) -> String { - let now = SystemTime::now(); - if all_non_pending_payables.is_empty() { - "Payable scan found no debts".to_string() - } else { - struct PayableInfo { - balance: u128, - age: Duration, - } - let init = ( - PayableInfo { - balance: 0, - age: Duration::ZERO, - }, - PayableInfo { - balance: 0, - age: Duration::ZERO, - }, - ); - let (biggest, oldest) = all_non_pending_payables.iter().fold(init, |sofar, p| { - let (mut biggest, mut oldest) = sofar; - let p_age = now - .duration_since(p.last_paid_timestamp) - .expect("Payable time is corrupt"); - { - //look at a test if not understandable - let check_age_parameter_if_the_first_is_the_same = - || -> bool { p.balance_wei == biggest.balance && p_age > biggest.age }; - - if p.balance_wei > biggest.balance - || check_age_parameter_if_the_first_is_the_same() - { - biggest = PayableInfo { - balance: p.balance_wei, - age: p_age, - } - } - - let check_balance_parameter_if_the_first_is_the_same = - || -> bool { p_age == oldest.age && p.balance_wei > oldest.balance }; - - if p_age > oldest.age || check_balance_parameter_if_the_first_is_the_same() { - oldest = PayableInfo { - balance: p.balance_wei, - age: p_age, - } - } - } - (biggest, oldest) - }); - format!("Payable scan found {} debts; the biggest is {} owed for {}sec, the oldest is {} owed for {}sec", - all_non_pending_payables.len(), biggest.balance, biggest.age.as_secs(), - oldest.balance, oldest.age.as_secs()) - } - } - - fn payables_debug_summary(&self, qualified_payables: &[PayableAccount]) { - if qualified_payables.is_empty() { - return; - } - debug!(self.logger, "Paying qualified debts:\n{}", { - let now = SystemTime::now(); - qualified_payables - .iter() - .map(|payable| { - let p_age = now - .duration_since(payable.last_paid_timestamp) - .expect("Payable time is corrupt"); - let threshold = self - .payable_exceeded_threshold(payable) - .expect("Threshold suddenly changed!"); - format!( - "{} wei owed for {} sec exceeds threshold: {} wei; creditor: {}", - payable.balance_wei.separate_with_commas(), - p_age.as_secs(), - threshold.separate_with_commas(), - payable.wallet - ) - }) - .join("\n") - }) - } - fn handle_bind_message(&mut self, msg: BindMessage) { - self.report_accounts_payable_sub = + self.report_accounts_payable_sub_opt = Some(msg.peer_actors.blockchain_bridge.report_accounts_payable); self.retrieve_transactions_sub = Some(msg.peer_actors.blockchain_bridge.retrieve_transactions); self.report_new_payments_sub = Some(msg.peer_actors.accountant.report_new_payments); self.report_sent_payments_sub = Some(msg.peer_actors.accountant.report_sent_payments); self.ui_message_sub = Some(msg.peer_actors.ui_gateway.node_to_ui_message_sub); - self.confirmation_tools - .request_transaction_receipts_subs_opt = Some( + self.request_transaction_receipts_subs_opt = Some( msg.peer_actors .blockchain_bridge .request_transaction_receipts, @@ -797,69 +555,6 @@ impl Accountant { info!(self.logger, "Accountant bound"); } - fn handle_received_payments(&mut self, msg: ReceivedPayments) { - if !msg.payments.is_empty() { - let total_newly_paid_receivable = msg - .payments - .iter() - .fold(0, |so_far, now| so_far + now.wei_amount); - self.receivable_dao - .as_mut() - .more_money_received(msg.timestamp, msg.payments); - self.financial_statistics.total_paid_receivable_wei += total_newly_paid_receivable; - } - if let Some(response_skeleton) = msg.response_skeleton_opt { - self.ui_message_sub - .as_ref() - .expect("UIGateway is not bound") - .try_send(NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - .expect("UIGateway is dead"); - } - } - - fn handle_sent_payables(&self, sent_payables: SentPayables) { - let (ok, err) = Self::separate_early_errors(&sent_payables, &self.logger); - debug!(self.logger, "We gathered these errors at sending transactions for payable: {:?}, out of the total of {} attempts", err, ok.len() + err.len()); - self.mark_pending_payable(ok); - if !err.is_empty() { - err.into_iter().for_each(|err| - if let Some(hash) = err.carries_transaction_hash(){ - self.discard_incomplete_transaction_with_a_failure(hash) - } else {debug!(self.logger,"Forgetting a transaction attempt that even did not reach the signing stage")}) - } - if let Some(response_skeleton) = &sent_payables.response_skeleton_opt { - self.ui_message_sub - .as_ref() - .expect("UIGateway is not bound") - .try_send(NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - .expect("UIGateway is dead"); - } - } - - fn discard_incomplete_transaction_with_a_failure(&self, hash: H256) { - if let Some(rowid) = self.pending_payable_dao.fingerprint_rowid(hash) { - debug!( - self.logger, - "Deleting an existing fingerprint for a failed transaction {:?}", hash - ); - if let Err(e) = self.pending_payable_dao.delete_fingerprint(rowid) { - panic!("Database unmaintainable; payable fingerprint deletion for transaction {:?} has stayed undone due to {:?}", hash,e) - } - }; - - warning!( - self.logger, - "Failed transaction with a hash '{:?}' but without the record - thrown out", - hash - ) - } - fn handle_report_routing_service_provided_message( &mut self, msg: ReportRoutingServiceProvidedMessage, @@ -988,14 +683,13 @@ impl Accountant { fn process_stats(&self, msg: &UiFinancialsRequest) -> Option { if msg.stats_required { + let financial_statistics = self.financial_statistics(); Some(UiFinancialStatistics { total_unpaid_and_pending_payable_gwei: wei_to_gwei(self.payable_dao.total()), - total_paid_payable_gwei: wei_to_gwei( - self.financial_statistics.total_paid_payable_wei, - ), + total_paid_payable_gwei: wei_to_gwei(financial_statistics.total_paid_payable_wei), total_unpaid_receivable_gwei: wei_to_gwei(self.receivable_dao.total()), total_paid_receivable_gwei: wei_to_gwei( - self.financial_statistics.total_paid_receivable_wei, + financial_statistics.total_paid_receivable_wei, ), }) } else { @@ -1069,279 +763,118 @@ impl Accountant { } } + fn handle_request_of_scan_for_payable( + &mut self, + response_skeleton_opt: Option, + ) { + match self.scanners.payable.begin_scan( + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ) { + Ok(scan_message) => { + self.report_accounts_payable_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"); + } + Err(e) => e.handle_error( + &self.logger, + ScanType::Payables, + response_skeleton_opt.is_some(), + ), + } + } + + fn handle_request_of_scan_for_pending_payable( + &mut self, + response_skeleton_opt: Option, + ) { + match self.scanners.pending_payable.begin_scan( + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ) { + Ok(scan_message) => self + .request_transaction_receipts_subs_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"), + Err(e) => e.handle_error( + &self.logger, + ScanType::PendingPayables, + response_skeleton_opt.is_some(), + ), + } + } + + fn handle_request_of_scan_for_receivable( + &mut self, + response_skeleton_opt: Option, + ) { + match self.scanners.receivable.begin_scan( + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ) { + Ok(scan_message) => self + .retrieve_transactions_sub + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"), + Err(e) => e.handle_error( + &self.logger, + ScanType::Receivables, + response_skeleton_opt.is_some(), + ), + }; + } + fn handle_externally_triggered_scan( - &self, + &mut self, _ctx: &mut Context, scan_type: ScanType, response_skeleton: ResponseSkeleton, ) { match scan_type { - ScanType::Payables => self.scanners.payables.scan(self, Some(response_skeleton)), - ScanType::Receivables => self - .scanners - .receivables - .scan(self, Some(response_skeleton)), - ScanType::PendingPayables => self - .scanners - .pending_payables - .scan(self, Some(response_skeleton)), + ScanType::Payables => self.handle_request_of_scan_for_payable(Some(response_skeleton)), + ScanType::PendingPayables => { + self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)); + } + ScanType::Receivables => { + self.handle_request_of_scan_for_receivable(Some(response_skeleton)) + } } } - fn handle_cancel_pending_transaction(&self, msg: CancelFailedPendingTransaction) { + fn handle_new_pending_payable_fingerprint(&self, msg: PendingPayableFingerprint) { match self .pending_payable_dao - .mark_failure(msg.id.rowid) + .insert_new_fingerprint(msg.hash, msg.amount, msg.timestamp) { - Ok(_) => warning!( + Ok(_) => debug!( self.logger, - "Broken transaction {:?} left with an error mark; you should take over the care of this transaction to make sure your debts will be paid because there is no automated process that can fix this without you", msg.id.hash), - Err(e) => panic!("Unsuccessful attempt for transaction {:?} to mark fatal error at payable fingerprint due to {:?}; database unreliable", msg.id.hash,e), - } - } - - fn handle_confirm_pending_transaction(&mut self, msg: ConfirmPendingTransaction) { - if let Err(e) = self - .payable_dao - .transaction_confirmed(&msg.pending_payable_fingerprint) - { - panic!( - "Was unable to uncheck pending payable '{:?}' after confirmation due to '{:?}'", - msg.pending_payable_fingerprint.hash, e - ) - } else { - self.financial_statistics.total_paid_payable_wei += - msg.pending_payable_fingerprint.amount; - debug!( + "Processed a pending payable fingerprint for '{:?}'", msg.hash + ), + Err(e) => error!( self.logger, - "Confirmation of transaction {:?}; record for payable was modified", - msg.pending_payable_fingerprint.hash - ); - if let Err(e) = self.pending_payable_dao.delete_fingerprint( - msg.pending_payable_fingerprint - .rowid_opt - .expectv("initialized rowid"), - ) { - panic!("Was unable to delete payable fingerprint '{:?}' after successful transaction due to '{:?}'",msg.pending_payable_fingerprint.hash,e) - } else { - info!( - self.logger, - "Transaction {:?} has gone through the whole confirmation process succeeding", - msg.pending_payable_fingerprint.hash - ) - } + "Failed to make a fingerprint for pending payable '{:?}' due to '{:?}'", + msg.hash, + e + ), } } - fn separate_early_errors( - sent_payments: &SentPayables, - logger: &Logger, - ) -> (Vec, Vec) { - sent_payments - .payable - .iter() - .fold((vec![],vec![]),|so_far,payment| { - match payment{ - Ok(payment_sent) => (plus(so_far.0,payment_sent.clone()),so_far.1), - Err(error) => { - logger.warning(|| match &error { - BlockchainError::TransactionFailed { .. } => format!("Encountered transaction error at this end: '{:?}'", error), - x => format!("Outbound transaction failure due to '{:?}'. Please check your blockchain service URL configuration.", x) - }); - (so_far.0,plus(so_far.1,error.clone())) - } - } - }) - } - - fn mark_pending_payable(&self, sent_payments: Vec) { - sent_payments - .into_iter() - .for_each(|payable| { - let rowid = match self.pending_payable_dao.fingerprint_rowid(payable.tx_hash) { - Some(rowid) => rowid, - None => panic!("Payable fingerprint for {:?} doesn't exist but should by now; system unreliable", payable.tx_hash) - }; - match self.payable_dao.as_ref().mark_pending_payable_rowid(&payable.to, rowid ) { - Ok(()) => (), - Err(e) => panic!("Was unable to create a mark in payables for a new pending payable '{:?}' due to '{:?}'", payable.tx_hash, e) - } - debug!(self.logger, "Payable '{:?}' has been marked as pending in the payable table",payable.tx_hash) - }) - } - - fn handle_pending_transaction_with_its_receipt( - &self, - msg: &ReportTransactionReceipts, - ) -> Vec { - fn handle_none_receipt( - payable: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - debug!(logger, - "DEBUG: Accountant: Interpreting a receipt for transaction '{:?}' but none was given; attempt {}, {}ms since sending", - payable.hash, payable.attempt_opt.expectv("initialized attempt"),elapsed_in_ms(payable.timestamp) - ); - PendingTransactionStatus::StillPending(PendingPayableId { - hash: payable.hash, - rowid: payable.rowid_opt.expectv("initialized rowid"), - }) - } - msg.fingerprints_with_receipts - .iter() - .map(|(receipt_opt, fingerprint)| match receipt_opt { - Some(receipt) => { - self.interpret_transaction_receipt(receipt, fingerprint, &self.logger) - } - None => handle_none_receipt(fingerprint, &self.logger), - }) - .collect() - } - - fn interpret_transaction_receipt( - &self, - receipt: &TransactionReceipt, - fingerprint: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - fn handle_none_status( - fingerprint: &PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, - ) -> PendingTransactionStatus { - info!(logger,"Pending transaction '{:?}' couldn't be confirmed at attempt {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt_opt.expectv("initialized attempt"), - elapsed_in_ms(fingerprint.timestamp) - ); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let transaction_id = PendingPayableId { - hash: fingerprint.hash, - rowid: fingerprint.rowid_opt.expectv("initialized rowid"), - }; - if max_pending_interval <= elapsed.as_secs() { - error!(logger,"Pending transaction '{:?}' has exceeded the maximum pending time ({}sec) and the confirmation process is going to be aborted now at the final attempt {}; \ - manual resolution is required from the user to complete the transaction.", fingerprint.hash, max_pending_interval, fingerprint.attempt_opt.expectv("initialized attempt")); - PendingTransactionStatus::Failure(transaction_id) - } else { - PendingTransactionStatus::StillPending(transaction_id) - } - } - fn handle_status_with_success( - fingerprint: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - info!( - logger, - "Transaction '{:?}' has been added to the blockchain; detected locally at attempt {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt_opt.expectv("initialized attempt"), - elapsed_in_ms(fingerprint.timestamp) - ); - PendingTransactionStatus::Confirmed(fingerprint.clone()) - } - fn handle_status_with_failure( - fingerprint: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - error!(logger,"Pending transaction '{:?}' announced as a failure, interpreting attempt {} after {}ms from the sending", - fingerprint.hash, - fingerprint.attempt_opt.expectv("initialized attempt"), - elapsed_in_ms(fingerprint.timestamp) - ); - PendingTransactionStatus::Failure(fingerprint.into()) - } - match receipt.status{ - None => handle_none_status(fingerprint, self.config.when_pending_too_long_sec, logger), - Some(status_code) => - match status_code.as_u64(){ - 0 => handle_status_with_failure(fingerprint, logger), - 1 => handle_status_with_success(fingerprint, logger), - other => unreachable!("tx receipt for pending '{:?}' - tx status: code other than 0 or 1 shouldn't be possible, but was {}", fingerprint.hash, other) - } - } - } - - fn update_payable_fingerprint(&self, pending_payable_id: PendingPayableId) { - match self - .pending_payable_dao - .update_fingerprint(pending_payable_id.rowid) - { - Ok(_) => trace!( - self.logger, - "Updated record for rowid: {} ", - pending_payable_id.rowid - ), - Err(e) => panic!( - "Failure on updating payable fingerprint '{:?}' due to {:?}", - pending_payable_id.hash, e - ), - } - } - - fn process_transactions_by_their_status( - &self, - statuses: Vec, - ctx: &mut Context, - ) { - statuses.into_iter().for_each(|status| { - if let PendingTransactionStatus::StillPending(transaction_id) = status { - self.update_payable_fingerprint(transaction_id) - } else if let PendingTransactionStatus::Failure(transaction_id) = status { - self.order_cancel_failed_transaction(transaction_id, ctx) - } else if let PendingTransactionStatus::Confirmed(fingerprint) = status { - self.order_confirm_transaction(fingerprint, ctx) - } - }); - } - - fn order_cancel_failed_transaction( - &self, - transaction_id: PendingPayableId, - ctx: &mut Context, - ) { - self.confirmation_tools - .notify_cancel_failed_transaction - .notify(CancelFailedPendingTransaction { id: transaction_id }, ctx) - } - - fn order_confirm_transaction( - &self, - pending_payable_fingerprint: PendingPayableFingerprint, - ctx: &mut Context, - ) { - self.confirmation_tools.notify_confirm_transaction.notify( - ConfirmPendingTransaction { - pending_payable_fingerprint, - }, - ctx, - ); - } - - fn handle_new_pending_payable_fingerprint(&self, msg: PendingPayableFingerprint) { - match self - .pending_payable_dao - .insert_new_fingerprint(msg.hash, msg.amount, msg.timestamp) - { - Ok(_) => debug!( - self.logger, - "Processed a pending payable fingerprint for '{:?}'", msg.hash - ), - Err(e) => error!( - self.logger, - "Failed to make a fingerprint for pending payable '{:?}' due to '{:?}'", - msg.hash, - e - ), - } + fn financial_statistics(&self) -> Ref<'_, FinancialStatistics> { + self.financial_statistics.borrow() } } #[derive(Debug, PartialEq, Eq, Clone)] -enum PendingTransactionStatus { +pub enum PendingTransactionStatus { StillPending(PendingPayableId), //updates slightly the record, waits an interval and starts a new round Failure(PendingPayableId), //standard tx failure Confirmed(PendingPayableFingerprint), //tx was fully processed and successful @@ -1364,102 +897,6 @@ impl From<&PendingPayableFingerprint> for PendingPayableId { } } -trait PayableThresholdsGauge { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool; - fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool; - fn calculate_payout_threshold_in_gwei( - &self, - payment_thresholds: &PaymentThresholds, - x: u64, - ) -> u128; - as_any_dcl!(); -} - -#[derive(Default)] -struct PayableThresholdsGaugeReal {} - -impl PayableThresholdsGauge for PayableThresholdsGaugeReal { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool { - age <= limit - } - - fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool { - balance <= limit - } - - fn calculate_payout_threshold_in_gwei( - &self, - payment_thresholds: &PaymentThresholds, - debt_age: u64, - ) -> u128 { - ThresholdUtils::calculate_finite_debt_limit_by_age(payment_thresholds, debt_age) - } - as_any_impl!(); -} - -pub struct ThresholdUtils {} - -impl ThresholdUtils { - pub fn slope(payment_thresholds: &PaymentThresholds) -> i128 { - /* - Slope is an integer, rather than a float, to improve performance. Since there are - computations that divide by the slope, it cannot be allowed to be zero; but since it's - an integer, it can't get any closer to zero than -1. - - If the numerator of this computation is less than the denominator, the slope will be - calculated as 0; therefore, .permanent_debt_allowed_gwei must be less than - .debt_threshold_gwei, so that the numerator will be no greater than -10^9 (-gwei_to_wei(1)), - and the denominator must be less than or equal to 10^9. - - These restrictions do not seem over-strict, since having .permanent_debt_allowed greater - than or equal to .debt_threshold_gwei would result in chaos, and setting - .threshold_interval_sec over 10^9 would mean continuing to declare debts delinquent after - more than 31 years. - - If payment_thresholds are ever configurable by the user, these validations should be done - on the values before they are accepted. - */ - - (gwei_to_wei::(payment_thresholds.permanent_debt_allowed_gwei) - - gwei_to_wei::(payment_thresholds.debt_threshold_gwei)) - / payment_thresholds.threshold_interval_sec as i128 - } - - fn calculate_finite_debt_limit_by_age( - payment_thresholds: &PaymentThresholds, - debt_age_s: u64, - ) -> u128 { - if Self::qualifies_for_permanent_debt_limit(debt_age_s, payment_thresholds) { - return gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei); - }; - let m = ThresholdUtils::slope(payment_thresholds); - let b = ThresholdUtils::compute_theoretical_interception_with_y_axis( - m, - payment_thresholds.maturity_threshold_sec as i128, - gwei_to_wei(payment_thresholds.debt_threshold_gwei), - ); - let y = m * debt_age_s as i128 + b; - y as u128 - } - - fn compute_theoretical_interception_with_y_axis( - m: i128, //is negative - maturity_threshold_sec: i128, - debt_threshold_wei: i128, - ) -> i128 { - debt_threshold_wei - (maturity_threshold_sec * m) - } - - fn qualifies_for_permanent_debt_limit( - debt_age_s: u64, - payment_thresholds: &PaymentThresholds, - ) -> bool { - debt_age_s - > (payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec) - } -} - pub fn sign_conversion>(num: T) -> Result { S::try_from(num).map_err(|_| num) } @@ -1488,13 +925,6 @@ pub fn wei_to_gwei, S: Display + Copy + Div + From(wei.div(S::from(WEIS_OF_GWEI as u32))) } -fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() -} - #[cfg(test)] pub mod check_sqlite_fns { use super::*; @@ -1523,22 +953,22 @@ pub mod check_sqlite_fns { #[cfg(test)] mod tests { use super::*; - use std::cell::RefCell; + use std::any::TypeId; use std::collections::HashMap; use std::ops::{Add, Sub}; - use std::rc::Rc; + use std::sync::Arc; use std::sync::Mutex; - use std::sync::{Arc, MutexGuard}; use std::time::Duration; - use std::time::SystemTime; + use std::vec; use actix::{Arbiter, System}; use ethereum_types::{BigEndianHash, U64}; use ethsign_crypto::Keccak256; + use itertools::Itertools; use log::Level; use masq_lib::constants::{ - MASQ_TOTAL_SUPPLY, REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, - SCAN_ERROR, VALUE_EXCEEDS_ALLOWED_LIMIT, + REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, + VALUE_EXCEEDS_ALLOWED_LIMIT, }; use web3::types::U256; @@ -1552,120 +982,54 @@ mod tests { use crate::accountant::dao_utils::from_time_t; use crate::accountant::dao_utils::{to_time_t, CustomQuery}; - use crate::accountant::payable_dao::PayableDaoError; + use crate::accountant::payable_dao::{PayableAccount, PayableDaoError}; use crate::accountant::pending_payable_dao::PendingPayableDaoError; use crate::accountant::receivable_dao::ReceivableAccount; + use crate::accountant::scanners::{BeginScanError, NullScanner, ScannerMock}; + use crate::accountant::test_utils::DaoWithDestination::{ + AccountantBodyDest, PayableScannerDest, PendingPayableScannerDest, ReceivableScannerDest, + }; use crate::accountant::test_utils::{ - bc_from_ac_plus_earning_wallet, bc_from_ac_plus_wallets, make_payable_account, - make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, + bc_from_earning_wallet, bc_from_wallets, make_payables, BannedDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock, }; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; - use crate::accountant::tools::accountant_tools::{NullScanner, ReceivablesScanner}; use crate::accountant::Accountant; use crate::blockchain::blockchain_bridge::BlockchainBridge; use crate::blockchain::blockchain_interface::BlockchainError; use crate::blockchain::blockchain_interface::BlockchainTransaction; use crate::blockchain::test_utils::BlockchainInterfaceMock; use crate::blockchain::tool_wrappers::SendTransactionToolsWrapperNull; - use crate::bootstrapper::BootstrapperConfig; use crate::sub_lib::accountant::{ - ExitServiceConsumed, RoutingServiceConsumed, ScanIntervals, DEFAULT_PAYMENT_THRESHOLDS, + ExitServiceConsumed, PaymentThresholds, RoutingServiceConsumed, ScanIntervals, + DEFAULT_PAYMENT_THRESHOLDS, }; use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; - use crate::sub_lib::utils::{NotifyHandleReal, NotifyLaterHandleReal}; + use crate::sub_lib::utils::NotifyLaterHandleReal; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; use crate::test_utils::unshared_test_utils::{ - make_accountant_config_null, make_populated_accountant_config_with_defaults, - prove_that_crash_request_handler_is_hooked_up, NotifyHandleMock, NotifyLaterHandleMock, - SystemKillerActor, + make_bc_with_defaults, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, + NotifyLaterHandleMock, SystemKillerActor, }; use crate::test_utils::{make_paying_wallet, make_wallet}; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; use masq_lib::ui_gateway::MessagePath::Conversation; use web3::types::{TransactionReceipt, H256}; - #[derive(Default)] - struct PayableThresholdsGaugeMock { - is_innocent_age_params: Arc>>, - is_innocent_age_results: RefCell>, - is_innocent_balance_params: Arc>>, - is_innocent_balance_results: RefCell>, - calculate_payout_threshold_in_gwei_params: Arc>>, - calculate_payout_threshold_in_gwei_results: RefCell>, - } - - impl PayableThresholdsGauge for PayableThresholdsGaugeMock { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool { - self.is_innocent_age_params - .lock() - .unwrap() - .push((age, limit)); - self.is_innocent_age_results.borrow_mut().remove(0) - } - - fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool { - self.is_innocent_balance_params - .lock() - .unwrap() - .push((balance, limit)); - self.is_innocent_balance_results.borrow_mut().remove(0) - } - - fn calculate_payout_threshold_in_gwei( - &self, - payment_thresholds: &PaymentThresholds, - x: u64, - ) -> u128 { - self.calculate_payout_threshold_in_gwei_params - .lock() - .unwrap() - .push((*payment_thresholds, x)); - self.calculate_payout_threshold_in_gwei_results - .borrow_mut() - .remove(0) - } - } - - impl PayableThresholdsGaugeMock { - fn is_innocent_age_params(mut self, params: &Arc>>) -> Self { - self.is_innocent_age_params = params.clone(); - self - } - - fn is_innocent_age_result(self, result: bool) -> Self { - self.is_innocent_age_results.borrow_mut().push(result); - self - } - - fn is_innocent_balance_params(mut self, params: &Arc>>) -> Self { - self.is_innocent_balance_params = params.clone(); - self - } - - fn is_innocent_balance_result(self, result: bool) -> Self { - self.is_innocent_balance_results.borrow_mut().push(result); - self - } - - fn calculate_payout_threshold_in_gwei_params( - mut self, - params: &Arc>>, - ) -> Self { - self.calculate_payout_threshold_in_gwei_params = params.clone(); - self - } + impl Handler> for Accountant { + type Result = (); - fn calculate_payout_threshold_in_gwei_result(self, result: u128) -> Self { - self.calculate_payout_threshold_in_gwei_results - .borrow_mut() - .push(result); - self + fn handle( + &mut self, + msg: AssertionsMessage, + _ctx: &mut Self::Context, + ) -> Self::Result { + (msg.assertions)(self) } } @@ -1677,132 +1041,127 @@ mod tests { #[test] fn new_calls_factories_properly() { - let mut config = BootstrapperConfig::new(); - config.accountant_config_opt = Some(make_accountant_config_null()); - let payable_dao_factory_called = Rc::new(RefCell::new(false)); - let payable_dao = PayableDaoMock::new(); - let payable_dao_factory = - PayableDaoFactoryMock::new(payable_dao).called(&payable_dao_factory_called); - let receivable_dao_factory_called = Rc::new(RefCell::new(false)); - let receivable_dao = ReceivableDaoMock::new(); - let receivable_dao_factory = - ReceivableDaoFactoryMock::new(receivable_dao).called(&receivable_dao_factory_called); - let pending_payable_dao_factory_called = Rc::new(RefCell::new(false)); - let pending_payable_dao = PendingPayableDaoMock::default(); - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new(pending_payable_dao) - .called(&pending_payable_dao_factory_called); - let banned_dao_factory_called = Rc::new(RefCell::new(false)); - let banned_dao = BannedDaoMock::new(); - let banned_dao_factory = - BannedDaoFactoryMock::new(banned_dao).called(&banned_dao_factory_called); + let config = make_bc_with_defaults(); + let payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let receivable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let banned_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao_factory = PayableDaoFactoryMock::new() + .make_params(&payable_dao_factory_params_arc) + .make_result(PayableDaoMock::new()) // For Accountant + .make_result(PayableDaoMock::new()) // For Payable Scanner + .make_result(PayableDaoMock::new()); // For PendingPayable Scanner + let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() + .make_params(&pending_payable_dao_factory_params_arc) + .make_result(PendingPayableDaoMock::new()) // For Accountant + .make_result(PendingPayableDaoMock::new()) // For Payable Scanner + .make_result(PendingPayableDaoMock::new()); // For PendingPayable Scanner + let receivable_dao_factory = ReceivableDaoFactoryMock::new() + .make_params(&receivable_dao_factory_params_arc) + .make_result(ReceivableDaoMock::new()) // For Accountant + .make_result(ReceivableDaoMock::new()); // For Receivable Scanner + let banned_dao_factory = BannedDaoFactoryMock::new() + .make_params(&banned_dao_factory_params_arc) + .make_result(BannedDaoMock::new()); // For Receivable Scanner let _ = Accountant::new( - &config, - Box::new(payable_dao_factory), - Box::new(receivable_dao_factory), - Box::new(pending_payable_dao_factory), - Box::new(banned_dao_factory), + config, + DaoFactories { + payable_dao_factory: Box::new(payable_dao_factory), + pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + receivable_dao_factory: Box::new(receivable_dao_factory), + banned_dao_factory: Box::new(banned_dao_factory), + }, ); - assert_eq!(payable_dao_factory_called.as_ref(), &RefCell::new(true)); - assert_eq!(receivable_dao_factory_called.as_ref(), &RefCell::new(true)); assert_eq!( - pending_payable_dao_factory_called.as_ref(), - &RefCell::new(true) + *payable_dao_factory_params_arc.lock().unwrap(), + vec![(), (), ()] + ); + assert_eq!( + *pending_payable_dao_factory_params_arc.lock().unwrap(), + vec![(), (), ()] + ); + assert_eq!( + *receivable_dao_factory_params_arc.lock().unwrap(), + vec![(), ()] ); - assert_eq!(banned_dao_factory_called.as_ref(), &RefCell::new(true)); + assert_eq!(*banned_dao_factory_params_arc.lock().unwrap(), vec![()]); } #[test] fn accountant_have_proper_defaulted_values() { - let mut bootstrapper_config = BootstrapperConfig::new(); - bootstrapper_config.accountant_config_opt = - Some(make_populated_accountant_config_with_defaults()); - let payable_dao_factory = Box::new(PayableDaoFactoryMock::new(PayableDaoMock::new())); - let receivable_dao_factory = - Box::new(ReceivableDaoFactoryMock::new(ReceivableDaoMock::new())); - let pending_payable_dao_factory = Box::new(PendingPayableDaoFactoryMock::new( - PendingPayableDaoMock::default(), - )); - let banned_dao_factory = Box::new(BannedDaoFactoryMock::new(BannedDaoMock::new())); + let bootstrapper_config = make_bc_with_defaults(); + let payable_dao_factory = Box::new( + PayableDaoFactoryMock::new() + .make_result(PayableDaoMock::new()) // For Accountant + .make_result(PayableDaoMock::new()) // For Payable Scanner + .make_result(PayableDaoMock::new()), // For PendingPayable Scanner + ); + let pending_payable_dao_factory = Box::new( + PendingPayableDaoFactoryMock::new() + .make_result(PendingPayableDaoMock::new()) // For Accountant + .make_result(PendingPayableDaoMock::new()) // For Payable Scanner + .make_result(PendingPayableDaoMock::new()), // For PendingPayable Scanner + ); + let receivable_dao_factory = Box::new( + ReceivableDaoFactoryMock::new() + .make_result(ReceivableDaoMock::new()) // For Accountant + .make_result(ReceivableDaoMock::new()), // For Scanner + ); + let banned_dao_factory = + Box::new(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); let result = Accountant::new( - &bootstrapper_config, - payable_dao_factory, - receivable_dao_factory, - pending_payable_dao_factory, - banned_dao_factory, + bootstrapper_config, + DaoFactories { + payable_dao_factory, + pending_payable_dao_factory, + receivable_dao_factory, + banned_dao_factory, + }, ); - let transaction_confirmation_tools = result.confirmation_tools; - transaction_confirmation_tools - .notify_confirm_transaction - .as_any() - .downcast_ref::>() - .unwrap(); - transaction_confirmation_tools - .notify_cancel_failed_transaction - .as_any() - .downcast_ref::>() - .unwrap(); - transaction_confirmation_tools - .notify_later_scan_for_pending_payable + let financial_statistics = result.financial_statistics().clone(); + let notify_later = result.notify_later; + notify_later + .scan_for_pending_payable .as_any() .downcast_ref::>() .unwrap(); - transaction_confirmation_tools - .notify_later_scan_for_payable + notify_later + .scan_for_payable .as_any() .downcast_ref::>() .unwrap(); - transaction_confirmation_tools - .notify_later_scan_for_receivable - .as_any() - .downcast_ref::>() - .unwrap(); - //testing presence of real scanners, there is a different test covering them all - result - .scanners - .receivables - .as_any() - .downcast_ref::() - .unwrap(); - result - .payable_threshold_gauge + notify_later + .scan_for_receivable .as_any() - .downcast_ref::() - .unwrap(); - assert_eq!(result.crashable, false); - assert_eq!(result.financial_statistics.total_paid_receivable_wei, 0); - assert_eq!(result.financial_statistics.total_paid_payable_wei, 0); + .downcast_ref::>(); result .message_id_generator .as_any() .downcast_ref::() .unwrap(); + assert_eq!(result.crashable, false); + assert_eq!(financial_statistics.total_paid_receivable_wei, 0); + assert_eq!(financial_statistics.total_paid_payable_wei, 0); } #[test] fn scan_receivables_request() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: Default::default(), - }, - make_wallet("earning_wallet"), - ); + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .receivable_dao(receivable_dao) + .receivable_daos(vec![ReceivableScannerDest(receivable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let subject_addr = subject.start(); @@ -1830,7 +1189,7 @@ mod tests { recipient: make_wallet("earning_wallet"), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, - context_id: 4321 + context_id: 4321, }), } ); @@ -1838,19 +1197,13 @@ mod tests { #[test] fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("earning_wallet"), - ); + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); + config.suppress_initial_scans = true; let subject = AccountantBuilder::default() .bootstrapper_config(config) .build(); @@ -1884,19 +1237,7 @@ mod tests { #[test] fn scan_payables_request() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("some_wallet_address"), - ); + let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); let payable_account = PayableAccount { wallet: make_wallet("wallet"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), @@ -1909,7 +1250,7 @@ mod tests { PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) + .payable_daos(vec![PayableScannerDest(payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let subject_addr = subject.start(); @@ -1937,7 +1278,7 @@ mod tests { accounts: vec![payable_account], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, - context_id: 4321 + context_id: 4321, }), } ); @@ -1945,19 +1286,7 @@ mod tests { #[test] fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("earning_wallet"), - ); + let config = bc_from_earning_wallet(make_wallet("earning_wallet")); let subject = AccountantBuilder::default() .bootstrapper_config(config) .build(); @@ -1991,19 +1320,13 @@ mod tests { #[test] fn scan_pending_payables_request() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("some_wallet_address"), - ); + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.suppress_initial_scans = true; + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); let fingerprint = PendingPayableFingerprint { rowid_opt: Some(1234), timestamp: SystemTime::now(), @@ -2016,7 +1339,7 @@ mod tests { .return_all_fingerprints_result(vec![fingerprint.clone()]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .pending_payable_dao(pending_payable_dao) + .pending_payable_daos(vec![PendingPayableScannerDest(pending_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let subject_addr = subject.start(); @@ -2044,27 +1367,75 @@ mod tests { pending_payable: vec![fingerprint], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, - context_id: 4321 + context_id: 4321, }), } ); } + #[test] + fn scan_request_from_ui_is_handled_in_case_the_scan_is_already_running() { + init_test_logging(); + let test_name = "scan_request_from_ui_is_handled_in_case_the_scan_is_already_running"; + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.suppress_initial_scans = true; + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(1234), + timestamp: SystemTime::now(), + hash: Default::default(), + attempt_opt: Some(1), + amount: 1_000_000, + process_error: None, + }; + let pending_payable_dao = + PendingPayableDaoMock::default().return_all_fingerprints_result(vec![fingerprint]); + let subject = AccountantBuilder::default() + .bootstrapper_config(config) + .logger(Logger::new(test_name)) + .pending_payable_daos(vec![PendingPayableScannerDest(pending_payable_dao)]) + .build(); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let first_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: ScanType::PendingPayables, + } + .tmb(4321), + }; + let second_message = first_message.clone(); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + subject_addr.try_send(first_message).unwrap(); + + subject_addr.try_send(second_message).unwrap(); + + System::current().stop(); + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {}: PendingPayables scan was already initiated", + test_name + )); + assert_eq!(blockchain_bridge_recording.len(), 1); + } + #[test] fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("earning_wallet"), - ); + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); let subject = AccountantBuilder::default() .bootstrapper_config(config) .build(); @@ -2112,12 +1483,9 @@ mod tests { .mark_pending_payable_rowid_result(Ok(())); let system = System::new("accountant_calls_payable_dao_to_mark_pending_payable"); let accountant = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![PayableScannerDest(payable_dao)]) + .pending_payable_daos(vec![PayableScannerDest(pending_payable_dao)]) .build(); let expected_payable = Payable::new( expected_wallet.clone(), @@ -2153,7 +1521,7 @@ mod tests { let system = System::new("sent payable failure without backup"); let pending_payable_dao = PendingPayableDaoMock::default().fingerprint_rowid_result(None); let accountant = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) + .pending_payable_daos(vec![PayableScannerDest(pending_payable_dao)]) .build(); let hash = H256::from_uint(&U256::from(12345)); let sent_payable = SentPayables { @@ -2207,8 +1575,8 @@ mod tests { .delete_fingerprint_params(&delete_fingerprint_params_arc) .delete_fingerprint_result(Ok(())); let subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .payable_daos(vec![PayableScannerDest(payable_dao)]) + .pending_payable_daos(vec![PayableScannerDest(pending_payable_dao)]) // For Scanner .build(); let wallet = make_wallet("blah"); let hash_tx_1 = H256::from_uint(&U256::from(5555)); @@ -2265,43 +1633,19 @@ mod tests { fn accountant_sends_report_accounts_payable_to_blockchain_bridge_when_qualified_payable_found() { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let accounts = vec![ - PayableAccount { - wallet: make_wallet("blah"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 55), - last_paid_timestamp: from_time_t( - to_time_t(SystemTime::now()) - - checked_conversion::( - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec, - ) - - 5, - ), - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("foo"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 66), - last_paid_timestamp: from_time_t( - to_time_t(SystemTime::now()) - - checked_conversion::( - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec, - ) - - 500, - ), - pending_payable_opt: None, - }, - ]; - let payable_dao = PayableDaoMock::new().non_pending_payables_result(accounts.clone()); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let (qualified_payables, _, all_non_pending_payables) = + make_payables(now, &payment_thresholds); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let system = System::new("report_accounts_payable forwarded to blockchain_bridge"); let mut subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .payable_dao(payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![PayableScannerDest(payable_dao)]) .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.receivables = Box::new(NullScanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); + subject.scanners.receivable = Box::new(NullScanner::new()); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2315,18 +1659,13 @@ mod tests { system.run(); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recorder.len(), 1); - let report_accounts_payables_msgs: Vec<&ReportAccountsPayable> = (0 - ..blockchain_bridge_recorder.len()) - .flat_map(|index| { - blockchain_bridge_recorder.get_record_opt::(index) - }) - .collect(); + let message = blockchain_bridge_recorder.get_record::(0); assert_eq!( - report_accounts_payables_msgs, - vec![&ReportAccountsPayable { - accounts, - response_skeleton_opt: None - }] + message, + &ReportAccountsPayable { + accounts: qualified_payables, + response_skeleton_opt: None, + } ); } @@ -2338,20 +1677,15 @@ mod tests { let system = System::new( "accountant_sends_a_request_to_blockchain_bridge_to_scan_for_received_payments", ); - let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let mut subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - )) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) + .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) + .receivable_daos(vec![ReceivableScannerDest(receivable_dao)]) .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.payables = Box::new(NullScanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); + subject.scanners.payable = Box::new(NullScanner::new()); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2395,12 +1729,8 @@ mod tests { .more_money_received_parameters(&more_money_received_params_arc) .more_money_received_result(Ok(())); let accountant = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - )) - .payable_dao(PayableDaoMock::new().non_pending_payables_result(vec![])) - .receivable_dao(receivable_dao) + .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) + .receivable_daos(vec![ReceivableScannerDest(receivable_dao)]) .build(); let system = System::new("accountant_receives_new_payments_to_the_receivables_dao"); let subject = accountant.start(); @@ -2425,43 +1755,30 @@ mod tests { #[test] fn accountant_scans_after_startup() { init_test_logging(); - let return_all_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let payable_params_arc = Arc::new(Mutex::new(vec![])); let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); let paid_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, _) = make_recorder(); + let earning_wallet = make_wallet("earning"); let system = System::new("accountant_scans_after_startup"); - let config = bc_from_ac_plus_wallets( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(100), //making sure we cannot enter the first repeated scanning - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_millis(100), //except here, where we use it to stop the system - }, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - }, - make_wallet("buy"), - make_wallet("hi"), - ); - let mut pending_payable_dao = PendingPayableDaoMock::default() - .return_all_fingerprints_params(&return_all_fingerprints_params_arc) + let config = bc_from_wallets(make_wallet("buy"), earning_wallet.clone()); + let payable_dao = PayableDaoMock::new() + .non_pending_payables_params(&payable_params_arc) + .non_pending_payables_result(vec![]); + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_fingerprints_params(&pending_payable_params_arc) .return_all_fingerprints_result(vec![]); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_parameters(&new_delinquencies_params_arc) .new_delinquencies_result(vec![]) .paid_delinquencies_parameters(&paid_delinquencies_params_arc) .paid_delinquencies_result(vec![]); - let payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) - .pending_payable_dao(pending_payable_dao) + .payable_daos(vec![PayableScannerDest(payable_dao)]) + .pending_payable_daos(vec![PendingPayableScannerDest(pending_payable_dao)]) + .receivable_daos(vec![ReceivableScannerDest(receivable_dao)]) .build(); let peer_actors = peer_actors_builder() .blockchain_bridge(blockchain_bridge) @@ -2472,129 +1789,82 @@ mod tests { send_start_message!(subject_subs); + System::current().stop(); system.run(); - let tlh = TestLogHandler::new(); - tlh.await_log_containing("INFO: Accountant: Scanning for payables", 1000); - tlh.exists_log_containing(&format!( - "INFO: Accountant: Scanning for receivables to {}", - make_wallet("hi") - )); - tlh.exists_log_containing("INFO: Accountant: Scanning for delinquencies"); - tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); - //some more weak proofs but still good enough - //proof of calling a piece of scan_for_pending_payable - let return_all_fingerprints_params = return_all_fingerprints_params_arc.lock().unwrap(); - //the last ends this test calling System::current.stop() - assert_eq!(*return_all_fingerprints_params, vec![(), ()]); - //proof of calling a piece of scan_for_payable() - let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); - assert_eq!(*non_pending_payables_params, vec![()]); + let payable_params = payable_params_arc.lock().unwrap(); + let pending_payable_params = pending_payable_params_arc.lock().unwrap(); //proof of calling pieces of scan_for_delinquencies() let mut new_delinquencies_params = new_delinquencies_params_arc.lock().unwrap(); let (captured_timestamp, captured_curves) = new_delinquencies_params.remove(0); + let paid_delinquencies_params = paid_delinquencies_params_arc.lock().unwrap(); + assert_eq!(*payable_params, vec![()]); + assert_eq!(*pending_payable_params, vec![()]); assert!(new_delinquencies_params.is_empty()); assert!( captured_timestamp < SystemTime::now() && captured_timestamp >= from_time_t(to_time_t(SystemTime::now()) - 5) ); - assert_eq!(captured_curves, *DEFAULT_PAYMENT_THRESHOLDS); - let paid_delinquencies_params = paid_delinquencies_params_arc.lock().unwrap(); + assert_eq!(captured_curves, PaymentThresholds::default()); assert_eq!(paid_delinquencies_params.len(), 1); - assert_eq!(paid_delinquencies_params[0], *DEFAULT_PAYMENT_THRESHOLDS); + assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); + let tlh = TestLogHandler::new(); + tlh.exists_log_containing("INFO: Accountant: Scanning for payables"); + tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); + tlh.exists_log_containing(&format!( + "INFO: Accountant: Scanning for receivables to {}", + earning_wallet + )); + tlh.exists_log_containing("INFO: Accountant: Scanning for delinquencies"); } #[test] fn periodical_scanning_for_receivables_and_delinquencies_works() { init_test_logging(); - let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); - let ban_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_receivables_and_delinquencies_works"; + let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_receivable_params_arc = Arc::new(Mutex::new(vec![])); - let earning_wallet = make_wallet("earner3000"); - let wallet_to_be_banned = make_wallet("bad_luck"); - let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(100), - receivable_scan_interval: Duration::from_millis(99), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - earning_wallet.clone(), - ); - let new_delinquent_account = ReceivableAccount { - wallet: wallet_to_be_banned.clone(), - balance_wei: 4567, - last_received_timestamp: from_time_t(200_000_000), - }; - let system = System::new("periodical_scanning_for_receivables_and_delinquencies_works"); - let banned_dao = BannedDaoMock::new().ban_parameters(&ban_params_arc); - let mut receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_parameters(&new_delinquencies_params_arc) - //this is the immediate try, not with our interval - .new_delinquencies_result(vec![]) - //after the interval we actually process data - .new_delinquencies_result(vec![new_delinquent_account]) - .paid_delinquencies_result(vec![]) - .paid_delinquencies_result(vec![]) - .paid_delinquencies_result(vec![]); - receivable_dao.have_new_delinquencies_shutdown_the_system = true; + let system = System::new(test_name); + SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let receivable_scanner = ScannerMock::new() + .begin_scan_params(&begin_scan_params_arc) + .begin_scan_result(Err(BeginScanError::NothingToProcess)) + .begin_scan_result(Ok(RetrieveTransactions { + recipient: make_wallet("some_recipient"), + response_skeleton_opt: None, + })) + .stop_the_system(); + let mut config = make_bc_with_defaults(); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_secs(100), + receivable_scan_interval: Duration::from_millis(99), + pending_payable_scan_interval: Duration::from_secs(100), + }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .receivable_dao(receivable_dao) - .banned_dao(banned_dao) + .logger(Logger::new(test_name)) .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.payables = Box::new(NullScanner); - subject.confirmation_tools.notify_later_scan_for_receivable = Box::new( + subject.scanners.payable = Box::new(NullScanner::new()); // Skipping + subject.scanners.pending_payable = Box::new(NullScanner::new()); // Skipping + subject.scanners.receivable = Box::new(receivable_scanner); + subject.notify_later.scan_for_receivable = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_receivable_params_arc) .permit_to_send_out(), ); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - let subject_addr: Addr = subject.start(); + let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); + let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); system.run(); - let retrieve_transactions_recording = blockchain_bridge_recording.lock().unwrap(); - assert_eq!(retrieve_transactions_recording.len(), 3); - let retrieve_transactions_msgs: Vec<&RetrieveTransactions> = (0 - ..retrieve_transactions_recording.len()) - .map(|index| retrieve_transactions_recording.get_record::(index)) - .collect(); - assert_eq!( - *retrieve_transactions_msgs, - vec![ - &RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt: None, - }, - &RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt: None, - }, - &RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt: None, - } - ] - ); - //sadly I cannot effectively assert on the exact params - //they are a) real timestamp of now, b) constant payment_thresholds - //the Rust type system gives me enough support to be okay with counting occurrences - let new_delinquencies_params = new_delinquencies_params_arc.lock().unwrap(); - assert_eq!(new_delinquencies_params.len(), 3); //the third one is the signal to shut the system down - let ban_params = ban_params_arc.lock().unwrap(); - assert_eq!(*ban_params, vec![wallet_to_be_banned]); + let begin_scan_params = begin_scan_params_arc.lock().unwrap(); let notify_later_receivable_params = notify_later_receivable_params_arc.lock().unwrap(); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: There was nothing to process during Receivables scan." + )); + assert_eq!(begin_scan_params.len(), 2); assert_eq!( *notify_later_receivable_params, vec![ @@ -2610,94 +1880,59 @@ mod tests { }, Duration::from_millis(99) ), - ( - ScanForReceivables { - response_skeleton_opt: None - }, - Duration::from_millis(99) - ) ] ) } #[test] fn periodical_scanning_for_pending_payable_works() { - //in the very first round we scan without waiting but we cannot find any pending payable init_test_logging(); - let return_all_pending_payable_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_pending_payable_works"; + let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let system = - System::new("accountant_payable_scan_timer_triggers_scanning_for_pending_payable"); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(100), - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_millis(98), - }, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - }, - make_wallet("hi"), - ); - // slightly above minimum balance, to the right of the curve (time intersection) - let pending_payable_fingerprint_record = PendingPayableFingerprint { - rowid_opt: Some(45454), - timestamp: SystemTime::now(), - hash: H256::from_uint(&U256::from(565)), - attempt_opt: Some(1), - amount: 4589, - process_error: None, - }; - let mut pending_payable_dao = PendingPayableDaoMock::default() - .return_all_fingerprints_params(&return_all_pending_payable_fingerprints_params_arc) - .return_all_fingerprints_result(vec![]) - .return_all_fingerprints_result(vec![pending_payable_fingerprint_record.clone()]); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); + let system = System::new(test_name); + SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let pending_payable_scanner = ScannerMock::new() + .begin_scan_params(&begin_scan_params_arc) + .begin_scan_result(Err(BeginScanError::NothingToProcess)) + .begin_scan_result(Ok(RequestTransactionReceipts { + pending_payable: vec![], + response_skeleton_opt: None, + })) + .stop_the_system(); + let mut config = make_bc_with_defaults(); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_secs(100), + receivable_scan_interval: Duration::from_secs(100), + pending_payable_scan_interval: Duration::from_millis(98), + }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .pending_payable_dao(pending_payable_dao) + .logger(Logger::new(test_name)) .build(); - subject.scanners.receivables = Box::new(NullScanner); //skipping - subject.scanners.payables = Box::new(NullScanner); //skipping - subject - .confirmation_tools - .notify_later_scan_for_pending_payable = Box::new( + subject.scanners.payable = Box::new(NullScanner::new()); //skipping + subject.scanners.pending_payable = Box::new(pending_payable_scanner); + subject.scanners.receivable = Box::new(NullScanner::new()); //skipping + subject.notify_later.scan_for_pending_payable = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_pending_payable_params_arc) .permit_to_send_out(), ); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); + let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); system.run(); - let return_all_pending_payable_fingerprints = - return_all_pending_payable_fingerprints_params_arc - .lock() - .unwrap(); - //the third attempt is the one where the queue is empty and System::current.stop() ends the cycle - assert_eq!(*return_all_pending_payable_fingerprints, vec![(), (), ()]); - let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recorder.len(), 1); - let request_transaction_receipt_msg = - blockchain_bridge_recorder.get_record::(0); - assert_eq!( - request_transaction_receipt_msg, - &RequestTransactionReceipts { - pending_payable: vec![pending_payable_fingerprint_record], - response_skeleton_opt: None, - } - ); + let begin_scan_params = begin_scan_params_arc.lock().unwrap(); let notify_later_pending_payable_params = notify_later_pending_payable_params_arc.lock().unwrap(); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." + )); + assert_eq!(begin_scan_params.len(), 2); assert_eq!( *notify_later_pending_payable_params, vec![ @@ -2713,90 +1948,59 @@ mod tests { }, Duration::from_millis(98) ), - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ) ] ) } #[test] - fn accountant_payable_scan_timer_triggers_periodical_scanning_for_payables() { - //in the very first round we scan without waiting but we cannot find any payable records + fn periodical_scanning_for_payable_works() { init_test_logging(); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_payable_works"; + let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_payables_params_arc = Arc::new(Mutex::new(vec![])); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let system = System::new("accountant_payable_scan_timer_triggers_scanning_for_payables"); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(97), - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("hi"), - ); - let now = to_time_t(SystemTime::now()); - // slightly above minimum balance, to the right of the curve (time intersection) - let account = PayableAccount { - wallet: make_wallet("wallet"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 5), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 10, - ), - ), - pending_payable_opt: None, - }; - let mut payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![]) - .non_pending_payables_result(vec![account.clone()]); - payable_dao.have_non_pending_payable_shut_down_the_system = true; - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); + let system = System::new(test_name); + SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let payable_scanner = ScannerMock::new() + .begin_scan_params(&begin_scan_params_arc) + .begin_scan_result(Err(BeginScanError::NothingToProcess)) + .begin_scan_result(Ok(ReportAccountsPayable { + accounts: vec![], + response_skeleton_opt: None, + })) + .stop_the_system(); + let mut config = bc_from_earning_wallet(make_wallet("hi")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(97), + receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + pending_payable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) + .logger(Logger::new(test_name)) .build(); - subject.scanners.pending_payables = Box::new(NullScanner); //skipping - subject.scanners.receivables = Box::new(NullScanner); //skipping - subject.confirmation_tools.notify_later_scan_for_payable = Box::new( + subject.scanners.payable = Box::new(payable_scanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); //skipping + subject.scanners.receivable = Box::new(NullScanner::new()); //skipping + subject.notify_later.scan_for_payable = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_payables_params_arc) .permit_to_send_out(), ); let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); + let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); system.run(); - let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); - //the third attempt is the one where the queue is empty and System::current.stop() ends the cycle - assert_eq!(*non_pending_payables_params, vec![(), (), ()]); - let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recorder.len(), 1); - let report_accounts_payables_msg = - blockchain_bridge_recorder.get_record::(0); - assert_eq!( - report_accounts_payables_msg, - &ReportAccountsPayable { - accounts: vec![account], - response_skeleton_opt: None, - } - ); + //the second attempt is the one where the queue is empty and System::current.stop() ends the cycle + let begin_scan_params = begin_scan_params_arc.lock().unwrap(); let notify_later_payables_params = notify_later_payables_params_arc.lock().unwrap(); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: There was nothing to process during Payables scan." + )); + assert_eq!(begin_scan_params.len(), 2); assert_eq!( *notify_later_payables_params, vec![ @@ -2812,12 +2016,6 @@ mod tests { }, Duration::from_millis(97) ), - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ) ] ) } @@ -2825,27 +2023,19 @@ mod tests { #[test] fn start_message_triggers_no_scans_in_suppress_mode() { init_test_logging(); - let system = System::new("start_message_triggers_no_scans_in_suppress_mode"); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(1), - receivable_scan_interval: Duration::from_millis(1), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("hi"), - ); - let payable_dao = PayableDaoMock::new(); // No payables: demanding one would cause a panic - let receivable_dao = ReceivableDaoMock::new(); // No delinquencies: demanding one would cause a panic + let test_name = "start_message_triggers_no_scans_in_suppress_mode"; + let system = System::new(test_name); + let mut config = bc_from_earning_wallet(make_wallet("hi")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(100), + receivable_scan_interval: Duration::from_millis(100), + pending_payable_scan_interval: Duration::from_millis(100), + }); + config.suppress_initial_scans = true; let peer_actors = peer_actors_builder().build(); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) + .logger(Logger::new(test_name)) .build(); let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); @@ -2857,16 +2047,13 @@ mod tests { assert_eq!(system.run(), 0); // no panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes TestLogHandler::new().exists_log_containing( - "Started with --scans off; declining to begin database and blockchain scans", + &format!("{test_name}: Started with --scans off; declining to begin database and blockchain scans"), ); } #[test] fn scan_for_payables_message_does_not_trigger_payment_for_balances_below_the_curve() { init_test_logging(); - let accountant_config = make_populated_accountant_config_with_defaults(); - let config = bc_from_ac_plus_earning_wallet(accountant_config, make_wallet("mine")); - let now = to_time_t(SystemTime::now()); let payment_thresholds = PaymentThresholds { threshold_interval_sec: 2_592_000, debt_threshold_gwei: 1_000_000_000, @@ -2875,6 +2062,8 @@ mod tests { permanent_debt_allowed_gwei: 10_000_000, unban_below_gwei: 10_000_000, }; + let config = bc_from_earning_wallet(make_wallet("mine")); + let now = to_time_t(SystemTime::now()); let accounts = vec![ // below minimum balance, to the right of time intersection (inside buffer zone) PayableAccount { @@ -2922,12 +2111,14 @@ mod tests { blockchain_bridge_addr.recipient::(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) + .payable_daos(vec![PayableScannerDest(payable_dao)]) .build(); - subject.report_accounts_payable_sub = Some(report_accounts_payable_sub); - subject.config.payment_thresholds = payment_thresholds; + subject.report_accounts_payable_sub_opt = Some(report_accounts_payable_sub); - subject.scan_for_payables(None); + let _result = subject + .scanners + .payable + .begin_scan(SystemTime::now(), None, &subject.logger); System::current().stop_with_code(0); system.run(); @@ -2938,19 +2129,12 @@ mod tests { #[test] fn scan_for_payable_message_triggers_payment_for_balances_over_the_curve() { init_test_logging(); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(50_000), - payable_scan_interval: Duration::from_millis(100), - receivable_scan_interval: Duration::from_secs(50_000), - }, - payment_thresholds: DEFAULT_PAYMENT_THRESHOLDS.clone(), - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - }, - make_wallet("mine"), - ); + let mut config = bc_from_earning_wallet(make_wallet("mine")); + config.scan_intervals_opt = Some(ScanIntervals { + pending_payable_scan_interval: Duration::from_secs(50_000), + payable_scan_interval: Duration::from_secs(50_000), + receivable_scan_interval: Duration::from_secs(50_000), + }); let now = to_time_t(SystemTime::now()); let accounts = vec![ // slightly above minimum balance, to the right of the curve (time intersection) @@ -2980,11 +2164,12 @@ mod tests { pending_payable_opt: None, }, ]; - let mut payable_dao = PayableDaoMock::default() - .non_pending_payables_result(accounts.clone()) - .non_pending_payables_result(vec![]); - payable_dao.have_non_pending_payable_shut_down_the_system = true; + let payable_dao = PayableDaoMock::default().non_pending_payables_result(accounts.clone()); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); + let mut expected_messages_by_type = HashMap::new(); + expected_messages_by_type.insert(TypeId::of::(), 1); + let blockchain_bridge = blockchain_bridge + .stop_after_messages_and_start_system_killer(expected_messages_by_type); let system = System::new("scan_for_payable_message_triggers_payment_for_balances_over_the_curve"); let peer_actors = peer_actors_builder() @@ -2992,10 +2177,10 @@ mod tests { .build(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) + .payable_daos(vec![PayableScannerDest(payable_dao)]) // For PendingPayable Scanner .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.receivables = Box::new(NullScanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); + subject.scanners.receivable = Box::new(NullScanner::new()); let subject_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&subject_addr); send_bind_message!(accountant_subs, peer_actors); @@ -3004,91 +2189,62 @@ mod tests { system.run(); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); + let message = blockchain_bridge_recordings.get_record::(0); assert_eq!( - blockchain_bridge_recordings.get_record::(0), + message, &ReportAccountsPayable { accounts, - response_skeleton_opt: None + response_skeleton_opt: None, } ); } #[test] - fn scan_for_delinquencies_triggers_bans_and_unbans() { + fn accountant_does_not_initiate_another_scan_in_case_it_receives_the_message_and_the_scanner_is_running( + ) { init_test_logging(); - let accountant_config = make_populated_accountant_config_with_defaults(); - let config = bc_from_ac_plus_earning_wallet(accountant_config, make_wallet("mine")); - let newly_banned_1 = make_receivable_account(1234, true); - let newly_banned_2 = make_receivable_account(2345, true); - let newly_unbanned_1 = make_receivable_account(3456, false); - let newly_unbanned_2 = make_receivable_account(4567, false); - let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![]); - let new_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); - let paid_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); - let receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_parameters(&new_delinquencies_parameters_arc) - .new_delinquencies_result(vec![newly_banned_1.clone(), newly_banned_2.clone()]) - .paid_delinquencies_parameters(&paid_delinquencies_parameters_arc) - .paid_delinquencies_result(vec![newly_unbanned_1.clone(), newly_unbanned_2.clone()]); - let ban_parameters_arc = Arc::new(Mutex::new(vec![])); - let unban_parameters_arc = Arc::new(Mutex::new(vec![])); - let banned_dao = BannedDaoMock::new() - .ban_list_result(vec![]) - .ban_parameters(&ban_parameters_arc) - .unban_parameters(&unban_parameters_arc); - let subject = AccountantBuilder::default() + let test_name = "accountant_does_not_initiate_another_scan_in_case_it_receives_the_message_and_the_scanner_is_running"; + let payable_dao = PayableDaoMock::default(); + let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); + let report_accounts_payable_sub = blockchain_bridge.start().recipient(); + let last_paid_timestamp = to_time_t(SystemTime::now()) + - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec as i64 + - 1; + let payable_account = PayableAccount { + wallet: make_wallet("scan_for_payables"), + balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), + last_paid_timestamp: from_time_t(last_paid_timestamp), + pending_payable_opt: None, + }; + let payable_dao = payable_dao.non_pending_payables_result(vec![payable_account.clone()]); + let config = bc_from_earning_wallet(make_wallet("mine")); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .payable_daos(vec![PayableScannerDest(payable_dao)]) // For PendingPayable Scanner .bootstrapper_config(config) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) - .banned_dao(banned_dao) - .build(); - - subject.scan_for_delinquencies(); - - let new_delinquencies_parameters: MutexGuard> = - new_delinquencies_parameters_arc.lock().unwrap(); - assert_eq!( - DEFAULT_PAYMENT_THRESHOLDS.clone(), - new_delinquencies_parameters[0].1 - ); - let paid_delinquencies_parameters: MutexGuard> = - paid_delinquencies_parameters_arc.lock().unwrap(); - assert_eq!( - DEFAULT_PAYMENT_THRESHOLDS.clone(), - paid_delinquencies_parameters[0] - ); - let ban_parameters = ban_parameters_arc.lock().unwrap(); - assert!(ban_parameters.contains(&newly_banned_1.wallet)); - assert!(ban_parameters.contains(&newly_banned_2.wallet)); - assert_eq!(2, ban_parameters.len()); - let unban_parameters = unban_parameters_arc.lock().unwrap(); - assert!(unban_parameters.contains(&newly_unbanned_1.wallet)); - assert!(unban_parameters.contains(&newly_unbanned_2.wallet)); - assert_eq!(2, unban_parameters.len()); - let tlh = TestLogHandler::new(); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c65743132333464 \\(balance: 1234 MASQ, age: \\d+ sec\\) banned for delinquency"); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c65743233343564 \\(balance: 2345 MASQ, age: \\d+ sec\\) banned for delinquency"); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c6574333435366e \\(balance: 3456 MASQ, age: \\d+ sec\\) is no longer delinquent: unbanned"); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c6574343536376e \\(balance: 4567 MASQ, age: \\d+ sec\\) is no longer delinquent: unbanned"); - } - - #[test] - fn scan_for_pending_payable_found_no_pending_payable() { - init_test_logging(); - let return_all_backup_records_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_fingerprints_params(&return_all_backup_records_params_arc) - .return_all_fingerprints_result(vec![]); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) .build(); + subject.report_accounts_payable_sub_opt = Some(report_accounts_payable_sub); + let addr = subject.start(); + addr.try_send(ScanForPayables { + response_skeleton_opt: None, + }) + .unwrap(); - let _ = subject.scan_for_pending_payable(None); + addr.try_send(ScanForPayables { + response_skeleton_opt: None, + }) + .unwrap(); - let return_all_backup_records_params = return_all_backup_records_params_arc.lock().unwrap(); - assert_eq!(*return_all_backup_records_params, vec![()]); - TestLogHandler::new() - .exists_log_containing("DEBUG: Accountant: No pending payable found during last scan"); + System::current().stop(); + system.run(); + let recording = blockchain_bridge_recording.lock().unwrap(); + let messages_received = recording.len(); + assert_eq!(messages_received, 0); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {}: Payables scan was already initiated", + test_name + )); } #[test] @@ -3116,19 +2272,14 @@ mod tests { payable_fingerprint_1.clone(), payable_fingerprint_2.clone(), ]); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("mine"), - ); + let config = bc_from_earning_wallet(make_wallet("mine")); let system = System::new("pending payable scan"); let mut subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) + .pending_payable_daos(vec![PendingPayableScannerDest(pending_payable_dao)]) .bootstrapper_config(config) .build(); let blockchain_bridge_addr = blockchain_bridge.start(); - subject - .confirmation_tools - .request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); + subject.request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); let _ = account_addr @@ -3158,9 +2309,7 @@ mod tests { fn report_routing_service_provided_message_is_received() { init_test_logging(); let now = SystemTime::now(); - let mut bootstrapper_config = BootstrapperConfig::default(); - bootstrapper_config.accountant_config_opt = Some(make_accountant_config_null()); - bootstrapper_config.earning_wallet = make_wallet("hi"); + let bootstrapper_config = bc_from_earning_wallet(make_wallet("hi")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() @@ -3168,8 +2317,8 @@ mod tests { .more_money_receivable_result(Ok(())); let subject = AccountantBuilder::default() .bootstrapper_config(bootstrapper_config) - .payable_dao(payable_dao_mock) - .receivable_dao(receivable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao_mock)]) .build(); let system = System::new("report_routing_service_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3207,19 +2356,15 @@ mod tests { fn report_routing_service_provided_message_is_received_from_our_consuming_wallet() { init_test_logging(); let consuming_wallet = make_wallet("our consuming wallet"); - let config = bc_from_ac_plus_wallets( - make_populated_accountant_config_with_defaults(), - consuming_wallet.clone(), - make_wallet("our earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("our earning wallet")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .receivable_dao(receivable_dao_mock) - .payable_dao(payable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao_mock)]) .build(); let system = System::new("report_routing_service_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3256,18 +2401,15 @@ mod tests { fn report_routing_service_provided_message_is_received_from_our_earning_wallet() { init_test_logging(); let earning_wallet = make_wallet("our earning wallet"); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - ); + let config = bc_from_earning_wallet(earning_wallet.clone()); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao_mock) - .receivable_dao(receivable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao_mock)]) .build(); let system = System::new("report_routing_service_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3304,10 +2446,7 @@ mod tests { fn report_exit_service_provided_message_is_received() { init_test_logging(); let now = SystemTime::now(); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("hi"), - ); + let config = bc_from_earning_wallet(make_wallet("hi")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() @@ -3315,8 +2454,8 @@ mod tests { .more_money_receivable_result(Ok(())); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .receivable_dao(receivable_dao_mock) - .payable_dao(payable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao_mock)]) .build(); let system = System::new("report_exit_service_provided_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3354,19 +2493,15 @@ mod tests { fn report_exit_service_provided_message_is_received_from_our_consuming_wallet() { init_test_logging(); let consuming_wallet = make_wallet("my consuming wallet"); - let config = bc_from_ac_plus_wallets( - make_accountant_config_null(), - consuming_wallet.clone(), - make_wallet("my earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("my earning wallet")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao_mock) - .receivable_dao(receivable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao_mock)]) .build(); let system = System::new("report_exit_service_provided_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3403,16 +2538,15 @@ mod tests { fn report_exit_service_provided_message_is_received_from_our_earning_wallet() { init_test_logging(); let earning_wallet = make_wallet("my earning wallet"); - let config = - bc_from_ac_plus_earning_wallet(make_accountant_config_null(), earning_wallet.clone()); + let config = bc_from_earning_wallet(earning_wallet.clone()); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao_mock) - .receivable_dao(receivable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao_mock)]) .build(); let system = System::new("report_exit_service_provided_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3448,10 +2582,7 @@ mod tests { #[test] fn report_services_consumed_message_is_received() { init_test_logging(); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("hi"), - ); + let config = make_bc_with_defaults(); let more_money_payable_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new() .more_money_payable_params(more_money_payable_params_arc.clone()) @@ -3460,7 +2591,7 @@ mod tests { .more_money_payable_result(Ok(())); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) .build(); subject.message_id_generator = Box::new(MessageIdGeneratorMock::default().id_result(123)); let system = System::new("report_services_consumed_message_is_received"); @@ -3549,7 +2680,7 @@ mod tests { .more_money_payable_params(more_money_payable_parameters_arc.clone()); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao_mock) + .payable_daos(vec![AccountantBodyDest(payable_dao_mock)]) .build(); let system = System::new("test"); let subject_addr: Addr = subject.start(); @@ -3570,11 +2701,7 @@ mod tests { fn routing_service_consumed_is_reported_for_our_consuming_wallet() { init_test_logging(); let consuming_wallet = make_wallet("the consuming wallet"); - let config = bc_from_ac_plus_wallets( - make_populated_accountant_config_with_defaults(), - consuming_wallet.clone(), - make_wallet("the earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("the earning wallet")); let foreign_wallet = make_wallet("exit wallet"); let timestamp = SystemTime::now(); let report_message = ReportServicesConsumedMessage { @@ -3617,10 +2744,7 @@ mod tests { let earning_wallet = make_wallet("routing_service_consumed_is_reported_for_our_earning_wallet"); let foreign_wallet = make_wallet("exit wallet"); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - ); + let config = bc_from_earning_wallet(earning_wallet.clone()); let timestamp = SystemTime::now(); let report_message = ReportServicesConsumedMessage { timestamp, @@ -3661,11 +2785,7 @@ mod tests { init_test_logging(); let consuming_wallet = make_wallet("exit_service_consumed_is_reported_for_our_consuming_wallet"); - let config = bc_from_ac_plus_wallets( - make_accountant_config_null(), - consuming_wallet.clone(), - make_wallet("own earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("own earning wallet")); let report_message = ReportServicesConsumedMessage { timestamp: SystemTime::now(), exit: ExitServiceConsumed { @@ -3695,8 +2815,7 @@ mod tests { fn exit_service_consumed_is_reported_for_our_earning_wallet() { init_test_logging(); let earning_wallet = make_wallet("own earning wallet"); - let config = - bc_from_ac_plus_earning_wallet(make_accountant_config_null(), earning_wallet.clone()); + let config = bc_from_earning_wallet(earning_wallet.clone()); let report_message = ReportServicesConsumedMessage { timestamp: SystemTime::now(), exit: ExitServiceConsumed { @@ -3736,7 +2855,7 @@ mod tests { ), )); let subject = AccountantBuilder::default() - .receivable_dao(receivable_dao) + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); let _ = subject.record_service_provided(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); @@ -3749,7 +2868,7 @@ mod tests { let receivable_dao = ReceivableDaoMock::new() .more_money_receivable_result(Err(ReceivableDaoError::SignConversion(1234))); let subject = AccountantBuilder::default() - .receivable_dao(receivable_dao) + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); subject.record_service_provided(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); @@ -3768,7 +2887,7 @@ mod tests { let payable_dao = PayableDaoMock::new() .more_money_payable_result(Err(PayableDaoError::SignConversion(1234))); let subject = AccountantBuilder::default() - .payable_dao(payable_dao) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) .build(); let service_rate = i64::MAX as u64; @@ -3777,7 +2896,7 @@ mod tests { TestLogHandler::new().exists_log_containing(&format!( "ERROR: Accountant: Overflow error recording consumed services from {}: total charge {}, service rate {}, byte rate 1, payload size 2. Skipping", wallet, - i64::MAX as u64 +1*2, + i64::MAX as u64 + 1 * 2, i64::MAX as u64 )); } @@ -3794,42 +2913,19 @@ mod tests { PayableDaoError::RusqliteError("we cannot help ourselves; this is baaad".to_string()), )); let subject = AccountantBuilder::default() - .payable_dao(payable_dao) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) .build(); let _ = subject.record_service_consumed(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); } - #[test] - #[should_panic( - expected = "Was unable to create a mark in payables for a new pending payable '0x000000000000000000000000000000000000000000000000000000000000007b' due to 'SignConversion(9999999999999)'" - )] - fn handle_sent_payable_fails_to_make_a_mark_in_payables_and_so_panics() { - let payable = Payable::new( - make_wallet("blah"), - 6789, - H256::from_uint(&U256::from(123)), - SystemTime::now(), - ); - let payable_dao = PayableDaoMock::new() - .mark_pending_payable_rowid_result(Err(PayableDaoError::SignConversion(9999999999999))); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprint_rowid_result(Some(7879)); - let subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - - let _ = subject.mark_pending_payable(vec![payable]); - } - #[test] #[should_panic( expected = "Database unmaintainable; payable fingerprint deletion for transaction 0x000000000000000000000000000000000000000000000000000000000000007b \ has stayed undone due to RecordDeletion(\"we slept over, sorry\")" )] - fn handle_sent_payable_dealing_with_failed_payment_fails_to_delete_the_existing_pending_payable_fingerprint_and_panics( - ) { + fn accountant_panics_in_case_it_receives_an_error_from_scanner_while_handling_sent_payable_msg() + { let rowid = 4; let hash = H256::from_uint(&U256::from(123)); let sent_payable = SentPayables { @@ -3845,795 +2941,36 @@ mod tests { .delete_fingerprint_result(Err(PendingPayableDaoError::RecordDeletion( "we slept over, sorry".to_string(), ))); + let system = System::new("test"); let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - - let _ = subject.handle_sent_payables(sent_payable); - } - - #[test] - fn handle_sent_payable_receives_two_payments_one_incorrect_and_one_correct() { - //the two failures differ in the logged messages - init_test_logging(); - let fingerprint_rowid_params_arc = Arc::new(Mutex::new(vec![])); - let now_system = SystemTime::now(); - let payable_1 = Err(BlockchainError::InvalidResponse); - let payable_2_rowid = 126; - let payable_hash_2 = H256::from_uint(&U256::from(166)); - let payable_2 = Payable::new(make_wallet("booga"), 6789, payable_hash_2, now_system); - let payable_3 = Err(BlockchainError::TransactionFailed { - msg: "closing hours, sorry".to_string(), - hash_opt: None, - }); - let sent_payable = SentPayables { - timestamp: SystemTime::now(), - payable: vec![payable_1, Ok(payable_2.clone()), payable_3], - response_skeleton_opt: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprint_rowid_params(&fingerprint_rowid_params_arc) - .fingerprint_rowid_result(Some(payable_2_rowid)); - let subject = AccountantBuilder::default() - .payable_dao(PayableDaoMock::new().mark_pending_payable_rowid_result(Ok(()))) - .pending_payable_dao(pending_payable_dao) + .pending_payable_daos(vec![PayableScannerDest(pending_payable_dao)]) .build(); + let addr = subject.start(); - subject.handle_sent_payables(sent_payable); + let _ = addr.try_send(sent_payable); - let fingerprint_rowid_params = fingerprint_rowid_params_arc.lock().unwrap(); - assert_eq!(*fingerprint_rowid_params, vec![payable_hash_2]); //we know the other two errors are associated with an initiated transaction having a backup - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("WARN: Accountant: Outbound transaction failure due to 'InvalidResponse'. Please check your blockchain service URL configuration."); - log_handler.exists_log_containing("DEBUG: Accountant: Payable '0x00000000000000000000000000000000000000000000000000000000000000a6' has been marked as pending in the payable table"); - log_handler.exists_log_containing("WARN: Accountant: Encountered transaction error at this end: 'TransactionFailed { msg: \"closing hours, sorry\", hash_opt: None }'"); - log_handler.exists_log_containing("DEBUG: Accountant: Forgetting a transaction attempt that even did not reach the signing stage"); + System::current().stop(); + system.run(); } #[test] #[should_panic( - expected = "Payable fingerprint for 0x0000000000000000000000000000000000000000000000000000000000000315 doesn't exist but should by now; system unreliable" + expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" )] - fn handle_sent_payable_receives_proper_payment_but_fingerprint_not_found_so_it_panics() { - init_test_logging(); - let now_system = SystemTime::now(); - let payment_hash = H256::from_uint(&U256::from(789)); - let payment = Payable::new(make_wallet("booga"), 6789, payment_hash, now_system); - let pending_payable_dao = PendingPayableDaoMock::default().fingerprint_rowid_result(None); - let subject = AccountantBuilder::default() - .payable_dao(PayableDaoMock::new().mark_pending_payable_rowid_result(Ok(()))) - .pending_payable_dao(pending_payable_dao) + fn accountant_can_be_crashed_properly_but_not_improperly() { + let mut config = make_bc_with_defaults(); + config.crash_point = CrashPoint::Message; + let accountant = AccountantBuilder::default() + .bootstrapper_config(config) .build(); - let _ = subject.mark_pending_payable(vec![payment]); + prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); } #[test] - fn handle_confirm_transaction_works() { + fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { init_test_logging(); - let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_pending_payable_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transaction_confirmed_params(&transaction_confirmed_params_arc) - .transaction_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprint_params(&delete_pending_payable_fingerprint_params_arc) - .delete_fingerprint_result(Ok(())); - let mut subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let tx_hash = H256::from("sometransactionhash".keccak256()); - let amount = 4567; - let timestamp_from_time_of_payment = from_time_t(200_000_000); - let rowid = 2; - let pending_payable_fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: timestamp_from_time_of_payment, - hash: tx_hash, - attempt_opt: Some(1), - amount, - process_error: None, - }; - - let _ = subject.handle_confirm_pending_transaction(ConfirmPendingTransaction { - pending_payable_fingerprint: pending_payable_fingerprint.clone(), - }); - - let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!( - *transaction_confirmed_params, - vec![pending_payable_fingerprint] - ); - let delete_pending_payable_fingerprint_params = - delete_pending_payable_fingerprint_params_arc - .lock() - .unwrap(); - assert_eq!(*delete_pending_payable_fingerprint_params, vec![rowid]); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("DEBUG: Accountant: Confirmation of transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19; record for payable was modified"); - log_handler.exists_log_containing("INFO: Accountant: Transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 has gone through the whole confirmation process succeeding"); - } - - #[test] - #[should_panic( - expected = "Was unable to uncheck pending payable '0x0000000000000000000000000000000000000000000000000000000000000315' after confirmation due to 'RusqliteError(\"record change not successful\")" - )] - fn handle_confirm_pending_transaction_panics_on_unchecking_payable_table() { - init_test_logging(); - let hash = H256::from_uint(&U256::from(789)); - let rowid = 3; - let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Err( - PayableDaoError::RusqliteError("record change not successful".to_string()), - )); - let mut subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .build(); - let mut payment = make_pending_payable_fingerprint(); - payment.rowid_opt = Some(rowid); - payment.hash = hash; - let msg = ConfirmPendingTransaction { - pending_payable_fingerprint: payment.clone(), - }; - - let _ = subject.handle_confirm_pending_transaction(msg); - } - - #[test] - #[should_panic( - expected = "Was unable to delete payable fingerprint '0x0000000000000000000000000000000000000000000000000000000000000315' after successful transaction due to 'RecordDeletion(\"the database is fooling around with us\")'" - )] - fn handle_confirm_pending_transaction_panics_on_deleting_pending_payable_fingerprint() { - init_test_logging(); - let hash = H256::from_uint(&U256::from(789)); - let rowid = 3; - let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprint_result(Err( - PendingPayableDaoError::RecordDeletion( - "the database is fooling around with us".to_string(), - ), - )); - let mut subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - pending_payable_fingerprint.rowid_opt = Some(rowid); - pending_payable_fingerprint.hash = hash; - let msg = ConfirmPendingTransaction { - pending_payable_fingerprint: pending_payable_fingerprint.clone(), - }; - - let _ = subject.handle_confirm_pending_transaction(msg); - } - - #[test] - fn handle_cancel_pending_transaction_works() { - init_test_logging(); - let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failure_params(&mark_failure_params_arc) - .mark_failure_result(Ok(())); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - let tx_hash = H256::from("sometransactionhash".keccak256()); - let rowid = 2; - let transaction_id = PendingPayableId { - hash: tx_hash, - rowid, - }; - - let _ = subject.handle_cancel_pending_transaction(CancelFailedPendingTransaction { - id: transaction_id, - }); - - let mark_failure_params = mark_failure_params_arc.lock().unwrap(); - assert_eq!(*mark_failure_params, vec![rowid]); - TestLogHandler::new().exists_log_containing( - "WARN: Accountant: Broken transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 left with an error mark; you should take over \ - the care of this transaction to make sure your debts will be paid because there is no automated process that can fix this without you", - ); - } - - #[test] - #[should_panic( - expected = "Unsuccessful attempt for transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\")" - )] - fn handle_cancel_pending_transaction_panics_on_its_inability_to_mark_failure() { - let payable_dao = PayableDaoMock::default().transaction_canceled_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().mark_failure_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), - )); - let subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let rowid = 2; - let hash = H256::from("sometransactionhash".keccak256()); - - let _ = subject.handle_cancel_pending_transaction(CancelFailedPendingTransaction { - id: PendingPayableId { hash, rowid }, - }); - } - - #[test] - #[should_panic( - expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" - )] - fn accountant_can_be_crashed_properly_but_not_improperly() { - let mut config = BootstrapperConfig::default(); - config.crash_point = CrashPoint::Message; - config.accountant_config_opt = Some(make_accountant_config_null()); - let accountant = AccountantBuilder::default() - .bootstrapper_config(config) - .build(); - - prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); - } - - #[test] - fn investigate_debt_extremes_picks_the_most_relevant_records() { - let now = to_time_t(SystemTime::now()); - let same_amount_significance = 2_000_000; - let same_age_significance = from_time_t(now - 30000); - let payables = &[ - PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: same_amount_significance, - last_paid_timestamp: from_time_t(now - 5000), - pending_payable_opt: None, - }, - //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked - PayableAccount { - wallet: make_wallet("wallet1"), - balance_wei: same_amount_significance, - last_paid_timestamp: from_time_t(now - 10000), - pending_payable_opt: None, - }, - //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen - PayableAccount { - wallet: make_wallet("wallet3"), - balance_wei: 100, - last_paid_timestamp: same_age_significance, - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("wallet2"), - balance_wei: 330, - last_paid_timestamp: same_age_significance, - pending_payable_opt: None, - }, - ]; - - let result = Accountant::investigate_debt_extremes(payables); - - assert_eq!(result,"Payable scan found 4 debts; the biggest is 2000000 owed for 10000sec, the oldest is 330 owed for 30000sec") - } - - #[test] - fn payables_debug_summary_prints_pretty_summary() { - init_test_logging(); - let now = to_time_t(SystemTime::now()); - let payment_thresholds = PaymentThresholds { - threshold_interval_sec: 2_592_000, - debt_threshold_gwei: 1_000_000_000, - payment_grace_period_sec: 86_400, - maturity_threshold_sec: 86_400, - permanent_debt_allowed_gwei: 10_000_000, - unban_below_gwei: 10_000_000, - }; - let qualified_payables = &[ - PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2000), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec, - ), - ), - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("wallet1"), - balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec + 55, - ), - ), - pending_payable_opt: None, - }, - ]; - let mut config = BootstrapperConfig::default(); - config.accountant_config_opt = Some(make_populated_accountant_config_with_defaults()); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(config) - .build(); - subject.config.payment_thresholds = payment_thresholds; - - subject.payables_debug_summary(qualified_payables); - - TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ - 10,002,000,000,000,000 wei owed for 2678400 sec exceeds threshold: 10,000,000,001,152,000 wei; creditor: 0x0000000000000000000000000077616c6c657430\n\ - 999,999,999,000,000,000 wei owed for 86455 sec exceeds threshold: 999,978,993,055,555,580 wei; creditor: 0x0000000000000000000000000077616c6c657431"); - } - - #[test] - fn payables_debug_summary_stays_still_if_no_qualified_payments() { - init_test_logging(); - let mut subject = AccountantBuilder::default().build(); - subject.logger = Logger::new("payables_debug_summary_stays_still_if_no_qualified_payments"); - - subject.payables_debug_summary(&vec![]); - - TestLogHandler::new().exists_no_log_containing("DEBUG: payables_debug_summary_prints_nothing_if_no_qualified_payments: Paying qualified debts:"); - } - - #[test] - fn payout_sloped_segment_in_payment_thresholds_goes_along_proper_line() { - let payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 333, - payment_grace_period_sec: 444, - permanent_debt_allowed_gwei: 4444, - debt_threshold_gwei: 8888, - threshold_interval_sec: 1111111, - unban_below_gwei: 0, - }; - let higher_corner_timestamp = payment_thresholds.maturity_threshold_sec; - let middle_point_timestamp = payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec / 2; - let lower_corner_timestamp = - payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec; - let tested_fn = |payment_thresholds: &PaymentThresholds, time| { - PayableThresholdsGaugeReal {} - .calculate_payout_threshold_in_gwei(payment_thresholds, time) as i128 - }; - - let higher_corner_point = tested_fn(&payment_thresholds, higher_corner_timestamp); - let middle_point = tested_fn(&payment_thresholds, middle_point_timestamp); - let lower_corner_point = tested_fn(&payment_thresholds, lower_corner_timestamp); - - let allowed_imprecision = WEIS_OF_GWEI; - let ideal_template_higher: i128 = gwei_to_wei(payment_thresholds.debt_threshold_gwei); - let ideal_template_middle: i128 = gwei_to_wei( - (payment_thresholds.debt_threshold_gwei - - payment_thresholds.permanent_debt_allowed_gwei) - / 2 - + payment_thresholds.permanent_debt_allowed_gwei, - ); - let ideal_template_lower: i128 = - gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei); - assert!( - higher_corner_point <= ideal_template_higher + allowed_imprecision - && ideal_template_higher - allowed_imprecision <= higher_corner_point, - "ideal: {}, real: {}", - ideal_template_higher, - higher_corner_point - ); - assert!( - middle_point <= ideal_template_middle + allowed_imprecision - && ideal_template_middle - allowed_imprecision <= middle_point, - "ideal: {}, real: {}", - ideal_template_middle, - middle_point - ); - assert!( - lower_corner_point <= ideal_template_lower + allowed_imprecision - && ideal_template_lower - allowed_imprecision <= lower_corner_point, - "ideal: {}, real: {}", - ideal_template_lower, - lower_corner_point - ) - } - - fn gap_tester(payment_thresholds: &PaymentThresholds) -> (u64, u64) { - let mut counts_of_unique_elements: HashMap = HashMap::new(); - (1_u64..20) - .map(|to_add| { - ThresholdUtils::calculate_finite_debt_limit_by_age( - &payment_thresholds, - 1500 + to_add, - ) as u64 - }) - .for_each(|point_height| { - counts_of_unique_elements - .entry(point_height) - .and_modify(|q| *q += 1) - .or_insert(1); - }); - - let mut heights_and_counts = counts_of_unique_elements.drain().collect::>(); - heights_and_counts.sort_by_key(|(height, _)| (u64::MAX - height)); - let mut counts_of_groups_of_the_same_size: HashMap = HashMap::new(); - let mut previous_height = - ThresholdUtils::calculate_finite_debt_limit_by_age(&payment_thresholds, 1500) as u64; - heights_and_counts - .into_iter() - .for_each(|(point_height, unique_count)| { - let height_change = if point_height <= previous_height { - previous_height - point_height - } else { - panic!("unexpected trend; previously: {previous_height}, now: {point_height}") - }; - counts_of_groups_of_the_same_size - .entry(unique_count) - .and_modify(|(_height_change, occurrence_so_far)| *occurrence_so_far += 1) - .or_insert((height_change, 1)); - previous_height = point_height; - }); - - let mut sortable = counts_of_groups_of_the_same_size - .drain() - .collect::>(); - sortable.sort_by_key(|(_key, (_height_change, occurrence))| *occurrence); - - let (number_of_seconds_detected, (height_change, occurrence)) = - sortable.last().expect("no values to analyze"); - //checking if the sample of undistorted results (consist size groups) has enough weight compared to 20 tries from the beginning - if number_of_seconds_detected * occurrence >= 15 { - (*number_of_seconds_detected as u64, *height_change) - } else { - panic!("couldn't provide a relevant amount of data for the analysis") - } - } - - fn assert_on_height_granularity_with_advancing_time( - description_of_given_pt: &str, - payment_thresholds: &PaymentThresholds, - expected_height_change_wei: u64, - ) { - const WE_EXPECT_ALWAYS_JUST_ONE_SECOND_TO_CHANGE_THE_HEIGHT: u64 = 1; - let (seconds_needed_for_smallest_change_in_height, absolute_height_change_wei) = - gap_tester(&payment_thresholds); - assert_eq!(seconds_needed_for_smallest_change_in_height, WE_EXPECT_ALWAYS_JUST_ONE_SECOND_TO_CHANGE_THE_HEIGHT, - "while testing {} we expected that these thresholds: {:?} will require only {} s until we see the height change but computed {} s instead", - description_of_given_pt, payment_thresholds, WE_EXPECT_ALWAYS_JUST_ONE_SECOND_TO_CHANGE_THE_HEIGHT, seconds_needed_for_smallest_change_in_height); - assert_eq!(absolute_height_change_wei, expected_height_change_wei, - "while testing {} we expected that these thresholds: {:?} will cause a height change of {} wei as a result of advancement in time by {} s but the true result is {}", - description_of_given_pt, payment_thresholds, expected_height_change_wei, seconds_needed_for_smallest_change_in_height, absolute_height_change_wei) - } - - #[test] - fn testing_granularity_calculate_sloped_threshold_by_time() { - let payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1000, - payment_grace_period_sec: 0, - permanent_debt_allowed_gwei: 100, - debt_threshold_gwei: 10_000, - threshold_interval_sec: 10_000, - unban_below_gwei: 100, - }; - - assert_on_height_granularity_with_advancing_time( - "135° slope", - &payment_thresholds, - 990_000_000, - ); - - let payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1000, - payment_grace_period_sec: 0, - permanent_debt_allowed_gwei: 100, - debt_threshold_gwei: 3_420, - threshold_interval_sec: 10_000, - unban_below_gwei: 100, - }; - - assert_on_height_granularity_with_advancing_time( - "160° slope", - &payment_thresholds, - 332_000_000, - ); - - let payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1000, - payment_grace_period_sec: 0, - permanent_debt_allowed_gwei: 100, - debt_threshold_gwei: 875, - threshold_interval_sec: 10_000, - unban_below_gwei: 100, - }; - - assert_on_height_granularity_with_advancing_time( - "175° slope", - &payment_thresholds, - 77_500_000, - ); - } - - #[test] - fn checking_chosen_values_for_the_payment_thresholds_defaults_on_height_values_granularity() { - let payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; - - assert_on_height_granularity_with_advancing_time( - "default thresholds", - &payment_thresholds, - 23_148_148_148_148, - ); - } - - #[test] - fn slope_has_loose_enough_limitations_to_allow_work_with_number_bigger_than_masq_token_max_supply( - ) { - //max masq token supply by August 2022: 37,500,000 - let payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 20, - payment_grace_period_sec: 33, - permanent_debt_allowed_gwei: 1, - debt_threshold_gwei: MASQ_TOTAL_SUPPLY * WEIS_OF_GWEI as u64, - threshold_interval_sec: 1, - unban_below_gwei: 0, - }; - - let slope = ThresholdUtils::slope(&payment_thresholds); - - assert_eq!(slope, -37499999999999999000000000); - let check = { - let y_interception = ThresholdUtils::compute_theoretical_interception_with_y_axis( - slope, - payment_thresholds.maturity_threshold_sec as i128, - gwei_to_wei(payment_thresholds.debt_threshold_gwei), - ); - slope * (payment_thresholds.maturity_threshold_sec + 1) as i128 + y_interception - }; - assert_eq!(check, WEIS_OF_GWEI) - } - - #[test] - fn slope_after_its_end_turns_into_permanent_debt_allowed() { - let payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1000, - payment_grace_period_sec: 444, - permanent_debt_allowed_gwei: 44, - debt_threshold_gwei: 8888, - threshold_interval_sec: 11111, - unban_below_gwei: 0, - }; - - let right_at_the_end = ThresholdUtils::calculate_finite_debt_limit_by_age( - &payment_thresholds, - payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec - + 1, - ); - let a_certain_distance_further = ThresholdUtils::calculate_finite_debt_limit_by_age( - &payment_thresholds, - payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec - + 1234, - ); - - assert_eq!( - right_at_the_end, - gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei) - ); - assert_eq!( - a_certain_distance_further, - gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei) - ) - } - - #[test] - fn is_innocent_age_works_for_age_smaller_than_innocent_age() { - let payable_age = 999; - - let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_age_works_for_age_equal_to_innocent_age() { - let payable_age = 1000; - - let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_age_works_for_excessive_age() { - let payable_age = 1001; - - let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); - - assert_eq!(result, false) - } - - #[test] - fn is_innocent_balance_works_for_balance_smaller_than_innocent_balance() { - let payable_balance = 999; - - let result = - PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_balance_works_for_balance_equal_to_innocent_balance() { - let payable_balance = 1000; - - let result = - PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_balance_works_for_excessive_balance() { - let payable_balance = 1001; - - let result = - PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); - - assert_eq!(result, false) - } - - #[test] - fn payable_is_found_innocent_by_age_and_returns() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result(true); - let mut subject = AccountantBuilder::default().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - let last_paid_timestamp = SystemTime::now() - .checked_sub(Duration::from_secs(123456)) - .unwrap(); - let mut payable = make_payable_account(111); - payable.last_paid_timestamp = last_paid_timestamp; - let before = SystemTime::now(); - - let result = subject.payable_exceeded_threshold(&payable); - - let after = SystemTime::now(); - assert_eq!(result, None); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age, threshold_value) = is_innocent_age_params.remove(0); - assert!(is_innocent_age_params.is_empty()); - let time_elapsed_before = before - .duration_since(last_paid_timestamp) - .unwrap() - .as_secs(); - let time_elapsed_after = after.duration_since(last_paid_timestamp).unwrap().as_secs(); - assert!(time_elapsed_before <= debt_age && debt_age <= time_elapsed_after); - assert_eq!( - threshold_value, - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec - ) - //no other method was called (absence of panic) and that means we returned early - } - - #[test] - fn payable_is_found_innocent_by_balance_and_returns() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result(false) - .is_innocent_balance_params(&is_innocent_balance_params_arc) - .is_innocent_balance_result(true); - let mut subject = AccountantBuilder::default().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - let last_paid_timestamp = SystemTime::now() - .checked_sub(Duration::from_secs(111111)) - .unwrap(); - let mut payable = make_payable_account(222); - payable.last_paid_timestamp = last_paid_timestamp; - payable.balance_wei = 123456; - let before = SystemTime::now(); - - let result = subject.payable_exceeded_threshold(&payable); - - let after = SystemTime::now(); - assert_eq!(result, None); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age, _) = is_innocent_age_params.remove(0); - assert!(is_innocent_age_params.is_empty()); - let time_elapsed_before = before - .duration_since(last_paid_timestamp) - .unwrap() - .as_secs(); - let time_elapsed_after = after.duration_since(last_paid_timestamp).unwrap().as_secs(); - assert!(time_elapsed_before <= debt_age && debt_age <= time_elapsed_after); - let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); - assert_eq!( - *is_innocent_balance_params, - vec![( - 123456_u128, - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) - )] - ) - //no other method was called (absence of panic) and that means we returned early - } - - #[test] - fn threshold_calculation_depends_on_user_defined_payment_thresholds() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); - let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); - let balance = gwei_to_wei(5555_u64); - let how_far_in_past = Duration::from_secs(1111 + 1); - let last_paid_timestamp = SystemTime::now().sub(how_far_in_past); - let payable_account = PayableAccount { - wallet: make_wallet("hi"), - balance_wei: balance, - last_paid_timestamp, - pending_payable_opt: None, - }; - let custom_payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1111, - payment_grace_period_sec: 2222, - permanent_debt_allowed_gwei: 3333, - debt_threshold_gwei: 4444, - threshold_interval_sec: 5555, - unban_below_gwei: 5555, - }; - let mut bootstrapper_config = BootstrapperConfig::default(); - bootstrapper_config.accountant_config_opt = Some(AccountantConfig { - scan_intervals: Default::default(), - payment_thresholds: custom_payment_thresholds, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - }); - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result( - how_far_in_past.as_secs() - <= custom_payment_thresholds.maturity_threshold_sec as u64, - ) - .is_innocent_balance_params(&is_innocent_balance_params_arc) - .is_innocent_balance_result( - balance <= gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei), - ) - .calculate_payout_threshold_in_gwei_params(&calculate_payable_threshold_params_arc) - .calculate_payout_threshold_in_gwei_result(4567898); //made up value - let mut subject = AccountantBuilder::default() - .bootstrapper_config(bootstrapper_config) - .build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - let before = SystemTime::now(); - - let result = subject.payable_exceeded_threshold(&payable_account); - - let after = SystemTime::now(); - assert_eq!(result, Some(4567898)); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (time_elapsed, curve_derived_time) = is_innocent_age_params.remove(0); - assert_eq!(*is_innocent_age_params, vec![]); - let time_elapsed_before = before - .duration_since(last_paid_timestamp) - .unwrap() - .as_secs(); - let time_elapsed_after = after.duration_since(last_paid_timestamp).unwrap().as_secs(); - assert!(time_elapsed_before <= time_elapsed && time_elapsed <= time_elapsed_after); - assert_eq!( - curve_derived_time, - custom_payment_thresholds.maturity_threshold_sec as u64 - ); - let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); - assert_eq!( - *is_innocent_balance_params, - vec![( - payable_account.balance_wei, - gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei) - )] - ); - let mut calculate_payable_curves_params = - calculate_payable_threshold_params_arc.lock().unwrap(); - let (payment_thresholds, time_elapsed) = calculate_payable_curves_params.remove(0); - assert_eq!(*calculate_payable_curves_params, vec![]); - assert!(time_elapsed_before <= time_elapsed && time_elapsed <= time_elapsed_after); - assert_eq!(payment_thresholds, custom_payment_thresholds) - } - - #[test] - fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { - init_test_logging(); - let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); let get_transaction_receipt_params_arc = Arc::new(Mutex::new(vec![])); let return_all_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); @@ -4645,12 +2982,6 @@ mod tests { let notify_later_scan_for_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_scan_for_pending_payable_arc_cloned = notify_later_scan_for_pending_payable_params_arc.clone(); //because it moves into a closure - let notify_cancel_failed_transaction_params_arc = Arc::new(Mutex::new(vec![])); - let notify_cancel_failed_transaction_params_arc_cloned = - notify_cancel_failed_transaction_params_arc.clone(); //because it moves into a closure - let notify_confirm_transaction_params_arc = Arc::new(Mutex::new(vec![])); - let notify_confirm_transaction_params_arc_cloned = - notify_confirm_transaction_params_arc.clone(); //because it moves into a closure let pending_tx_hash_1 = H256::from_uint(&U256::from(123)); let pending_tx_hash_2 = H256::from_uint(&U256::from(567)); let rowid_for_account_1 = 3; @@ -4718,29 +3049,23 @@ mod tests { pending_payable_opt: None, }; let pending_payable_scan_interval = 200; //should be slightly less than 1/5 of the time until shutting the system - let payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![account_1, account_2]) + let payable_dao_for_accountant = PayableDaoMock::new(); + let payable_dao_for_payable_scanner = PayableDaoMock::new() .mark_pending_payable_rowid_params(&mark_pending_payable_params_arc) .mark_pending_payable_rowid_result(Ok(())) .mark_pending_payable_rowid_result(Ok(())) + .non_pending_payables_params(&non_pending_payables_params_arc) + .non_pending_payables_result(vec![account_1, account_2]); + let payable_dao_for_pending_payable_scanner = PayableDaoMock::new() .transaction_confirmed_params(&transaction_confirmed_params_arc) .transaction_confirmed_result(Ok(())); - let bootstrapper_config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan - receivable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan - pending_payable_scan_interval: Duration::from_millis( - pending_payable_scan_interval, - ), - }, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - }, - make_wallet("some_wallet_address"), - ); + + let mut bootstrapper_config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + bootstrapper_config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan + receivable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan + pending_payable_scan_interval: Duration::from_millis(pending_payable_scan_interval), + }); let fingerprint_1_first_round = PendingPayableFingerprint { rowid_opt: Some(rowid_for_account_1), timestamp: this_payable_timestamp_1, @@ -4777,7 +3102,14 @@ mod tests { attempt_opt: Some(4), ..fingerprint_2_first_round.clone() }; - let mut pending_payable_dao = PendingPayableDaoMock::default() + let pending_payable_dao_for_accountant = PendingPayableDaoMock::default(); + let pending_payable_dao_for_payable_scanner = PendingPayableDaoMock::default() + .fingerprint_rowid_result(Some(rowid_for_account_1)) + .fingerprint_rowid_result(Some(rowid_for_account_2)) + .insert_fingerprint_params(&insert_fingerprint_params_arc) + .insert_fingerprint_result(Ok(())) + .insert_fingerprint_result(Ok(())); + let mut pending_payable_dao_for_pending_payable_scanner = PendingPayableDaoMock::new() .return_all_fingerprints_params(&return_all_fingerprints_params_arc) .return_all_fingerprints_result(vec![]) .return_all_fingerprints_result(vec![ @@ -4793,11 +3125,6 @@ mod tests { fingerprint_2_third_round, ]) .return_all_fingerprints_result(vec![fingerprint_2_fourth_round.clone()]) - .insert_fingerprint_params(&insert_fingerprint_params_arc) - .insert_fingerprint_result(Ok(())) - .insert_fingerprint_result(Ok(())) - .fingerprint_rowid_result(Some(rowid_for_account_1)) - .fingerprint_rowid_result(Some(rowid_for_account_2)) .update_fingerprint_params(&update_fingerprint_params_arc) .update_fingerprint_results(Ok(())) .update_fingerprint_results(Ok(())) @@ -4811,31 +3138,29 @@ mod tests { .delete_fingerprint_params(&delete_record_params_arc) //this is used during confirmation of the successful one .delete_fingerprint_result(Ok(())); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; + pending_payable_dao_for_pending_payable_scanner + .have_return_all_fingerprints_shut_down_the_system = true; let accountant_addr = Arbiter::builder() .stop_system_on_panic(true) .start(move |_| { let mut subject = AccountantBuilder::default() .bootstrapper_config(bootstrapper_config) - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .payable_daos(vec![ + AccountantBodyDest(payable_dao_for_accountant), + PayableScannerDest(payable_dao_for_payable_scanner), + PendingPayableScannerDest(payable_dao_for_pending_payable_scanner), + ]) + .pending_payable_daos(vec![ + AccountantBodyDest(pending_payable_dao_for_accountant), + PayableScannerDest(pending_payable_dao_for_payable_scanner), + PendingPayableScannerDest(pending_payable_dao_for_pending_payable_scanner), + ]) .build(); - subject.scanners.receivables = Box::new(NullScanner); + subject.scanners.receivable = Box::new(NullScanner::new()); let notify_later_half_mock = NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_scan_for_pending_payable_arc_cloned) .permit_to_send_out(); - subject - .confirmation_tools - .notify_later_scan_for_pending_payable = Box::new(notify_later_half_mock); - let notify_half_mock = NotifyHandleMock::default() - .notify_params(¬ify_cancel_failed_transaction_params_arc_cloned) - .permit_to_send_out(); - subject.confirmation_tools.notify_cancel_failed_transaction = - Box::new(notify_half_mock); - let notify_half_mock = NotifyHandleMock::default() - .notify_params(¬ify_confirm_transaction_params_arc_cloned) - .permit_to_send_out(); - subject.confirmation_tools.notify_confirm_transaction = Box::new(notify_half_mock); + subject.notify_later.scan_for_pending_payable = Box::new(notify_later_half_mock); subject }); let mut peer_actors = peer_actors_builder().build(); @@ -4877,7 +3202,7 @@ mod tests { pending_tx_hash_2, pending_tx_hash_1, pending_tx_hash_2, - pending_tx_hash_2 + pending_tx_hash_2, ] ); let update_backup_after_cycle_params = update_fingerprint_params_arc.lock().unwrap(); @@ -4888,7 +3213,7 @@ mod tests { rowid_for_account_2, rowid_for_account_1, rowid_for_account_2, - rowid_for_account_2 + rowid_for_account_2, ] ); let mark_failure_params = mark_failure_params_arc.lock().unwrap(); @@ -4924,15 +3249,6 @@ mod tests { expected_scan_pending_payable_msg_and_interval, ] ); - let mut notify_confirm_transaction_params = - notify_confirm_transaction_params_arc.lock().unwrap(); - let actual_confirmed_payable: ConfirmPendingTransaction = - notify_confirm_transaction_params.remove(0); - assert!(notify_confirm_transaction_params.is_empty()); - let expected_confirmed_payable = ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint_2_fourth_round, - }; - assert_eq!(actual_confirmed_payable, expected_confirmed_payable); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing( "WARN: Accountant: Broken transaction 0x000000000000000000000000000000000000000000000000000000000000007b left with an error mark; you should take over the care of this transaction to make sure your debts will be paid because there \ @@ -4941,225 +3257,15 @@ mod tests { log_handler.exists_log_containing("INFO: Accountant: Transaction 0x0000000000000000000000000000000000000000000000000000000000000237 has gone through the whole confirmation process succeeding"); } - #[test] - fn handle_pending_tx_handles_none_returned_for_transaction_receipt() { - init_test_logging(); - let subject = AccountantBuilder::default().build(); - let tx_receipt_opt = None; - let rowid = 455; - let hash = H256::from_uint(&U256::from(2323)); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt_opt: Some(3), - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![(tx_receipt_opt, fingerprint.clone())], - response_skeleton_opt: None, - }; - - let result = subject.handle_pending_transaction_with_its_receipt(&msg); - - assert_eq!( - result, - vec![PendingTransactionStatus::StillPending(PendingPayableId { - hash, - rowid - })] - ); - TestLogHandler::new().exists_log_matching("DEBUG: Accountant: Interpreting a receipt for transaction '0x0000000000000000000000000000000000000000000000000000000000000913' but none was given; attempt 3, 100\\d\\dms since sending"); - } - - #[test] - fn accountant_receives_reported_transaction_receipts_and_processes_them_all() { - let notify_handle_params_arc = Arc::new(Mutex::new(vec![])); - let mut subject = AccountantBuilder::default().build(); - subject.confirmation_tools.notify_confirm_transaction = - Box::new(NotifyHandleMock::default().notify_params(¬ify_handle_params_arc)); - let subject_addr = subject.start(); - let transaction_hash_1 = H256::from_uint(&U256::from(4545)); - let mut transaction_receipt_1 = TransactionReceipt::default(); - transaction_receipt_1.transaction_hash = transaction_hash_1; - transaction_receipt_1.status = Some(U64::from(1)); //success - let fingerprint_1 = PendingPayableFingerprint { - rowid_opt: Some(5), - timestamp: from_time_t(200_000_000), - hash: transaction_hash_1, - attempt_opt: Some(2), - amount: 444, - process_error: None, - }; - let transaction_hash_2 = H256::from_uint(&U256::from(3333333)); - let mut transaction_receipt_2 = TransactionReceipt::default(); - transaction_receipt_2.transaction_hash = transaction_hash_2; - transaction_receipt_2.status = Some(U64::from(1)); //success - let fingerprint_2 = PendingPayableFingerprint { - rowid_opt: Some(10), - timestamp: from_time_t(199_780_000), - hash: Default::default(), - attempt_opt: Some(15), - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - (Some(transaction_receipt_1), fingerprint_1.clone()), - (Some(transaction_receipt_2), fingerprint_2.clone()), - ], - response_skeleton_opt: None, - }; - - let _ = subject_addr.try_send(msg).unwrap(); - - let system = System::new("processing reported receipts"); - System::current().stop(); - system.run(); - let notify_handle_params = notify_handle_params_arc.lock().unwrap(); - assert_eq!( - *notify_handle_params, - vec![ - ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint_1 - }, - ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint_2 - } - ] - ); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { - init_test_logging(); - let subject = AccountantBuilder::default().build(); - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = H256::from_uint(&U256::from(4567)); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(777777), - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt_opt: Some(5), - amount: 2222, - process_error: None, - }; - - let result = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("receipt_check_logger"), - ); - - assert_eq!( - result, - PendingTransactionStatus::Failure(PendingPayableId { - hash, - rowid: 777777 - }) - ); - TestLogHandler::new().exists_log_matching("ERROR: receipt_check_logger: Pending \ - transaction '0x00000000000000000000000000000000000000000000000000000000000011d7' announced as a failure, interpreting attempt 5 after 1500\\d\\dms from the sending"); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - init_test_logging(); - let hash = H256::from_uint(&U256::from(567)); - let rowid = 466; - let tx_receipt = TransactionReceipt::default(); //status defaulted to None - let when_sent = SystemTime::now().sub(Duration::from_millis(100)); - let subject = AccountantBuilder::default().build(); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: when_sent, - hash, - attempt_opt: Some(1), - amount: 123, - process_error: None, - }; - - let result = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("none_within_waiting"), - ); - - assert_eq!( - result, - PendingTransactionStatus::StillPending(PendingPayableId { hash, rowid }) - ); - TestLogHandler::new().exists_log_containing( - "INFO: none_within_waiting: Pending \ - transaction '0x0000000000000000000000000000000000000000000000000000000000000237' couldn't be confirmed at attempt 1 at ", - ); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() - { - init_test_logging(); - let hash = H256::from_uint(&U256::from(567)); - let rowid = 466; - let tx_receipt = TransactionReceipt::default(); //status defaulted to None - let when_sent = - SystemTime::now().sub(Duration::from_secs(DEFAULT_PENDING_TOO_LONG_SEC + 5)); //old transaction - let subject = AccountantBuilder::default().build(); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: when_sent, - hash, - attempt_opt: Some(10), - amount: 123, - process_error: None, - }; - - let result = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("receipt_check_logger"), - ); - - assert_eq!( - result, - PendingTransactionStatus::Failure(PendingPayableId { hash, rowid }) - ); - TestLogHandler::new().exists_log_containing( - "ERROR: receipt_check_logger: Pending transaction '0x0000000000000000000000000000000000000000000000000000000000000237' has exceeded the maximum \ - pending time (21600sec) and the confirmation process is going to be aborted now at the final attempt 10; manual resolution is required from the user to \ - complete the transaction", - ); - } - - #[test] - #[should_panic( - expected = "tx receipt for pending '0x000000000000000000000000000000000000000000000000000000000000007b' - tx status: code other than 0 or 1 shouldn't be possible, but was 456" - )] - fn interpret_transaction_receipt_panics_at_undefined_status_code() { - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(456)); - let mut fingerprint = make_pending_payable_fingerprint(); - fingerprint.hash = H256::from_uint(&U256::from(123)); - let subject = AccountantBuilder::default().build(); - - let _ = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("receipt_check_logger"), - ); - } - #[test] fn accountant_handles_pending_payable_fingerprint() { init_test_logging(); let insert_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payment_dao = PendingPayableDaoMock::default() + let pending_payable_dao = PendingPayableDaoMock::default() .insert_fingerprint_params(&insert_fingerprint_params_arc) .insert_fingerprint_result(Ok(())); let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payment_dao) + .pending_payable_daos(vec![AccountantBodyDest(pending_payable_dao)]) .build(); let accountant_addr = subject.start(); let tx_hash = H256::from_uint(&U256::from(55)); @@ -5206,7 +3312,7 @@ mod tests { let amount = 2345; let transaction_hash = H256::from_uint(&U256::from(456)); let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) + .pending_payable_daos(vec![AccountantBodyDest(pending_payable_dao)]) .build(); let timestamp_secs = 150_000_000; let fingerprint = PendingPayableFingerprint { @@ -5225,104 +3331,67 @@ mod tests { *insert_fingerprint_params, vec![(transaction_hash, amount, from_time_t(timestamp_secs))] ); - TestLogHandler::new().exists_log_containing("ERROR: Accountant: Failed to make a fingerprint for pending payable '0x00000000000000000000000000000000000000000000000000000000000001c8' due to 'InsertionFailed(\"Crashed\")'"); - } - - #[test] - fn separate_early_errors_works() { - let payable_ok = Payable { - to: make_wallet("blah"), - amount: 5555, - timestamp: SystemTime::now(), - tx_hash: Default::default(), - }; - let error = BlockchainError::SignedValueConversion(666); - let sent_payable = SentPayables { - timestamp: SystemTime::now(), - payable: vec![Ok(payable_ok.clone()), Err(error.clone())], - response_skeleton_opt: None, - }; - - let (ok, err) = Accountant::separate_early_errors(&sent_payable, &Logger::new("test")); - - assert_eq!(ok, vec![payable_ok]); - assert_eq!(err, vec![error]) - } - - #[test] - fn update_payable_fingerprint_happy_path() { - let update_after_cycle_params_arc = Arc::new(Mutex::new(vec![])); - let hash = H256::from_uint(&U256::from(444888)); - let rowid = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .update_fingerprint_params(&update_after_cycle_params_arc) - .update_fingerprint_results(Ok(())); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id = PendingPayableId { hash, rowid }; - - let _ = subject.update_payable_fingerprint(transaction_id); - - let update_after_cycle_params = update_after_cycle_params_arc.lock().unwrap(); - assert_eq!(*update_after_cycle_params, vec![rowid]) - } - - #[test] - #[should_panic( - expected = "Failure on updating payable fingerprint '0x000000000000000000000000000000000000000000000000000000000006c9d8' \ - due to UpdateFailed(\"yeah, bad\")" - )] - fn update_payable_fingerprint_sad_path() { - let hash = H256::from_uint(&U256::from(444888)); - let rowid = 3456; - let pending_payable_dao = PendingPayableDaoMock::default().update_fingerprint_results(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id = PendingPayableId { hash, rowid }; - - let _ = subject.update_payable_fingerprint(transaction_id); + TestLogHandler::new().exists_log_containing( + "ERROR: Accountant: Failed to make a fingerprint \ + for pending payable '0x00000000000000000000000000000000000000000000000000000000000001c8' \ + due to 'InsertionFailed(\"Crashed\")'", + ); } #[test] fn handles_scan_error() { - let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); - let subject = AccountantBuilder::default().build(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - - subject_addr - .try_send(ScanError { + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }; + let msg = "My tummy hurts"; + assert_scan_error_is_handled_properly( + "payables_externally_triggered", + ScanError { scan_type: ScanType::Payables, - response_skeleton: ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }, - msg: "My tummy hurts".to_string(), - }) - .unwrap(); - - System::current().stop(); - system.run(); - let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); - assert_eq!( - ui_gateway_recording.get_record::(0), - &NodeToUiMessage { - target: ClientId(1234), - body: MessageBody { - opcode: "scan".to_string(), - path: MessagePath::Conversation(4321), - payload: Err(( - SCAN_ERROR, - "Payables scan failed: 'My tummy hurts'".to_string() - )) - } - } + response_skeleton_opt: Some(response_skeleton), + msg: msg.to_string(), + }, + ); + assert_scan_error_is_handled_properly( + "pending_payables_externally_triggered", + ScanError { + scan_type: ScanType::PendingPayables, + response_skeleton_opt: Some(response_skeleton), + msg: msg.to_string(), + }, + ); + assert_scan_error_is_handled_properly( + "receivables_externally_triggered", + ScanError { + scan_type: ScanType::Receivables, + response_skeleton_opt: Some(response_skeleton), + msg: msg.to_string(), + }, + ); + assert_scan_error_is_handled_properly( + "payables_internally_triggered", + ScanError { + scan_type: ScanType::Payables, + response_skeleton_opt: None, + msg: msg.to_string(), + }, + ); + assert_scan_error_is_handled_properly( + "pending_payables_internally_triggered", + ScanError { + scan_type: ScanType::PendingPayables, + response_skeleton_opt: None, + msg: msg.to_string(), + }, + ); + assert_scan_error_is_handled_properly( + "receivables_internally_triggered", + ScanError { + scan_type: ScanType::Receivables, + response_skeleton_opt: None, + msg: msg.to_string(), + }, ); } @@ -5330,10 +3399,7 @@ mod tests { fn financials_request_with_nothing_to_respond_to_is_refused() { let system = System::new("test"); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .build(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); @@ -5373,10 +3439,7 @@ mod tests { #[test] fn financials_request_allows_only_one_kind_of_view_into_books_at_a_time() { let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .build(); let request = UiFinancialsRequest { stats_required: false, @@ -5418,12 +3481,9 @@ mod tests { let receivable_dao = ReceivableDaoMock::new().total_result(987_654_328_996); let system = System::new("test"); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) - .payable_dao(payable_dao) + .bootstrapper_config(make_bc_with_defaults()) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); @@ -5459,23 +3519,26 @@ mod tests { }), query_results_opt: None, } - ); + ) } #[test] fn compute_financials_processes_defaulted_request() { let payable_dao = PayableDaoMock::new().total_result(u64::MAX as u128 + 123456); let receivable_dao = ReceivableDaoMock::new().total_result((i64::MAX as i128) * 3); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) - .payable_dao(payable_dao) + let subject = AccountantBuilder::default() + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); - subject.financial_statistics.total_paid_payable_wei = 172_345_602_235_454_454; - subject.financial_statistics.total_paid_receivable_wei = 4_455_656_989_415_777_555; + subject + .financial_statistics + .borrow_mut() + .total_paid_payable_wei = 172_345_602_235_454_454; + subject + .financial_statistics + .borrow_mut() + .total_paid_receivable_wei = 4_455_656_989_415_777_555; let context_id = 1234; let request = UiFinancialsRequest { stats_required: true, @@ -5494,7 +3557,7 @@ mod tests { total_unpaid_receivable_gwei: 27670116110, total_paid_receivable_gwei: 4455656989 }), - query_results_opt: None, + query_results_opt: None } .tmb(context_id) ) @@ -5540,12 +3603,9 @@ mod tests { .custom_query_params(&receivable_custom_query_params_arc) .custom_query_result(Some(receivable_accounts_retrieved)); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) - .payable_dao(payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) // For PendingPayable Scanner + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); let context_id_expected = 1234; let request = UiFinancialsRequest { @@ -5624,12 +3684,9 @@ mod tests { .custom_query_params(&receivable_custom_query_params_arc) .custom_query_result(None); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) - .payable_dao(payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); let context_id_expected = 1234; let request = UiFinancialsRequest { @@ -5702,12 +3759,9 @@ mod tests { .custom_query_params(&receivable_custom_query_params_arc) .custom_query_result(Some(receivable_accounts_retrieved)); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) - .payable_dao(payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); let context_id_expected = 1234; let request = UiFinancialsRequest { @@ -5839,11 +3893,8 @@ mod tests { .custom_query_params(&receivable_custom_query_params_arc) .custom_query_result(Some(receivable_accounts_retrieved)); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) // For PendingPayable Scanner + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); let context_id_expected = 1234; let request = UiFinancialsRequest { @@ -5873,10 +3924,7 @@ mod tests { err_msg: &str, ) { let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .build(); let context_id_expected = 1234; @@ -5954,11 +4002,8 @@ mod tests { let payable_dao = PayableDaoMock::new().custom_query_result(Some(payable_accounts_retrieved)); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .payable_dao(payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![AccountantBodyDest(payable_dao)]) .build(); let context_id_expected = 1234; let request = UiFinancialsRequest { @@ -5993,11 +4038,8 @@ mod tests { let receivable_dao = ReceivableDaoMock::new().custom_query_result(Some(receivable_accounts_retrieved)); let subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .receivable_daos(vec![AccountantBodyDest(receivable_dao)]) .build(); let context_id_expected = 1234; let request = UiFinancialsRequest { @@ -6017,81 +4059,6 @@ mod tests { subject.compute_financials(&request, context_id_expected); } - #[test] - fn total_paid_payable_rises_with_each_bill_paid() { - let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(5), - timestamp: from_time_t(189_999_888), - hash: H256::from_uint(&U256::from(56789)), - attempt_opt: Some(1), - amount: 5478, - process_error: None, - }; - let mut pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprint_result(Ok(())); - let payable_dao = PayableDaoMock::default() - .transaction_confirmed_params(&transaction_confirmed_params_arc) - .transaction_confirmed_result(Ok(())) - .transaction_confirmed_result(Ok(())); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; - let mut subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .payable_dao(payable_dao) - .build(); - subject.financial_statistics.total_paid_payable_wei += 1111; - let msg = ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint.clone(), - }; - - subject.handle_confirm_pending_transaction(msg); - - assert_eq!( - subject.financial_statistics.total_paid_payable_wei, - 1111 + 5478 - ); - let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transaction_confirmed_params, vec![fingerprint]) - } - - #[test] - fn total_paid_receivable_rises_with_each_bill_paid() { - let more_money_received_params_arc = Arc::new(Mutex::new(vec![])); - let receivable_dao = ReceivableDaoMock::new() - .more_money_received_parameters(&more_money_received_params_arc) - .more_money_receivable_result(Ok(())); - let mut subject = AccountantBuilder::default() - .receivable_dao(receivable_dao) - .build(); - subject.financial_statistics.total_paid_receivable_wei += 2222; - let receivables = vec![ - BlockchainTransaction { - block_number: 4578910, - from: make_wallet("wallet_1"), - wei_amount: 45780, - }, - BlockchainTransaction { - block_number: 4569898, - from: make_wallet("wallet_2"), - wei_amount: 33345, - }, - ]; - let now = SystemTime::now(); - - subject.handle_received_payments(ReceivedPayments { - timestamp: now, - payments: receivables.clone(), - response_skeleton_opt: None, - }); - - assert_eq!( - subject.financial_statistics.total_paid_receivable_wei, - 2222 + 45780 + 33345 - ); - let more_money_received_params = more_money_received_params_arc.lock().unwrap(); - assert_eq!(*more_money_received_params, vec![(now, receivables)]); - } - #[test] #[cfg(not(feature = "no_test_share"))] fn msg_id_generates_numbers_only_if_debug_log_enabled() { @@ -6172,4 +4139,85 @@ mod tests { fn wei_to_gwei_blows_up_on_overflow() { let _: u64 = wei_to_gwei(u128::MAX); } + + fn assert_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + init_test_logging(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + match message.scan_type { + ScanType::Payables => subject.scanners.payable.mark_as_started(SystemTime::now()), + ScanType::PendingPayables => subject + .scanners + .pending_payable + .mark_as_started(SystemTime::now()), + ScanType::Receivables => subject + .scanners + .receivable + .mark_as_started(SystemTime::now()), + } + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + + subject_addr.try_send(message.clone()).unwrap(); + + subject_addr + .try_send(AssertionsMessage { + assertions: Box::new(move |actor: &mut Accountant| { + let scan_started_at_opt = match message.scan_type { + ScanType::Payables => actor.scanners.payable.scan_started_at(), + ScanType::PendingPayables => { + actor.scanners.pending_payable.scan_started_at() + } + ScanType::Receivables => actor.scanners.receivable.scan_started_at(), + }; + assert_eq!(scan_started_at_opt, None); + }), + }) + .unwrap(); + System::current().stop(); + system.run(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + match message.response_skeleton_opt { + Some(response_skeleton) => { + let expected_message_sent_to_ui = NodeToUiMessage { + target: ClientId(response_skeleton.client_id), + body: MessageBody { + opcode: "scan".to_string(), + path: MessagePath::Conversation(response_skeleton.context_id), + payload: Err(( + SCAN_ERROR, + format!("{:?} scan failed: '{}'", message.scan_type, message.msg), + )), + }, + }; + assert_eq!( + ui_gateway_recording.get_record::(0), + &expected_message_sent_to_ui + ); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!("ERROR: {}: Received ScanError: {:?}", test_name, message), + &format!( + "ERROR: {}: Sending UiScanResponse: {:?}", + test_name, expected_message_sent_to_ui + ), + ]); + } + None => { + assert_eq!(ui_gateway_recording.len(), 0); + let tlh = TestLogHandler::new(); + tlh.exists_log_containing(&format!( + "ERROR: {}: Received ScanError: {:?}", + test_name, message + )); + tlh.exists_no_log_containing(&format!( + "ERROR: {}: Sending UiScanResponse", + test_name + )); + } + } + } } diff --git a/node/src/accountant/receivable_dao.rs b/node/src/accountant/receivable_dao.rs index af48f85a5..b6081199d 100644 --- a/node/src/accountant/receivable_dao.rs +++ b/node/src/accountant/receivable_dao.rs @@ -8,12 +8,12 @@ use crate::accountant::big_int_processing::big_int_db_processor::{ BigIntDbProcessor, BigIntSqlConfig, Param, SQLParamsBuilder, TableNameDAO, }; use crate::accountant::big_int_processing::big_int_divider::BigIntDivider; +use crate::accountant::checked_conversion; use crate::accountant::dao_utils::{ sum_i128_values_from_table, to_time_t, AssemblerFeeder, CustomQuery, DaoFactoryReal, - RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten, + RangeStmConfig, ThresholdUtils, TopStmConfig, VigilantRusqliteFlatten, }; use crate::accountant::receivable_dao::ReceivableDaoError::RusqliteError; -use crate::accountant::{checked_conversion, ThresholdUtils}; use crate::accountant::{dao_utils, gwei_to_wei}; use crate::blockchain::blockchain_interface::BlockchainTransaction; use crate::database::connection_wrapper::ConnectionWrapper; @@ -94,18 +94,15 @@ pub trait ReceivableDaoFactory { impl ReceivableDaoFactory for DaoFactoryReal { fn make(&self) -> Box { - let init_config = self - .init_config - .take() - .expectv("init config") - .add_special_conn_setup( - BigIntDivider::register_big_int_deconstruction_for_sqlite_connection, - ); - Box::new(ReceivableDaoReal::new(connection_or_panic( + let init_config = self.init_config.clone().add_special_conn_setup( + BigIntDivider::register_big_int_deconstruction_for_sqlite_connection, + ); + let conn = connection_or_panic( &DbInitializerReal::default(), self.data_directory.as_path(), init_config, - ))) + ); + Box::new(ReceivableDaoReal::new(conn)) } } diff --git a/node/src/accountant/scanners.rs b/node/src/accountant/scanners.rs new file mode 100644 index 000000000..9e1f512e4 --- /dev/null +++ b/node/src/accountant/scanners.rs @@ -0,0 +1,2277 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::payable_dao::{Payable, PayableAccount, PayableDao}; +use crate::accountant::pending_payable_dao::PendingPayableDao; +use crate::accountant::receivable_dao::ReceivableDao; +use crate::accountant::scanners_utils::payable_scanner_utils::{ + investigate_debt_extremes, payables_debug_summary, separate_errors, PayableThresholdsGauge, + PayableThresholdsGaugeReal, +}; +use crate::accountant::scanners_utils::pending_payable_scanner_utils::{ + elapsed_in_ms, handle_none_status, handle_status_with_failure, handle_status_with_success, +}; +use crate::accountant::scanners_utils::receivable_scanner_utils::balance_and_age; +use crate::accountant::{ + gwei_to_wei, Accountant, ReceivedPayments, ReportTransactionReceipts, + RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, ScanForPendingPayables, + ScanForReceivables, SentPayables, +}; +use crate::accountant::{PendingPayableId, PendingTransactionStatus, ReportAccountsPayable}; +use crate::banned_dao::BannedDao; +use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; +use crate::blockchain::blockchain_interface::BlockchainError; +use crate::sub_lib::accountant::{DaoFactories, FinancialStatistics, PaymentThresholds}; +use crate::sub_lib::utils::NotifyLaterHandle; +use crate::sub_lib::wallet::Wallet; +use actix::{Message, System}; +use itertools::Itertools; +use masq_lib::logger::Logger; +use masq_lib::logger::TIME_FORMATTING_STRING; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use masq_lib::utils::ExpectValue; +#[cfg(test)] +use std::any::Any; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; +use time::format_description::parse; +use time::OffsetDateTime; +use web3::types::TransactionReceipt; + +pub struct Scanners { + pub payable: Box>, + pub pending_payable: Box>, + pub receivable: Box>, +} + +impl Scanners { + pub fn new( + dao_factories: DaoFactories, + payment_thresholds: Rc, + earning_wallet: Rc, + when_pending_too_long_sec: u64, + financial_statistics: Rc>, + ) -> Self { + Scanners { + payable: Box::new(PayableScanner::new( + dao_factories.payable_dao_factory.make(), + dao_factories.pending_payable_dao_factory.make(), + Rc::clone(&payment_thresholds), + )), + pending_payable: Box::new(PendingPayableScanner::new( + dao_factories.payable_dao_factory.make(), + dao_factories.pending_payable_dao_factory.make(), + Rc::clone(&payment_thresholds), + when_pending_too_long_sec, + Rc::clone(&financial_statistics), + )), + receivable: Box::new(ReceivableScanner::new( + dao_factories.receivable_dao_factory.make(), + dao_factories.banned_dao_factory.make(), + Rc::clone(&payment_thresholds), + earning_wallet, + financial_statistics, + )), + } + } +} + +pub trait Scanner +where + BeginMessage: Message, + EndMessage: Message, +{ + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result; + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> Option; + fn scan_started_at(&self) -> Option; + fn mark_as_started(&mut self, timestamp: SystemTime); + fn mark_as_ended(&mut self, logger: &Logger); + as_any_dcl!(); +} + +pub struct ScannerCommon { + initiated_at_opt: Option, + pub payment_thresholds: Rc, +} + +impl ScannerCommon { + fn new(payment_thresholds: Rc) -> Self { + Self { + initiated_at_opt: None, + payment_thresholds, + } + } + + fn remove_timestamp(&mut self, scan_type: ScanType, logger: &Logger) { + match self.initiated_at_opt.take() { + Some(timestamp) => { + let elapsed_time = SystemTime::now() + .duration_since(timestamp) + .expect("Unable to calculate elapsed time for the scan.") + .as_millis(); + info!( + logger, + "The {:?} scan ended in {}ms.", scan_type, elapsed_time + ); + } + None => { + error!( + logger, + "Called scan_finished() for {:?} scanner but timestamp was not found", + scan_type + ); + } + }; + } +} + +macro_rules! time_marking_methods { + ($scan_type_variant: ident) => { + fn scan_started_at(&self) -> Option { + self.common.initiated_at_opt + } + + fn mark_as_started(&mut self, timestamp: SystemTime) { + self.common.initiated_at_opt = Some(timestamp); + } + + fn mark_as_ended(&mut self, logger: &Logger) { + self.common + .remove_timestamp(ScanType::$scan_type_variant, logger); + } + }; +} + +pub struct PayableScanner { + pub common: ScannerCommon, + pub payable_dao: Box, + pub pending_payable_dao: Box, + pub payable_threshold_gauge: Box, +} + +impl Scanner for PayableScanner { + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(timestamp) = self.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + } + self.mark_as_started(timestamp); + info!(logger, "Scanning for payables"); + let all_non_pending_payables = self.payable_dao.non_pending_payables(); + + debug!( + logger, + "{}", + investigate_debt_extremes(timestamp, &all_non_pending_payables) + ); + + let qualified_payable = + self.sniff_out_alarming_payables_and_log_them(all_non_pending_payables, logger); + + match qualified_payable.is_empty() { + true => { + self.mark_as_ended(logger); + Err(BeginScanError::NothingToProcess) + } + false => { + info!( + logger, + "Chose {} qualified debts to pay", + qualified_payable.len() + ); + Ok(ReportAccountsPayable { + accounts: qualified_payable, + response_skeleton_opt, + }) + } + } + } + + fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { + let (sent_payables, errors) = separate_errors(&message, logger); + debug!( + logger, + "We gathered these errors at sending transactions for payable: {:?}, out of the \ + total of {} attempts", + errors, + sent_payables.len() + errors.len() + ); + + self.handle_sent_payables(sent_payables, logger); + self.handle_errors(errors, logger); + + self.mark_as_ended(logger); + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) + } + + time_marking_methods!(Payables); + + as_any_impl!(); +} + +impl PayableScanner { + pub fn new( + payable_dao: Box, + pending_payable_dao: Box, + payment_thresholds: Rc, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + payable_dao, + pending_payable_dao, + payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), + } + } + + fn handle_sent_payables(&self, sent_payables: Vec, logger: &Logger) { + for payable in sent_payables { + if let Some(rowid) = self.pending_payable_dao.fingerprint_rowid(payable.tx_hash) { + if let Err(e) = self + .payable_dao + .as_ref() + .mark_pending_payable_rowid(&payable.to, rowid) + { + panic!( + "Was unable to create a mark in payables for a new pending payable \ + '{}' due to '{:?}'", + payable.tx_hash, e + ); + } + } else { + panic!( + "Payable fingerprint for {} doesn't exist but should by now; system unreliable", + payable.tx_hash + ); + }; + + debug!( + logger, + "Payable '{}' has been marked as pending in the payable table", payable.tx_hash + ) + } + } + + fn sniff_out_alarming_payables_and_log_them( + &self, + non_pending_payables: Vec, + logger: &Logger, + ) -> Vec { + fn pass_payables_and_drop_points( + qp_tp: impl Iterator, + ) -> Vec { + let (payables, _) = qp_tp.unzip::<_, _, Vec, Vec<_>>(); + payables + } + + let qualified_payables_and_points_uncollected = + non_pending_payables.into_iter().flat_map(|account| { + self.payable_exceeded_threshold(&account, SystemTime::now()) + .map(|threshold_point| (account, threshold_point)) + }); + match logger.debug_enabled() { + false => pass_payables_and_drop_points(qualified_payables_and_points_uncollected), + true => { + let qualified_and_points_collected = + qualified_payables_and_points_uncollected.collect_vec(); + payables_debug_summary(&qualified_and_points_collected, logger); + pass_payables_and_drop_points(qualified_and_points_collected.into_iter()) + } + } + } + + fn payable_exceeded_threshold( + &self, + payable: &PayableAccount, + now: SystemTime, + ) -> Option { + let debt_age = now + .duration_since(payable.last_paid_timestamp) + .expect("Internal error") + .as_secs(); + + if self.payable_threshold_gauge.is_innocent_age( + debt_age, + self.common.payment_thresholds.maturity_threshold_sec, + ) { + return None; + } + + if self.payable_threshold_gauge.is_innocent_balance( + payable.balance_wei, + gwei_to_wei(self.common.payment_thresholds.permanent_debt_allowed_gwei), + ) { + return None; + } + + let threshold = self + .payable_threshold_gauge + .calculate_payout_threshold_in_gwei(&self.common.payment_thresholds, debt_age); + if payable.balance_wei > threshold { + Some(threshold) + } else { + None + } + } + + fn handle_errors(&self, errors: Vec, logger: &Logger) { + for blockchain_error in errors { + if let Some(hash) = blockchain_error.carries_transaction_hash() { + if let Some(rowid) = self.pending_payable_dao.fingerprint_rowid(hash) { + debug!( + logger, + "Deleting an existing fingerprint for a failed transaction {:?}", hash + ); + if let Err(e) = self.pending_payable_dao.delete_fingerprint(rowid) { + panic!( + "Database unmaintainable; payable fingerprint deletion for \ + transaction {:?} has stayed undone due to {:?}", + hash, e + ); + }; + }; + + warning!( + logger, + "Failed transaction with a hash '{:?}' but without the record - thrown out", + hash + ) + } else { + debug!( + logger, + "Forgetting a transaction attempt that even did not reach the signing stage" + ) + }; + } + } +} + +pub struct PendingPayableScanner { + pub common: ScannerCommon, + pub payable_dao: Box, + pub pending_payable_dao: Box, + pub when_pending_too_long_sec: u64, + pub financial_statistics: Rc>, +} + +impl Scanner for PendingPayableScanner { + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(timestamp) = self.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + } + self.mark_as_started(timestamp); + info!(logger, "Scanning for pending payable"); + let filtered_pending_payable = self.pending_payable_dao.return_all_fingerprints(); + match filtered_pending_payable.is_empty() { + true => { + self.mark_as_ended(logger); + Err(BeginScanError::NothingToProcess) + } + false => { + debug!( + logger, + "Found {} pending payables to process", + filtered_pending_payable.len() + ); + Ok(RequestTransactionReceipts { + pending_payable: filtered_pending_payable, + response_skeleton_opt, + }) + } + } + } + + fn finish_scan( + &mut self, + message: ReportTransactionReceipts, + logger: &Logger, + ) -> Option { + if !message.fingerprints_with_receipts.is_empty() { + debug!( + logger, + "Processing receipts for {} transactions", + message.fingerprints_with_receipts.len() + ); + let statuses = self.handle_pending_transaction_with_its_receipt(&message, logger); + self.process_transaction_by_status(statuses, logger); + } else { + debug!(logger, "No transaction receipts found."); + } + + self.mark_as_ended(logger); + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) + } + + time_marking_methods!(PendingPayables); + + as_any_impl!(); +} + +impl PendingPayableScanner { + pub fn new( + payable_dao: Box, + pending_payable_dao: Box, + payment_thresholds: Rc, + when_pending_too_long_sec: u64, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + payable_dao, + pending_payable_dao, + when_pending_too_long_sec, + financial_statistics, + } + } + + fn handle_pending_transaction_with_its_receipt( + &self, + msg: &ReportTransactionReceipts, + logger: &Logger, + ) -> Vec { + msg.fingerprints_with_receipts + .iter() + .map(|(receipt_opt, fingerprint)| match receipt_opt { + Some(receipt) => self.interpret_transaction_receipt(receipt, fingerprint, logger), + None => { + debug!( + logger, + "Interpreting a receipt for transaction '{}' but none was given; \ + attempt {}, {}ms since sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + PendingTransactionStatus::StillPending(PendingPayableId { + hash: fingerprint.hash, + rowid: fingerprint.rowid_opt.expectv("initialized rowid"), + }) + } + }) + .collect() + } + + fn interpret_transaction_receipt( + &self, + receipt: &TransactionReceipt, + fingerprint: &PendingPayableFingerprint, + logger: &Logger, + ) -> PendingTransactionStatus { + match receipt.status { + None => handle_none_status(fingerprint, self.when_pending_too_long_sec, logger), + Some(status_code) => match status_code.as_u64() { + 0 => handle_status_with_failure(fingerprint, logger), + 1 => handle_status_with_success(fingerprint, logger), + other => unreachable!( + "tx receipt for pending '{}': status code other than 0 or 1 shouldn't be possible, but was {}", + fingerprint.hash, other + ), + }, + } + } + + fn process_transaction_by_status( + &mut self, + statuses: Vec, + logger: &Logger, + ) { + for status in statuses { + match status { + PendingTransactionStatus::StillPending(transaction_id) => { + self.update_payable_fingerprint(transaction_id, logger); + } + PendingTransactionStatus::Failure(transaction_id) => { + self.cancel_tailed_transaction(transaction_id, logger); + } + PendingTransactionStatus::Confirmed(fingerprint) => { + self.confirm_transaction(fingerprint, logger); + } + } + } + } + + fn update_payable_fingerprint(&self, pending_payable_id: PendingPayableId, logger: &Logger) { + if let Err(e) = self + .pending_payable_dao + .update_fingerprint(pending_payable_id.rowid) + { + panic!( + "Failure on updating payable fingerprint '{:?}' due to {:?}", + pending_payable_id.hash, e + ); + } else { + trace!( + logger, + "Updated record for rowid: {} ", + pending_payable_id.rowid + ); + } + } + + fn cancel_tailed_transaction(&self, transaction_id: PendingPayableId, logger: &Logger) { + if let Err(e) = self.pending_payable_dao.mark_failure(transaction_id.rowid) { + panic!( + "Unsuccessful attempt for transaction {} to mark fatal error at payable \ + fingerprint due to {:?}; database unreliable", + transaction_id.hash, e + ) + } else { + warning!( + logger, + "Broken transaction {:?} left with an error mark; you should take over the care \ + of this transaction to make sure your debts will be paid because there is no \ + automated process that can fix this without you", transaction_id.hash + ); + } + } + + fn confirm_transaction( + &mut self, + pending_payable_fingerprint: PendingPayableFingerprint, + logger: &Logger, + ) { + let hash = pending_payable_fingerprint.hash; + let amount = pending_payable_fingerprint.amount; + let rowid = pending_payable_fingerprint + .rowid_opt + .expectv("initialized rowid"); + + if let Err(e) = self + .payable_dao + .transaction_confirmed(&pending_payable_fingerprint) + { + panic!( + "Was unable to uncheck pending payable '{:?}' after confirmation due to '{:?}'", + hash, e + ); + } else { + self.financial_statistics + .borrow_mut() + .total_paid_payable_wei += amount; + debug!( + logger, + "Confirmation of transaction {}; record for payable was modified", hash + ); + if let Err(e) = self.pending_payable_dao.delete_fingerprint(rowid) { + panic!( + "Was unable to delete payable fingerprint for successful transaction '{:?}' \ + due to '{:?}'", + hash, e + ); + } else { + info!( + logger, + "Transaction {:?} has gone through the whole confirmation process succeeding", + hash + ); + } + } + } +} + +pub struct ReceivableScanner { + pub common: ScannerCommon, + pub receivable_dao: Box, + pub banned_dao: Box, + pub earning_wallet: Rc, + pub financial_statistics: Rc>, +} + +impl Scanner for ReceivableScanner { + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(timestamp) = self.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + } + self.mark_as_started(timestamp); + info!( + logger, + "Scanning for receivables to {}", self.earning_wallet + ); + self.scan_for_delinquencies(timestamp, logger); + + Ok(RetrieveTransactions { + recipient: self.earning_wallet.as_ref().clone(), + response_skeleton_opt, + }) + } + + fn finish_scan( + &mut self, + message: ReceivedPayments, + logger: &Logger, + ) -> Option { + if message.payments.is_empty() { + info!( + logger, + "No new received payments were detected during the scanning process." + ) + } else { + let total_newly_paid_receivable = message + .payments + .iter() + .fold(0, |so_far, now| so_far + now.wei_amount); + self.receivable_dao + .as_mut() + .more_money_received(message.timestamp, message.payments); + self.financial_statistics + .borrow_mut() + .total_paid_receivable_wei += total_newly_paid_receivable; + } + + self.mark_as_ended(logger); + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) + } + + time_marking_methods!(Receivables); + + as_any_impl!(); +} + +impl ReceivableScanner { + pub fn new( + receivable_dao: Box, + banned_dao: Box, + payment_thresholds: Rc, + earning_wallet: Rc, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + earning_wallet, + receivable_dao, + banned_dao, + financial_statistics, + } + } + + pub fn scan_for_delinquencies(&self, timestamp: SystemTime, logger: &Logger) { + info!(logger, "Scanning for delinquencies"); + self.find_and_ban_delinquents(timestamp, logger); + self.find_and_unban_reformed_nodes(timestamp, logger); + } + + fn find_and_ban_delinquents(&self, timestamp: SystemTime, logger: &Logger) { + self.receivable_dao + .new_delinquencies(timestamp, self.common.payment_thresholds.as_ref()) + .into_iter() + .for_each(|account| { + self.banned_dao.ban(&account.wallet); + let (balance_str_wei, age) = balance_and_age(timestamp, &account); + info!( + logger, + "Wallet {} (balance: {} gwei, age: {} sec) banned for delinquency", + account.wallet, + balance_str_wei, + age.as_secs() + ) + }); + } + + fn find_and_unban_reformed_nodes(&self, timestamp: SystemTime, logger: &Logger) { + self.receivable_dao + .paid_delinquencies(self.common.payment_thresholds.as_ref()) + .into_iter() + .for_each(|account| { + self.banned_dao.unban(&account.wallet); + let (balance_str_wei, age) = balance_and_age(timestamp, &account); + info!( + logger, + "Wallet {} (balance: {} gwei, age: {} sec) is no longer delinquent: unbanned", + account.wallet, + balance_str_wei, + age.as_secs() + ) + }); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum BeginScanError { + NothingToProcess, + ScanAlreadyRunning(SystemTime), + CalledFromNullScanner, // Exclusive for tests +} + +impl BeginScanError { + pub fn handle_error( + &self, + logger: &Logger, + scan_type: ScanType, + is_externally_triggered: bool, + ) { + let log_message_opt = match self { + BeginScanError::NothingToProcess => Some(format!( + "There was nothing to process during {:?} scan.", + scan_type + )), + BeginScanError::ScanAlreadyRunning(timestamp) => Some(format!( + "{:?} scan was already initiated at {}. \ + Hence, this scan request will be ignored.", + scan_type, + BeginScanError::timestamp_as_string(timestamp) + )), + BeginScanError::CalledFromNullScanner => match cfg!(test) { + true => None, + false => panic!("Null Scanner shouldn't be running inside production code."), + }, + }; + + if let Some(log_message) = log_message_opt { + match is_externally_triggered { + true => info!(logger, "{}", log_message), + false => debug!(logger, "{}", log_message), + } + } + } + + fn timestamp_as_string(timestamp: &SystemTime) -> String { + let offset_date_time = OffsetDateTime::from(*timestamp); + offset_date_time + .format( + &parse(TIME_FORMATTING_STRING) + .expect("Error while parsing the time formatting string."), + ) + .expect("Error while formatting timestamp as string.") + } +} + +pub struct NullScanner {} + +impl Scanner for NullScanner +where + BeginMessage: Message, + EndMessage: Message, +{ + fn begin_scan( + &mut self, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + Err(BeginScanError::CalledFromNullScanner) + } + + fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> Option { + panic!("Called finish_scan() from NullScanner"); + } + + fn scan_started_at(&self) -> Option { + panic!("Called scan_started_at() from NullScanner"); + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + panic!("Called mark_as_started() from NullScanner"); + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + panic!("Called mark_as_ended() from NullScanner"); + } + + as_any_impl!(); +} + +impl Default for NullScanner { + fn default() -> Self { + Self::new() + } +} + +impl NullScanner { + pub fn new() -> Self { + Self {} + } +} + +pub struct ScannerMock { + begin_scan_params: Arc>>, + begin_scan_results: RefCell>>, + end_scan_params: Arc>>, + end_scan_results: RefCell>>, + stop_system_after_last_message: RefCell, +} + +impl Scanner + for ScannerMock +where + BeginMessage: Message, + EndMessage: Message, +{ + fn begin_scan( + &mut self, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + self.begin_scan_params.lock().unwrap().push(()); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.begin_scan_results.borrow_mut().remove(0) + } + + fn finish_scan(&mut self, message: EndMessage, _logger: &Logger) -> Option { + self.end_scan_params.lock().unwrap().push(message); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.end_scan_results.borrow_mut().remove(0) + } + + fn scan_started_at(&self) -> Option { + intentionally_blank!() + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + intentionally_blank!() + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + +impl Default for ScannerMock { + fn default() -> Self { + Self::new() + } +} + +impl ScannerMock { + pub fn new() -> Self { + Self { + begin_scan_params: Arc::new(Mutex::new(vec![])), + begin_scan_results: RefCell::new(vec![]), + end_scan_params: Arc::new(Mutex::new(vec![])), + end_scan_results: RefCell::new(vec![]), + stop_system_after_last_message: RefCell::new(false), + } + } + + pub fn begin_scan_params(mut self, params: &Arc>>) -> Self { + self.begin_scan_params = params.clone(); + self + } + + pub fn begin_scan_result(self, result: Result) -> Self { + self.begin_scan_results.borrow_mut().push(result); + self + } + + pub fn stop_the_system(self) -> Self { + self.stop_system_after_last_message.replace(true); + self + } + + pub fn is_allowed_to_stop_the_system(&self) -> bool { + *self.stop_system_after_last_message.borrow() + } + + pub fn is_last_message(&self) -> bool { + self.is_last_message_from_begin_scan() || self.is_last_message_from_end_scan() + } + + pub fn is_last_message_from_begin_scan(&self) -> bool { + self.begin_scan_results.borrow().len() == 1 && self.end_scan_results.borrow().is_empty() + } + + pub fn is_last_message_from_end_scan(&self) -> bool { + self.end_scan_results.borrow().len() == 1 && self.begin_scan_results.borrow().is_empty() + } +} + +#[derive(Default)] +pub struct NotifyLaterForScanners { + pub scan_for_pending_payable: Box>, + pub scan_for_payable: Box>, + pub scan_for_receivable: Box>, +} + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::{ + BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, Scanner, + ScannerCommon, Scanners, + }; + use crate::accountant::test_utils::{ + make_custom_payment_thresholds, make_payable_account, make_payables, + make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, + BannedDaoMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, + PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, + PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, + ReceivableScannerBuilder, + }; + use crate::accountant::{ + gwei_to_wei, PendingPayableId, PendingTransactionStatus, ReceivedPayments, + ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, + DEFAULT_PENDING_TOO_LONG_SEC, + }; + use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; + use std::cell::RefCell; + use std::ops::Sub; + + use crate::accountant::dao_utils::{from_time_t, to_time_t}; + use crate::accountant::payable_dao::{Payable, PayableAccount, PayableDaoError}; + use crate::accountant::pending_payable_dao::PendingPayableDaoError; + use crate::accountant::scanners_utils::payable_scanner_utils::PayableThresholdsGaugeReal; + use crate::blockchain::blockchain_interface::{BlockchainError, BlockchainTransaction}; + use crate::sub_lib::accountant::{ + DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, + }; + use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; + use crate::test_utils::make_wallet; + use ethereum_types::{BigEndianHash, U64}; + use ethsign_crypto::Keccak256; + use masq_lib::logger::Logger; + use masq_lib::messages::ScanType; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; + use web3::types::{TransactionReceipt, H256, U256}; + + #[test] + fn scanners_struct_can_be_constructed_with_the_respective_scanners() { + let payable_dao_factory = PayableDaoFactoryMock::new() + .make_result(PayableDaoMock::new()) + .make_result(PayableDaoMock::new()); + let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() + .make_result(PendingPayableDaoMock::new()) + .make_result(PendingPayableDaoMock::new()); + let receivable_dao_factory = + ReceivableDaoFactoryMock::new().make_result(ReceivableDaoMock::new()); + let banned_dao_factory = BannedDaoFactoryMock::new().make_result(BannedDaoMock::new()); + let when_pending_too_long_sec = 1234; + let financial_statistics = FinancialStatistics { + total_paid_payable_wei: 1, + total_paid_receivable_wei: 2, + }; + let earning_wallet = make_wallet("unique_wallet"); + let payment_thresholds = make_custom_payment_thresholds(); + let payment_thresholds_rc = Rc::new(payment_thresholds); + let initial_rc_count = Rc::strong_count(&payment_thresholds_rc); + + let scanners = Scanners::new( + DaoFactories { + payable_dao_factory: Box::new(payable_dao_factory), + pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + receivable_dao_factory: Box::new(receivable_dao_factory), + banned_dao_factory: Box::new(banned_dao_factory), + }, + Rc::clone(&payment_thresholds_rc), + Rc::new(earning_wallet.clone()), + when_pending_too_long_sec, + Rc::new(RefCell::new(financial_statistics.clone())), + ); + + let payable_scanner = scanners + .payable + .as_any() + .downcast_ref::() + .unwrap(); + let pending_payable_scanner = scanners + .pending_payable + .as_any() + .downcast_ref::() + .unwrap(); + let receivable_scanner = scanners + .receivable + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!( + payable_scanner.common.payment_thresholds.as_ref(), + &payment_thresholds + ); + assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); + payable_scanner + .payable_threshold_gauge + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!( + pending_payable_scanner.when_pending_too_long_sec, + when_pending_too_long_sec + ); + assert_eq!( + *pending_payable_scanner.financial_statistics.borrow(), + financial_statistics + ); + assert_eq!( + pending_payable_scanner.common.payment_thresholds.as_ref(), + &payment_thresholds + ); + assert_eq!( + pending_payable_scanner.common.initiated_at_opt.is_some(), + false + ); + assert_eq!( + *receivable_scanner.financial_statistics.borrow(), + financial_statistics + ); + assert_eq!( + receivable_scanner.earning_wallet.address(), + earning_wallet.address() + ); + assert_eq!( + receivable_scanner.common.payment_thresholds.as_ref(), + &payment_thresholds + ); + assert_eq!(receivable_scanner.common.initiated_at_opt.is_some(), false); + assert_eq!( + Rc::strong_count(&payment_thresholds_rc), + initial_rc_count + 3 + ); + } + + #[test] + fn payable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "payable_scanner_can_initiate_a_scan"; + let now = SystemTime::now(); + let (qualified_payable_accounts, _, all_non_pending_payables) = + make_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + + let result = subject.begin_scan(now, None, &Logger::new(test_name)); + + let timestamp = subject.scan_started_at(); + assert_eq!(timestamp, Some(now)); + assert_eq!( + result, + Ok(ReportAccountsPayable { + accounts: qualified_payable_accounts.clone(), + response_skeleton_opt: None, + }) + ); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for payables"), + &format!( + "INFO: {test_name}: Chose {} qualified debts to pay", + qualified_payable_accounts.len() + ), + ]) + } + + #[test] + fn payable_scanner_throws_error_when_a_scan_is_already_running() { + let now = SystemTime::now(); + let (_, _, all_non_pending_payables) = make_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + let _result = subject.begin_scan(now, None, &Logger::new("test")); + + let run_again_result = subject.begin_scan(SystemTime::now(), None, &Logger::new("test")); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + run_again_result, + Err(BeginScanError::ScanAlreadyRunning(now)) + ); + } + + #[test] + fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + let now = SystemTime::now(); + let (_, unqualified_payable_accounts, _) = + make_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); + let mut subject = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + + let result = subject.begin_scan(now, None, &Logger::new("test")); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(is_scan_running, false); + assert_eq!(result, Err(BeginScanError::NothingToProcess)); + } + + #[test] + fn payable_scanner_handles_sent_payable_message() { + //one payment out of three was successful + //those two failures differ in their log messages + init_test_logging(); + let test_name = "payable_scanner_handles_sent_payable_message"; + let fingerprint_rowid_params_arc = Arc::new(Mutex::new(vec![])); + let now = SystemTime::now(); + let payable_1 = Err(BlockchainError::InvalidResponse); + let payable_2_rowid = 126; + let payable_2_hash = H256::from_uint(&U256::from(166)); + let payable_2 = Payable::new(make_wallet("booga"), 6789, payable_2_hash, now); + let payable_3 = Err(BlockchainError::TransactionFailed { + msg: "closing hours, sorry".to_string(), + hash_opt: None, + }); + let sent_payable = SentPayables { + timestamp: SystemTime::now(), + payable: vec![payable_1, Ok(payable_2.clone()), payable_3], + response_skeleton_opt: None, + }; + let payable_dao = PayableDaoMock::new().mark_pending_payable_rowid_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default() + .fingerprint_rowid_params(&fingerprint_rowid_params_arc) + .fingerprint_rowid_result(Some(payable_2_rowid)); + let mut subject = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + subject.mark_as_started(SystemTime::now()); + + let message_opt = subject.finish_scan(sent_payable, &Logger::new(test_name)); + + let is_scan_running = subject.scan_started_at().is_some(); + let fingerprint_rowid_params = fingerprint_rowid_params_arc.lock().unwrap(); + assert_eq!(message_opt, None); + assert_eq!(is_scan_running, false); + //we know the other two errors are associated with an initiated transaction having its existing fingerprint + assert_eq!(*fingerprint_rowid_params, vec![payable_2_hash]); + let log_handler = TestLogHandler::new(); + log_handler.assert_logs_contain_in_order(vec![ + &format!( + "WARN: {test_name}: Outbound transaction failure due to 'InvalidResponse'. \ + Please check your blockchain service URL configuration." + ), + &format!( + "WARN: {test_name}: Encountered transaction error at this end: 'TransactionFailed \ + {{ msg: \"closing hours, sorry\", hash_opt: None }}'" + ), + &format!( + "DEBUG: {test_name}: Payable '0x0000…00a6' has been marked as pending in the payable table" + ), + &format!( + "DEBUG: {test_name}: Forgetting a transaction attempt that even did not reach the signing stage" + ), + ]); + log_handler.exists_log_matching(&format!( + "INFO: {test_name}: The Payables scan ended in \\d+ms." + )); + } + + #[test] + #[should_panic( + expected = "Payable fingerprint for 0x0000…0315 doesn't exist but should by now; system unreliable" + )] + fn payable_scanner_panics_when_fingerprint_is_not_found() { + let now = SystemTime::now(); + let payment_hash = H256::from_uint(&U256::from(789)); + let payable = Payable::new(make_wallet("booga"), 6789, payment_hash, now); + let pending_payable_dao = PendingPayableDaoMock::default().fingerprint_rowid_result(None); + let mut subject = PayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let sent_payable = SentPayables { + timestamp: now, + payable: vec![Ok(payable)], + response_skeleton_opt: None, + }; + + let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + } + + #[test] + #[should_panic( + expected = "Database unmaintainable; payable fingerprint deletion for transaction \ + 0x000000000000000000000000000000000000000000000000000000000000007b has stayed \ + undone due to RecordDeletion(\"we slept over, sorry\")" + )] + fn payable_scanner_panics_when_failed_payment_fails_to_delete_the_existing_pending_payable_fingerprint( + ) { + let rowid = 4; + let hash = H256::from_uint(&U256::from(123)); + let sent_payable = SentPayables { + timestamp: SystemTime::now(), + payable: vec![Err(BlockchainError::TransactionFailed { + msg: "blah".to_string(), + hash_opt: Some(hash), + })], + response_skeleton_opt: None, + }; + let pending_payable_dao = PendingPayableDaoMock::default() + .fingerprint_rowid_result(Some(rowid)) + .delete_fingerprint_result(Err(PendingPayableDaoError::RecordDeletion( + "we slept over, sorry".to_string(), + ))); + let mut subject = PayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + + let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + } + + #[test] + #[should_panic( + expected = "Was unable to create a mark in payables for a new pending payable '0x0000…007b' \ + due to 'SignConversion(9999999999999)'" + )] + fn payable_scanner_panics_when_it_fails_to_make_a_mark_in_payables() { + let payable = Payable::new( + make_wallet("blah"), + 6789, + H256::from_uint(&U256::from(123)), + SystemTime::now(), + ); + let payable_dao = PayableDaoMock::new() + .mark_pending_payable_rowid_result(Err(PayableDaoError::SignConversion(9999999999999))); + let pending_payable_dao = + PendingPayableDaoMock::default().fingerprint_rowid_result(Some(7879)); + let mut subject = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let sent_payable = SentPayables { + timestamp: SystemTime::now(), + payable: vec![Ok(payable)], + response_skeleton_opt: None, + }; + + let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + } + + #[test] + fn payable_is_found_innocent_by_age_and_returns() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result(true); + let mut subject = PayableScannerBuilder::new().build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + let now = SystemTime::now(); + let debt_age_s = 111_222; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let mut payable = make_payable_account(111); + payable.last_paid_timestamp = last_paid_timestamp; + + let result = subject.payable_exceeded_threshold(&payable, now); + + assert_eq!(result, None); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned, threshold_value) = is_innocent_age_params.remove(0); + assert!(is_innocent_age_params.is_empty()); + assert_eq!(debt_age_returned, debt_age_s); + assert_eq!( + threshold_value, + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + ) + //no other method was called (absence of panic) and that means we returned early + } + + #[test] + fn payable_is_found_innocent_by_balance_and_returns() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result(false) + .is_innocent_balance_params(&is_innocent_balance_params_arc) + .is_innocent_balance_result(true); + let mut subject = PayableScannerBuilder::new().build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + let now = SystemTime::now(); + let debt_age_s = 3_456; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let mut payable = make_payable_account(222); + payable.last_paid_timestamp = last_paid_timestamp; + payable.balance_wei = 123456; + + let result = subject.payable_exceeded_threshold(&payable, now); + + assert_eq!(result, None); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned, _) = is_innocent_age_params.remove(0); + assert!(is_innocent_age_params.is_empty()); + assert_eq!(debt_age_returned, debt_age_s); + let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); + assert_eq!( + *is_innocent_balance_params, + vec![( + 123456_u128, + gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) + )] + ) + //no other method was called (absence of panic) and that means we returned early + } + + #[test] + fn threshold_calculation_depends_on_user_defined_payment_thresholds() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); + let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); + let balance = gwei_to_wei(5555_u64); + let now = SystemTime::now(); + let debt_age_s = 1111 + 1; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let payable_account = PayableAccount { + wallet: make_wallet("hi"), + balance_wei: balance, + last_paid_timestamp, + pending_payable_opt: None, + }; + let custom_payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 1111, + payment_grace_period_sec: 2222, + permanent_debt_allowed_gwei: 3333, + debt_threshold_gwei: 4444, + threshold_interval_sec: 5555, + unban_below_gwei: 5555, + }; + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result( + debt_age_s <= custom_payment_thresholds.maturity_threshold_sec as u64, + ) + .is_innocent_balance_params(&is_innocent_balance_params_arc) + .is_innocent_balance_result( + balance <= gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei), + ) + .calculate_payout_threshold_in_gwei_params(&calculate_payable_threshold_params_arc) + .calculate_payout_threshold_in_gwei_result(4567898); //made up value + let mut subject = PayableScannerBuilder::new() + .payment_thresholds(custom_payment_thresholds) + .build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + + let result = subject.payable_exceeded_threshold(&payable_account, now); + + assert_eq!(result, Some(4567898)); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned_innocent, curve_derived_time) = is_innocent_age_params.remove(0); + assert_eq!(*is_innocent_age_params, vec![]); + assert_eq!(debt_age_returned_innocent, debt_age_s); + assert_eq!( + curve_derived_time, + custom_payment_thresholds.maturity_threshold_sec as u64 + ); + let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); + assert_eq!( + *is_innocent_balance_params, + vec![( + payable_account.balance_wei, + gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei) + )] + ); + let mut calculate_payable_curves_params = + calculate_payable_threshold_params_arc.lock().unwrap(); + let (payment_thresholds, debt_age_returned_curves) = + calculate_payable_curves_params.remove(0); + assert_eq!(*calculate_payable_curves_params, vec![]); + assert_eq!(debt_age_returned_curves, debt_age_s); + assert_eq!(payment_thresholds, custom_payment_thresholds) + } + + #[test] + fn payable_with_debt_under_the_slope_is_marked_unqualified() { + init_test_logging(); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let debt = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); + let time = to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; + let unqualified_payable_account = vec![PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: debt, + last_paid_timestamp: from_time_t(time), + pending_payable_opt: None, + }]; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let test_name = + "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; + let logger = Logger::new(test_name); + + let result = + subject.sniff_out_alarming_payables_and_log_them(unqualified_payable_account, &logger); + + assert_eq!(result, vec![]); + TestLogHandler::new() + .exists_no_log_containing(&format!("DEBUG: {}: Paying qualified debts", test_name)); + } + + #[test] + fn payable_with_debt_above_the_slope_is_qualified() { + init_test_logging(); + let payment_thresholds = PaymentThresholds::default(); + let debt = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); + let time = (payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec + - 1) as i64; + let qualified_payable = PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: debt, + last_paid_timestamp: from_time_t(time), + pending_payable_opt: None, + }; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let test_name = "payable_with_debt_above_the_slope_is_qualified"; + let logger = Logger::new(test_name); + + let result = subject + .sniff_out_alarming_payables_and_log_them(vec![qualified_payable.clone()], &logger); + + assert_eq!(result, vec![qualified_payable]); + TestLogHandler::new().exists_log_matching(&format!( + "DEBUG: {}: Paying qualified debts:\n999,999,999,000,000,\ + 000 wei owed for \\d+ sec exceeds threshold: 500,000,000,000,000,000 wei; creditor: \ + 0x0000000000000000000000000077616c6c657430", + test_name + )); + } + + #[test] + fn accounts_qualified_to_payment_returns_an_empty_vector_if_all_unqualified() { + init_test_logging(); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let unqualified_payable_account = vec![PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + ), + pending_payable_opt: None, + }]; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let test_name = "qualified_payables_and_summary_returns_an_empty_vector_if_all_unqualified"; + let logger = Logger::new(test_name); + + let result = + subject.sniff_out_alarming_payables_and_log_them(unqualified_payable_account, &logger); + + assert_eq!(result, vec![]); + TestLogHandler::new() + .exists_no_log_containing(&format!("DEBUG: {}: Paying qualified debts", test_name)); + } + + #[test] + fn pending_payable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "pending_payable_scanner_can_initiate_a_scan"; + let now = SystemTime::now(); + let fingerprints = vec![PendingPayableFingerprint { + rowid_opt: Some(1234), + timestamp: SystemTime::now(), + hash: Default::default(), + attempt_opt: Some(1), + amount: 1_000_000, + process_error: None, + }]; + let pending_payable_dao = + PendingPayableDaoMock::new().return_all_fingerprints_result(fingerprints.clone()); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + + let result = pending_payable_scanner.begin_scan(now, None, &Logger::new(test_name)); + + let no_of_pending_payables = fingerprints.len(); + let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Ok(RequestTransactionReceipts { + pending_payable: fingerprints, + response_skeleton_opt: None + }) + ); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for pending payable"), + &format!( + "DEBUG: {test_name}: Found {no_of_pending_payables} pending payables to process" + ), + ]) + } + + #[test] + fn pending_payable_scanner_throws_error_in_case_scan_is_already_running() { + let now = SystemTime::now(); + let pending_payable_dao = + PendingPayableDaoMock::new().return_all_fingerprints_result(vec![ + PendingPayableFingerprint { + rowid_opt: Some(1234), + timestamp: SystemTime::now(), + hash: Default::default(), + attempt_opt: Some(1), + amount: 1_000_000, + process_error: None, + }, + ]); + let mut subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let _ = subject.begin_scan(now, None, &Logger::new("test")); + + let result = subject.begin_scan(SystemTime::now(), None, &Logger::new("test")); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + } + + #[test] + fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { + let now = SystemTime::now(); + let pending_payable_dao = + PendingPayableDaoMock::new().return_all_fingerprints_result(vec![]); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + + let result = pending_payable_scanner.begin_scan(now, None, &Logger::new("test")); + + let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); + assert_eq!(result, Err(BeginScanError::NothingToProcess)); + assert_eq!(is_scan_running, false); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() + { + init_test_logging(); + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; + let hash = H256::from_uint(&U256::from(567)); + let rowid = 466; + let tx_receipt = TransactionReceipt::default(); //status defaulted to None + let when_sent = + SystemTime::now().sub(Duration::from_secs(DEFAULT_PENDING_TOO_LONG_SEC + 5)); //old transaction + let subject = PendingPayableScannerBuilder::new().build(); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: when_sent, + hash, + attempt_opt: Some(10), + amount: 123, + process_error: None, + }; + + let result = subject.interpret_transaction_receipt( + &tx_receipt, + &fingerprint, + &Logger::new(test_name), + ); + + assert_eq!( + result, + PendingTransactionStatus::Failure(PendingPayableId { hash, rowid }) + ); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Pending transaction '0x0000…0237' has exceeded the maximum \ + pending time (21600sec) and the confirmation process is going to be aborted now \ + at the final attempt 10; manual resolution is required from the user to complete \ + the transaction" + )); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { + init_test_logging(); + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; + let subject = PendingPayableScannerBuilder::new().build(); + let hash = H256::from_uint(&U256::from(567)); + let rowid = 466; + let tx_receipt = TransactionReceipt::default(); //status defaulted to None + let duration_in_ms = 100; + let when_sent = SystemTime::now().sub(Duration::from_millis(duration_in_ms)); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: when_sent, + hash, + attempt_opt: Some(1), + amount: 123, + process_error: None, + }; + + let result = subject.interpret_transaction_receipt( + &tx_receipt, + &fingerprint, + &Logger::new(test_name), + ); + + assert_eq!( + result, + PendingTransactionStatus::StillPending(PendingPayableId { hash, rowid }) + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {}: Pending transaction '{:?}' couldn't be confirmed at attempt 1 at {}ms after its sending", + test_name, hash, duration_in_ms + )); + } + + #[test] + #[should_panic( + expected = "tx receipt for pending '0x0000…007b': status code other than 0 or 1 shouldn't be possible, but was 456" + )] + fn interpret_transaction_receipt_panics_at_undefined_status_code() { + let mut tx_receipt = TransactionReceipt::default(); + tx_receipt.status = Some(U64::from(456)); + let mut fingerprint = make_pending_payable_fingerprint(); + fingerprint.hash = H256::from_uint(&U256::from(123)); + let subject = PendingPayableScannerBuilder::new().build(); + + let _ = + subject.interpret_transaction_receipt(&tx_receipt, &fingerprint, &Logger::new("test")); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { + init_test_logging(); + let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; + let subject = PendingPayableScannerBuilder::new().build(); + let mut tx_receipt = TransactionReceipt::default(); + tx_receipt.status = Some(U64::from(0)); //failure + let hash = H256::from_uint(&U256::from(4567)); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(777777), + timestamp: SystemTime::now().sub(Duration::from_millis(150000)), + hash, + attempt_opt: Some(5), + amount: 2222, + process_error: None, + }; + + let result = subject.interpret_transaction_receipt( + &tx_receipt, + &fingerprint, + &Logger::new(test_name), + ); + + assert_eq!( + result, + PendingTransactionStatus::Failure(PendingPayableId { + hash, + rowid: 777777, + }) + ); + TestLogHandler::new().exists_log_matching(&format!( + "ERROR: {test_name}: Pending transaction '0x0000…11d7' announced as a failure, \ + interpreting attempt 5 after 1500\\d\\dms from the sending" + )); + } + + #[test] + fn handle_pending_tx_handles_none_returned_for_transaction_receipt() { + init_test_logging(); + let test_name = "handle_pending_tx_handles_none_returned_for_transaction_receipt"; + let subject = PendingPayableScannerBuilder::new().build(); + let tx_receipt_opt = None; + let rowid = 455; + let hash = H256::from_uint(&U256::from(2323)); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: SystemTime::now().sub(Duration::from_millis(10000)), + hash, + attempt_opt: Some(3), + amount: 111, + process_error: None, + }; + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: vec![(tx_receipt_opt, fingerprint.clone())], + response_skeleton_opt: None, + }; + + let result = + subject.handle_pending_transaction_with_its_receipt(&msg, &Logger::new(test_name)); + + assert_eq!( + result, + vec![PendingTransactionStatus::StillPending(PendingPayableId { + hash, + rowid, + })] + ); + TestLogHandler::new().exists_log_matching(&format!( + "DEBUG: {test_name}: Interpreting a receipt for transaction '0x0000…0913' \ + but none was given; attempt 3, 100\\d\\dms since sending" + )); + } + + #[test] + fn update_payable_fingerprint_happy_path() { + let update_after_cycle_params_arc = Arc::new(Mutex::new(vec![])); + let hash = H256::from_uint(&U256::from(444888)); + let rowid = 3456; + let pending_payable_dao = PendingPayableDaoMock::default() + .update_fingerprint_params(&update_after_cycle_params_arc) + .update_fingerprint_results(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let transaction_id = PendingPayableId { hash, rowid }; + + subject.update_payable_fingerprint(transaction_id, &Logger::new("test")); + + let update_after_cycle_params = update_after_cycle_params_arc.lock().unwrap(); + assert_eq!(*update_after_cycle_params, vec![rowid]) + } + + #[test] + #[should_panic(expected = "Failure on updating payable fingerprint \ + '0x000000000000000000000000000000000000000000000000000000000006c9d8' \ + due to UpdateFailed(\"yeah, bad\")")] + fn update_payable_fingerprint_sad_path() { + let hash = H256::from_uint(&U256::from(444888)); + let rowid = 3456; + let pending_payable_dao = PendingPayableDaoMock::default().update_fingerprint_results(Err( + PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), + )); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let transaction_id = PendingPayableId { hash, rowid }; + + subject.update_payable_fingerprint(transaction_id, &Logger::new("test")); + } + + #[test] + fn cancel_tailed_transaction_works() { + init_test_logging(); + let test_name = "order_cancel_pending_transaction_works"; + let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_dao = PendingPayableDaoMock::default() + .mark_failure_params(&mark_failure_params_arc) + .mark_failure_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let tx_hash = H256::from("sometransactionhash".keccak256()); + let rowid = 2; + let transaction_id = PendingPayableId { + hash: tx_hash, + rowid, + }; + + subject.cancel_tailed_transaction(transaction_id, &Logger::new(test_name)); + + let mark_failure_params = mark_failure_params_arc.lock().unwrap(); + assert_eq!(*mark_failure_params, vec![rowid]); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Broken transaction \ + 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 left with an error \ + mark; you should take over the care of this transaction to make sure your debts will \ + be paid because there is no automated process that can fix this without you", + )); + } + + #[test] + #[should_panic( + expected = "Unsuccessful attempt for transaction 0x051a…8c19 to mark fatal error at payable \ + fingerprint due to UpdateFailed(\"no no no\"); database unreliable" + )] + fn cancel_tailed_transaction_panics_when_it_fails_to_mark_failure() { + let payable_dao = PayableDaoMock::default().transaction_canceled_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default().mark_failure_result(Err( + PendingPayableDaoError::UpdateFailed("no no no".to_string()), + )); + let subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let rowid = 2; + let hash = H256::from("sometransactionhash".keccak256()); + let transaction_id = PendingPayableId { hash, rowid }; + + subject.cancel_tailed_transaction(transaction_id, &Logger::new("test")); + } + + #[test] + #[should_panic( + expected = "Was unable to delete payable fingerprint for successful transaction '0x000000000\ + 0000000000000000000000000000000000000000000000000000315' due to 'RecordDeletion(\"the database \ + is fooling around with us\")'" + )] + fn confirm_transaction_panics_while_deleting_pending_payable_fingerprint() { + let hash = H256::from_uint(&U256::from(789)); + let rowid = 3; + let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprint_result(Err( + PendingPayableDaoError::RecordDeletion( + "the database is fooling around with us".to_string(), + ), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); + pending_payable_fingerprint.rowid_opt = Some(rowid); + pending_payable_fingerprint.hash = hash; + + subject.confirm_transaction(pending_payable_fingerprint, &Logger::new("test")); + } + + #[test] + fn confirm_transaction_works() { + init_test_logging(); + let test_name = "confirm_transaction_works"; + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let delete_pending_payable_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transaction_confirmed_params(&transaction_confirmed_params_arc) + .transaction_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default() + .delete_fingerprint_params(&delete_pending_payable_fingerprint_params_arc) + .delete_fingerprint_result(Ok(())); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let tx_hash = H256::from("sometransactionhash".keccak256()); + let amount = 4567; + let timestamp_from_time_of_payment = from_time_t(200_000_000); + let rowid = 2; + let pending_payable_fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: timestamp_from_time_of_payment, + hash: tx_hash, + attempt_opt: Some(1), + amount, + process_error: None, + }; + + subject.confirm_transaction(pending_payable_fingerprint.clone(), &Logger::new(test_name)); + + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + let delete_pending_payable_fingerprint_params = + delete_pending_payable_fingerprint_params_arc + .lock() + .unwrap(); + assert_eq!( + *transaction_confirmed_params, + vec![pending_payable_fingerprint] + ); + assert_eq!(*delete_pending_payable_fingerprint_params, vec![rowid]); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!( + "DEBUG: {test_name}: Confirmation of transaction 0x051a…8c19; \ + record for payable was modified" + ), + &format!( + "INFO: {test_name}: Transaction \ + 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 \ + has gone through the whole confirmation process succeeding" + ), + ]); + } + + #[test] + #[should_panic( + expected = "Was unable to uncheck pending payable '0x0000000000000000000000000000000000000000000\ + 000000000000000000315' after confirmation due to 'RusqliteError(\"record change not successful\")'" + )] + fn confirm_transaction_panics_on_unchecking_payable_table() { + let hash = H256::from_uint(&U256::from(789)); + let rowid = 3; + let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Err( + PayableDaoError::RusqliteError("record change not successful".to_string()), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + let mut fingerprint = make_pending_payable_fingerprint(); + fingerprint.rowid_opt = Some(rowid); + fingerprint.hash = hash; + + subject.confirm_transaction(fingerprint, &Logger::new("test")); + } + + #[test] + fn total_paid_payable_rises_with_each_bill_paid() { + let test_name = "total_paid_payable_rises_with_each_bill_paid"; + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(5), + timestamp: from_time_t(189_999_888), + hash: H256::from_uint(&U256::from(56789)), + attempt_opt: Some(1), + amount: 5478, + process_error: None, + }; + let payable_dao = PayableDaoMock::default() + .transaction_confirmed_params(&transaction_confirmed_params_arc) + .transaction_confirmed_result(Ok(())) + .transaction_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprint_result(Ok(())); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let mut financial_statistics = subject.financial_statistics.borrow().clone(); + financial_statistics.total_paid_payable_wei += 1111; + subject.financial_statistics.replace(financial_statistics); + + subject.confirm_transaction(fingerprint.clone(), &Logger::new(test_name)); + + let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + assert_eq!(total_paid_payable, 1111 + 5478); + assert_eq!(*transaction_confirmed_params, vec![fingerprint]) + } + + #[test] + fn pending_payable_scanner_handles_report_transaction_receipts_message() { + init_test_logging(); + let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message"; + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::new() + .transaction_confirmed_params(&transaction_confirmed_params_arc) + .transaction_confirmed_result(Ok(())) + .transaction_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::new() + .delete_fingerprint_result(Ok(())) + .delete_fingerprint_result(Ok(())); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let transaction_hash_1 = H256::from_uint(&U256::from(4545)); + let mut transaction_receipt_1 = TransactionReceipt::default(); + transaction_receipt_1.transaction_hash = transaction_hash_1; + transaction_receipt_1.status = Some(U64::from(1)); //success + let fingerprint_1 = PendingPayableFingerprint { + rowid_opt: Some(5), + timestamp: from_time_t(200_000_000), + hash: transaction_hash_1, + attempt_opt: Some(2), + amount: 444, + process_error: None, + }; + let transaction_hash_2 = H256::from_uint(&U256::from(1234)); + let mut transaction_receipt_2 = TransactionReceipt::default(); + transaction_receipt_2.transaction_hash = transaction_hash_2; + transaction_receipt_2.status = Some(U64::from(1)); //success + let fingerprint_2 = PendingPayableFingerprint { + rowid_opt: Some(10), + timestamp: from_time_t(199_780_000), + hash: transaction_hash_2, + attempt_opt: Some(15), + amount: 1212, + process_error: None, + }; + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: vec![ + (Some(transaction_receipt_1), fingerprint_1.clone()), + (Some(transaction_receipt_2), fingerprint_2.clone()), + ], + response_skeleton_opt: None, + }; + subject.mark_as_started(SystemTime::now()); + + let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + assert_eq!(message_opt, None); + assert_eq!( + *transaction_confirmed_params, + vec![fingerprint_1, fingerprint_2] + ); + assert_eq!(subject.scan_started_at(), None); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!( + "INFO: {}: Transaction {:?} has gone through the whole confirmation process succeeding", + test_name, transaction_hash_1 + ), + &format!( + "INFO: {}: Transaction {:?} has gone through the whole confirmation process succeeding", + test_name, transaction_hash_2 + ), + &format!("INFO: {test_name}: The PendingPayables scan ended in \\d+ms."), + ]); + } + + #[test] + fn pending_payable_scanner_handles_empty_report_transaction_receipts_message() { + init_test_logging(); + let test_name = + "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; + let mut subject = PendingPayableScannerBuilder::new().build(); + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: vec![], + response_skeleton_opt: None, + }; + subject.mark_as_started(SystemTime::now()); + + let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(message_opt, None); + assert_eq!(is_scan_running, false); + let tlh = TestLogHandler::new(); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: No transaction receipts found." + )); + tlh.exists_log_matching(&format!( + "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." + )); + } + + #[test] + fn receivable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "receivable_scanner_can_initiate_a_scan"; + let now = SystemTime::now(); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_result(vec![]) + .paid_delinquencies_result(vec![]); + let earning_wallet = make_wallet("earning"); + let mut receivable_scanner = ReceivableScannerBuilder::new() + .receivable_dao(receivable_dao) + .earning_wallet(earning_wallet.clone()) + .build(); + + let result = receivable_scanner.begin_scan(now, None, &Logger::new(test_name)); + + let is_scan_running = receivable_scanner.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Ok(RetrieveTransactions { + recipient: earning_wallet.clone(), + response_skeleton_opt: None + }) + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Scanning for receivables to {earning_wallet}" + )); + } + + #[test] + fn receivable_scanner_throws_error_in_case_scan_is_already_running() { + let now = SystemTime::now(); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_result(vec![]) + .paid_delinquencies_result(vec![]); + let earning_wallet = make_wallet("earning"); + let mut receivable_scanner = ReceivableScannerBuilder::new() + .receivable_dao(receivable_dao) + .earning_wallet(earning_wallet) + .build(); + let _ = receivable_scanner.begin_scan(now, None, &Logger::new("test")); + + let result = receivable_scanner.begin_scan(SystemTime::now(), None, &Logger::new("test")); + + let is_scan_running = receivable_scanner.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + } + + #[test] + fn receivable_scanner_scans_for_delinquencies() { + init_test_logging(); + let newly_banned_1 = make_receivable_account(1234, true); + let newly_banned_2 = make_receivable_account(2345, true); + let newly_unbanned_1 = make_receivable_account(3456, false); + let newly_unbanned_2 = make_receivable_account(4567, false); + let new_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); + let paid_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_parameters(&new_delinquencies_parameters_arc) + .new_delinquencies_result(vec![newly_banned_1.clone(), newly_banned_2.clone()]) + .paid_delinquencies_parameters(&paid_delinquencies_parameters_arc) + .paid_delinquencies_result(vec![newly_unbanned_1.clone(), newly_unbanned_2.clone()]); + let ban_parameters_arc = Arc::new(Mutex::new(vec![])); + let unban_parameters_arc = Arc::new(Mutex::new(vec![])); + let payment_thresholds = make_custom_payment_thresholds(); + let earning_wallet = make_wallet("earning"); + let banned_dao = BannedDaoMock::new() + .ban_list_result(vec![]) + .ban_parameters(&ban_parameters_arc) + .unban_parameters(&unban_parameters_arc); + let mut receivable_scanner = ReceivableScannerBuilder::new() + .receivable_dao(receivable_dao) + .banned_dao(banned_dao) + .payment_thresholds(payment_thresholds) + .earning_wallet(earning_wallet.clone()) + .build(); + let now = SystemTime::now(); + + let result = receivable_scanner.begin_scan(now, None, &Logger::new("DELINQUENCY_TEST")); + + assert_eq!( + result, + Ok(RetrieveTransactions { + recipient: earning_wallet, + response_skeleton_opt: None + }) + ); + let new_delinquencies_parameters = new_delinquencies_parameters_arc.lock().unwrap(); + assert_eq!(new_delinquencies_parameters.len(), 1); + let (timestamp_actual, payment_thresholds_actual) = new_delinquencies_parameters[0]; + assert_eq!(timestamp_actual, now); + assert_eq!(payment_thresholds_actual, payment_thresholds); + let paid_delinquencies_parameters = paid_delinquencies_parameters_arc.lock().unwrap(); + assert_eq!(paid_delinquencies_parameters.len(), 1); + assert_eq!(payment_thresholds, paid_delinquencies_parameters[0]); + let ban_parameters = ban_parameters_arc.lock().unwrap(); + assert!(ban_parameters.contains(&newly_banned_1.wallet)); + assert!(ban_parameters.contains(&newly_banned_2.wallet)); + assert_eq!(2, ban_parameters.len()); + let unban_parameters = unban_parameters_arc.lock().unwrap(); + assert!(unban_parameters.contains(&newly_unbanned_1.wallet)); + assert!(unban_parameters.contains(&newly_unbanned_2.wallet)); + assert_eq!(2, unban_parameters.len()); + let tlh = TestLogHandler::new(); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c65743132333464 \ + \\(balance: 1,234 gwei, age: \\d+ sec\\) banned for delinquency", + ); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c65743233343564 \ + \\(balance: 2,345 gwei, age: \\d+ sec\\) banned for delinquency", + ); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c6574333435366e \ + \\(balance: 3,456 gwei, age: \\d+ sec\\) is no longer delinquent: unbanned", + ); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c6574343536376e \ + \\(balance: 4,567 gwei, age: \\d+ sec\\) is no longer delinquent: unbanned", + ); + } + + #[test] + fn receivable_scanner_aborts_scan_if_no_payments_were_supplied() { + init_test_logging(); + let test_name = "receivable_scanner_aborts_scan_if_no_payments_were_supplied"; + let mut subject = ReceivableScannerBuilder::new().build(); + let msg = ReceivedPayments { + timestamp: SystemTime::now(), + payments: vec![], + response_skeleton_opt: None, + }; + + let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + + assert_eq!(message_opt, None); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: No new received payments were detected during the scanning process." + )); + } + + #[test] + fn receivable_scanner_handles_received_payments_message() { + init_test_logging(); + let test_name = "receivable_scanner_handles_received_payments_message"; + let now = SystemTime::now(); + let more_money_received_params_arc = Arc::new(Mutex::new(vec![])); + let receivable_dao = ReceivableDaoMock::new() + .more_money_received_parameters(&more_money_received_params_arc) + .more_money_receivable_result(Ok(())); + let mut subject = ReceivableScannerBuilder::new() + .receivable_dao(receivable_dao) + .build(); + let mut financial_statistics = subject.financial_statistics.borrow().clone(); + financial_statistics.total_paid_receivable_wei += 2_222_123_123; + subject.financial_statistics.replace(financial_statistics); + let receivables = vec![ + BlockchainTransaction { + block_number: 4578910, + from: make_wallet("wallet_1"), + wei_amount: 45_780, + }, + BlockchainTransaction { + block_number: 4569898, + from: make_wallet("wallet_2"), + wei_amount: 3_333_345, + }, + ]; + let msg = ReceivedPayments { + timestamp: now, + payments: receivables.clone(), + response_skeleton_opt: None, + }; + subject.mark_as_started(SystemTime::now()); + + let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + + let total_paid_receivable = subject + .financial_statistics + .borrow() + .total_paid_receivable_wei; + let more_money_received_params = more_money_received_params_arc.lock().unwrap(); + assert_eq!(message_opt, None); + assert_eq!(subject.scan_started_at(), None); + assert_eq!(total_paid_receivable, 2_222_123_123 + 45_780 + 3_333_345); + assert_eq!(*more_money_received_params, vec![(now, receivables)]); + TestLogHandler::new().exists_log_matching( + "INFO: receivable_scanner_handles_received_payments_message: The Receivables scan ended in \\d+ms.", + ); + } + + #[test] + fn remove_timestamp_and_log_if_timestamp_is_correct() { + init_test_logging(); + let test_name = "remove_timestamp_and_log_if_timestamp_is_correct"; + let time_in_past = SystemTime::now().sub(Duration::from_secs(10)); + let logger = Logger::new(test_name); + let mut subject = ScannerCommon::new(Rc::new(make_custom_payment_thresholds())); + subject.initiated_at_opt = Some(time_in_past); + + subject.remove_timestamp(ScanType::Payables, &logger); + + TestLogHandler::new().exists_log_matching(&format!( + "INFO: {test_name}: The Payables scan ended in \\d+ms." + )); + } + + #[test] + fn remove_timestamp_and_log_if_timestamp_is_not_found() { + init_test_logging(); + let test_name = "remove_timestamp_and_log_if_timestamp_is_not_found"; + let logger = Logger::new(test_name); + let mut subject = ScannerCommon::new(Rc::new(make_custom_payment_thresholds())); + subject.initiated_at_opt = None; + + subject.remove_timestamp(ScanType::Receivables, &logger); + + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Called scan_finished() for Receivables scanner but timestamp was not found" + )); + } +} diff --git a/node/src/accountant/scanners_utils.rs b/node/src/accountant/scanners_utils.rs new file mode 100644 index 000000000..33506d000 --- /dev/null +++ b/node/src/accountant/scanners_utils.rs @@ -0,0 +1,540 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod payable_scanner_utils { + use crate::accountant::dao_utils::ThresholdUtils; + use crate::accountant::payable_dao::{Payable, PayableAccount}; + use crate::accountant::SentPayables; + use crate::blockchain::blockchain_interface::BlockchainError; + use crate::sub_lib::accountant::PaymentThresholds; + use itertools::Itertools; + use masq_lib::logger::Logger; + use masq_lib::utils::plus; + #[cfg(test)] + use std::any::Any; + use std::cmp::Ordering; + use std::time::SystemTime; + use thousands::Separable; + + //debugging purposes only + pub fn investigate_debt_extremes( + timestamp: SystemTime, + all_non_pending_payables: &[PayableAccount], + ) -> String { + #[derive(Clone, Copy, Default)] + struct PayableInfo { + balance_wei: u128, + age: u64, + } + fn bigger(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { + match payable_1.balance_wei.cmp(&payable_2.balance_wei) { + Ordering::Greater => payable_1, + Ordering::Less => payable_2, + Ordering::Equal => { + if payable_1.age == payable_2.age { + payable_1 + } else { + older(payable_1, payable_2) + } + } + } + } + fn older(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { + match payable_1.age.cmp(&payable_2.age) { + Ordering::Greater => payable_1, + Ordering::Less => payable_2, + Ordering::Equal => { + if payable_1.balance_wei == payable_2.balance_wei { + payable_1 + } else { + bigger(payable_1, payable_2) + } + } + } + } + + if all_non_pending_payables.is_empty() { + return "Payable scan found no debts".to_string(); + } + + let (biggest, oldest) = all_non_pending_payables + .iter() + .map(|payable| PayableInfo { + balance_wei: payable.balance_wei, + age: timestamp + .duration_since(payable.last_paid_timestamp) + .expect("Payable time is corrupt") + .as_secs(), + }) + .fold( + Default::default(), + |(so_far_biggest, so_far_oldest): (PayableInfo, PayableInfo), payable| { + ( + bigger(so_far_biggest, payable), + older(so_far_oldest, payable), + ) + }, + ); + + format!("Payable scan found {} debts; the biggest is {} owed for {}sec, the oldest is {} owed for {}sec", + all_non_pending_payables.len(), biggest.balance_wei, biggest.age, + oldest.balance_wei, oldest.age) + } + + pub fn separate_errors( + sent_payments: &SentPayables, + logger: &Logger, + ) -> (Vec, Vec) { + sent_payments + .payable + .iter() + .fold((vec![], vec![]), |so_far, payment| { + match payment { + Ok(payment_sent) => (plus(so_far.0, payment_sent.clone()), so_far.1), + Err(error) => { + + logger.warning(|| match &error { + BlockchainError::TransactionFailed { .. } => format!("Encountered transaction error at this end: '{:?}'", error), + x => format!("Outbound transaction failure due to '{:?}'. Please check your blockchain service URL configuration.", x) + }); + + (so_far.0, plus(so_far.1, error.clone())) + } + } + }) + } + + pub fn payables_debug_summary(qualified_accounts: &[(PayableAccount, u128)], logger: &Logger) { + if qualified_accounts.is_empty() { + return; + } + debug!(logger, "Paying qualified debts:\n{}", { + let now = SystemTime::now(); + qualified_accounts + .iter() + .map(|(payable, threshold_point)| { + let p_age = now + .duration_since(payable.last_paid_timestamp) + .expect("Payable time is corrupt"); + format!( + "{} wei owed for {} sec exceeds threshold: {} wei; creditor: {}", + payable.balance_wei.separate_with_commas(), + p_age.as_secs(), + threshold_point.separate_with_commas(), + payable.wallet + ) + }) + .join("\n") + }) + } + + pub trait PayableThresholdsGauge { + fn is_innocent_age(&self, age: u64, limit: u64) -> bool; + fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool; + fn calculate_payout_threshold_in_gwei( + &self, + payment_thresholds: &PaymentThresholds, + x: u64, + ) -> u128; + as_any_dcl!(); + } + + #[derive(Default)] + pub struct PayableThresholdsGaugeReal {} + + impl PayableThresholdsGauge for PayableThresholdsGaugeReal { + fn is_innocent_age(&self, age: u64, limit: u64) -> bool { + age <= limit + } + + fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool { + balance <= limit + } + + fn calculate_payout_threshold_in_gwei( + &self, + payment_thresholds: &PaymentThresholds, + debt_age: u64, + ) -> u128 { + ThresholdUtils::calculate_finite_debt_limit_by_age(payment_thresholds, debt_age) + } + as_any_impl!(); + } +} + +pub mod pending_payable_scanner_utils { + use crate::accountant::{PendingPayableId, PendingTransactionStatus}; + use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; + use masq_lib::logger::Logger; + use masq_lib::utils::ExpectValue; + use std::time::SystemTime; + + pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { + timestamp + .elapsed() + .expect("time calculation for elapsed failed") + .as_millis() + } + + pub fn handle_none_status( + fingerprint: &PendingPayableFingerprint, + max_pending_interval: u64, + logger: &Logger, + ) -> PendingTransactionStatus { + info!( + logger, + "Pending transaction '{:?}' couldn't be confirmed at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + let elapsed = fingerprint + .timestamp + .elapsed() + .expect("we should be older now"); + let transaction_id = PendingPayableId { + hash: fingerprint.hash, + rowid: fingerprint.rowid_opt.expectv("initialized rowid"), + }; + if max_pending_interval <= elapsed.as_secs() { + error!( + logger, + "Pending transaction '{}' has exceeded the maximum pending time \ + ({}sec) and the confirmation process is going to be aborted now \ + at the final attempt {}; manual resolution is required from the \ + user to complete the transaction.", + fingerprint.hash, + max_pending_interval, + fingerprint.attempt_opt.expectv("initialized attempt") + ); + PendingTransactionStatus::Failure(transaction_id) + } else { + PendingTransactionStatus::StillPending(transaction_id) + } + } + + pub fn handle_status_with_success( + fingerprint: &PendingPayableFingerprint, + logger: &Logger, + ) -> PendingTransactionStatus { + info!( + logger, + "Transaction '{:?}' has been added to the blockchain; detected locally at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + PendingTransactionStatus::Confirmed(fingerprint.clone()) + } + + pub fn handle_status_with_failure( + fingerprint: &PendingPayableFingerprint, + logger: &Logger, + ) -> PendingTransactionStatus { + error!( + logger, + "Pending transaction '{}' announced as a failure, interpreting attempt \ + {} after {}ms from the sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + PendingTransactionStatus::Failure(fingerprint.into()) + } +} + +pub mod receivable_scanner_utils { + use crate::accountant::receivable_dao::ReceivableAccount; + use crate::accountant::wei_to_gwei; + use std::time::{Duration, SystemTime}; + use thousands::Separable; + + pub fn balance_and_age(time: SystemTime, account: &ReceivableAccount) -> (String, Duration) { + let balance = wei_to_gwei::(account.balance_wei).separate_with_commas(); + let age = time + .duration_since(account.last_received_timestamp) + .unwrap_or_else(|_| Duration::new(0, 0)); + (balance, age) + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::dao_utils::{from_time_t, to_time_t}; + use crate::accountant::payable_dao::{Payable, PayableAccount}; + use crate::accountant::receivable_dao::ReceivableAccount; + use crate::accountant::scanners_utils::payable_scanner_utils::{ + investigate_debt_extremes, payables_debug_summary, separate_errors, PayableThresholdsGauge, + PayableThresholdsGaugeReal, + }; + use crate::accountant::scanners_utils::receivable_scanner_utils::balance_and_age; + use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; + use crate::blockchain::blockchain_interface::BlockchainError; + use crate::sub_lib::accountant::PaymentThresholds; + use crate::test_utils::make_wallet; + use masq_lib::constants::WEIS_OF_GWEI; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::time::SystemTime; + + #[test] + fn investigate_debt_extremes_picks_the_most_relevant_records() { + let now = SystemTime::now(); + let now_t = to_time_t(now); + let same_amount_significance = 2_000_000; + let same_age_significance = from_time_t(now_t - 30000); + let payables = &[ + PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: same_amount_significance, + last_paid_timestamp: from_time_t(now_t - 5000), + pending_payable_opt: None, + }, + //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked + PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: same_amount_significance, + last_paid_timestamp: from_time_t(now_t - 10000), + pending_payable_opt: None, + }, + //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen + PayableAccount { + wallet: make_wallet("wallet3"), + balance_wei: 100, + last_paid_timestamp: same_age_significance, + pending_payable_opt: None, + }, + PayableAccount { + wallet: make_wallet("wallet2"), + balance_wei: 330, + last_paid_timestamp: same_age_significance, + pending_payable_opt: None, + }, + ]; + + let result = investigate_debt_extremes(now, payables); + + assert_eq!(result, "Payable scan found 4 debts; the biggest is 2000000 owed for 10000sec, the oldest is 330 owed for 30000sec") + } + + #[test] + fn balance_and_age_is_calculated_as_expected() { + let now = SystemTime::now(); + let offset = 1000; + let receivable_account = ReceivableAccount { + wallet: make_wallet("wallet0"), + balance_wei: 10_000_000_000, + last_received_timestamp: from_time_t(to_time_t(now) - offset), + }; + + let (balance, age) = balance_and_age(now, &receivable_account); + + assert_eq!(balance, "10"); + assert_eq!(age.as_secs(), offset as u64); + } + + #[test] + fn separate_errors_works() { + init_test_logging(); + let test_name = "separate_errors_works"; + let payable_ok = Payable { + to: make_wallet("blah"), + amount: 5555, + timestamp: SystemTime::now(), + tx_hash: Default::default(), + }; + let error = BlockchainError::SignedValueConversion(666); + let sent_payable = SentPayables { + timestamp: SystemTime::now(), + payable: vec![Ok(payable_ok.clone()), Err(error.clone())], + response_skeleton_opt: None, + }; + + let (ok, err) = separate_errors(&sent_payable, &Logger::new(test_name)); + + assert_eq!(ok, vec![payable_ok]); + assert_eq!(err, vec![error.clone()]); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {}: Outbound transaction failure due to '{:?}", + test_name, error + )); + } + + #[test] + fn payables_debug_summary_stays_inert_if_no_qualified_payments() { + init_test_logging(); + let logger = Logger::new("payables_debug_summary_stays_inert_if_no_qualified_payments"); + + payables_debug_summary(&vec![], &logger); + + TestLogHandler::new().exists_no_log_containing( + "DEBUG: payables_debug_summary_stays_\ + inert_if_no_qualified_payments: Paying qualified debts:", + ); + } + + #[test] + fn payables_debug_summary_prints_pretty_summary() { + init_test_logging(); + let now = to_time_t(SystemTime::now()); + let payment_thresholds = PaymentThresholds { + threshold_interval_sec: 2_592_000, + debt_threshold_gwei: 1_000_000_000, + payment_grace_period_sec: 86_400, + maturity_threshold_sec: 86_400, + permanent_debt_allowed_gwei: 10_000_000, + unban_below_gwei: 10_000_000, + }; + let qualified_payables_and_threshold_points = vec![ + ( + PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2000), + last_paid_timestamp: from_time_t( + now - checked_conversion::( + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec, + ), + ), + pending_payable_opt: None, + }, + 10_000_000_001_152_000_u128, + ), + ( + PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1), + last_paid_timestamp: from_time_t( + now - checked_conversion::( + payment_thresholds.maturity_threshold_sec + 55, + ), + ), + pending_payable_opt: None, + }, + 999_978_993_055_555_580, + ), + ]; + let logger = Logger::new("test"); + + payables_debug_summary(&qualified_payables_and_threshold_points, &logger); + + TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ + 10,002,000,000,000,000 wei owed for 2678400 sec exceeds threshold: 10,000,000,001,152,000 wei; creditor: 0x0000000000000000000000000077616c6c657430\n\ + 999,999,999,000,000,000 wei owed for 86455 sec exceeds threshold: 999,978,993,055,555,580 wei; creditor: 0x0000000000000000000000000077616c6c657431"); + } + + #[test] + fn payout_sloped_segment_in_payment_thresholds_goes_along_proper_line() { + let payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 333, + payment_grace_period_sec: 444, + permanent_debt_allowed_gwei: 4444, + debt_threshold_gwei: 8888, + threshold_interval_sec: 1111111, + unban_below_gwei: 0, + }; + let higher_corner_timestamp = payment_thresholds.maturity_threshold_sec; + let middle_point_timestamp = payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec / 2; + let lower_corner_timestamp = + payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec; + let tested_fn = |payment_thresholds: &PaymentThresholds, time| { + PayableThresholdsGaugeReal {} + .calculate_payout_threshold_in_gwei(payment_thresholds, time) as i128 + }; + + let higher_corner_point = tested_fn(&payment_thresholds, higher_corner_timestamp); + let middle_point = tested_fn(&payment_thresholds, middle_point_timestamp); + let lower_corner_point = tested_fn(&payment_thresholds, lower_corner_timestamp); + + let allowed_imprecision = WEIS_OF_GWEI; + let ideal_template_higher: i128 = gwei_to_wei(payment_thresholds.debt_threshold_gwei); + let ideal_template_middle: i128 = gwei_to_wei( + (payment_thresholds.debt_threshold_gwei + - payment_thresholds.permanent_debt_allowed_gwei) + / 2 + + payment_thresholds.permanent_debt_allowed_gwei, + ); + let ideal_template_lower: i128 = + gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei); + assert!( + higher_corner_point <= ideal_template_higher + allowed_imprecision + && ideal_template_higher - allowed_imprecision <= higher_corner_point, + "ideal: {}, real: {}", + ideal_template_higher, + higher_corner_point + ); + assert!( + middle_point <= ideal_template_middle + allowed_imprecision + && ideal_template_middle - allowed_imprecision <= middle_point, + "ideal: {}, real: {}", + ideal_template_middle, + middle_point + ); + assert!( + lower_corner_point <= ideal_template_lower + allowed_imprecision + && ideal_template_lower - allowed_imprecision <= lower_corner_point, + "ideal: {}, real: {}", + ideal_template_lower, + lower_corner_point + ) + } + + #[test] + fn is_innocent_age_works_for_age_smaller_than_innocent_age() { + let payable_age = 999; + + let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_age_works_for_age_equal_to_innocent_age() { + let payable_age = 1000; + + let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_age_works_for_excessive_age() { + let payable_age = 1001; + + let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); + + assert_eq!(result, false) + } + + #[test] + fn is_innocent_balance_works_for_balance_smaller_than_innocent_balance() { + let payable_balance = 999; + + let result = + PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_balance_works_for_balance_equal_to_innocent_balance() { + let payable_balance = 1000; + + let result = + PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_balance_works_for_excessive_balance() { + let payable_balance = 1001; + + let result = + PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); + + assert_eq!(result, false) + } +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 736bbf005..57bc85971 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -12,20 +12,26 @@ use crate::accountant::pending_payable_dao::{ use crate::accountant::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; -use crate::accountant::{gwei_to_wei, Accountant, PendingPayableId}; +use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; +use crate::accountant::{gwei_to_wei, Accountant, PendingPayableId, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::banned_dao::{BannedDao, BannedDaoFactory}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::BlockchainTransaction; use crate::bootstrapper::BootstrapperConfig; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; use crate::db_config::mocks::ConfigDaoMock; -use crate::sub_lib::accountant::{AccountantConfig, MessageIdGenerator, PaymentThresholds}; +use crate::sub_lib::accountant::{DaoFactories, FinancialStatistics}; +use crate::sub_lib::accountant::{MessageIdGenerator, PaymentThresholds}; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; -use crate::test_utils::unshared_test_utils::make_populated_accountant_config_with_defaults; +use crate::test_utils::unshared_test_utils::make_bc_with_defaults; use actix::System; use ethereum_types::{BigEndianHash, H256, U256}; +use masq_lib::logger::Logger; +use masq_lib::utils::plus; use rusqlite::{Connection, Row}; +use std::any::type_name; use std::cell::RefCell; use std::fmt::Debug; use std::rc::Rc; @@ -70,10 +76,11 @@ pub fn make_payable_account_with_recipient_and_balance_and_timestamp_opt( pub struct AccountantBuilder { config: Option, - payable_dao_factory: Option>, - receivable_dao_factory: Option>, - pending_payable_dao_factory: Option>, - banned_dao_factory: Option>, + logger: Option, + payable_dao_factory: Option, + receivable_dao_factory: Option, + pending_payable_dao_factory: Option, + banned_dao_factory: Option, config_dao_factory: Option>, } @@ -81,6 +88,7 @@ impl Default for AccountantBuilder { fn default() -> Self { Self { config: None, + logger: None, payable_dao_factory: None, receivable_dao_factory: None, pending_payable_dao_factory: None, @@ -90,31 +98,187 @@ impl Default for AccountantBuilder { } } +pub enum DaoWithDestination { + AccountantBodyDest(T), + PayableScannerDest(T), + ReceivableScannerDest(T), + PendingPayableScannerDest(T), +} + +enum DestinationMarker { + AccountantBody, + PayableScanner, + ReceivableScanner, + PendingPayableScanner, +} + +impl DaoWithDestination { + fn matches(&self, dest_marker: &DestinationMarker) -> bool { + match self { + Self::AccountantBodyDest(_) => matches!(dest_marker, DestinationMarker::AccountantBody), + Self::PayableScannerDest(_) => { + matches!(dest_marker, DestinationMarker::PayableScanner) + } + Self::ReceivableScannerDest(_) => { + matches!(dest_marker, DestinationMarker::ReceivableScanner) + } + Self::PendingPayableScannerDest(_) => { + matches!(dest_marker, DestinationMarker::PendingPayableScanner) + } + } + } + fn inner_value(self) -> T { + match self { + Self::AccountantBodyDest(dao) => dao, + Self::PayableScannerDest(dao) => dao, + Self::ReceivableScannerDest(dao) => dao, + Self::PendingPayableScannerDest(dao) => dao, + } + } +} + +fn fill_vacancies_with_given_or_default_daos( + std_dao_initialization_order: [DestinationMarker; N], + mut customized_dao_set: Vec>, +) -> Vec> { + let input_count = customized_dao_set.len(); + + let fold_init: (Vec>, usize) = (vec![], 0); + let (factory_make_queue, used_input) = std_dao_initialization_order.into_iter().fold( + fold_init, + |(acc, used_input), std_position: DestinationMarker| { + if let Some(idx) = customized_dao_set + .iter() + .position(|customized_dao| customized_dao.matches(&std_position)) + { + let customized_dao = customized_dao_set.remove(idx).inner_value(); + (plus(acc, Box::new(customized_dao)), used_input + 1) + } else { + (plus(acc, Box::new(Default::default())), used_input) + } + }, + ); + if input_count != used_input { + panic!( + "you supplied DAO for unrealistic destination; look at the destination matrix that \ + describes all proper usages of {:?} and decode those places by the num_rep() function \ + pattern", + type_name::() + ) + } + factory_make_queue +} + +macro_rules! init_or_update_factory { + ( + $dao_set: expr, //Vec> + $dao_initialization_order_in_accountant: expr, //[DestinationMarker;N] + $dao_factory_mock: ident, // XxxDaoFactoryMock + $factory_field_in_builder: ident, //Option + $dao_trait: ident, + $self: expr //mut AccountantBuilder + ) => {{ + let populated_queue = fill_vacancies_with_given_or_default_daos( + $dao_initialization_order_in_accountant, + $dao_set, + ); + let populated_queue: Vec> = populated_queue + .into_iter() + .map(|elem| elem as Box) + .collect(); + let prepared_factory = match $self.$factory_field_in_builder.take() { + Some(existing_factory) => { + existing_factory.make_results.replace(populated_queue); + existing_factory + } + None => { + let mut new_factory = $dao_factory_mock::new(); + new_factory.make_results = RefCell::new(populated_queue); + new_factory + } + }; + $self.$factory_field_in_builder = Some(prepared_factory); + $self + }}; +} + impl AccountantBuilder { pub fn bootstrapper_config(mut self, config: BootstrapperConfig) -> Self { self.config = Some(config); self } - pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> Self { - self.payable_dao_factory = Some(Box::new(PayableDaoFactoryMock::new(payable_dao))); + pub fn logger(mut self, logger: Logger) -> Self { + self.logger = Some(logger); self } - pub fn receivable_dao(mut self, receivable_dao: ReceivableDaoMock) -> Self { - self.receivable_dao_factory = Some(Box::new(ReceivableDaoFactoryMock::new(receivable_dao))); - self + pub fn payable_daos( + mut self, + specially_configured_daos: Vec>, + ) -> Self { + let initialization_order_in_accountant = [ + DestinationMarker::AccountantBody, + DestinationMarker::PayableScanner, + DestinationMarker::PendingPayableScanner, + ]; + init_or_update_factory!( + specially_configured_daos, + initialization_order_in_accountant, + PayableDaoFactoryMock, + payable_dao_factory, + PayableDao, + self + ) } - pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { - self.pending_payable_dao_factory = Some(Box::new(PendingPayableDaoFactoryMock::new( - pending_payable_dao, - ))); - self + pub fn receivable_daos( + mut self, + specially_configured_daos: Vec>, + ) -> Self { + let initialization_order_in_accountant = [ + DestinationMarker::AccountantBody, + DestinationMarker::ReceivableScanner, + ]; + init_or_update_factory!( + specially_configured_daos, + initialization_order_in_accountant, + ReceivableDaoFactoryMock, + receivable_dao_factory, + ReceivableDao, + self + ) + } + + pub fn pending_payable_daos( + mut self, + specially_configured_daos: Vec>, + ) -> Self { + let initialization_order_in_accountant = [ + DestinationMarker::AccountantBody, + DestinationMarker::PayableScanner, + DestinationMarker::PendingPayableScanner, + ]; + init_or_update_factory!( + specially_configured_daos, + initialization_order_in_accountant, + PendingPayableDaoFactoryMock, + pending_payable_dao_factory, + PendingPayableDao, + self + ) } + //TODO this method seems to be never used? pub fn banned_dao(mut self, banned_dao: BannedDaoMock) -> Self { - self.banned_dao_factory = Some(Box::new(BannedDaoFactoryMock::new(banned_dao))); + match self.banned_dao_factory { + None => { + self.banned_dao_factory = Some(BannedDaoFactoryMock::new().make_result(banned_dao)) + } + Some(banned_dao_factory) => { + self.banned_dao_factory = Some(banned_dao_factory.make_result(banned_dao)) + } + } self } @@ -124,110 +288,146 @@ impl AccountantBuilder { } pub fn build(self) -> Accountant { - let config = self.config.unwrap_or({ - let mut config = BootstrapperConfig::default(); - config.accountant_config_opt = Some(make_populated_accountant_config_with_defaults()); - config - }); - let payable_dao_factory = self - .payable_dao_factory - .unwrap_or(Box::new(PayableDaoFactoryMock::new(PayableDaoMock::new()))); - let receivable_dao_factory = - self.receivable_dao_factory - .unwrap_or(Box::new(ReceivableDaoFactoryMock::new( - ReceivableDaoMock::new(), - ))); - let pending_payable_dao_factory = self.pending_payable_dao_factory.unwrap_or(Box::new( - PendingPayableDaoFactoryMock::new(PendingPayableDaoMock::default()), - )); + let config = self.config.unwrap_or(make_bc_with_defaults()); + let payable_dao_factory = self.payable_dao_factory.unwrap_or( + PayableDaoFactoryMock::new() + .make_result(PayableDaoMock::new()) + .make_result(PayableDaoMock::new()) + .make_result(PayableDaoMock::new()), + ); + let receivable_dao_factory = self.receivable_dao_factory.unwrap_or( + ReceivableDaoFactoryMock::new() + .make_result(ReceivableDaoMock::new()) + .make_result(ReceivableDaoMock::new()), + ); + let pending_payable_dao_factory = self.pending_payable_dao_factory.unwrap_or( + PendingPayableDaoFactoryMock::new() + .make_result(PendingPayableDaoMock::new()) + .make_result(PendingPayableDaoMock::new()) + .make_result(PendingPayableDaoMock::new()), + ); let banned_dao_factory = self .banned_dao_factory - .unwrap_or(Box::new(BannedDaoFactoryMock::new(BannedDaoMock::new()))); - let accountant = Accountant::new( - &config, - payable_dao_factory, - receivable_dao_factory, - pending_payable_dao_factory, - banned_dao_factory, + .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); + let mut accountant = Accountant::new( + config, + DaoFactories { + payable_dao_factory: Box::new(payable_dao_factory), + pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + receivable_dao_factory: Box::new(receivable_dao_factory), + banned_dao_factory: Box::new(banned_dao_factory), + }, ); + if let Some(logger) = self.logger { + accountant.logger = logger; + } + accountant } } pub struct PayableDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl PayableDaoFactory for PayableDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().remove(0)) + if self.make_results.borrow().len() == 0 { + panic!( + "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + ) + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl PayableDaoFactoryMock { - pub fn new(mock: PayableDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(vec![mock]), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: PayableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } } pub struct ReceivableDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl ReceivableDaoFactory for ReceivableDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().remove(0)) + if self.make_results.borrow().len() == 0 { + panic!( + "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only supplied for Accountant and not for the Scanner while building Accountant." + ) + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl ReceivableDaoFactoryMock { - pub fn new(mock: ReceivableDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(vec![mock]), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: ReceivableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } } pub struct BannedDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl BannedDaoFactory for BannedDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().take().unwrap()) + if self.make_results.borrow().len() == 0 { + panic!("BannedDao Missing.") + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl BannedDaoFactoryMock { - pub fn new(mock: BannedDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(Some(mock)), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: BannedDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } } @@ -273,7 +473,7 @@ pub struct PayableDaoMock { custom_query_params: Arc>>>, custom_query_result: RefCell>>>, total_results: RefCell>, - pub have_non_pending_payable_shut_down_the_system: bool, + pub have_non_pending_payable_shut_down_the_system: bool, //TODO maybe we should get rid of this kind of fields and go the Utkarshe's way } impl PayableDao for PayableDaoMock { @@ -626,23 +826,14 @@ impl BannedDaoMock { } } -pub fn bc_from_ac_plus_earning_wallet( - ac: AccountantConfig, - earning_wallet: Wallet, -) -> BootstrapperConfig { - let mut bc = BootstrapperConfig::new(); - bc.accountant_config_opt = Some(ac); +pub fn bc_from_earning_wallet(earning_wallet: Wallet) -> BootstrapperConfig { + let mut bc = make_bc_with_defaults(); bc.earning_wallet = earning_wallet; bc } -pub fn bc_from_ac_plus_wallets( - ac: AccountantConfig, - consuming_wallet: Wallet, - earning_wallet: Wallet, -) -> BootstrapperConfig { - let mut bc = BootstrapperConfig::new(); - bc.accountant_config_opt = Some(ac); +pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> BootstrapperConfig { + let mut bc = make_bc_with_defaults(); bc.consuming_wallet_opt = Some(consuming_wallet); bc.earning_wallet = earning_wallet; bc @@ -715,6 +906,10 @@ impl PendingPayableDao for PendingPayableDaoMock { } impl PendingPayableDaoMock { + pub fn new() -> Self { + PendingPayableDaoMock::default() + } + pub fn fingerprint_rowid_params(mut self, params: &Arc>>) -> Self { self.fingerprint_rowid_params = params.clone(); self @@ -782,29 +977,182 @@ impl PendingPayableDaoMock { } pub struct PendingPayableDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().remove(0)) + if self.make_results.borrow().len() == 0 { + panic!( + "PendingPayableDao Missing. This problem mostly occurs when PendingPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + ) + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl PendingPayableDaoFactoryMock { - pub fn new(mock: PendingPayableDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(vec![mock]), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: PendingPayableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); + self + } +} + +pub struct PayableScannerBuilder { + payable_dao: PayableDaoMock, + pending_payable_dao: PendingPayableDaoMock, + payment_thresholds: PaymentThresholds, +} + +impl PayableScannerBuilder { + pub fn new() -> Self { + Self { + payable_dao: PayableDaoMock::new(), + pending_payable_dao: PendingPayableDaoMock::new(), + payment_thresholds: PaymentThresholds::default(), + } + } + + pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> PayableScannerBuilder { + self.payable_dao = payable_dao; + self + } + + pub fn payment_thresholds(mut self, payment_thresholds: PaymentThresholds) -> Self { + self.payment_thresholds = payment_thresholds; + self + } + + pub fn pending_payable_dao( + mut self, + pending_payable_dao: PendingPayableDaoMock, + ) -> PayableScannerBuilder { + self.pending_payable_dao = pending_payable_dao; self } + + pub fn build(self) -> PayableScanner { + PayableScanner::new( + Box::new(self.payable_dao), + Box::new(self.pending_payable_dao), + Rc::new(self.payment_thresholds), + ) + } +} + +pub struct PendingPayableScannerBuilder { + payable_dao: PayableDaoMock, + pending_payable_dao: PendingPayableDaoMock, + payment_thresholds: PaymentThresholds, + when_pending_too_long_sec: u64, + financial_statistics: FinancialStatistics, +} + +impl PendingPayableScannerBuilder { + pub fn new() -> Self { + Self { + payable_dao: PayableDaoMock::new(), + pending_payable_dao: PendingPayableDaoMock::new(), + payment_thresholds: PaymentThresholds::default(), + when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, + financial_statistics: FinancialStatistics::default(), + } + } + + pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> Self { + self.payable_dao = payable_dao; + self + } + + pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { + self.pending_payable_dao = pending_payable_dao; + self + } + + pub fn build(self) -> PendingPayableScanner { + PendingPayableScanner::new( + Box::new(self.payable_dao), + Box::new(self.pending_payable_dao), + Rc::new(self.payment_thresholds), + self.when_pending_too_long_sec, + Rc::new(RefCell::new(self.financial_statistics)), + ) + } +} + +pub struct ReceivableScannerBuilder { + receivable_dao: ReceivableDaoMock, + banned_dao: BannedDaoMock, + payment_thresholds: PaymentThresholds, + earning_wallet: Wallet, + financial_statistics: FinancialStatistics, +} + +impl ReceivableScannerBuilder { + pub fn new() -> Self { + Self { + receivable_dao: ReceivableDaoMock::new(), + banned_dao: BannedDaoMock::new(), + payment_thresholds: PaymentThresholds::default(), + earning_wallet: make_wallet("earning_default"), + financial_statistics: FinancialStatistics::default(), + } + } + + pub fn receivable_dao(mut self, receivable_dao: ReceivableDaoMock) -> Self { + self.receivable_dao = receivable_dao; + self + } + + pub fn banned_dao(mut self, banned_dao: BannedDaoMock) -> Self { + self.banned_dao = banned_dao; + self + } + + pub fn payment_thresholds(mut self, payment_thresholds: PaymentThresholds) -> Self { + self.payment_thresholds = payment_thresholds; + self + } + + pub fn earning_wallet(mut self, earning_wallet: Wallet) -> Self { + self.earning_wallet = earning_wallet; + self + } + + pub fn build(self) -> ReceivableScanner { + ReceivableScanner::new( + Box::new(self.receivable_dao), + Box::new(self.banned_dao), + Rc::new(self.payment_thresholds), + Rc::new(self.earning_wallet), + Rc::new(RefCell::new(self.financial_statistics)), + ) + } +} + +pub fn make_custom_payment_thresholds() -> PaymentThresholds { + PaymentThresholds { + threshold_interval_sec: 2_592_000, + debt_threshold_gwei: 1_000_000_000, + payment_grace_period_sec: 86_400, + maturity_threshold_sec: 86_400, + permanent_debt_allowed_gwei: 10_000_000, + unban_below_gwei: 10_000_000, + } } pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { @@ -818,6 +1166,56 @@ pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { } } +pub fn make_payables( + now: SystemTime, + payment_thresholds: &PaymentThresholds, +) -> ( + Vec, + Vec, + Vec, +) { + let unqualified_payable_accounts = vec![PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + ), + pending_payable_opt: None, + }]; + let qualified_payable_accounts = vec![ + PayableAccount { + wallet: make_wallet("wallet2"), + balance_wei: gwei_to_wei( + payment_thresholds.permanent_debt_allowed_gwei + 1_000_000_000, + ), + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 1, + ), + pending_payable_opt: None, + }, + PayableAccount { + wallet: make_wallet("wallet3"), + balance_wei: gwei_to_wei( + payment_thresholds.permanent_debt_allowed_gwei + 1_200_000_000, + ), + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 100, + ), + pending_payable_opt: None, + }, + ]; + + let mut all_non_pending_payables = Vec::new(); + all_non_pending_payables.extend(qualified_payable_accounts.clone()); + all_non_pending_payables.extend(unqualified_payable_accounts.clone()); + + ( + qualified_payable_accounts, + unqualified_payable_accounts, + all_non_pending_payables, + ) +} + pub fn convert_to_all_string_values(str_args: Vec<(&str, &str)>) -> Vec<(String, String)> { str_args .into_iter() @@ -856,3 +1254,82 @@ where conn.query_row("select exclamations from whatever", [], tested_fn) .unwrap(); } + +#[derive(Default)] +pub struct PayableThresholdsGaugeMock { + is_innocent_age_params: Arc>>, + is_innocent_age_results: RefCell>, + is_innocent_balance_params: Arc>>, + is_innocent_balance_results: RefCell>, + calculate_payout_threshold_in_gwei_params: Arc>>, + calculate_payout_threshold_in_gwei_results: RefCell>, +} + +impl PayableThresholdsGauge for PayableThresholdsGaugeMock { + fn is_innocent_age(&self, age: u64, limit: u64) -> bool { + self.is_innocent_age_params + .lock() + .unwrap() + .push((age, limit)); + self.is_innocent_age_results.borrow_mut().remove(0) + } + + fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool { + self.is_innocent_balance_params + .lock() + .unwrap() + .push((balance, limit)); + self.is_innocent_balance_results.borrow_mut().remove(0) + } + + fn calculate_payout_threshold_in_gwei( + &self, + payment_thresholds: &PaymentThresholds, + x: u64, + ) -> u128 { + self.calculate_payout_threshold_in_gwei_params + .lock() + .unwrap() + .push((*payment_thresholds, x)); + self.calculate_payout_threshold_in_gwei_results + .borrow_mut() + .remove(0) + } +} + +impl PayableThresholdsGaugeMock { + pub fn is_innocent_age_params(mut self, params: &Arc>>) -> Self { + self.is_innocent_age_params = params.clone(); + self + } + + pub fn is_innocent_age_result(self, result: bool) -> Self { + self.is_innocent_age_results.borrow_mut().push(result); + self + } + + pub fn is_innocent_balance_params(mut self, params: &Arc>>) -> Self { + self.is_innocent_balance_params = params.clone(); + self + } + + pub fn is_innocent_balance_result(self, result: bool) -> Self { + self.is_innocent_balance_results.borrow_mut().push(result); + self + } + + pub fn calculate_payout_threshold_in_gwei_params( + mut self, + params: &Arc>>, + ) -> Self { + self.calculate_payout_threshold_in_gwei_params = params.clone(); + self + } + + pub fn calculate_payout_threshold_in_gwei_result(self, result: u128) -> Self { + self.calculate_payout_threshold_in_gwei_results + .borrow_mut() + .push(result); + self + } +} diff --git a/node/src/accountant/tools.rs b/node/src/accountant/tools.rs deleted file mode 100644 index 399d9e536..000000000 --- a/node/src/accountant/tools.rs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -pub(in crate::accountant) mod accountant_tools { - use crate::accountant::{ - Accountant, CancelFailedPendingTransaction, ConfirmPendingTransaction, - RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, ScanForPendingPayables, - ScanForReceivables, - }; - use crate::sub_lib::utils::{NotifyHandle, NotifyLaterHandle}; - use actix::{Context, Recipient}; - #[cfg(test)] - use std::any::Any; - - pub struct Scanners { - pub pending_payables: Box, - pub payables: Box, - pub receivables: Box, - } - - impl Default for Scanners { - fn default() -> Self { - Scanners { - pending_payables: Box::new(PendingPayablesScanner), - payables: Box::new(PayablesScanner), - receivables: Box::new(ReceivablesScanner), - } - } - } - - pub trait Scanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option); - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context); - as_any_dcl!(); - } - - #[derive(Debug, PartialEq, Eq)] - pub struct PendingPayablesScanner; - - impl Scanner for PendingPayablesScanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option) { - accountant.scan_for_pending_payable(response_skeleton_opt) - } - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context) { - let _ = accountant - .confirmation_tools - .notify_later_scan_for_pending_payable - .notify_later( - ScanForPendingPayables { - response_skeleton_opt: None, // because scheduled scans don't respond - }, - accountant - .config - .scan_intervals - .pending_payable_scan_interval, - ctx, - ); - } - as_any_impl!(); - } - - #[derive(Debug, PartialEq, Eq)] - pub struct PayablesScanner; - - impl Scanner for PayablesScanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option) { - accountant.scan_for_payables(response_skeleton_opt) - } - - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context) { - let _ = accountant - .confirmation_tools - .notify_later_scan_for_payable - .notify_later( - ScanForPayables { - response_skeleton_opt: None, - }, - accountant.config.scan_intervals.payable_scan_interval, - ctx, - ); - } - - as_any_impl!(); - } - - #[derive(Debug, PartialEq, Eq)] - pub struct ReceivablesScanner; - - impl Scanner for ReceivablesScanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option) { - // TODO: Figure out how to combine the results of these two into a single response to the UI - accountant.scan_for_received_payments(response_skeleton_opt); - accountant.scan_for_delinquencies() - } - - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context) { - let _ = accountant - .confirmation_tools - .notify_later_scan_for_receivable - .notify_later( - ScanForReceivables { - response_skeleton_opt: None, - }, - accountant.config.scan_intervals.receivable_scan_interval, - ctx, - ); - } - - as_any_impl!(); - } - - //this is for turning off a certain scanner in testing to prevent it make "noise" - #[derive(Debug, PartialEq, Eq)] - pub struct NullScanner; - - impl Scanner for NullScanner { - fn scan(&self, _accountant: &Accountant, _response_skeleton_opt: Option) { - } - fn notify_later_assertable( - &self, - _accountant: &Accountant, - _ctx: &mut Context, - ) { - } - as_any_impl!(); - } - - #[derive(Default)] - pub struct TransactionConfirmationTools { - pub notify_later_scan_for_pending_payable: - Box>, - pub notify_later_scan_for_payable: Box>, - pub notify_later_scan_for_receivable: - Box>, - pub notify_confirm_transaction: - Box>, - pub notify_cancel_failed_transaction: - Box>, - pub request_transaction_receipts_subs_opt: Option>, - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::tools::accountant_tools::{ - PayablesScanner, PendingPayablesScanner, ReceivablesScanner, Scanners, - }; - - #[test] - fn scanners_are_properly_defaulted() { - let subject = Scanners::default(); - - assert_eq!( - subject.pending_payables.as_any().downcast_ref(), - Some(&PendingPayablesScanner) - ); - assert_eq!( - subject.payables.as_any().downcast_ref(), - Some(&PayablesScanner) - ); - assert_eq!( - subject.receivables.as_any().downcast_ref(), - Some(&ReceivablesScanner) - ) - } -} diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 06f980069..1bc660215 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -18,7 +18,7 @@ use crate::database::db_initializer::{connection_or_panic, DbInitializer, DbInit use crate::db_config::persistent_configuration::PersistentConfiguration; use crate::node_configurator::configurator::Configurator; use crate::sub_lib::accountant::{ - AccountantSubs, AccountantSubsFactory, AccountantSubsFactoryReal, + AccountantSubs, AccountantSubsFactory, AccountantSubsFactoryReal, DaoFactories, }; use crate::sub_lib::blockchain_bridge::BlockchainBridgeSubs; use crate::sub_lib::configurator::ConfiguratorSubs; @@ -152,7 +152,7 @@ impl ActorSystemFactoryTools for ActorSystemFactoryToolsReal { let blockchain_bridge_subs = actor_factory.make_and_start_blockchain_bridge(&config); let neighborhood_subs = actor_factory.make_and_start_neighborhood(cryptdes.main, &config); let accountant_subs = actor_factory.make_and_start_accountant( - &config, + config.clone(), &db_initializer, &BannedCacheLoaderReal {}, &AccountantSubsFactoryReal {}, @@ -359,7 +359,7 @@ pub trait ActorFactory { ) -> NeighborhoodSubs; fn make_and_start_accountant( &self, - config: &BootstrapperConfig, + config: BootstrapperConfig, db_initializer: &dyn DbInitializer, banned_cache_loader: &dyn BannedCacheLoader, accountant_subs_factory: &dyn AccountantSubsFactory, @@ -438,30 +438,31 @@ impl ActorFactory for ActorFactoryReal { fn make_and_start_accountant( &self, - config: &BootstrapperConfig, + config: BootstrapperConfig, db_initializer: &dyn DbInitializer, banned_cache_loader: &dyn BannedCacheLoader, accountant_subs_factory: &dyn AccountantSubsFactory, ) -> AccountantSubs { let data_directory = config.data_directory.as_path(); - let payable_dao_factory = Accountant::dao_factory(data_directory); - let receivable_dao_factory = Accountant::dao_factory(data_directory); - let pending_payable_dao_factory = Accountant::dao_factory(data_directory); - let banned_dao_factory = Accountant::dao_factory(data_directory); + let payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let pending_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let receivable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let banned_dao_factory = Box::new(Accountant::dao_factory(data_directory)); banned_cache_loader.load(connection_or_panic( db_initializer, data_directory, DbInitializationConfig::panic_on_migration(), )); - let cloned_config = config.clone(); let arbiter = Arbiter::builder().stop_system_on_panic(true); let addr: Addr = arbiter.start(move |_| { Accountant::new( - &cloned_config, - Box::new(payable_dao_factory), - Box::new(receivable_dao_factory), - Box::new(pending_payable_dao_factory), - Box::new(banned_dao_factory), + config, + DaoFactories { + payable_dao_factory, + pending_payable_dao_factory, + receivable_dao_factory, + banned_dao_factory, + }, ) }); accountant_subs_factory.make(&addr) @@ -601,7 +602,8 @@ impl LogRecipientSetter for LogRecipientSetterReal { mod tests { use super::*; use crate::accountant::check_sqlite_fns::TestUserDefinedSqliteFnsForNewDelinquencies; - use crate::accountant::test_utils::bc_from_ac_plus_earning_wallet; + use crate::accountant::test_utils::bc_from_earning_wallet; + use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::actor_system_factory::tests::ShouldWeRunTheTest::{GoAhead, Skip}; use crate::bootstrapper::{Bootstrapper, RealUser}; use crate::database::connection_wrapper::ConnectionWrapper; @@ -609,6 +611,7 @@ mod tests { make_stream_handler_pool_subs_from, make_stream_handler_pool_subs_from_recorder, start_recorder_refcell_opt, }; + use crate::sub_lib::accountant::{PaymentThresholds, ScanIntervals}; use crate::sub_lib::blockchain_bridge::BlockchainBridgeConfig; use crate::sub_lib::cryptde::{PlainData, PublicKey}; use crate::sub_lib::cryptde_null::CryptDENull; @@ -630,9 +633,7 @@ mod tests { make_ui_gateway_subs_from_recorder, Recording, }; use crate::test_utils::recorder::{make_recorder, Recorder}; - use crate::test_utils::unshared_test_utils::{ - make_populated_accountant_config_with_defaults, ArbitraryIdStamp, SystemKillerActor, - }; + use crate::test_utils::unshared_test_utils::{ArbitraryIdStamp, SystemKillerActor}; use crate::test_utils::{alias_cryptde, rate_pack}; use crate::test_utils::{main_cryptde, make_cryptde_pair}; use crate::{hopper, proxy_client, proxy_server, stream_handler_pool, ui_gateway}; @@ -864,7 +865,7 @@ mod tests { fn make_and_start_accountant( &self, - config: &BootstrapperConfig, + config: BootstrapperConfig, _db_initializer: &dyn DbInitializer, _banned_cache_loader: &dyn BannedCacheLoader, _accountant_subs_factory: &dyn AccountantSubsFactory, @@ -1037,7 +1038,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: Some(make_populated_accountant_config_with_defaults()), + scan_intervals_opt: Some(ScanIntervals::default()), + suppress_initial_scans: false, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1063,6 +1065,8 @@ mod tests { rate_pack(100), ), }, + payment_thresholds_opt: Some(PaymentThresholds::default()), + when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, }; let persistent_config = PersistentConfigurationMock::default().chain_name_result("eth-ropsten".to_string()); @@ -1107,7 +1111,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: None, + scan_intervals_opt: None, + suppress_initial_scans: false, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1133,6 +1138,8 @@ mod tests { rate_pack(100), ), }, + payment_thresholds_opt: Default::default(), + when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC }; let add_mapping_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = make_subject_with_null_setter(); @@ -1403,7 +1410,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: None, + scan_intervals_opt: None, + suppress_initial_scans: false, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1425,6 +1433,8 @@ mod tests { neighborhood_config: NeighborhoodConfig { mode: NeighborhoodMode::ConsumeOnly(vec![]), }, + payment_thresholds_opt: Default::default(), + when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC }; let system = System::new("MASQNode"); let mut subject = make_subject_with_null_setter(); @@ -1585,7 +1595,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: None, + scan_intervals_opt: None, + suppress_initial_scans: false, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1611,6 +1622,8 @@ mod tests { ), }, node_descriptor: Default::default(), + payment_thresholds_opt: Default::default(), + when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, }; let subject = make_subject_with_null_setter(); let system = System::new("MASQNode"); @@ -1938,11 +1951,10 @@ mod tests { "actor_system_factory", "our_big_int_sqlite_functions_are_linked_to_receivable_dao_within_accountant", ); - let accountant_config = make_populated_accountant_config_with_defaults(); let _ = DbInitializerReal::default() .initialize(data_dir.as_ref(), DbInitializationConfig::test_default()) .unwrap(); - let mut b_config = bc_from_ac_plus_earning_wallet(accountant_config, make_wallet("mine")); + let mut b_config = bc_from_earning_wallet(make_wallet("mine")); b_config.data_directory = data_dir; let system = System::new( "our_big_int_sqlite_functions_are_linked_to_receivable_dao_within_accountant", @@ -1951,7 +1963,7 @@ mod tests { let subject = ActorFactoryReal {}; subject.make_and_start_accountant( - &b_config, + b_config, &DbInitializerReal::default(), &BannedCacheLoaderMock::default(), &AccountantSubsFactoryTestOnly { diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index cd59bc4e5..0b5eefd6c 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -327,21 +327,19 @@ impl BlockchainBridge { _ => so_far, }); let (vector_of_results, error_opt) = short_circuit_result; - if !vector_of_results.is_empty() { - let pairs = vector_of_results - .into_iter() - .zip(msg.pending_payable.iter().cloned()) - .collect_vec(); - self.payment_confirmation - .report_transaction_receipts_sub_opt - .as_ref() - .expect("Accountant is unbound") - .try_send(ReportTransactionReceipts { - fingerprints_with_receipts: pairs, - response_skeleton_opt: msg.response_skeleton_opt, - }) - .expect("Accountant is dead"); - } + let pairs = vector_of_results + .into_iter() + .zip(msg.pending_payable.iter().cloned()) + .collect_vec(); + self.payment_confirmation + .report_transaction_receipts_sub_opt + .as_ref() + .expect("Accountant is unbound") + .try_send(ReportTransactionReceipts { + fingerprints_with_receipts: pairs, + response_skeleton_opt: msg.response_skeleton_opt, + }) + .expect("Accountant is dead"); if let Some((e, hash)) = error_opt { return Err (format! ( "Aborting scanning; request of a transaction receipt for '{:?}' failed due to '{:?}'", @@ -362,23 +360,15 @@ impl BlockchainBridge { Ok(_r) => (), Err(e) => { warning!(self.logger, "{}", e); - if let Some(skeleton) = skeleton_opt { - debug!(self.logger, "Skeleton is populated; sending ScanError"); - self.scan_error_subs_opt - .as_ref() - .expect("Accountant not bound") - .try_send(ScanError { - scan_type, - response_skeleton: skeleton, - msg: e, - }) - .expect("Accountant is dead"); - } else { - debug!( - self.logger, - "Skeleton is unpopulated; not sending ScanError" - ); - } + self.scan_error_subs_opt + .as_ref() + .expect("Accountant not bound") + .try_send(ScanError { + scan_type, + response_skeleton_opt: skeleton_opt, + msg: e, + }) + .expect("Accountant is dead"); } } } @@ -732,6 +722,8 @@ mod tests { #[test] fn handle_report_account_payable_manages_gas_price_error() { init_test_logging(); + let (accountant, _, accountant_recording_arc) = make_recorder(); + let scan_error_recipient: Recipient = accountant.start().recipient(); let blockchain_interface_mock = BlockchainInterfaceMock::default() .get_transaction_count_result(Ok(web3::types::U256::from(1))); let persistent_configuration_mock = PersistentConfigurationMock::new() @@ -743,6 +735,7 @@ mod tests { false, Some(consuming_wallet), ); + subject.scan_error_subs_opt = Some(scan_error_recipient); let request = ReportAccountsPayable { accounts: vec![PayableAccount { wallet: make_wallet("blah"), @@ -752,6 +745,7 @@ mod tests { }], response_skeleton_opt: None, }; + let system = System::new("test"); subject.handle_scan( BlockchainBridge::handle_report_accounts_payable, @@ -759,6 +753,19 @@ mod tests { &request, ); + System::current().stop(); + system.run(); + let recording = accountant_recording_arc.lock().unwrap(); + let message = recording.get_record::(0); + assert_eq!( + message, + &ScanError { + scan_type: ScanType::Payables, + response_skeleton_opt: None, + msg: "ReportAccountPayable: gas-price: TransactionError".to_string() + } + ); + assert_eq!(recording.len(), 1); TestLogHandler::new().exists_log_containing( "WARN: BlockchainBridge: ReportAccountPayable: gas-price: TransactionError", ); @@ -835,6 +842,8 @@ mod tests { #[test] fn blockchain_bridge_logs_error_from_retrieving_received_payments() { init_test_logging(); + let (accountant, _, accountant_recording_arc) = make_recorder(); + let scan_error_recipient: Recipient = accountant.start().recipient(); let blockchain_interface = BlockchainInterfaceMock::default().retrieve_transactions_result( Err(BlockchainError::QueryFailed("we have no luck".to_string())), ); @@ -845,10 +854,12 @@ mod tests { false, None, ); + subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RetrieveTransactions { recipient: make_wallet("blah"), response_skeleton_opt: None, }; + let system = System::new("test"); subject.handle_scan( BlockchainBridge::handle_retrieve_transactions, @@ -856,6 +867,19 @@ mod tests { &msg, ); + System::current().stop(); + system.run(); + let recording = accountant_recording_arc.lock().unwrap(); + let message = recording.get_record::(0); + assert_eq!( + message, + &ScanError { + scan_type: ScanType::Receivables, + response_skeleton_opt: None, + msg: "Tried to retrieve received payments but failed: QueryFailed(\"we have no luck\")".to_string() + } + ); + assert_eq!(recording.len(), 1); TestLogHandler::new().exists_log_containing( "WARN: BlockchainBridge: Tried to retrieve \ received payments but failed: QueryFailed(\"we have no luck\")", @@ -968,7 +992,7 @@ mod tests { let scan_error_msg = accountant_recording.get_record::(1); assert_eq!(*scan_error_msg, ScanError { scan_type: ScanType::PendingPayables, - response_skeleton: ResponseSkeleton { client_id: 1234, context_id: 4321 }, + response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 }), msg: "Aborting scanning; request of a transaction receipt \ for '0x000000000000000000000000000000000000000000000000000000000001348d' failed due to 'QueryFailed(\"bad bad bad\")'".to_string() }); @@ -977,9 +1001,49 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_on_failure_of_the_first_payment_and_it_does_not_send_any_message_just_aborts_and_logs( + fn blockchain_bridge_can_return_report_transaction_receipts_with_an_empty_vector() { + let (accountant, _, accountant_recording) = make_recorder(); + let recipient = accountant.start().recipient(); + let mut subject = BlockchainBridge::new( + Box::new(BlockchainInterfaceClandestine::new(Chain::Dev)), + Box::new(PersistentConfigurationMock::default()), + false, + Some(Wallet::new("mine")), + ); + subject + .payment_confirmation + .report_transaction_receipts_sub_opt = Some(recipient); + let msg = RequestTransactionReceipts { + pending_payable: vec![], + response_skeleton_opt: None, + }; + let system = System::new( + "blockchain_bridge_can_return_report_transaction_receipts_with_an_empty_vector", + ); + + let _ = subject.handle_request_transaction_receipts(&msg); + + System::current().stop(); + system.run(); + let recording = accountant_recording.lock().unwrap(); + assert_eq!( + recording.get_record::(0), + &ReportTransactionReceipts { + fingerprints_with_receipts: vec![], + response_skeleton_opt: None + } + ) + } + + #[test] + fn handle_request_transaction_receipts_short_circuits_on_failure_of_the_first_payment_and_it_sends_a_message_with_empty_vector_and_logs( ) { init_test_logging(); + let (accountant, _, accountant_recording) = make_recorder(); + let accountant_addr = accountant.start(); + let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); + let report_transaction_recipient: Recipient = + accountant_addr.recipient(); let get_transaction_receipt_params_arc = Arc::new(Mutex::new(vec![])); let hash_1 = H256::from_uint(&U256::from(111334)); let fingerprint_1 = PendingPayableFingerprint { @@ -1010,11 +1074,13 @@ mod tests { subject .payment_confirmation //due to this None we would've panicked if we tried to send a msg - .report_transaction_receipts_sub_opt = None; + .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); + subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { pending_payable: vec![fingerprint_1, fingerprint_2], response_skeleton_opt: None, }; + let system = System::new("test"); let _ = subject.handle_scan( BlockchainBridge::handle_request_transaction_receipts, @@ -1022,8 +1088,27 @@ mod tests { &msg, ); + System::current().stop(); + system.run(); let get_transaction_receipts_params = get_transaction_receipt_params_arc.lock().unwrap(); + let recording = accountant_recording.lock().unwrap(); assert_eq!(*get_transaction_receipts_params, vec![hash_1]); + assert_eq!( + recording.get_record::(0), + &ReportTransactionReceipts { + fingerprints_with_receipts: vec![], + response_skeleton_opt: None + } + ); + assert_eq!( + recording.get_record::(1), + &ScanError { + scan_type: ScanType::PendingPayables, + response_skeleton_opt: None, + msg: "Aborting scanning; request of a transaction receipt for '0x000000000000000000000000000000000000000000000000000000000001b2e6' failed due to 'QueryFailed(\"booga\")'".to_string() + } + ); + assert_eq!(recording.len(), 2); TestLogHandler::new().exists_log_containing("WARN: BlockchainBridge: Aborting scanning; request of a transaction \ receipt for '0x000000000000000000000000000000000000000000000000000000000001b2e6' failed due to 'QueryFailed(\"booga\")'"); } @@ -1290,7 +1375,16 @@ mod tests { System::current().stop(); system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - assert_eq!(accountant_recording.len(), 0); + let message = accountant_recording.get_record::(0); + assert_eq!( + message, + &ScanError { + scan_type: ScanType::Receivables, + response_skeleton_opt: None, + msg: "My tummy hurts".to_string() + } + ); + assert_eq!(accountant_recording.len(), 1); TestLogHandler::new().exists_log_containing("WARN: BlockchainBridge: My tummy hurts"); } @@ -1327,10 +1421,10 @@ mod tests { accountant_recording.get_record::(0), &ScanError { scan_type: ScanType::Receivables, - response_skeleton: ResponseSkeleton { + response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 - }, + }), msg: "My tummy hurts".to_string() } ); diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index bcb71cd26..f424812b4 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -1,4 +1,5 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::actor_system_factory::ActorSystemFactory; use crate::actor_system_factory::ActorSystemFactoryReal; use crate::actor_system_factory::{ActorFactoryReal, ActorSystemFactoryToolsReal}; @@ -21,7 +22,7 @@ use crate::node_configurator::{initialize_database, DirsWrapper, NodeConfigurato use crate::privilege_drop::{IdWrapper, IdWrapperReal}; use crate::server_initializer::LoggerInitializerWrapper; use crate::sub_lib::accountant; -use crate::sub_lib::accountant::AccountantConfig; +use crate::sub_lib::accountant::{PaymentThresholds, ScanIntervals}; use crate::sub_lib::blockchain_bridge::BlockchainBridgeConfig; use crate::sub_lib::cryptde::CryptDE; use crate::sub_lib::cryptde_null::CryptDENull; @@ -325,7 +326,9 @@ pub struct BootstrapperConfig { // These fields can be set while privileged without penalty pub log_level: LevelFilter, pub dns_servers: Vec, - pub accountant_config_opt: Option, + pub scan_intervals_opt: Option, + pub suppress_initial_scans: bool, + pub when_pending_too_long_sec: u64, pub crash_point: CrashPoint, pub clandestine_discriminator_factories: Vec>, pub ui_gateway_config: UiGatewayConfig, @@ -337,6 +340,7 @@ pub struct BootstrapperConfig { pub alias_cryptde_null_opt: Option, pub mapping_protocol_opt: Option, pub real_user: RealUser, + pub payment_thresholds_opt: Option, // These fields must be set without privilege: otherwise the database will be created as root pub db_password_opt: Option, @@ -358,7 +362,8 @@ impl BootstrapperConfig { // These fields can be set while privileged without penalty log_level: LevelFilter::Off, dns_servers: vec![], - accountant_config_opt: Default::default(), + scan_intervals_opt: None, + suppress_initial_scans: false, crash_point: CrashPoint::None, clandestine_discriminator_factories: vec![], ui_gateway_config: UiGatewayConfig { @@ -376,6 +381,7 @@ impl BootstrapperConfig { alias_cryptde_null_opt: None, mapping_protocol_opt: None, real_user: RealUser::new(None, None, None), + payment_thresholds_opt: Default::default(), // These fields must be set without privilege: otherwise the database will be created as root db_password_opt: None, @@ -385,6 +391,7 @@ impl BootstrapperConfig { neighborhood_config: NeighborhoodConfig { mode: NeighborhoodMode::ZeroHop, }, + when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, } } @@ -398,7 +405,10 @@ impl BootstrapperConfig { self.earning_wallet = unprivileged.earning_wallet; self.consuming_wallet_opt = unprivileged.consuming_wallet_opt; self.db_password_opt = unprivileged.db_password_opt; - self.accountant_config_opt = unprivileged.accountant_config_opt; + self.scan_intervals_opt = unprivileged.scan_intervals_opt; + self.suppress_initial_scans = unprivileged.suppress_initial_scans; + self.payment_thresholds_opt = unprivileged.payment_thresholds_opt; + self.when_pending_too_long_sec = unprivileged.when_pending_too_long_sec; } pub fn exit_service_rate(&self) -> u64 { @@ -687,6 +697,7 @@ impl Bootstrapper { #[cfg(test)] mod tests { + use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::actor_system_factory::{ActorFactory, ActorSystemFactory}; use crate::bootstrapper::{ main_cryptde_ref, Bootstrapper, BootstrapperConfig, EnvironmentWrapper, PortConfiguration, @@ -708,6 +719,7 @@ mod tests { use crate::server_initializer::LoggerInitializerWrapper; use crate::stream_handler_pool::StreamHandlerPoolSubs; use crate::stream_messages::AddStreamMsg; + use crate::sub_lib::accountant::ScanIntervals; use crate::sub_lib::cryptde::PublicKey; use crate::sub_lib::cryptde::{CryptDE, PlainData}; use crate::sub_lib::cryptde_null::CryptDENull; @@ -721,9 +733,7 @@ mod tests { use crate::test_utils::recorder::Recording; use crate::test_utils::tokio_wrapper_mocks::ReadHalfWrapperMock; use crate::test_utils::tokio_wrapper_mocks::WriteHalfWrapperMock; - use crate::test_utils::unshared_test_utils::{ - make_populated_accountant_config_with_defaults, make_simplified_multi_config, - }; + use crate::test_utils::unshared_test_utils::make_simplified_multi_config; use crate::test_utils::{assert_contains, rate_pack}; use crate::test_utils::{main_cryptde, make_wallet}; use actix::Recipient; @@ -1220,8 +1230,9 @@ mod tests { unprivileged_config.earning_wallet = earning_wallet.clone(); unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); - unprivileged_config.accountant_config_opt = - Some(make_populated_accountant_config_with_defaults()); + unprivileged_config.scan_intervals_opt = Some(ScanIntervals::default()); + unprivileged_config.suppress_initial_scans = false; + unprivileged_config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; privileged_config.merge_unprivileged(unprivileged_config); @@ -1242,8 +1253,13 @@ mod tests { assert_eq!(privileged_config.consuming_wallet_opt, consuming_wallet_opt); assert_eq!(privileged_config.db_password_opt, db_password_opt); assert_eq!( - privileged_config.accountant_config_opt, - Some(make_populated_accountant_config_with_defaults()) + privileged_config.scan_intervals_opt, + Some(ScanIntervals::default()) + ); + assert_eq!(privileged_config.suppress_initial_scans, false); + assert_eq!( + privileged_config.when_pending_too_long_sec, + DEFAULT_PENDING_TOO_LONG_SEC ); //some values from the privileged config assert_eq!(privileged_config.log_level, Off); diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 2ac51e43f..a83120fa4 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -19,7 +19,8 @@ use crate::node_configurator::unprivileged_parse_args_configuration::{ use crate::node_configurator::{ data_directory_from_context, determine_config_file_path, DirsWrapper, DirsWrapperReal, }; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant::PaymentThresholds as PaymentThresholdsFromAccountant; +use crate::sub_lib::accountant::DEFAULT_SCAN_INTERVALS; use crate::sub_lib::neighborhood::NodeDescriptor; use crate::sub_lib::neighborhood::{NeighborhoodMode as NeighborhoodModeEnum, DEFAULT_RATE_PACK}; use crate::sub_lib::utils::make_new_multi_config; @@ -863,7 +864,10 @@ impl ValueRetriever for PaymentThresholds { _db_password_opt: &Option, ) -> Option<(String, UiSetupResponseValueStatus)> { let pc_value = pc.payment_thresholds().expectv("payment-thresholds"); - payment_thresholds_rate_pack_and_scan_intervals(pc_value, *DEFAULT_PAYMENT_THRESHOLDS) + payment_thresholds_rate_pack_and_scan_intervals( + pc_value, + PaymentThresholdsFromAccountant::default(), + ) } fn is_required(&self, _params: &SetupCluster) -> bool { @@ -927,7 +931,7 @@ fn payment_thresholds_rate_pack_and_scan_intervals( default: T, ) -> Option<(String, UiSetupResponseValueStatus)> where - T: PartialEq + Display + Copy, + T: PartialEq + Display + Clone, { if persistent_config_value == default { Some((default.to_string(), Default)) @@ -1041,6 +1045,9 @@ mod tests { }; use crate::node_configurator::{DirsWrapper, DirsWrapperReal}; use crate::node_test_utils::DirsWrapperMock; + use crate::sub_lib::accountant::{ + PaymentThresholds as PaymentThresholdsFromAccountant, DEFAULT_PAYMENT_THRESHOLDS, + }; use crate::sub_lib::cryptde::PublicKey; use crate::sub_lib::node_addr::NodeAddr; use crate::sub_lib::wallet::Wallet; @@ -3112,7 +3119,7 @@ mod tests { #[test] fn payment_thresholds_computed_default_persistent_config_unequal_to_default() { - let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; + let mut payment_thresholds = PaymentThresholdsFromAccountant::default(); payment_thresholds.maturity_threshold_sec += 12; payment_thresholds.unban_below_gwei -= 12; payment_thresholds.debt_threshold_gwei += 1111; @@ -3150,14 +3157,16 @@ mod tests { pc_method_result_setter: &C, ) where C: Fn(PersistentConfigurationMock, T) -> PersistentConfigurationMock, - T: Display + PartialEq + Copy, + T: Display + PartialEq + Clone, { let mut bootstrapper_config = BootstrapperConfig::new(); //the rate_pack within the mode setting does not determine the result, so I just set a nonsense bootstrapper_config.neighborhood_config.mode = NeighborhoodModeEnum::OriginateOnly(vec![], rate_pack(0)); - let persistent_config = - pc_method_result_setter(PersistentConfigurationMock::new(), persistent_config_value); + let persistent_config = pc_method_result_setter( + PersistentConfigurationMock::new(), + persistent_config_value.clone(), + ); let result = subject.computed_default(&bootstrapper_config, &persistent_config, &None); diff --git a/node/src/database/dao_utils.rs b/node/src/database/dao_utils.rs new file mode 100644 index 000000000..e69de29bb diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index eee796c22..b82487cae 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -4,9 +4,7 @@ use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::blockchain::bip32::Bip32ECKeyProvider; use crate::bootstrapper::BootstrapperConfig; use crate::db_config::persistent_configuration::{PersistentConfigError, PersistentConfiguration}; -use crate::sub_lib::accountant::{ - AccountantConfig, PaymentThresholds, ScanIntervals, DEFAULT_EARNING_WALLET, -}; +use crate::sub_lib::accountant::{PaymentThresholds, ScanIntervals, DEFAULT_EARNING_WALLET}; use crate::sub_lib::cryptde::CryptDE; use crate::sub_lib::cryptde_null::CryptDENull; use crate::sub_lib::cryptde_real::CryptDEReal; @@ -479,17 +477,6 @@ fn configure_accountant_config( config: &mut BootstrapperConfig, persist_config: &mut dyn PersistentConfiguration, ) -> Result<(), ConfiguratorError> { - let suppress_initial_scans = - value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == *"off"; - - let scan_intervals = process_combined_params( - "scan-intervals", - multi_config, - persist_config, - |str: &str| ScanIntervals::try_from(str), - |pc: &dyn PersistentConfiguration| pc.scan_intervals(), - |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), - )?; let payment_thresholds = process_combined_params( "payment-thresholds", multi_config, @@ -501,13 +488,21 @@ fn configure_accountant_config( check_payment_thresholds(&payment_thresholds)?; - let accountant_config = AccountantConfig { - scan_intervals, - payment_thresholds, - suppress_initial_scans, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - }; - config.accountant_config_opt = Some(accountant_config); + let scan_intervals = process_combined_params( + "scan-intervals", + multi_config, + persist_config, + |str: &str| ScanIntervals::try_from(str), + |pc: &dyn PersistentConfiguration| pc.scan_intervals(), + |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), + )?; + let suppress_initial_scans = + value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == *"off"; + + config.payment_thresholds_opt = Some(payment_thresholds); + config.scan_intervals_opt = Some(scan_intervals); + config.suppress_initial_scans = suppress_initial_scans; + config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; Ok(()) } @@ -618,7 +613,7 @@ fn is_user_specified(multi_config: &MultiConfig, parameter: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::accountant::ThresholdUtils; + use crate::accountant::dao_utils::ThresholdUtils; use crate::apps::app_node; use crate::blockchain::bip32::Bip32ECKeyProvider; use crate::database::db_initializer::DbInitializationConfig; @@ -1764,25 +1759,29 @@ mod tests { ) .unwrap(); - let actual_accountant_config = config.accountant_config_opt.unwrap(); - let expected_accountant_config = AccountantConfig { - scan_intervals: ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), - receivable_scan_interval: Duration::from_secs(130), - }, - payment_thresholds: PaymentThresholds { - threshold_interval_sec: 1000, - debt_threshold_gwei: 100000, - payment_grace_period_sec: 1000, - maturity_threshold_sec: 10000, - permanent_debt_allowed_gwei: 20000, - unban_below_gwei: 20000, - }, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, + let expected_scan_intervals = ScanIntervals { + pending_payable_scan_interval: Duration::from_secs(180), + payable_scan_interval: Duration::from_secs(150), + receivable_scan_interval: Duration::from_secs(130), + }; + let expected_payment_thresholds = PaymentThresholds { + threshold_interval_sec: 1000, + debt_threshold_gwei: 100000, + payment_grace_period_sec: 1000, + maturity_threshold_sec: 10000, + permanent_debt_allowed_gwei: 20000, + unban_below_gwei: 20000, }; - assert_eq!(actual_accountant_config, expected_accountant_config); + assert_eq!( + config.payment_thresholds_opt, + Some(expected_payment_thresholds) + ); + assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); + assert_eq!(config.suppress_initial_scans, false); + assert_eq!( + config.when_pending_too_long_sec, + DEFAULT_PENDING_TOO_LONG_SEC + ); let set_scan_intervals_params = set_scan_intervals_params_arc.lock().unwrap(); assert_eq!(*set_scan_intervals_params, vec!["180|150|130".to_string()]); let set_payment_thresholds_params = set_payment_thresholds_params_arc.lock().unwrap(); @@ -1832,25 +1831,34 @@ mod tests { ) .unwrap(); - let actual_accountant_config = config.accountant_config_opt.unwrap(); - let expected_accountant_config = AccountantConfig { - scan_intervals: ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), - receivable_scan_interval: Duration::from_secs(130), - }, - payment_thresholds: PaymentThresholds { - threshold_interval_sec: 1000, - debt_threshold_gwei: 100000, - payment_grace_period_sec: 1000, - maturity_threshold_sec: 1000, - permanent_debt_allowed_gwei: 20000, - unban_below_gwei: 20000, - }, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, + let expected_payment_thresholds = PaymentThresholds { + threshold_interval_sec: 1000, + debt_threshold_gwei: 100000, + payment_grace_period_sec: 1000, + maturity_threshold_sec: 1000, + permanent_debt_allowed_gwei: 20000, + unban_below_gwei: 20000, + }; + let expected_scan_intervals = ScanIntervals { + pending_payable_scan_interval: Duration::from_secs(180), + payable_scan_interval: Duration::from_secs(150), + receivable_scan_interval: Duration::from_secs(130), }; - assert_eq!(actual_accountant_config, expected_accountant_config); + let expected_suppress_initial_scans = false; + let expected_when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; + assert_eq!( + config.payment_thresholds_opt, + Some(expected_payment_thresholds) + ); + assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); + assert_eq!( + config.suppress_initial_scans, + expected_suppress_initial_scans + ); + assert_eq!( + config.when_pending_too_long_sec, + expected_when_pending_too_long_sec + ); //no prepared results for the setter methods, that is they were uncalled } @@ -2470,13 +2478,7 @@ mod tests { ) .unwrap(); - assert_eq!( - bootstrapper_config - .accountant_config_opt - .unwrap() - .suppress_initial_scans, - true - ); + assert_eq!(bootstrapper_config.suppress_initial_scans, true); } #[test] @@ -2497,13 +2499,7 @@ mod tests { ) .unwrap(); - assert_eq!( - bootstrapper_config - .accountant_config_opt - .unwrap() - .suppress_initial_scans, - false - ); + assert_eq!(bootstrapper_config.suppress_initial_scans, false); } #[test] @@ -2524,13 +2520,7 @@ mod tests { ) .unwrap(); - assert_eq!( - bootstrapper_config - .accountant_config_opt - .unwrap() - .suppress_initial_scans, - false - ); + assert_eq!(bootstrapper_config.suppress_initial_scans, false); } fn make_persistent_config( diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index f6b0ac41a..4b092f400 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -1,8 +1,12 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::payable_dao::PayableDaoFactory; +use crate::accountant::pending_payable_dao::PendingPayableDaoFactory; +use crate::accountant::receivable_dao::ReceivableDaoFactory; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, SentPayables, }; +use crate::banned_dao::BannedDaoFactory; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::sub_lib::peer_actors::{BindMessage, StartMessage}; use crate::sub_lib::wallet::Wallet; @@ -39,7 +43,7 @@ lazy_static! { } //please, alphabetical order -#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct PaymentThresholds { pub debt_threshold_gwei: u64, pub maturity_threshold_sec: u64, @@ -49,6 +53,13 @@ pub struct PaymentThresholds { pub unban_below_gwei: u64, } +impl Default for PaymentThresholds { + fn default() -> Self { + *DEFAULT_PAYMENT_THRESHOLDS + } +} + +//this code is used in tests in Accountant impl PaymentThresholds { pub fn sugg_and_grace(&self, now: i64) -> i64 { now - checked_conversion::(self.maturity_threshold_sec) @@ -56,19 +67,24 @@ impl PaymentThresholds { } } -#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] +pub struct DaoFactories { + pub payable_dao_factory: Box, + pub pending_payable_dao_factory: Box, + pub receivable_dao_factory: Box, + pub banned_dao_factory: Box, +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct ScanIntervals { pub pending_payable_scan_interval: Duration, pub payable_scan_interval: Duration, pub receivable_scan_interval: Duration, } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct AccountantConfig { - pub scan_intervals: ScanIntervals, - pub payment_thresholds: PaymentThresholds, - pub suppress_initial_scans: bool, - pub when_pending_too_long_sec: u64, +impl Default for ScanIntervals { + fn default() -> Self { + *DEFAULT_SCAN_INTERVALS + } } #[derive(Clone, PartialEq, Eq)] diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index a00210dd0..dfe188a81 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -305,7 +305,6 @@ fn unreachable() -> ! { #[cfg(test)] mod tests { use super::*; - use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; use crate::sub_lib::combined_parameters::CombinedParamsDataTypes::U128; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use std::panic::catch_unwind; @@ -440,7 +439,7 @@ mod tests { let panic_2 = catch_unwind(|| { let _: &[(&str, CombinedParamsDataTypes)] = - (&CombinedParams::PaymentThresholds(Initialized(*DEFAULT_PAYMENT_THRESHOLDS))) + (&CombinedParams::PaymentThresholds(Initialized(PaymentThresholds::default()))) .into(); }) .unwrap_err(); @@ -450,13 +449,13 @@ mod tests { panic_2_msg, &format!( "should be called only on uninitialized object, not: PaymentThresholds(Initialized({:?}))", - *DEFAULT_PAYMENT_THRESHOLDS + PaymentThresholds::default() ) ); let panic_3 = catch_unwind(|| { let _: &[(&str, CombinedParamsDataTypes)] = - (&CombinedParams::ScanIntervals(Initialized(*DEFAULT_SCAN_INTERVALS))).into(); + (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))).into(); }) .unwrap_err(); let panic_3_msg = panic_3.downcast_ref::().unwrap(); @@ -465,7 +464,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - *DEFAULT_SCAN_INTERVALS + ScanIntervals::default() ) ); } @@ -488,7 +487,7 @@ mod tests { ); let panic_2 = catch_unwind(|| { - (&CombinedParams::PaymentThresholds(Initialized(*DEFAULT_PAYMENT_THRESHOLDS))) + (&CombinedParams::PaymentThresholds(Initialized(PaymentThresholds::default()))) .initialize_objects(HashMap::new()); }) .unwrap_err(); @@ -498,12 +497,12 @@ mod tests { panic_2_msg, &format!( "should be called only on uninitialized object, not: PaymentThresholds(Initialized({:?}))", - *DEFAULT_PAYMENT_THRESHOLDS + PaymentThresholds::default() ) ); let panic_3 = catch_unwind(|| { - (&CombinedParams::ScanIntervals(Initialized(*DEFAULT_SCAN_INTERVALS))) + (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))) .initialize_objects(HashMap::new()); }) .unwrap_err(); @@ -513,7 +512,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - *DEFAULT_SCAN_INTERVALS + ScanIntervals::default() ) ); } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index b9c7b4849..f1da0e62a 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -513,13 +513,12 @@ pub struct TestRawTransaction { pub mod unshared_test_utils { use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::apps::app_node; + use crate::bootstrapper::BootstrapperConfig; use crate::daemon::{ChannelFactory, DaemonBindMessage}; use crate::db_config::config_dao_null::ConfigDaoNull; use crate::db_config::persistent_configuration::PersistentConfigurationReal; use crate::node_test_utils::DirsWrapperMock; - use crate::sub_lib::accountant::{ - AccountantConfig, DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS, - }; + use crate::sub_lib::accountant::{PaymentThresholds, ScanIntervals}; use crate::sub_lib::neighborhood::{ConnectionProgressMessage, DEFAULT_RATE_PACK}; use crate::sub_lib::utils::{ NLSpawnHandleHolder, NLSpawnHandleHolderReal, NotifyHandle, NotifyLaterHandle, @@ -598,30 +597,21 @@ pub mod unshared_test_utils { persistent_config_mock: PersistentConfigurationMock, ) -> PersistentConfigurationMock { persistent_config_mock - .payment_thresholds_result(Ok(*DEFAULT_PAYMENT_THRESHOLDS)) - .scan_intervals_result(Ok(*DEFAULT_SCAN_INTERVALS)) + .payment_thresholds_result(Ok(PaymentThresholds::default())) + .scan_intervals_result(Ok(ScanIntervals::default())) } pub fn make_persistent_config_real_with_config_dao_null() -> PersistentConfigurationReal { PersistentConfigurationReal::new(Box::new(ConfigDaoNull::default())) } - pub fn make_populated_accountant_config_with_defaults() -> AccountantConfig { - AccountantConfig { - scan_intervals: *DEFAULT_SCAN_INTERVALS, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - } - } - - pub fn make_accountant_config_null() -> AccountantConfig { - AccountantConfig { - scan_intervals: Default::default(), - payment_thresholds: Default::default(), - when_pending_too_long_sec: Default::default(), - suppress_initial_scans: false, - } + pub fn make_bc_with_defaults() -> BootstrapperConfig { + let mut config = BootstrapperConfig::new(); + config.scan_intervals_opt = Some(ScanIntervals::default()); + config.suppress_initial_scans = false; + config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; + config.payment_thresholds_opt = Some(PaymentThresholds::default()); + config } pub fn make_recipient_and_recording_arc( diff --git a/port_exposer/Cargo.lock b/port_exposer/Cargo.lock index 5161d3815..0c1b45562 100644 --- a/port_exposer/Cargo.lock +++ b/port_exposer/Cargo.lock @@ -20,7 +20,7 @@ checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "port_exposer" -version = "0.7.0" +version = "0.7.1" dependencies = [ "default-net", ] diff --git a/port_exposer/Cargo.toml b/port_exposer/Cargo.toml index 6a6ec87f0..ad667bc26 100644 --- a/port_exposer/Cargo.toml +++ b/port_exposer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "port_exposer" -version = "0.7.0" +version = "0.7.1" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved."