diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 7237110a8..39ead2d76 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,9 +76,9 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 041fe2196..02aa19459 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,6 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod payable_scanner_extension; +pub mod pending_payable_scanner; +pub mod receivable_scanner; pub mod scan_schedulers; pub mod scanners_utils; pub mod test_utils; @@ -13,15 +15,12 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableT LocallyCausedError, RemotelyCausedErrors, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; -use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::accountant::db_access_objects::banned_dao::BannedDao; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::sub_lib::accountant::{ DaoFactories, FinancialStatistics, PaymentThresholds, @@ -46,9 +45,12 @@ use variant_count::VariantCount; use web3::types::H256; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, UnpricedQualifiedPayables}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; -use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; +use crate::db_config::persistent_configuration::{PersistentConfigurationReal}; // Leave the individual scanner objects private! pub struct Scanners { @@ -126,7 +128,7 @@ impl Scanners { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( - MTError::AutomaticScanConflict, + ManulTriggerError::AutomaticScanConflict, )); } if let Some(started_at) = self.payable.scan_started_at() { @@ -230,7 +232,7 @@ impl Scanners { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( - MTError::AutomaticScanConflict, + ManulTriggerError::AutomaticScanConflict, )); } if let Some(started_at) = self.receivable.scan_started_at() { @@ -336,7 +338,7 @@ impl Scanners { ) -> Result<(), StartScanError> { if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( - MTError::AutomaticScanConflict, + ManulTriggerError::AutomaticScanConflict, )); } if self.initial_pending_payable_scan { @@ -344,7 +346,7 @@ impl Scanners { } if triggered_manually && !self.aware_of_unresolved_pending_payable { return Err(StartScanError::ManualTriggerError( - MTError::UnnecessaryRequest { + ManulTriggerError::UnnecessaryRequest { hint_opt: Some("Run the Payable scanner first.".to_string()), }, )); @@ -849,433 +851,6 @@ impl PayableScanner { } } -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 - PrivateScanner< - ScanForPendingPayables, - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - > for PendingPayableScanner -{ -} - -impl StartableScanner - for PendingPayableScanner -{ - fn start_scan( - &mut self, - _wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.mark_as_started(timestamp); - info!(logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); - match filtered_pending_payable.is_empty() { - true => { - self.mark_as_ended(logger); - Err(StartScanError::NothingToProcess) - } - false => { - debug!( - logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - Ok(RequestTransactionReceipts { - pending_payable_fingerprints: filtered_pending_payable, - response_skeleton_opt, - }) - } - } - } -} - -impl Scanner for PendingPayableScanner { - fn finish_scan( - &mut self, - message: ReportTransactionReceipts, - logger: &Logger, - ) -> PendingPayableScanResult { - let response_skeleton_opt = message.response_skeleton_opt; - - let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { - true => { - warning!(logger, "No transaction receipts found."); - todo!("This requires the payment retry. GH-631 must be completed first"); - } - false => { - debug!( - logger, - "Processing receipts for {} transactions", - message.fingerprints_with_receipts.len() - ); - let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - let requires_payment_retry = - self.process_transactions_by_reported_state(scan_report, logger); - - self.mark_as_ended(logger); - - requires_payment_retry - } - }; - - if requires_payment_retry { - PendingPayableScanResult::PaymentRetryRequired - } else { - let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }); - PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) - } - } - - time_marking_methods!(PendingPayables); - - as_any_ref_in_trait_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_receipts_for_pending_transactions( - &self, - msg: ReportTransactionReceipts, - logger: &Logger, - ) -> PendingPayableScanReport { - let scan_report = PendingPayableScanReport::default(); - msg.fingerprints_with_receipts.into_iter().fold( - scan_report, - |scan_report_so_far, (receipt_result, fingerprint)| match receipt_result { - TransactionReceiptResult::RpcResponse(tx_receipt) => match tx_receipt.status { - TxStatus::Pending => handle_none_receipt( - scan_report_so_far, - fingerprint, - "none was given", - logger, - ), - TxStatus::Failed => { - handle_status_with_failure(scan_report_so_far, fingerprint, logger) - } - TxStatus::Succeeded(_) => { - handle_status_with_success(scan_report_so_far, fingerprint, logger) - } - }, - TransactionReceiptResult::LocalError(e) => handle_none_receipt( - scan_report_so_far, - fingerprint, - &format!("failed due to {}", e), - logger, - ), - }, - ) - } - - fn process_transactions_by_reported_state( - &mut self, - scan_report: PendingPayableScanReport, - logger: &Logger, - ) -> bool { - let requires_payments_retry = scan_report.requires_payments_retry(); - - self.confirm_transactions(scan_report.confirmed, logger); - self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger); - - requires_payments_retry - } - - fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.increment_scan_attempts(&rowids) { - Ok(_) => trace!( - logger, - "Updated records for rowids: {} ", - comma_joined_stringifiable(&rowids, |id| id.to_string()) - ), - Err(e) => panic!( - "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", - PendingPayableId::serialize_hashes_to_string(&ids), - e - ), - } - } - } - - fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - //TODO this function is imperfect. It waits for GH-663 - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.mark_failures(&rowids) { - Ok(_) => warning!( - logger, - "Broken transactions {} marked as an error. You should take over the care \ - of those to make sure your debts are going to be settled properly. At the moment, \ - there is no automated process fixing that without your assistance", - PendingPayableId::serialize_hashes_to_string(&ids) - ), - Err(e) => panic!( - "Unsuccessful attempt for transactions {} \ - to mark fatal error at payable fingerprint due to {:?}; database unreliable", - PendingPayableId::serialize_hashes_to_string(&ids), - e - ), - } - } - } - - fn confirm_transactions( - &mut self, - fingerprints: Vec, - logger: &Logger, - ) { - fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { - comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) - } - - if !fingerprints.is_empty() { - if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { - panic!( - "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ - records due to {:?}", serialize_hashes(&fingerprints), e - ) - } else { - self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); - let rowids = fingerprints - .iter() - .map(|fingerprint| fingerprint.rowid) - .collect::>(); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { - panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", - serialize_hashes(&fingerprints), e) - } else { - info!( - logger, - "Transactions {} completed their confirmation process succeeding", - serialize_hashes(&fingerprints) - ) - } - } - } - } - - fn add_to_the_total_of_paid_payable( - &mut self, - fingerprints: &[PendingPayableFingerprint], - serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, - logger: &Logger, - ) { - fingerprints.iter().for_each(|fingerprint| { - self.financial_statistics - .borrow_mut() - .total_paid_payable_wei += fingerprint.amount - }); - debug!( - logger, - "Confirmation of transactions {}; record for total paid payable was modified", - serialize_hashes(fingerprints) - ); - } -} - -pub struct ReceivableScanner { - pub common: ScannerCommon, - pub receivable_dao: Box, - pub banned_dao: Box, - pub persistent_configuration: Box, - pub financial_statistics: Rc>, -} - -impl - PrivateScanner< - ScanForReceivables, - RetrieveTransactions, - ReceivedPayments, - Option, - > for ReceivableScanner -{ -} - -impl StartableScanner for ReceivableScanner { - fn start_scan( - &mut self, - earning_wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.mark_as_started(timestamp); - info!(logger, "Scanning for receivables to {}", earning_wallet); - self.scan_for_delinquencies(timestamp, logger); - - Ok(RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt, - }) - } -} - -impl Scanner> for ReceivableScanner { - fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { - self.handle_new_received_payments(&msg, logger); - self.mark_as_ended(logger); - - msg.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_ref_in_trait_impl!(); - as_any_mut_in_trait_impl!(); -} - -impl ReceivableScanner { - pub fn new( - receivable_dao: Box, - banned_dao: Box, - persistent_configuration: Box, - payment_thresholds: Rc, - financial_statistics: Rc>, - ) -> Self { - Self { - common: ScannerCommon::new(payment_thresholds), - receivable_dao, - banned_dao, - persistent_configuration, - financial_statistics, - } - } - - fn handle_new_received_payments( - &mut self, - received_payments_msg: &ReceivedPayments, - logger: &Logger, - ) { - if received_payments_msg.transactions.is_empty() { - info!( - logger, - "No newly received payments were detected during the scanning process." - ); - let new_start_block = received_payments_msg.new_start_block; - if let BlockMarker::Value(start_block_number) = new_start_block { - match self - .persistent_configuration - .set_start_block(Some(start_block_number)) - { - Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), - Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", - start_block_number, e - ), - } - } - } else { - let mut txn = self.receivable_dao.as_mut().more_money_received( - received_payments_msg.timestamp, - &received_payments_msg.transactions, - ); - let new_start_block = received_payments_msg.new_start_block; - if let BlockMarker::Value(start_block_number) = new_start_block { - match self - .persistent_configuration - .set_start_block_from_txn(Some(start_block_number), &mut txn) - { - Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), - Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", - start_block_number, e - ), - } - } else { - unreachable!("Failed to get start_block while transactions were present"); - } - match txn.commit() { - Ok(_) => { - debug!(logger, "Received payments have been commited to database"); - } - Err(e) => panic!("Commit of received transactions failed: {:?}", e), - } - let total_newly_paid_receivable = received_payments_msg - .transactions - .iter() - .fold(0, |so_far, now| so_far + now.wei_amount); - - self.financial_statistics - .borrow_mut() - .total_paid_receivable_wei += total_newly_paid_receivable; - } - } - - 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, Clone, VariantCount)] pub enum StartScanError { NothingToProcess, @@ -1285,7 +860,7 @@ pub enum StartScanError { started_at: SystemTime, }, CalledFromNullScanner, // Exclusive for tests - ManualTriggerError(MTError), + ManualTriggerError(ManulTriggerError), } impl StartScanError { @@ -1320,18 +895,20 @@ impl StartScanError { false => panic!("Null Scanner shouldn't be running inside production code."), }, StartScanError::ManualTriggerError(e) => match e { - MTError::AutomaticScanConflict => ErrorType::Permanent(format!( + ManulTriggerError::AutomaticScanConflict => ErrorType::Permanent(format!( "User requested {:?} scan was denied. Automatic mode prevents manual triggers.", scan_type )), - MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( - "User requested {:?} scan was denied expecting zero findings.{}", - scan_type, - match hint_opt { - Some(hint) => format!(" {}", hint), - None => "".to_string(), - } - )), + ManulTriggerError::UnnecessaryRequest { hint_opt } => { + ErrorType::Temporary(format!( + "User requested {:?} scan was denied expecting zero findings.{}", + scan_type, + match hint_opt { + Some(hint) => format!(" {}", hint), + None => "".to_string(), + } + )) + } }, }; @@ -1376,7 +953,7 @@ impl StartScanError { } #[derive(Debug, PartialEq, Eq, Clone)] -pub enum MTError { +pub enum ManulTriggerError { AutomaticScanConflict, UnnecessaryRequest { hint_opt: Option }, } @@ -1400,8 +977,7 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; - use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -1438,6 +1014,7 @@ mod tests { use web3::Error; use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; + use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; @@ -2017,190 +1594,6 @@ mod tests { )); } - #[test] - fn entries_must_be_kept_consistent_and_aligned() { - let wallet_1 = make_wallet("abc"); - let hash_1 = make_tx_hash(123); - let wallet_2 = make_wallet("def"); - let hash_2 = make_tx_hash(345); - let wallet_3 = make_wallet("ghi"); - let hash_3 = make_tx_hash(546); - let wallet_4 = make_wallet("jkl"); - let hash_4 = make_tx_hash(678); - let pending_payables_owned = vec![ - PendingPayable::new(wallet_1.clone(), hash_1), - PendingPayable::new(wallet_2.clone(), hash_2), - PendingPayable::new(wallet_3.clone(), hash_3), - PendingPayable::new(wallet_4.clone(), hash_4), - ]; - let pending_payables_ref = pending_payables_owned - .iter() - .collect::>(); - let pending_payable_dao = - PendingPayableDaoMock::new().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(4, hash_4), (1, hash_1), (3, hash_3), (2, hash_2)], - no_rowid_results: vec![], - }); - let subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - let (existent, nonexistent) = - subject.separate_existent_and_nonexistent_fingerprints(&pending_payables_ref); - - assert_eq!( - existent, - vec![ - PendingPayableMetadata::new(&wallet_4, hash_4, Some(4)), - PendingPayableMetadata::new(&wallet_1, hash_1, Some(1)), - PendingPayableMetadata::new(&wallet_3, hash_3, Some(3)), - PendingPayableMetadata::new(&wallet_2, hash_2, Some(2)), - ] - ); - assert!(nonexistent.is_empty()) - } - - struct TestingMismatchedDataAboutPendingPayables { - pending_payables: Vec, - common_hash_1: H256, - common_hash_3: H256, - intruder_for_hash_2: H256, - } - - fn prepare_values_for_mismatched_setting() -> TestingMismatchedDataAboutPendingPayables { - let hash_1 = make_tx_hash(123); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(789); - let intruder = make_tx_hash(567); - let pending_payables = vec![ - PendingPayable::new(make_wallet("abc"), hash_1), - PendingPayable::new(make_wallet("def"), hash_2), - PendingPayable::new(make_wallet("ghi"), hash_3), - ]; - TestingMismatchedDataAboutPendingPayables { - pending_payables, - common_hash_1: hash_1, - common_hash_3: hash_3, - intruder_for_hash_2: intruder, - } - } - - #[test] - #[should_panic( - expected = "Inconsistency in two maps, they cannot be matched by hashes. \ - Data set directly sent from BlockchainBridge: \ - [PendingPayable { recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000616263) }, \ - hash: 0x000000000000000000000000000000000000000000000000000000000000007b }, \ - PendingPayable { recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000646566) }, \ - hash: 0x00000000000000000000000000000000000000000000000000000000000001c8 }, \ - PendingPayable { recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000676869) }, \ - hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }], \ - set derived from the DB: \ - TransactionHashes { rowid_results: \ - [(4, 0x000000000000000000000000000000000000000000000000000000000000007b), \ - (1, 0x0000000000000000000000000000000000000000000000000000000000000237), \ - (3, 0x0000000000000000000000000000000000000000000000000000000000000315)], \ - no_rowid_results: [] }" - )] - fn two_sourced_information_of_new_pending_payables_and_their_fingerprints_is_not_symmetrical() { - let vals = prepare_values_for_mismatched_setting(); - let pending_payables_ref = vals - .pending_payables - .iter() - .collect::>(); - let pending_payable_dao = - PendingPayableDaoMock::new().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (4, vals.common_hash_1), - (1, vals.intruder_for_hash_2), - (3, vals.common_hash_3), - ], - no_rowid_results: vec![], - }); - let subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - subject.separate_existent_and_nonexistent_fingerprints(&pending_payables_ref); - } - - #[test] - fn symmetry_check_happy_path() { - let hash_1 = make_tx_hash(123); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(789); - let pending_payables_sent_from_blockchain_bridge = vec![ - PendingPayable::new(make_wallet("abc"), hash_1), - PendingPayable::new(make_wallet("def"), hash_2), - PendingPayable::new(make_wallet("ghi"), hash_3), - ]; - let pending_payables_ref = pending_payables_sent_from_blockchain_bridge - .iter() - .map(|ppayable| ppayable.hash) - .collect::>(); - let hashes_from_fingerprints = vec![(hash_1, 3), (hash_2, 5), (hash_3, 6)] - .iter() - .map(|(hash, _id)| *hash) - .collect::>(); - - let result = PayableScanner::is_symmetrical(pending_payables_ref, hashes_from_fingerprints); - - assert_eq!(result, true) - } - - #[test] - fn symmetry_check_sad_path_for_intruder() { - let vals = prepare_values_for_mismatched_setting(); - let pending_payables_ref_from_blockchain_bridge = vals - .pending_payables - .iter() - .map(|ppayable| ppayable.hash) - .collect::>(); - let rowids_and_hashes_from_fingerprints = vec![ - (vals.common_hash_1, 3), - (vals.intruder_for_hash_2, 5), - (vals.common_hash_3, 6), - ] - .iter() - .map(|(hash, _rowid)| *hash) - .collect::>(); - - let result = PayableScanner::is_symmetrical( - pending_payables_ref_from_blockchain_bridge, - rowids_and_hashes_from_fingerprints, - ); - - assert_eq!(result, false) - } - - #[test] - fn symmetry_check_indifferent_to_wrong_order_on_the_input() { - let hash_1 = make_tx_hash(123); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(789); - let pending_payables_sent_from_blockchain_bridge = vec![ - PendingPayable::new(make_wallet("abc"), hash_1), - PendingPayable::new(make_wallet("def"), hash_2), - PendingPayable::new(make_wallet("ghi"), hash_3), - ]; - let bb_returned_p_payables_ref = pending_payables_sent_from_blockchain_bridge - .iter() - .map(|ppayable| ppayable.hash) - .collect::>(); - // Not in ascending order - let rowids_and_hashes_from_fingerprints = vec![(hash_1, 3), (hash_3, 5), (hash_2, 6)] - .iter() - .map(|(hash, _id)| *hash) - .collect::>(); - - let result = PayableScanner::is_symmetrical( - bb_returned_p_payables_ref, - rowids_and_hashes_from_fingerprints, - ); - - assert_eq!(result, true) - } - #[test] #[should_panic( expected = "Expected pending payable fingerprints for (tx: 0x0000000000000000000000000000000000000000000000000000000000000315, \ @@ -3040,515 +2433,6 @@ mod tests { assert_eq!(subject.initial_pending_payable_scan, true); } - fn assert_interpreting_none_status_for_pending_payable( - test_name: &str, - when_pending_too_long_sec: u64, - pending_payable_age_sec: u64, - rowid: u64, - hash: H256, - ) -> PendingPayableScanReport { - init_test_logging(); - let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: when_sent, - hash, - attempt: 1, - amount: 123, - process_error: None, - }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - - handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) - } - - fn assert_log_msg_and_elapsed_time_in_log_makes_sense( - expected_msg: &str, - elapsed_after: u64, - capture_regex: &str, - ) { - let log_handler = TestLogHandler::default(); - let log_idx = log_handler.exists_log_matching(expected_msg); - let log = log_handler.get_log_at(log_idx); - let capture = captures_for_regex_time_in_sec(&log, capture_regex); - assert!(capture <= elapsed_after) - } - - fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { - let capture_regex = Regex::new(capture_regex).unwrap(); - let time_str = capture_regex - .captures(stack) - .unwrap() - .get(1) - .unwrap() - .as_str(); - time_str.parse().unwrap() - } - - fn elapsed_since_secs_back(sec: u64) -> u64 { - SystemTime::now() - .sub(Duration::from_secs(sec)) - .elapsed() - .unwrap() - .as_secs() - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() - { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; - let hash = make_tx_hash(0x237); - let rowid = 466; - - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - DEFAULT_PENDING_TOO_LONG_SEC + 1, - rowid, - hash, - ); - - let elapsed_after = elapsed_since_secs_back(DEFAULT_PENDING_TOO_LONG_SEC + 1); - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(rowid, hash)], - confirmed: vec![] - } - ); - let capture_regex = "(\\d+){2}sec"; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "ERROR: {}: Pending transaction 0x00000000000000000000000000000000000000\ - 00000000000000000000000237 has exceeded the maximum pending time \\({}sec\\) with the age \ - \\d+sec and the confirmation process is going to be aborted now at the final attempt 1; manual \ - resolution is required from the user to complete the transaction" - , test_name, DEFAULT_PENDING_TOO_LONG_SEC, ), elapsed_after, capture_regex) - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; - let hash = make_tx_hash(0x7b); - let rowid = 333; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; - - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, - ); - - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 00000000000007b couldn't be confirmed at attempt 1 at \\d+ms after its sending"), elapsed_after_ms, capture_regex); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; - let hash = make_tx_hash(0x237); - let rowid = 466; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; - - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, - ); - - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 000000000000237 couldn't be confirmed at attempt 1 at \\d+ms after its sending", - ), elapsed_after_ms, capture_regex); - } - - #[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 mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = make_tx_hash(0xd7); - let fingerprint = PendingPayableFingerprint { - rowid: 777777, - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt: 5, - amount: 2222, - process_error: None, - }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - - let result = handle_status_with_failure(scan_report, fingerprint, &logger); - - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(777777, hash,)], - confirmed: vec![] - } - ); - TestLogHandler::new().exists_log_matching(&format!( - "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ - 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ - 1500\\d\\dms from the sending" - )); - } - - #[test] - fn handle_pending_txs_with_receipts_handles_none_for_receipt() { - init_test_logging(); - let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; - let subject = PendingPayableScannerBuilder::new().build(); - let rowid = 455; - let hash = make_tx_hash(0x913); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt: 3, - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }), - fingerprint.clone(), - )], - response_skeleton_opt: None, - }; - - let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); - - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {test_name}: Interpreting a receipt for transaction \ - 0x0000000000000000000000000000000000000000000000000000000000000913 \ - but none was given; attempt 3, 100\\d\\dms since sending" - )); - } - - #[test] - fn increment_scan_attempts_happy_path() { - let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let hash_1 = make_tx_hash(444888); - let rowid_1 = 3456; - let hash_2 = make_tx_hash(444888); - let rowid_2 = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) - .increment_scan_attempts_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); - let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); - - let _ = subject.update_remaining_fingerprints( - vec![transaction_id_1, transaction_id_2], - &Logger::new("test"), - ); - - let update_remaining_fingerprints_params = - update_remaining_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *update_remaining_fingerprints_params, - vec![vec![rowid_1, rowid_2]] - ) - } - - #[test] - #[should_panic( - expected = "Failure on incrementing scan attempts for fingerprints of \ - 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ - due to UpdateFailed(\"yeah, bad\")" - )] - fn increment_scan_attempts_sad_path() { - let hash = make_tx_hash(0x6c9d8); - let rowid = 3456; - let pending_payable_dao = - PendingPayableDaoMock::default().increment_scan_attempts_result(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let logger = Logger::new("test"); - let transaction_id = PendingPayableId::new(rowid, hash); - - let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); - } - - #[test] - fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { - let subject = PendingPayableScannerBuilder::new().build(); - - subject.update_remaining_fingerprints(vec![], &Logger::new("test")) - - //mocked pending payable DAO didn't panic which means we skipped the actual process - } - - #[test] - fn cancel_failed_transactions_works() { - init_test_logging(); - let test_name = "cancel_failed_transactions_works"; - let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failures_params(&mark_failures_params_arc) - .mark_failures_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); - let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); - - subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); - - let mark_failures_params = mark_failures_params_arc.lock().unwrap(); - assert_eq!(*mark_failures_params, vec![vec![2, 3]]); - TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ - 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ - the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ - process fixing that without your assistance", - )); - } - - #[test] - #[should_panic( - expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ - 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ - 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ - database unreliable" - )] - fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { - let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), - )); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); - let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); - let transaction_ids = vec![transaction_id_1, transaction_id_2]; - - subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); - } - - #[test] - fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { - let subject = PendingPayableScannerBuilder::new().build(); - - subject.cancel_failed_transactions(vec![], &Logger::new("test")) - - //mocked pending payable DAO didn't panic which means we skipped the actual process - } - - #[test] - #[should_panic( - expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ - 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ - 0000000000021a of verified transactions due to RecordDeletion(\"the database \ - is fooling around with us\")" - )] - fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { - let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_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 fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.rowid = 1; - fingerprint_1.hash = make_tx_hash(0x315); - let mut fingerprint_2 = make_pending_payable_fingerprint(); - fingerprint_2.rowid = 1; - fingerprint_2.hash = make_tx_hash(0x21a); - - subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); - } - - #[test] - fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { - let mut subject = PendingPayableScannerBuilder::new().build(); - - subject.confirm_transactions(vec![], &Logger::new("test")) - - //mocked payable DAO didn't panic which means we skipped the actual process - } - - #[test] - fn confirm_transactions_works() { - init_test_logging(); - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); - let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let rowid_1 = 2; - let rowid_2 = 5; - let pending_payable_fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: from_unix_timestamp(199_000_000), - hash: make_tx_hash(0x123), - attempt: 1, - amount: 4567, - process_error: None, - }; - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: from_unix_timestamp(200_000_000), - hash: make_tx_hash(0x567), - attempt: 1, - amount: 5555, - process_error: None, - }; - - subject.confirm_transactions( - vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), - ], - &Logger::new("confirm_transactions_works"), - ); - - let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!( - *confirm_transactions_params, - vec![vec![ - pending_payable_fingerprint_1, - pending_payable_fingerprint_2 - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "DEBUG: confirm_transactions_works: \ - Confirmation of transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567; \ - record for total paid payable was modified", - ); - log_handler.exists_log_containing( - "INFO: confirm_transactions_works: \ - Transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567 \ - completed their confirmation process succeeding", - ); - } - - #[test] - #[should_panic( - expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ - 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ - (\"record change not successful\")" - )] - fn confirm_transactions_panics_on_unchecking_payable_table() { - let hash = make_tx_hash(0x315); - let rowid = 3; - let payable_dao = PayableDaoMock::new().transactions_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 = rowid; - fingerprint.hash = hash; - - subject.confirm_transactions(vec![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 fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(189_999_888), - hash: make_tx_hash(56789), - attempt: 1, - amount: 5478, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 6, - timestamp: from_unix_timestamp(200_000_011), - hash: make_tx_hash(33333), - attempt: 1, - amount: 6543, - process_error: None, - }; - let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_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_transactions( - vec![fingerprint_1.clone(), fingerprint_2.clone()], - &Logger::new(test_name), - ); - - let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; - assert_eq!(total_paid_payable, 1111 + 5478 + 6543); - } - #[test] fn pending_payable_scanner_handles_report_transaction_receipts_message() { init_test_logging(); @@ -4296,7 +3180,7 @@ mod tests { "DEBUG", ), ( - StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + StartScanError::ManualTriggerError(ManulTriggerError::AutomaticScanConflict), Box::new(|sev| { format!("{sev}: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") }), @@ -4304,7 +3188,7 @@ mod tests { "WARN", ), ( - StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + StartScanError::ManualTriggerError(ManulTriggerError::UnnecessaryRequest { hint_opt: Some("Wise words".to_string()), }), Box::new(|sev| { @@ -4314,7 +3198,9 @@ mod tests { "DEBUG", ), ( - StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: None }), + StartScanError::ManualTriggerError(ManulTriggerError::UnnecessaryRequest { + hint_opt: None, + }), Box::new(|sev| { format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings.") }), diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs new file mode 100644 index 000000000..cfb874f19 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -0,0 +1,804 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod utils; + +use std::cell::RefCell; +use std::rc::Rc; +use std::time::SystemTime; +use masq_lib::logger::Logger; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use crate::accountant::db_access_objects::payable_dao::PayableDao; +use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; +use crate::accountant::{comma_joined_stringifiable, PendingPayableId, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables}; +use crate::accountant::scanners::{PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner}; +use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; +use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; +use crate::sub_lib::wallet::Wallet; +use crate::time_marking_methods; + +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 + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + > for PendingPayableScanner +{ +} + +impl StartableScanner + for PendingPayableScanner +{ + fn start_scan( + &mut self, + _wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.mark_as_started(timestamp); + info!(logger, "Scanning for pending payable"); + let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); + match filtered_pending_payable.is_empty() { + true => { + self.mark_as_ended(logger); + Err(StartScanError::NothingToProcess) + } + false => { + debug!( + logger, + "Found {} pending payables to process", + filtered_pending_payable.len() + ); + Ok(RequestTransactionReceipts { + pending_payable_fingerprints: filtered_pending_payable, + response_skeleton_opt, + }) + } + } + } +} + +impl Scanner for PendingPayableScanner { + fn finish_scan( + &mut self, + message: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanResult { + let response_skeleton_opt = message.response_skeleton_opt; + + let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { + true => { + warning!(logger, "No transaction receipts found."); + todo!("This requires the payment retry. GH-631 must be completed first"); + } + false => { + debug!( + logger, + "Processing receipts for {} transactions", + message.fingerprints_with_receipts.len() + ); + let scan_report = self.handle_receipts_for_pending_transactions(message, logger); + let requires_payment_retry = + self.process_transactions_by_reported_state(scan_report, logger); + + self.mark_as_ended(logger); + + requires_payment_retry + } + }; + + if requires_payment_retry { + PendingPayableScanResult::PaymentRetryRequired + } else { + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) + } + } + + time_marking_methods!(PendingPayables); + + as_any_ref_in_trait_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_receipts_for_pending_transactions( + &self, + msg: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanReport { + let scan_report = PendingPayableScanReport::default(); + msg.fingerprints_with_receipts.into_iter().fold( + scan_report, + |scan_report_so_far, (receipt_result, fingerprint)| match receipt_result { + TransactionReceiptResult::RpcResponse(tx_receipt) => match tx_receipt.status { + TxStatus::Pending => handle_none_receipt( + scan_report_so_far, + fingerprint, + "none was given", + logger, + ), + TxStatus::Failed => { + handle_status_with_failure(scan_report_so_far, fingerprint, logger) + } + TxStatus::Succeeded(_) => { + handle_status_with_success(scan_report_so_far, fingerprint, logger) + } + }, + TransactionReceiptResult::LocalError(e) => handle_none_receipt( + scan_report_so_far, + fingerprint, + &format!("failed due to {}", e), + logger, + ), + }, + ) + } + + fn process_transactions_by_reported_state( + &mut self, + scan_report: PendingPayableScanReport, + logger: &Logger, + ) -> bool { + let requires_payments_retry = scan_report.requires_payments_retry(); + + self.confirm_transactions(scan_report.confirmed, logger); + self.cancel_failed_transactions(scan_report.failures, logger); + self.update_remaining_fingerprints(scan_report.still_pending, logger); + + requires_payments_retry + } + + fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { + if !ids.is_empty() { + let rowids = PendingPayableId::rowids(&ids); + match self.pending_payable_dao.increment_scan_attempts(&rowids) { + Ok(_) => trace!( + logger, + "Updated records for rowids: {} ", + comma_joined_stringifiable(&rowids, |id| id.to_string()) + ), + Err(e) => panic!( + "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", + PendingPayableId::serialize_hashes_to_string(&ids), + e + ), + } + } + } + + fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { + if !ids.is_empty() { + //TODO this function is imperfect. It waits for GH-663 + let rowids = PendingPayableId::rowids(&ids); + match self.pending_payable_dao.mark_failures(&rowids) { + Ok(_) => warning!( + logger, + "Broken transactions {} marked as an error. You should take over the care \ + of those to make sure your debts are going to be settled properly. At the moment, \ + there is no automated process fixing that without your assistance", + PendingPayableId::serialize_hashes_to_string(&ids) + ), + Err(e) => panic!( + "Unsuccessful attempt for transactions {} \ + to mark fatal error at payable fingerprint due to {:?}; database unreliable", + PendingPayableId::serialize_hashes_to_string(&ids), + e + ), + } + } + } + + fn confirm_transactions( + &mut self, + fingerprints: Vec, + logger: &Logger, + ) { + fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { + comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) + } + + if !fingerprints.is_empty() { + if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { + panic!( + "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ + records due to {:?}", serialize_hashes(&fingerprints), e + ) + } else { + self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); + let rowids = fingerprints + .iter() + .map(|fingerprint| fingerprint.rowid) + .collect::>(); + if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { + panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", + serialize_hashes(&fingerprints), e) + } else { + info!( + logger, + "Transactions {} completed their confirmation process succeeding", + serialize_hashes(&fingerprints) + ) + } + } + } + } + + fn add_to_the_total_of_paid_payable( + &mut self, + fingerprints: &[PendingPayableFingerprint], + serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, + logger: &Logger, + ) { + fingerprints.iter().for_each(|fingerprint| { + self.financial_statistics + .borrow_mut() + .total_paid_payable_wei += fingerprint.amount + }); + debug!( + logger, + "Confirmation of transactions {}; record for total paid payable was modified", + serialize_hashes(fingerprints) + ); + } +} + +#[cfg(test)] +mod tests { + use std::ops::Sub; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; + use ethereum_types::{H256, U64}; + use regex::Regex; + use web3::types::TransactionReceipt; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use crate::accountant::{PendingPayableId, ReportTransactionReceipts, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::db_access_objects::payable_dao::PayableDaoError; + use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoError; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; + use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; + use crate::accountant::test_utils::{make_pending_payable_fingerprint, PayableDaoMock, PendingPayableDaoMock, PendingPayableScannerBuilder}; + use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxReceipt, TxStatus}; + use crate::blockchain::test_utils::make_tx_hash; + + fn assert_interpreting_none_status_for_pending_payable( + test_name: &str, + when_pending_too_long_sec: u64, + pending_payable_age_sec: u64, + rowid: u64, + hash: H256, + ) -> PendingPayableScanReport { + init_test_logging(); + let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); + let fingerprint = PendingPayableFingerprint { + rowid, + timestamp: when_sent, + hash, + attempt: 1, + amount: 123, + process_error: None, + }; + let logger = Logger::new(test_name); + let scan_report = PendingPayableScanReport::default(); + + handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) + } + + fn assert_log_msg_and_elapsed_time_in_log_makes_sense( + expected_msg: &str, + elapsed_after: u64, + capture_regex: &str, + ) { + let log_handler = TestLogHandler::default(); + let log_idx = log_handler.exists_log_matching(expected_msg); + let log = log_handler.get_log_at(log_idx); + let capture = captures_for_regex_time_in_sec(&log, capture_regex); + assert!(capture <= elapsed_after) + } + + fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { + let capture_regex = Regex::new(capture_regex).unwrap(); + let time_str = capture_regex + .captures(stack) + .unwrap() + .get(1) + .unwrap() + .as_str(); + time_str.parse().unwrap() + } + + fn elapsed_since_secs_back(sec: u64) -> u64 { + SystemTime::now() + .sub(Duration::from_secs(sec)) + .elapsed() + .unwrap() + .as_secs() + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() + { + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; + let hash = make_tx_hash(0x237); + let rowid = 466; + + let result = assert_interpreting_none_status_for_pending_payable( + test_name, + DEFAULT_PENDING_TOO_LONG_SEC, + DEFAULT_PENDING_TOO_LONG_SEC + 1, + rowid, + hash, + ); + + let elapsed_after = elapsed_since_secs_back(DEFAULT_PENDING_TOO_LONG_SEC + 1); + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![], + failures: vec![PendingPayableId::new(rowid, hash)], + confirmed: vec![] + } + ); + let capture_regex = "(\\d+){2}sec"; + assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( + "ERROR: {}: Pending transaction 0x00000000000000000000000000000000000000\ + 00000000000000000000000237 has exceeded the maximum pending time \\({}sec\\) with the age \ + \\d+sec and the confirmation process is going to be aborted now at the final attempt 1; manual \ + resolution is required from the user to complete the transaction" + , test_name, DEFAULT_PENDING_TOO_LONG_SEC, ), elapsed_after, capture_regex) + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; + let hash = make_tx_hash(0x7b); + let rowid = 333; + let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; + + let result = assert_interpreting_none_status_for_pending_payable( + test_name, + DEFAULT_PENDING_TOO_LONG_SEC, + pending_payable_age, + rowid, + hash, + ); + + let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(rowid, hash)], + failures: vec![], + confirmed: vec![] + } + ); + let capture_regex = r#"\s(\d+)ms"#; + assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( + "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ + 00000000000007b couldn't be confirmed at attempt 1 at \\d+ms after its sending"), elapsed_after_ms, capture_regex); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; + let hash = make_tx_hash(0x237); + let rowid = 466; + let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; + + let result = assert_interpreting_none_status_for_pending_payable( + test_name, + DEFAULT_PENDING_TOO_LONG_SEC, + pending_payable_age, + rowid, + hash, + ); + + let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(rowid, hash)], + failures: vec![], + confirmed: vec![] + } + ); + let capture_regex = r#"\s(\d+)ms"#; + assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( + "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ + 000000000000237 couldn't be confirmed at attempt 1 at \\d+ms after its sending", + ), elapsed_after_ms, capture_regex); + } + + #[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 mut tx_receipt = TransactionReceipt::default(); + tx_receipt.status = Some(U64::from(0)); //failure + let hash = make_tx_hash(0xd7); + let fingerprint = PendingPayableFingerprint { + rowid: 777777, + timestamp: SystemTime::now().sub(Duration::from_millis(150000)), + hash, + attempt: 5, + amount: 2222, + process_error: None, + }; + let logger = Logger::new(test_name); + let scan_report = PendingPayableScanReport::default(); + + let result = handle_status_with_failure(scan_report, fingerprint, &logger); + + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![], + failures: vec![PendingPayableId::new(777777, hash,)], + confirmed: vec![] + } + ); + TestLogHandler::new().exists_log_matching(&format!( + "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ + 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ + 1500\\d\\dms from the sending" + )); + } + + #[test] + fn handle_pending_txs_with_receipts_handles_none_for_receipt() { + init_test_logging(); + let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; + let subject = PendingPayableScannerBuilder::new().build(); + let rowid = 455; + let hash = make_tx_hash(0x913); + let fingerprint = PendingPayableFingerprint { + rowid, + timestamp: SystemTime::now().sub(Duration::from_millis(10000)), + hash, + attempt: 3, + amount: 111, + process_error: None, + }; + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: hash, + status: TxStatus::Pending, + }), + fingerprint.clone(), + )], + response_skeleton_opt: None, + }; + + let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); + + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(rowid, hash)], + failures: vec![], + confirmed: vec![] + } + ); + TestLogHandler::new().exists_log_matching(&format!( + "DEBUG: {test_name}: Interpreting a receipt for transaction \ + 0x0000000000000000000000000000000000000000000000000000000000000913 \ + but none was given; attempt 3, 100\\d\\dms since sending" + )); + } + + #[test] + fn increment_scan_attempts_happy_path() { + let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let hash_1 = make_tx_hash(444888); + let rowid_1 = 3456; + let hash_2 = make_tx_hash(444888); + let rowid_2 = 3456; + let pending_payable_dao = PendingPayableDaoMock::default() + .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) + .increment_scan_attempts_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); + let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); + + let _ = subject.update_remaining_fingerprints( + vec![transaction_id_1, transaction_id_2], + &Logger::new("test"), + ); + + let update_remaining_fingerprints_params = + update_remaining_fingerprints_params_arc.lock().unwrap(); + assert_eq!( + *update_remaining_fingerprints_params, + vec![vec![rowid_1, rowid_2]] + ) + } + + #[test] + #[should_panic( + expected = "Failure on incrementing scan attempts for fingerprints of \ + 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ + due to UpdateFailed(\"yeah, bad\")" + )] + fn increment_scan_attempts_sad_path() { + let hash = make_tx_hash(0x6c9d8); + let rowid = 3456; + let pending_payable_dao = + PendingPayableDaoMock::default().increment_scan_attempts_result(Err( + PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), + )); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let logger = Logger::new("test"); + let transaction_id = PendingPayableId::new(rowid, hash); + + let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); + } + + #[test] + fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { + let subject = PendingPayableScannerBuilder::new().build(); + + subject.update_remaining_fingerprints(vec![], &Logger::new("test")) + + //mocked pending payable DAO didn't panic which means we skipped the actual process + } + + #[test] + fn cancel_failed_transactions_works() { + init_test_logging(); + let test_name = "cancel_failed_transactions_works"; + let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_dao = PendingPayableDaoMock::default() + .mark_failures_params(&mark_failures_params_arc) + .mark_failures_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); + let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); + + subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); + + let mark_failures_params = mark_failures_params_arc.lock().unwrap(); + assert_eq!(*mark_failures_params, vec![vec![2, 3]]); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ + 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ + the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ + process fixing that without your assistance", + )); + } + + #[test] + #[should_panic( + expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ + 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ + 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ + database unreliable" + )] + fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { + let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( + PendingPayableDaoError::UpdateFailed("no no no".to_string()), + )); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); + let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); + let transaction_ids = vec![transaction_id_1, transaction_id_2]; + + subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); + } + + #[test] + fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { + let subject = PendingPayableScannerBuilder::new().build(); + + subject.cancel_failed_transactions(vec![], &Logger::new("test")) + + //mocked pending payable DAO didn't panic which means we skipped the actual process + } + + #[test] + #[should_panic( + expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ + 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ + 0000000000021a of verified transactions due to RecordDeletion(\"the database \ + is fooling around with us\")" + )] + fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_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 fingerprint_1 = make_pending_payable_fingerprint(); + fingerprint_1.rowid = 1; + fingerprint_1.hash = make_tx_hash(0x315); + let mut fingerprint_2 = make_pending_payable_fingerprint(); + fingerprint_2.rowid = 1; + fingerprint_2.hash = make_tx_hash(0x21a); + + subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); + } + + #[test] + fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { + let mut subject = PendingPayableScannerBuilder::new().build(); + + subject.confirm_transactions(vec![], &Logger::new("test")) + + //mocked payable DAO didn't panic which means we skipped the actual process + } + + #[test] + fn confirm_transactions_works() { + init_test_logging(); + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transactions_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default() + .delete_fingerprints_params(&delete_fingerprints_params_arc) + .delete_fingerprints_result(Ok(())); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let rowid_1 = 2; + let rowid_2 = 5; + let pending_payable_fingerprint_1 = PendingPayableFingerprint { + rowid: rowid_1, + timestamp: from_unix_timestamp(199_000_000), + hash: make_tx_hash(0x123), + attempt: 1, + amount: 4567, + process_error: None, + }; + let pending_payable_fingerprint_2 = PendingPayableFingerprint { + rowid: rowid_2, + timestamp: from_unix_timestamp(200_000_000), + hash: make_tx_hash(0x567), + attempt: 1, + amount: 5555, + process_error: None, + }; + + subject.confirm_transactions( + vec![ + pending_payable_fingerprint_1.clone(), + pending_payable_fingerprint_2.clone(), + ], + &Logger::new("confirm_transactions_works"), + ); + + let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!( + *confirm_transactions_params, + vec![vec![ + pending_payable_fingerprint_1, + pending_payable_fingerprint_2 + ]] + ); + let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); + assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing( + "DEBUG: confirm_transactions_works: \ + Confirmation of transactions \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567; \ + record for total paid payable was modified", + ); + log_handler.exists_log_containing( + "INFO: confirm_transactions_works: \ + Transactions \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + completed their confirmation process succeeding", + ); + } + + #[test] + #[should_panic( + expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ + 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ + (\"record change not successful\")" + )] + fn confirm_transactions_panics_on_unchecking_payable_table() { + let hash = make_tx_hash(0x315); + let rowid = 3; + let payable_dao = PayableDaoMock::new().transactions_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 = rowid; + fingerprint.hash = hash; + + subject.confirm_transactions(vec![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 fingerprint_1 = PendingPayableFingerprint { + rowid: 5, + timestamp: from_unix_timestamp(189_999_888), + hash: make_tx_hash(56789), + attempt: 1, + amount: 5478, + process_error: None, + }; + let fingerprint_2 = PendingPayableFingerprint { + rowid: 6, + timestamp: from_unix_timestamp(200_000_011), + hash: make_tx_hash(33333), + attempt: 1, + amount: 6543, + process_error: None, + }; + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_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_transactions( + vec![fingerprint_1.clone(), fingerprint_2.clone()], + &Logger::new(test_name), + ); + + let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; + assert_eq!(total_paid_payable, 1111 + 5478 + 6543); + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs new file mode 100644 index 000000000..f277a1c91 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -0,0 +1,191 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::PendingPayableId; +use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use masq_lib::logger::Logger; +use masq_lib::ui_gateway::NodeToUiMessage; +use std::time::SystemTime; + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct PendingPayableScanReport { + pub still_pending: Vec, + pub failures: Vec, + pub confirmed: Vec, +} + +impl PendingPayableScanReport { + pub fn requires_payments_retry(&self) -> bool { + todo!("complete my within GH-642") + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PendingPayableScanResult { + NoPendingPayablesLeft(Option), + PaymentRetryRequired, +} + +pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { + timestamp + .elapsed() + .expect("time calculation for elapsed failed") + .as_millis() +} + +pub fn handle_none_status( + mut scan_report: PendingPayableScanReport, + fingerprint: PendingPayableFingerprint, + max_pending_interval: u64, + logger: &Logger, +) -> PendingPayableScanReport { + info!( + logger, + "Pending transaction {:?} couldn't be confirmed at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt, + elapsed_in_ms(fingerprint.timestamp) + ); + let elapsed = fingerprint + .timestamp + .elapsed() + .expect("we should be older now"); + let elapsed = elapsed.as_secs(); + if elapsed > max_pending_interval { + error!( + logger, + "Pending transaction {:?} has exceeded the maximum pending time \ + ({}sec) with the age {}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, + elapsed, + fingerprint.attempt + ); + scan_report.failures.push(fingerprint.into()) + } else { + scan_report.still_pending.push(fingerprint.into()) + } + scan_report +} + +pub fn handle_status_with_success( + mut scan_report: PendingPayableScanReport, + fingerprint: PendingPayableFingerprint, + logger: &Logger, +) -> PendingPayableScanReport { + info!( + logger, + "Transaction {:?} has been added to the blockchain; detected locally at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt, + elapsed_in_ms(fingerprint.timestamp) + ); + scan_report.confirmed.push(fingerprint); + scan_report +} + +//TODO: failures handling is going to need enhancement suggested by GH-693 +pub fn handle_status_with_failure( + mut scan_report: PendingPayableScanReport, + fingerprint: PendingPayableFingerprint, + logger: &Logger, +) -> PendingPayableScanReport { + error!( + logger, + "Pending transaction {:?} announced as a failure, interpreting attempt \ + {} after {}ms from the sending", + fingerprint.hash, + fingerprint.attempt, + elapsed_in_ms(fingerprint.timestamp) + ); + scan_report.failures.push(fingerprint.into()); + scan_report +} + +pub fn handle_none_receipt( + mut scan_report: PendingPayableScanReport, + payable: PendingPayableFingerprint, + error_msg: &str, + logger: &Logger, +) -> PendingPayableScanReport { + debug!( + logger, + "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", + payable.hash, + error_msg, + payable.attempt, + elapsed_in_ms(payable.timestamp) + ); + + scan_report + .still_pending + .push(PendingPayableId::new(payable.rowid, payable.hash)); + scan_report +} + +#[cfg(test)] +mod tests { + + #[test] + fn requires_payments_retry_says_yes() { + todo!("complete this test with GH-604") + // let cases = vec![ + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // ]; + // + // cases.into_iter().enumerate().for_each(|(idx, case)| { + // let result = case.requires_payments_retry(); + // assert_eq!( + // result, true, + // "We expected true, but got false for case of idx {}", + // idx + // ) + // }) + } + + #[test] + fn requires_payments_retry_says_no() { + todo!("complete this test with GH-604") + // let report = PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }; + // + // let result = report.requires_payments_retry(); + // + // assert_eq!(result, false) + } +} diff --git a/node/src/accountant/scanners/receivable_scanner/mod.rs b/node/src/accountant/scanners/receivable_scanner/mod.rs new file mode 100644 index 000000000..b7222df0d --- /dev/null +++ b/node/src/accountant/scanners/receivable_scanner/mod.rs @@ -0,0 +1,195 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod utils; + +use crate::accountant::db_access_objects::banned_dao::BannedDao; +use crate::accountant::db_access_objects::receivable_dao::ReceivableDao; +use crate::accountant::scanners::receivable_scanner::utils::balance_and_age; +use crate::accountant::scanners::{ + PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, +}; +use crate::accountant::{ReceivedPayments, ResponseSkeleton, ScanForReceivables}; +use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; +use crate::db_config::persistent_configuration::PersistentConfiguration; +use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; +use crate::sub_lib::wallet::Wallet; +use crate::time_marking_methods; +use masq_lib::logger::Logger; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use std::cell::RefCell; +use std::rc::Rc; +use std::time::SystemTime; + +pub struct ReceivableScanner { + pub common: ScannerCommon, + pub receivable_dao: Box, + pub banned_dao: Box, + pub persistent_configuration: Box, + pub financial_statistics: Rc>, +} + +impl + PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + > for ReceivableScanner +{ +} + +impl StartableScanner for ReceivableScanner { + fn start_scan( + &mut self, + earning_wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.mark_as_started(timestamp); + info!(logger, "Scanning for receivables to {}", earning_wallet); + self.scan_for_delinquencies(timestamp, logger); + + Ok(RetrieveTransactions { + recipient: earning_wallet.clone(), + response_skeleton_opt, + }) + } +} + +impl Scanner> for ReceivableScanner { + fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { + self.handle_new_received_payments(&msg, logger); + self.mark_as_ended(logger); + + msg.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_ref_in_trait_impl!(); + as_any_mut_in_trait_impl!(); +} + +impl ReceivableScanner { + pub fn new( + receivable_dao: Box, + banned_dao: Box, + persistent_configuration: Box, + payment_thresholds: Rc, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + receivable_dao, + banned_dao, + persistent_configuration, + financial_statistics, + } + } + + fn handle_new_received_payments( + &mut self, + received_payments_msg: &ReceivedPayments, + logger: &Logger, + ) { + if received_payments_msg.transactions.is_empty() { + info!( + logger, + "No newly received payments were detected during the scanning process." + ); + let new_start_block = received_payments_msg.new_start_block; + if let BlockMarker::Value(start_block_number) = new_start_block { + match self + .persistent_configuration + .set_start_block(Some(start_block_number)) + { + Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), + Err(e) => panic!( + "Attempt to set new start block to {} failed due to: {:?}", + start_block_number, e + ), + } + } + } else { + let mut txn = self.receivable_dao.as_mut().more_money_received( + received_payments_msg.timestamp, + &received_payments_msg.transactions, + ); + let new_start_block = received_payments_msg.new_start_block; + if let BlockMarker::Value(start_block_number) = new_start_block { + match self + .persistent_configuration + .set_start_block_from_txn(Some(start_block_number), &mut txn) + { + Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), + Err(e) => panic!( + "Attempt to set new start block to {} failed due to: {:?}", + start_block_number, e + ), + } + } else { + unreachable!("Failed to get start_block while transactions were present"); + } + match txn.commit() { + Ok(_) => { + debug!(logger, "Received payments have been commited to database"); + } + Err(e) => panic!("Commit of received transactions failed: {:?}", e), + } + let total_newly_paid_receivable = received_payments_msg + .transactions + .iter() + .fold(0, |so_far, now| so_far + now.wei_amount); + + self.financial_statistics + .borrow_mut() + .total_paid_receivable_wei += total_newly_paid_receivable; + } + } + + 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() + ) + }); + } +} diff --git a/node/src/accountant/scanners/receivable_scanner/utils.rs b/node/src/accountant/scanners/receivable_scanner/utils.rs new file mode 100644 index 000000000..45c8f6800 --- /dev/null +++ b/node/src/accountant/scanners/receivable_scanner/utils.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::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::db_access_objects::receivable_dao::ReceivableAccount; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::receivable_scanner::utils::balance_and_age; + use crate::test_utils::make_wallet; + use std::time::SystemTime; + + #[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_unix_timestamp(to_unix_timestamp(now) - offset), + }; + + let (balance, age) = balance_and_age(now, &receivable_account); + + assert_eq!(balance, "10"); + assert_eq!(age.as_secs(), offset as u64); + } +} diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 74e102aff..7a99605b0 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -383,7 +383,7 @@ mod tests { NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, }; - use crate::accountant::scanners::{MTError, StartScanError}; + use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; use itertools::Itertools; use lazy_static::lazy_static; @@ -538,7 +538,7 @@ mod tests { StartScanError::NothingToProcess, StartScanError::NoConsumingWalletFound, StartScanError::ScanAlreadyRunning { cross_scan_cause_opt: None, started_at: SystemTime::now()}, - StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + StartScanError::ManualTriggerError(ManulTriggerError::AutomaticScanConflict), StartScanError::CalledFromNullScanner ]; diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index b50a1388b..971030e14 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -321,154 +321,10 @@ pub mod payable_scanner_utils { } } -pub mod pending_payable_scanner_utils { - use crate::accountant::PendingPayableId; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; - use masq_lib::logger::Logger; - use masq_lib::ui_gateway::NodeToUiMessage; - use std::time::SystemTime; - - #[derive(Debug, Default, PartialEq, Eq, Clone)] - pub struct PendingPayableScanReport { - pub still_pending: Vec, - pub failures: Vec, - pub confirmed: Vec, - } - - impl PendingPayableScanReport { - pub fn requires_payments_retry(&self) -> bool { - todo!("complete my within GH-642") - } - } - - #[derive(Debug, PartialEq, Eq)] - pub enum PendingPayableScanResult { - NoPendingPayablesLeft(Option), - PaymentRetryRequired, - } - - pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() - } - - pub fn handle_none_status( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, - ) -> PendingPayableScanReport { - info!( - logger, - "Pending transaction {:?} couldn't be confirmed at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let elapsed = elapsed.as_secs(); - if elapsed > max_pending_interval { - error!( - logger, - "Pending transaction {:?} has exceeded the maximum pending time \ - ({}sec) with the age {}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, - elapsed, - fingerprint.attempt - ); - scan_report.failures.push(fingerprint.into()) - } else { - scan_report.still_pending.push(fingerprint.into()) - } - scan_report - } - - pub fn handle_status_with_success( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, - ) -> PendingPayableScanReport { - info!( - logger, - "Transaction {:?} has been added to the blockchain; detected locally at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.confirmed.push(fingerprint); - scan_report - } - - //TODO: failures handling is going to need enhancement suggested by GH-693 - pub fn handle_status_with_failure( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, - ) -> PendingPayableScanReport { - error!( - logger, - "Pending transaction {:?} announced as a failure, interpreting attempt \ - {} after {}ms from the sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.failures.push(fingerprint.into()); - scan_report - } - - pub fn handle_none_receipt( - mut scan_report: PendingPayableScanReport, - payable: PendingPayableFingerprint, - error_msg: &str, - logger: &Logger, - ) -> PendingPayableScanReport { - debug!( - logger, - "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", - payable.hash, - error_msg, - payable.attempt, - elapsed_in_ms(payable.timestamp) - ); - - scan_report - .still_pending - .push(PendingPayableId::new(payable.rowid, payable.hash)); - scan_report - } -} - -pub mod receivable_scanner_utils { - use crate::accountant::db_access_objects::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::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; - use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; @@ -477,7 +333,6 @@ mod tests { payables_debug_summary, separate_errors, PayableThresholdsGauge, PayableThresholdsGaugeReal, }; - use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; @@ -530,22 +385,6 @@ mod tests { 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_unix_timestamp(to_unix_timestamp(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_for_no_errs_just_oks() { let correct_payment_1 = PendingPayable { @@ -869,64 +708,4 @@ mod tests { "Got 0 properly sent payables of an unknown number of attempts" ) } - - #[test] - fn requires_payments_retry_says_yes() { - todo!("complete this test with GH-604") - // let cases = vec![ - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // ]; - // - // cases.into_iter().enumerate().for_each(|(idx, case)| { - // let result = case.requires_payments_retry(); - // assert_eq!( - // result, true, - // "We expected true, but got false for case of idx {}", - // idx - // ) - // }) - } - - #[test] - fn requires_payments_retry_says_no() { - todo!("complete this test with GH-604") - // let report = PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }; - // - // let result = report.requires_payments_retry(); - // - // assert_eq!(result, false) - } } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 2445ff565..637325091 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -8,12 +8,12 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ use crate::accountant::scanners::payable_scanner_extension::{ MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, }; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, ScanRescheduleAfterEarlyStop, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::accountant::scanners::{ PayableScanner, PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, Scanner, StartScanError, StartableScanner, diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index a09d734cd..44c888ff7 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -25,8 +25,10 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, }; use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; -use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::scanners::PayableScanner; use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount;