diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..17abb1fb6 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,1235 @@ + + + + \ No newline at end of file diff --git a/masq_lib/src/logger.rs b/masq_lib/src/logger.rs index c2cde53b6..3f1a62e8c 100644 --- a/masq_lib/src/logger.rs +++ b/masq_lib/src/logger.rs @@ -686,10 +686,10 @@ mod tests { let one_logger = Logger::new("logger_format_is_correct_one"); let another_logger = Logger::new("logger_format_is_correct_another"); - let before = OffsetDateTime::now_utc(); + let before = SystemTime::now(); error!(one_logger, "one log"); error!(another_logger, "another log"); - let after = OffsetDateTime::now_utc(); + let after = SystemTime::now(); let tlh = TestLogHandler::new(); let prefix_len = "0000-00-00T00:00:00.000".len(); @@ -702,8 +702,8 @@ mod tests { " Thd{}: ERROR: logger_format_is_correct_another: another log", thread_id_as_string(thread_id) ))); - let before_str = timestamp_as_string(before); - let after_str = timestamp_as_string(after); + let before_str = timestamp_as_string(&before); + let after_str = timestamp_as_string(&after); assert_between(&one_log[..prefix_len], &before_str, &after_str); assert_between(&another_log[..prefix_len], &before_str, &after_str); } @@ -872,7 +872,7 @@ mod tests { tlh.exists_log_containing("error! 42"); } - fn timestamp_as_string(timestamp: OffsetDateTime) -> String { + pub fn timestamp_as_string(timestamp: OffsetDateTime) -> String { timestamp .format(&parse(TIME_FORMATTING_STRING).unwrap()) .unwrap() diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 297784c9c..adb968b76 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -2,34 +2,35 @@ pub mod payable_dao; pub mod pending_payable_dao; pub mod receivable_dao; +pub mod scanners; pub mod tools; #[cfg(test)] pub mod test_utils; use masq_lib::constants::SCAN_ERROR; +use std::cell::RefCell; -use masq_lib::messages::{ScanType, UiScanRequest, UiScanResponse}; +use masq_lib::messages::{ScanType, UiScanRequest}; use masq_lib::ui_gateway::{MessageBody, MessagePath}; use crate::accountant::payable_dao::{Payable, PayableAccount, PayableDaoError, PayableDaoFactory}; use crate::accountant::pending_payable_dao::{PendingPayableDao, PendingPayableDaoFactory}; -use crate::accountant::receivable_dao::{ - ReceivableAccount, ReceivableDaoError, ReceivableDaoFactory, -}; -use crate::accountant::tools::accountant_tools::{Scanner, Scanners, TransactionConfirmationTools}; -use crate::banned_dao::{BannedDao, BannedDaoFactory}; +use crate::accountant::receivable_dao::{ReceivableDaoError, ReceivableDaoFactory}; +use crate::accountant::scanners::scanners::{BeginScanError, NotifyLaterForScanners, Scanners}; +use crate::accountant::tools::common_tools::timestamp_as_string; +use crate::banned_dao::BannedDaoFactory; use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::{BlockchainError, BlockchainTransaction}; use crate::bootstrapper::BootstrapperConfig; use crate::database::dao_utils::DaoFactoryReal; use crate::database::db_migrations::MigratorConfig; +use crate::sub_lib::accountant::ReportExitServiceProvidedMessage; use crate::sub_lib::accountant::ReportRoutingServiceProvidedMessage; -use crate::sub_lib::accountant::{AccountantConfig, FinancialStatistics, PaymentThresholds}; -use crate::sub_lib::accountant::{AccountantSubs, ReportServicesConsumedMessage}; -use crate::sub_lib::accountant::{ - MessageIdGenerator, MessageIdGeneratorReal, ReportExitServiceProvidedMessage, -}; +use crate::sub_lib::accountant::ReportServicesConsumedMessage; +use crate::sub_lib::accountant::{AccountantSubs, ScanIntervals}; +use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; +use crate::sub_lib::accountant::{MessageIdGenerator, MessageIdGeneratorReal}; use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; use crate::sub_lib::peer_actors::{BindMessage, StartMessage}; use crate::sub_lib::utils::{handle_ui_crash_request, NODE_MAILBOX_CAPACITY}; @@ -48,15 +49,14 @@ use masq_lib::messages::UiFinancialsResponse; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::ui_gateway::MessageTarget::ClientId; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; -use masq_lib::utils::{plus, ExpectValue}; +use masq_lib::utils::ExpectValue; use payable_dao::PayableDao; use receivable_dao::ReceivableDao; -#[cfg(test)] -use std::any::Any; use std::default::Default; use std::ops::Add; use std::path::Path; -use std::time::{Duration, SystemTime}; +use std::rc::Rc; +use std::time::SystemTime; use web3::types::{TransactionReceipt, H256}; pub const CRASH_KEY: &str = "ACCOUNTANT"; @@ -64,23 +64,23 @@ pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours pub struct Accountant { - config: AccountantConfig, + scan_intervals: ScanIntervals, + suppress_initial_scans_opt: Option, consuming_wallet: Option, - earning_wallet: Wallet, + earning_wallet: Rc, payable_dao: Box, receivable_dao: Box, pending_payable_dao: Box, - banned_dao: Box, crashable: bool, scanners: Scanners, - confirmation_tools: TransactionConfirmationTools, - financial_statistics: FinancialStatistics, - report_accounts_payable_sub: Option>, + notify_later: NotifyLaterForScanners, + financial_statistics: Rc>, + report_accounts_payable_sub_opt: Option>, retrieve_transactions_sub: Option>, + request_transaction_receipts_subs_opt: Option>, report_new_payments_sub: Option>, report_sent_payments_sub: Option>, ui_message_sub: Option>, - payable_threshold_tools: Box, message_id_generator: Box, logger: Logger, } @@ -144,7 +144,11 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, _msg: StartMessage, ctx: &mut Self::Context) -> Self::Result { - if self.config.suppress_initial_scans { + let suppress_initial_scans = self + .suppress_initial_scans_opt + .take() + .expect("Can't process StartMessage for Accountant."); + if suppress_initial_scans { info!( &self.logger, "Started with --scans off; declining to begin database and blockchain scans" @@ -167,19 +171,51 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { - self.handle_received_payments(msg); + fn handle(&mut self, msg: SentPayable, _ctx: &mut Self::Context) -> Self::Result { + if let Some(node_to_ui_msg) = self.scanners.payable.finish_scan(msg, &self.logger) { + self.ui_message_sub + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } } } -impl Handler for Accountant { +#[derive(Debug, PartialEq, Message, Clone)] +pub struct ReportTransactionReceipts { + pub fingerprints_with_receipts: Vec<(Option, PendingPayableFingerprint)>, + pub response_skeleton_opt: Option, +} + +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: SentPayable, _ctx: &mut Self::Context) -> Self::Result { - self.handle_sent_payable(msg); + fn handle(&mut self, msg: ReportTransactionReceipts, _ctx: &mut Self::Context) -> Self::Result { + if let Some(node_to_ui_msg) = self.scanners.pending_payable.finish_scan(msg, &self.logger) { + self.ui_message_sub + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { + if let Some(node_to_ui_msg) = self.scanners.receivable.finish_scan(msg, &self.logger) { + self.ui_message_sub + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } } } @@ -187,11 +223,14 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_scan_message( - self.scanners.payables.as_ref(), - msg.response_skeleton_opt, + self.handle_scan_for_payable_request(msg.response_skeleton_opt); + let _ = self.notify_later.scan_for_payable.notify_later( + ScanForPayables { + response_skeleton_opt: None, + }, + self.scan_intervals.payable_scan_interval, ctx, - ) + ); } } @@ -199,11 +238,14 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_scan_message( - self.scanners.pending_payables.as_ref(), - msg.response_skeleton_opt, + self.handle_scan_for_pending_payable_request(msg.response_skeleton_opt); + let _ = self.notify_later.scan_for_pending_payable.notify_later( + ScanForPendingPayables { + response_skeleton_opt: None, // because scheduled scans don't respond + }, + self.scan_intervals.pending_payable_scan_interval, ctx, - ) + ); } } @@ -211,11 +253,14 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - self.handle_scan_message( - self.scanners.receivables.as_ref(), - msg.response_skeleton_opt, + self.handle_scan_for_receivables_request(msg.response_skeleton_opt); + let _ = self.notify_later.scan_for_receivable.notify_later( + ScanForReceivables { + response_skeleton_opt: None, // because scheduled scans don't respond + }, + self.scan_intervals.receivable_scan_interval, ctx, - ) + ); } } @@ -299,66 +344,6 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -#[derive(Debug, PartialEq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(Option, PendingPayableFingerprint)>, - pub response_skeleton_opt: Option, -} - -impl Handler for Accountant { - type Result = (); - - fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { - debug!( - self.logger, - "Processing receipts for {} transactions", - msg.fingerprints_with_receipts.len() - ); - let statuses = self.handle_pending_transaction_with_its_receipt(&msg); - self.process_transaction_by_status(statuses, ctx); - if let Some(response_skeleton) = &msg.response_skeleton_opt { - self.ui_message_sub - .as_ref() - .expect("UIGateway not bound") - .try_send(NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - .expect("UIGateway is dead"); - } - } -} - -#[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct CancelFailedPendingTransaction { - pub id: PendingPayableId, -} - -impl Handler for Accountant { - type Result = (); - - fn handle( - &mut self, - msg: CancelFailedPendingTransaction, - _ctx: &mut Self::Context, - ) -> Self::Result { - self.handle_cancel_pending_transaction(msg) - } -} - -#[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ConfirmPendingTransaction { - pub pending_payable_fingerprint: PendingPayableFingerprint, -} - -impl Handler for Accountant { - type Result = (); - - fn handle(&mut self, msg: ConfirmPendingTransaction, _ctx: &mut Self::Context) -> Self::Result { - self.handle_confirm_pending_transaction(msg) - } -} - impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: PendingPayableFingerprint, _ctx: &mut Self::Context) -> Self::Result { @@ -381,7 +366,7 @@ impl Handler for Accountant { client_id, context_id, }, - ); + ) } else { handle_ui_crash_request(msg, &self.logger, self.crashable, CRASH_KEY) } @@ -390,34 +375,54 @@ impl Handler for Accountant { impl Accountant { pub fn new( - config: &BootstrapperConfig, + config: &mut BootstrapperConfig, payable_dao_factory: Box, receivable_dao_factory: Box, pending_payable_dao_factory: Box, banned_dao_factory: Box, ) -> Accountant { + let payment_thresholds = Rc::new( + config + .payment_thresholds_opt + .take() + .expectv("Payment thresholds"), + ); + let scan_intervals = config.scan_intervals_opt.take().expectv("Scan Intervals"); + let when_pending_too_long_sec = config + .when_pending_too_long_opt + .take() + .expectv("When pending too long sec"); + + let earning_wallet = Rc::new(config.earning_wallet.clone()); + let financial_statistics = Rc::new(RefCell::new(FinancialStatistics::default())); Accountant { - config: *config - .accountant_config_opt - .as_ref() - .expectv("Accountant config"), + scan_intervals, + suppress_initial_scans_opt: config.suppress_initial_scans_opt, consuming_wallet: config.consuming_wallet_opt.clone(), - earning_wallet: config.earning_wallet.clone(), + earning_wallet: Rc::clone(&earning_wallet), payable_dao: payable_dao_factory.make(), receivable_dao: receivable_dao_factory.make(), pending_payable_dao: pending_payable_dao_factory.make(), - banned_dao: banned_dao_factory.make(), crashable: config.crash_point == CrashPoint::Message, - scanners: Scanners::default(), - financial_statistics: FinancialStatistics::default(), - report_accounts_payable_sub: None, + scanners: Scanners::new( + payable_dao_factory, + pending_payable_dao_factory, + receivable_dao_factory.make(), + banned_dao_factory.make(), + payment_thresholds, + Rc::clone(&earning_wallet), + when_pending_too_long_sec, + Rc::clone(&financial_statistics), + ), + notify_later: NotifyLaterForScanners::default(), + financial_statistics: Rc::clone(&financial_statistics), + report_accounts_payable_sub_opt: None, retrieve_transactions_sub: None, + request_transaction_receipts_subs_opt: None, report_new_payments_sub: None, report_sent_payments_sub: None, ui_message_sub: None, - confirmation_tools: TransactionConfirmationTools::default(), message_id_generator: Box::new(MessageIdGeneratorReal::default()), - payable_threshold_tools: Box::new(PayableExceedThresholdToolsReal::default()), logger: Logger::new("Accountant"), } } @@ -442,166 +447,6 @@ impl Accountant { DaoFactoryReal::new(data_directory, false, MigratorConfig::panic_on_migration()) } - fn handle_scan_message( - &self, - scanner: &dyn Scanner, - response_skeleton_opt: Option, - ctx: &mut Context, - ) { - scanner.scan(self, response_skeleton_opt); - scanner.notify_later_assertable(self, ctx) - } - - fn scan_for_payables(&self, response_skeleton_opt: Option) { - info!(self.logger, "Scanning for payables"); - - let all_non_pending_payables = self.payable_dao.non_pending_payables(); - debug!( - self.logger, - "{}", - Self::investigate_debt_extremes(&all_non_pending_payables) - ); - let qualified_payables = all_non_pending_payables - .into_iter() - .filter(|account| self.should_pay(account)) - .collect::>(); - info!( - self.logger, - "Chose {} qualified debts to pay", - qualified_payables.len() - ); - debug!( - self.logger, - "{}", - self.payables_debug_summary(&qualified_payables) - ); - if !qualified_payables.is_empty() { - self.report_accounts_payable_sub - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(ReportAccountsPayable { - accounts: qualified_payables, - response_skeleton_opt, - }) - .expect("BlockchainBridge is dead") - } - } - - fn scan_for_delinquencies(&self) { - info!(self.logger, "Scanning for delinquencies"); - let now = SystemTime::now(); - self.receivable_dao - .new_delinquencies(now, &self.config.payment_thresholds) - .into_iter() - .for_each(|account| { - self.banned_dao.ban(&account.wallet); - let (balance, age) = Self::balance_and_age(&account); - info!( - self.logger, - "Wallet {} (balance: {} MASQ, age: {} sec) banned for delinquency", - account.wallet, - balance, - age.as_secs() - ) - }); - self.receivable_dao - .paid_delinquencies(&self.config.payment_thresholds) - .into_iter() - .for_each(|account| { - self.banned_dao.unban(&account.wallet); - let (balance, age) = Self::balance_and_age(&account); - info!( - self.logger, - "Wallet {} (balance: {} MASQ, age: {} sec) is no longer delinquent: unbanned", - account.wallet, - balance, - age.as_secs() - ) - }); - } - - fn scan_for_received_payments(&self, response_skeleton_opt: Option) { - info!( - self.logger, - "Scanning for receivables to {}", self.earning_wallet - ); - self.retrieve_transactions_sub - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(RetrieveTransactions { - recipient: self.earning_wallet.clone(), - response_skeleton_opt, - }) - .expect("BlockchainBridge is dead"); - } - - fn scan_for_pending_payable(&self, response_skeleton_opt: Option) { - info!(self.logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_fingerprints(); - if filtered_pending_payable.is_empty() { - debug!(self.logger, "No pending payable found during last scan") - } else { - debug!( - self.logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - self.confirmation_tools - .request_transaction_receipts_subs_opt - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(RequestTransactionReceipts { - pending_payable: filtered_pending_payable, - response_skeleton_opt, - }) - .expect("BlockchainBridge is dead"); - } - } - - fn balance_and_age(account: &ReceivableAccount) -> (String, Duration) { - let balance = format!("{}", (account.balance as f64) / 1_000_000_000.0); - let age = account - .last_received_timestamp - .elapsed() - .unwrap_or_else(|_| Duration::new(0, 0)); - (balance, age) - } - - fn should_pay(&self, payable: &PayableAccount) -> bool { - self.payable_exceeded_threshold(payable).is_some() - } - - fn payable_exceeded_threshold(&self, payable: &PayableAccount) -> Option { - // TODO: This calculation should be done in the database, if possible - let time_since_last_paid = SystemTime::now() - .duration_since(payable.last_paid_timestamp) - .expect("Internal error") - .as_secs(); - - if self.payable_threshold_tools.is_innocent_age( - time_since_last_paid, - self.config.payment_thresholds.maturity_threshold_sec as u64, - ) { - return None; - } - - if self.payable_threshold_tools.is_innocent_balance( - payable.balance, - self.config.payment_thresholds.permanent_debt_allowed_gwei, - ) { - return None; - } - - let threshold = self - .payable_threshold_tools - .calculate_payout_threshold(self.config.payment_thresholds, time_since_last_paid); - if payable.balance as f64 > threshold { - Some(threshold as u64) - } else { - None - } - } - fn record_service_provided( &self, service_rate: u64, @@ -678,95 +523,15 @@ impl Accountant { } } - //for debugging only - fn investigate_debt_extremes(all_non_pending_payables: &[PayableAccount]) -> String { - let now = SystemTime::now(); - if all_non_pending_payables.is_empty() { - "Payable scan found no debts".to_string() - } else { - struct PayableInfo { - balance: i64, - age: Duration, - } - let init = ( - PayableInfo { - balance: 0, - age: Duration::ZERO, - }, - PayableInfo { - balance: 0, - age: Duration::ZERO, - }, - ); - let (biggest, oldest) = all_non_pending_payables.iter().fold(init, |sofar, p| { - let (mut biggest, mut oldest) = sofar; - let p_age = now - .duration_since(p.last_paid_timestamp) - .expect("Payable time is corrupt"); - { - //look at a test if not understandable - let check_age_parameter_if_the_first_is_the_same = - || -> bool { p.balance == biggest.balance && p_age > biggest.age }; - - if p.balance > biggest.balance || check_age_parameter_if_the_first_is_the_same() - { - biggest = PayableInfo { - balance: p.balance, - age: p_age, - } - } - - let check_balance_parameter_if_the_first_is_the_same = - || -> bool { p_age == oldest.age && p.balance > oldest.balance }; - - if p_age > oldest.age || check_balance_parameter_if_the_first_is_the_same() { - oldest = PayableInfo { - balance: p.balance, - age: p_age, - } - } - } - (biggest, oldest) - }); - format!("Payable scan found {} debts; the biggest is {} owed for {}sec, the oldest is {} owed for {}sec", - all_non_pending_payables.len(), biggest.balance, biggest.age.as_secs(), - oldest.balance, oldest.age.as_secs()) - } - } - - fn payables_debug_summary(&self, qualified_payables: &[PayableAccount]) -> String { - let now = SystemTime::now(); - let list = qualified_payables - .iter() - .map(|payable| { - let p_age = now - .duration_since(payable.last_paid_timestamp) - .expect("Payable time is corrupt"); - let threshold = self - .payable_exceeded_threshold(payable) - .expect("Threshold suddenly changed!"); - format!( - "{} owed for {}sec exceeds threshold: {}; creditor: {}", - payable.balance, - p_age.as_secs(), - threshold, - payable.wallet - ) - }) - .join("\n"); - String::from("Paying qualified debts:\n").add(&list) - } - fn handle_bind_message(&mut self, msg: BindMessage) { - self.report_accounts_payable_sub = + self.report_accounts_payable_sub_opt = Some(msg.peer_actors.blockchain_bridge.report_accounts_payable); self.retrieve_transactions_sub = Some(msg.peer_actors.blockchain_bridge.retrieve_transactions); self.report_new_payments_sub = Some(msg.peer_actors.accountant.report_new_payments); self.report_sent_payments_sub = Some(msg.peer_actors.accountant.report_sent_payments); self.ui_message_sub = Some(msg.peer_actors.ui_gateway.node_to_ui_message_sub); - self.confirmation_tools - .request_transaction_receipts_subs_opt = Some( + self.request_transaction_receipts_subs_opt = Some( msg.peer_actors .blockchain_bridge .request_transaction_receipts, @@ -774,69 +539,6 @@ impl Accountant { info!(self.logger, "Accountant bound"); } - fn handle_received_payments(&mut self, msg: ReceivedPayments) { - if !msg.payments.is_empty() { - let total_newly_paid_receivable = msg - .payments - .iter() - .fold(0, |so_far, now| so_far + now.gwei_amount); - self.receivable_dao - .as_mut() - .more_money_received(msg.timestamp, msg.payments); - self.financial_statistics.total_paid_receivable += total_newly_paid_receivable; - } - if let Some(response_skeleton) = msg.response_skeleton_opt { - self.ui_message_sub - .as_ref() - .expect("UIGateway is not bound") - .try_send(NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - .expect("UIGateway is dead"); - } - } - - fn handle_sent_payable(&self, sent_payable: SentPayable) { - let (ok, err) = Self::separate_early_errors(&sent_payable, &self.logger); - debug!(self.logger, "We gathered these errors at sending transactions for payable: {:?}, out of the total of {} attempts", err, ok.len() + err.len()); - self.mark_pending_payable(ok); - if !err.is_empty() { - err.into_iter().for_each(|err| - if let Some(hash) = err.carries_transaction_hash(){ - self.discard_incomplete_transaction_with_a_failure(hash) - } else {debug!(self.logger,"Forgetting a transaction attempt that even did not reach the signing stage")}) - } - if let Some(response_skeleton) = &sent_payable.response_skeleton_opt { - self.ui_message_sub - .as_ref() - .expect("UIGateway is not bound") - .try_send(NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - .expect("UIGateway is dead"); - } - } - - fn discard_incomplete_transaction_with_a_failure(&self, hash: H256) { - if let Some(rowid) = self.pending_payable_dao.fingerprint_rowid(hash) { - debug!( - self.logger, - "Deleting an existing fingerprint for a failed transaction {:?}", hash - ); - if let Err(e) = self.pending_payable_dao.delete_fingerprint(rowid) { - panic!("Database unmaintainable; payable fingerprint deletion for transaction {:?} has stayed undone due to {:?}", hash,e) - } - }; - - warning!( - self.logger, - "Failed transaction with a hash '{:?}' but without the record - thrown out", - hash - ) - } - fn handle_report_routing_service_provided_message( &mut self, msg: ReportRoutingServiceProvidedMessage, @@ -918,10 +620,11 @@ impl Accountant { } fn handle_financials(&mut self, client_id: u64, context_id: u64) { + let financial_statistics = self.financial_statistics(); let total_unpaid_and_pending_payable = self.payable_dao.total(); - let total_paid_payable = self.financial_statistics.total_paid_payable; + let total_paid_payable = financial_statistics.total_paid_payable; let total_unpaid_receivable = self.receivable_dao.total(); - let total_paid_receivable = self.financial_statistics.total_paid_receivable; + let total_paid_receivable = financial_statistics.total_paid_receivable; let body = UiFinancialsResponse { total_unpaid_and_pending_payable, total_paid_payable, @@ -939,265 +642,153 @@ impl Accountant { .expect("UiGateway is dead"); } + fn handle_scan_for_payable_request(&mut self, response_skeleton_opt: Option) { + match self.scanners.payable.begin_scan( + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ) { + Ok(message) => { + self.report_accounts_payable_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(message.clone()) + .expect("BlockchainBridge is dead"); + eprintln!("Message was sent to the blockchain bridge, {:?}", message); + } + Err(BeginScanError::CalledFromNullScanner) => { + if cfg!(test) { + eprintln!("Payable scan is disabled."); + } else { + panic!("Null Scanner shouldn't be running inside production code.") + } + } + Err(BeginScanError::NothingToProcess) => { + eprintln!("No payable found to process. The Scan was ended."); + // TODO: Do something better than just using eprintln + } + Err(BeginScanError::ScanAlreadyRunning(timestamp)) => { + info!( + &self.logger, + "Payable scan was already initiated at {}. \ + Hence, this scan request will be ignored.", + timestamp_as_string(×tamp) + ) + } + } + } + + fn handle_scan_for_pending_payable_request( + &mut self, + response_skeleton_opt: Option, + ) { + match self.scanners.pending_payable.begin_scan( + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ) { + Ok(message) => self + .request_transaction_receipts_subs_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(message) + .expect("BlockchainBridge is dead"), + Err(BeginScanError::CalledFromNullScanner) => { + if cfg!(test) { + eprintln!("Pending payable scan is disabled."); + } else { + panic!("Null Scanner shouldn't be running inside production code.") + } + } + Err(BeginScanError::NothingToProcess) => { + eprintln!("No pending payable found to process. The Scan was ended."); + // TODO: Do something better than just using eprintln + } + Err(BeginScanError::ScanAlreadyRunning(timestamp)) => { + info!( + &self.logger, + "Pending Payable scan was already initiated at {}. \ + Hence, this scan request will be ignored.", + timestamp_as_string(×tamp) + ) + } + } + } + + fn handle_scan_for_receivables_request( + &mut self, + response_skeleton_opt: Option, + ) { + match self.scanners.receivable.begin_scan( + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ) { + Ok(message) => self + .retrieve_transactions_sub + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(message) + .expect("BlockchainBridge is dead"), + Err(BeginScanError::CalledFromNullScanner) => { + if cfg!(test) { + eprintln!("Receivable scan is disabled."); + } else { + panic!("Null Scanner shouldn't be running inside production code.") + } + } + Err(BeginScanError::NothingToProcess) => { + eprintln!("The Scan was ended."); + // TODO: Do something better than just using eprintln + } + Err(BeginScanError::ScanAlreadyRunning(timestamp)) => { + info!( + &self.logger, + "Receivable scan was already initiated at {}. \ + Hence, this scan request will be ignored.", + timestamp_as_string(×tamp) + ) + } + }; + } + fn handle_externally_triggered_scan( - &self, + &mut self, _ctx: &mut Context, scan_type: ScanType, response_skeleton: ResponseSkeleton, ) { match scan_type { - ScanType::Payables => self.scanners.payables.scan(self, Some(response_skeleton)), - ScanType::Receivables => self - .scanners - .receivables - .scan(self, Some(response_skeleton)), - ScanType::PendingPayables => self - .scanners - .pending_payables - .scan(self, Some(response_skeleton)), + ScanType::Payables => self.handle_scan_for_payable_request(Some(response_skeleton)), + ScanType::PendingPayables => { + self.handle_scan_for_pending_payable_request(Some(response_skeleton)); + } + ScanType::Receivables => { + self.handle_scan_for_receivables_request(Some(response_skeleton)) + } } } - fn handle_cancel_pending_transaction(&self, msg: CancelFailedPendingTransaction) { + fn handle_new_pending_payable_fingerprint(&self, msg: PendingPayableFingerprint) { match self .pending_payable_dao - .mark_failure(msg.id.rowid) + .insert_new_fingerprint(msg.hash, msg.amount, msg.timestamp) { - Ok(_) => warning!( + Ok(_) => debug!( + self.logger, + "Processed a pending payable fingerprint for '{:?}'", msg.hash + ), + Err(e) => error!( self.logger, - "Broken transaction {:?} left with an error mark; you should take over the care of this transaction to make sure your debts will be paid because there is no automated process that can fix this without you", msg.id.hash), - Err(e) => panic!("Unsuccessful attempt for transaction {:?} to mark fatal error at payable fingerprint due to {:?}; database unreliable", msg.id.hash,e), + "Failed to make a fingerprint for pending payable '{:?}' due to '{:?}'", + msg.hash, + e + ), } } - fn handle_confirm_pending_transaction(&mut self, msg: ConfirmPendingTransaction) { - if let Err(e) = self - .payable_dao - .transaction_confirmed(&msg.pending_payable_fingerprint) - { - panic!( - "Was unable to uncheck pending payable '{:?}' after confirmation due to '{:?}'", - msg.pending_payable_fingerprint.hash, e - ) - } else { - self.financial_statistics.total_paid_payable += msg.pending_payable_fingerprint.amount; - debug!( - self.logger, - "Confirmation of transaction {:?}; record for payable was modified", - msg.pending_payable_fingerprint.hash - ); - if let Err(e) = self.pending_payable_dao.delete_fingerprint( - msg.pending_payable_fingerprint - .rowid_opt - .expectv("initialized rowid"), - ) { - panic!("Was unable to delete payable fingerprint '{:?}' after successful transaction due to '{:?}'",msg.pending_payable_fingerprint.hash,e) - } else { - info!( - self.logger, - "Transaction {:?} has gone through the whole confirmation process succeeding", - msg.pending_payable_fingerprint.hash - ) - } - } - } - - fn separate_early_errors( - sent_payments: &SentPayable, - logger: &Logger, - ) -> (Vec, Vec) { - sent_payments - .payable - .iter() - .fold((vec![],vec![]),|so_far,payment| { - match payment{ - Ok(payment_sent) => (plus(so_far.0,payment_sent.clone()),so_far.1), - Err(error) => { - logger.warning(|| match &error { - BlockchainError::TransactionFailed { .. } => format!("Encountered transaction error at this end: '{:?}'", error), - x => format!("Outbound transaction failure due to '{:?}'. Please check your blockchain service URL configuration.", x) - }); - (so_far.0,plus(so_far.1,error.clone())) - } - } - }) - } - - fn mark_pending_payable(&self, sent_payments: Vec) { - sent_payments - .into_iter() - .for_each(|payable| { - let rowid = match self.pending_payable_dao.fingerprint_rowid(payable.tx_hash) { - Some(rowid) => rowid, - None => panic!("Payable fingerprint for {:?} doesn't exist but should by now; system unreliable", payable.tx_hash) - }; - match self.payable_dao.as_ref().mark_pending_payable_rowid(&payable.to, rowid ) { - Ok(()) => (), - Err(e) => panic!("Was unable to create a mark in payables for a new pending payable '{:?}' due to '{:?}'", payable.tx_hash, e) - } - debug!(self.logger, "Payable '{:?}' has been marked as pending in the payable table",payable.tx_hash) - }) - } - - fn handle_pending_transaction_with_its_receipt( - &self, - msg: &ReportTransactionReceipts, - ) -> Vec { - fn handle_none_receipt( - payable: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - debug!(logger, - "DEBUG: Accountant: Interpreting a receipt for transaction '{:?}' but none was given; attempt {}, {}ms since sending", - payable.hash, payable.attempt_opt.expectv("initialized attempt"),elapsed_in_ms(payable.timestamp) - ); - PendingTransactionStatus::StillPending(PendingPayableId { - hash: payable.hash, - rowid: payable.rowid_opt.expectv("initialized rowid"), - }) - } - msg.fingerprints_with_receipts - .iter() - .map(|(receipt_opt, fingerprint)| match receipt_opt { - Some(receipt) => { - self.interpret_transaction_receipt(receipt, fingerprint, &self.logger) - } - None => handle_none_receipt(fingerprint, &self.logger), - }) - .collect() - } - - fn interpret_transaction_receipt( - &self, - receipt: &TransactionReceipt, - fingerprint: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - fn handle_none_status( - fingerprint: &PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, - ) -> PendingTransactionStatus { - info!(logger,"Pending transaction '{:?}' couldn't be confirmed at attempt {} at {}ms after its sending",fingerprint.hash, fingerprint.attempt_opt.expectv("initialized attempt"), elapsed_in_ms(fingerprint.timestamp)); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let transaction_id = PendingPayableId { - hash: fingerprint.hash, - rowid: fingerprint.rowid_opt.expectv("initialized rowid"), - }; - if max_pending_interval <= elapsed.as_secs() { - error!(logger,"Pending transaction '{:?}' has exceeded the maximum pending time ({}sec) and the confirmation process is going to be aborted now at the final attempt {}; \ - manual resolution is required from the user to complete the transaction.", fingerprint.hash, max_pending_interval, fingerprint.attempt_opt.expectv("initialized attempt")); - PendingTransactionStatus::Failure(transaction_id) - } else { - PendingTransactionStatus::StillPending(transaction_id) - } - } - fn handle_status_with_success( - fingerprint: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - info!( - logger, - "Transaction '{:?}' has been added to the blockchain; detected locally at attempt {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt_opt.expectv("initialized attempt"), - elapsed_in_ms(fingerprint.timestamp) - ); - PendingTransactionStatus::Confirmed(fingerprint.clone()) - } - fn handle_status_with_failure( - fingerprint: &PendingPayableFingerprint, - logger: &Logger, - ) -> PendingTransactionStatus { - error!(logger,"Pending transaction '{:?}' announced as a failure, interpreting attempt {} after {}ms from the sending",fingerprint.hash,fingerprint.attempt_opt.expectv("initialized attempt"),elapsed_in_ms(fingerprint.timestamp)); - PendingTransactionStatus::Failure(fingerprint.into()) - } - match receipt.status{ - None => handle_none_status(fingerprint, self.config.when_pending_too_long_sec, logger), - Some(status_code) => - match status_code.as_u64(){ - 0 => handle_status_with_failure(fingerprint, logger), - 1 => handle_status_with_success(fingerprint, logger), - other => unreachable!("tx receipt for pending '{:?}' - tx status: code other than 0 or 1 shouldn't be possible, but was {}", fingerprint.hash, other) - } - } - } - - fn update_payable_fingerprint(&self, pending_payable_id: PendingPayableId) { - match self - .pending_payable_dao - .update_fingerprint(pending_payable_id.rowid) - { - Ok(_) => trace!( - self.logger, - "Updated record for rowid: {} ", - pending_payable_id.rowid - ), - Err(e) => panic!( - "Failure on updating payable fingerprint '{:?}' due to {:?}", - pending_payable_id.hash, e - ), - } - } - - fn process_transaction_by_status( - &self, - statuses: Vec, - ctx: &mut Context, - ) { - statuses.into_iter().for_each(|status| { - if let PendingTransactionStatus::StillPending(transaction_id) = status { - self.update_payable_fingerprint(transaction_id) - } else if let PendingTransactionStatus::Failure(transaction_id) = status { - self.order_cancel_failed_transaction(transaction_id, ctx) - } else if let PendingTransactionStatus::Confirmed(fingerprint) = status { - self.order_confirm_transaction(fingerprint, ctx) - } - }); - } - - fn order_cancel_failed_transaction( - &self, - transaction_id: PendingPayableId, - ctx: &mut Context, - ) { - self.confirmation_tools - .notify_cancel_failed_transaction - .notify(CancelFailedPendingTransaction { id: transaction_id }, ctx) - } - - fn order_confirm_transaction( - &self, - pending_payable_fingerprint: PendingPayableFingerprint, - ctx: &mut Context, - ) { - self.confirmation_tools.notify_confirm_transaction.notify( - ConfirmPendingTransaction { - pending_payable_fingerprint, - }, - ctx, - ); - } - - fn handle_new_pending_payable_fingerprint(&self, msg: PendingPayableFingerprint) { - match self - .pending_payable_dao - .insert_new_fingerprint(msg.hash, msg.amount, msg.timestamp) - { - Ok(_) => debug!( - self.logger, - "Processed a pending payable fingerprint for '{:?}'", msg.hash - ), - Err(e) => error!( - self.logger, - "Failed to make a fingerprint for pending payable '{:?}' due to '{:?}'", - msg.hash, - e - ), - } + fn financial_statistics(&self) -> FinancialStatistics { + self.financial_statistics.as_ref().borrow().clone() } } @@ -1205,17 +796,12 @@ pub fn unsigned_to_signed(unsigned: u64) -> Result { i64::try_from(unsigned).map_err(|_| unsigned) } -fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() -} - -#[derive(Debug, PartialEq, Eq, Clone)] -enum PendingTransactionStatus { - StillPending(PendingPayableId), //updates slightly the record, waits an interval and starts a new round - Failure(PendingPayableId), //official tx failure +#[derive(Debug, PartialEq, Clone)] +pub enum PendingTransactionStatus { + StillPending(PendingPayableId), + //updates slightly the record, waits an interval and starts a new round + Failure(PendingPayableId), + //official tx failure Confirmed(PendingPayableFingerprint), //tx was fully processed and successful } @@ -1236,48 +822,15 @@ impl From<&PendingPayableFingerprint> for PendingPayableId { } } -//TODO the data types should change with GH-497 (including signed => unsigned) -trait PayableExceedThresholdTools { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool; - fn is_innocent_balance(&self, balance: i64, limit: i64) -> bool; - fn calculate_payout_threshold(&self, payment_thresholds: PaymentThresholds, x: u64) -> f64; - as_any_dcl!(); -} - -#[derive(Default)] -struct PayableExceedThresholdToolsReal {} - -impl PayableExceedThresholdTools for PayableExceedThresholdToolsReal { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool { - age <= limit - } - - fn is_innocent_balance(&self, balance: i64, limit: i64) -> bool { - balance <= limit - } - - fn calculate_payout_threshold(&self, payment_thresholds: PaymentThresholds, x: u64) -> f64 { - let m = -((payment_thresholds.debt_threshold_gwei as f64 - - payment_thresholds.permanent_debt_allowed_gwei as f64) - / (payment_thresholds.threshold_interval_sec as f64 - - payment_thresholds.maturity_threshold_sec as f64)); - let b = payment_thresholds.debt_threshold_gwei as f64 - - m * payment_thresholds.maturity_threshold_sec as f64; - m * x as f64 + b - } - as_any_impl!(); -} - #[cfg(test)] mod tests { use super::*; - use std::cell::RefCell; + use std::any::TypeId; + use std::collections::HashMap; use std::ops::Sub; - use std::rc::Rc; + use std::sync::Arc; use std::sync::Mutex; - use std::sync::{Arc, MutexGuard}; use std::time::Duration; - use std::time::SystemTime; use actix::{Arbiter, System}; use ethereum_types::{BigEndianHash, U64}; @@ -1293,116 +846,38 @@ mod tests { use crate::accountant::payable_dao::PayableDaoError; use crate::accountant::pending_payable_dao::PendingPayableDaoError; - use crate::accountant::receivable_dao::ReceivableAccount; + use crate::accountant::scanners::scanners::{NullScanner, ScannerMock}; use crate::accountant::test_utils::{ - bc_from_ac_plus_earning_wallet, bc_from_ac_plus_wallets, make_pending_payable_fingerprint, - make_receivable_account, BannedDaoFactoryMock, MessageIdGeneratorMock, - PayableDaoFactoryMock, PayableDaoMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, - ReceivableDaoFactoryMock, ReceivableDaoMock, + bc_from_earning_wallet, bc_from_wallets, make_payables, BannedDaoFactoryMock, + MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, + PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, + ReceivableDaoMock, }; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; - use crate::accountant::tools::accountant_tools::{NullScanner, ReceivablesScanner}; use crate::accountant::Accountant; use crate::blockchain::blockchain_bridge::BlockchainBridge; use crate::blockchain::blockchain_interface::BlockchainError; use crate::blockchain::blockchain_interface::BlockchainTransaction; use crate::blockchain::test_utils::BlockchainInterfaceMock; use crate::blockchain::tool_wrappers::SendTransactionToolsWrapperNull; - use crate::bootstrapper::BootstrapperConfig; use crate::database::dao_utils::from_time_t; use crate::database::dao_utils::to_time_t; use crate::sub_lib::accountant::{ ExitServiceConsumed, RoutingServiceConsumed, ScanIntervals, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; - use crate::sub_lib::utils::{NotifyHandleReal, NotifyLaterHandleReal}; + use crate::sub_lib::utils::NotifyLaterHandleReal; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; use crate::test_utils::unshared_test_utils::{ - make_accountant_config_null, make_populated_accountant_config_with_defaults, - prove_that_crash_request_handler_is_hooked_up, NotifyHandleMock, NotifyLaterHandleMock, - SystemKillerActor, + make_bc_with_defaults, prove_that_crash_request_handler_is_hooked_up, + NotifyLaterHandleMock, SystemKillerActor, }; use crate::test_utils::{make_paying_wallet, make_wallet}; use web3::types::{TransactionReceipt, H256}; - #[derive(Default)] - struct PayableThresholdToolsMock { - is_innocent_age_params: Arc>>, - is_innocent_age_results: RefCell>, - is_innocent_balance_params: Arc>>, - is_innocent_balance_results: RefCell>, - calculate_payout_threshold_params: Arc>>, - calculate_payout_threshold_results: RefCell>, - } - - impl PayableExceedThresholdTools for PayableThresholdToolsMock { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool { - self.is_innocent_age_params - .lock() - .unwrap() - .push((age, limit)); - self.is_innocent_age_results.borrow_mut().remove(0) - } - - fn is_innocent_balance(&self, balance: i64, limit: i64) -> bool { - self.is_innocent_balance_params - .lock() - .unwrap() - .push((balance, limit)); - self.is_innocent_balance_results.borrow_mut().remove(0) - } - - fn calculate_payout_threshold(&self, payment_thresholds: PaymentThresholds, x: u64) -> f64 { - self.calculate_payout_threshold_params - .lock() - .unwrap() - .push((payment_thresholds, x)); - self.calculate_payout_threshold_results - .borrow_mut() - .remove(0) - } - } - - impl PayableThresholdToolsMock { - fn is_innocent_age_params(mut self, params: &Arc>>) -> Self { - self.is_innocent_age_params = params.clone(); - self - } - - fn is_innocent_age_result(self, result: bool) -> Self { - self.is_innocent_age_results.borrow_mut().push(result); - self - } - - fn is_innocent_balance_params(mut self, params: &Arc>>) -> Self { - self.is_innocent_balance_params = params.clone(); - self - } - - fn is_innocent_balance_result(self, result: bool) -> Self { - self.is_innocent_balance_results.borrow_mut().push(result); - self - } - - fn calculate_payout_threshold_params( - mut self, - params: &Arc>>, - ) -> Self { - self.calculate_payout_threshold_params = params.clone(); - self - } - - fn calculate_payout_threshold_result(self, result: f64) -> Self { - self.calculate_payout_threshold_results - .borrow_mut() - .push(result); - self - } - } - #[test] fn constants_have_correct_values() { assert_eq!(CRASH_KEY, "ACCOUNTANT"); @@ -1411,131 +886,129 @@ mod tests { #[test] fn new_calls_factories_properly() { - let mut config = BootstrapperConfig::new(); - config.accountant_config_opt = Some(make_accountant_config_null()); - let payable_dao_factory_called = Rc::new(RefCell::new(false)); - let payable_dao = PayableDaoMock::new(); - let payable_dao_factory = - PayableDaoFactoryMock::new(payable_dao).called(&payable_dao_factory_called); - let receivable_dao_factory_called = Rc::new(RefCell::new(false)); - let receivable_dao = ReceivableDaoMock::new(); - let receivable_dao_factory = - ReceivableDaoFactoryMock::new(receivable_dao).called(&receivable_dao_factory_called); - let pending_payable_dao_factory_called = Rc::new(RefCell::new(false)); - let pending_payable_dao = PendingPayableDaoMock::default(); - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new(pending_payable_dao) - .called(&pending_payable_dao_factory_called); - let banned_dao_factory_called = Rc::new(RefCell::new(false)); - let banned_dao = BannedDaoMock::new(); - let banned_dao_factory = - BannedDaoFactoryMock::new(banned_dao).called(&banned_dao_factory_called); + let mut config = make_bc_with_defaults(); + let payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let receivable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let banned_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao_factory = PayableDaoFactoryMock::new() + .make_params(&payable_dao_factory_params_arc) + .make_result(PayableDaoMock::new()) // For Accountant + .make_result(PayableDaoMock::new()) // For Payable Scanner + .make_result(PayableDaoMock::new()); // For PendingPayable Scanner + let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() + .make_params(&pending_payable_dao_factory_params_arc) + .make_result(PendingPayableDaoMock::new()) // For Accountant + .make_result(PendingPayableDaoMock::new()) // For Payable Scanner + .make_result(PendingPayableDaoMock::new()); // For PendingPayable Scanner + let receivable_dao_factory = ReceivableDaoFactoryMock::new() + .make_params(&receivable_dao_factory_params_arc) + .make_result(ReceivableDaoMock::new()) // For Accountant + .make_result(ReceivableDaoMock::new()); // For Receivable Scanner + let banned_dao_factory = BannedDaoFactoryMock::new() + .make_params(&banned_dao_factory_params_arc) + .make_result(BannedDaoMock::new()); // For Receivable Scanner let _ = Accountant::new( - &config, + &mut config, Box::new(payable_dao_factory), Box::new(receivable_dao_factory), Box::new(pending_payable_dao_factory), Box::new(banned_dao_factory), ); - assert_eq!(payable_dao_factory_called.as_ref(), &RefCell::new(true)); - assert_eq!(receivable_dao_factory_called.as_ref(), &RefCell::new(true)); assert_eq!( - pending_payable_dao_factory_called.as_ref(), - &RefCell::new(true) + *payable_dao_factory_params_arc.lock().unwrap(), + vec![(), (), ()] + ); + assert_eq!( + *pending_payable_dao_factory_params_arc.lock().unwrap(), + vec![(), (), ()] + ); + assert_eq!( + *receivable_dao_factory_params_arc.lock().unwrap(), + vec![(), ()] ); - assert_eq!(banned_dao_factory_called.as_ref(), &RefCell::new(true)); + assert_eq!(*banned_dao_factory_params_arc.lock().unwrap(), vec![()]); } #[test] fn accountant_have_proper_defaulted_values() { - let mut bootstrapper_config = BootstrapperConfig::new(); - bootstrapper_config.accountant_config_opt = - Some(make_populated_accountant_config_with_defaults()); - let payable_dao_factory = Box::new(PayableDaoFactoryMock::new(PayableDaoMock::new())); - let receivable_dao_factory = - Box::new(ReceivableDaoFactoryMock::new(ReceivableDaoMock::new())); - let pending_payable_dao_factory = Box::new(PendingPayableDaoFactoryMock::new( - PendingPayableDaoMock::default(), - )); - let banned_dao_factory = Box::new(BannedDaoFactoryMock::new(BannedDaoMock::new())); + // TODO: Verify Scanners are defaulted properly [write this test once GH-574's recorder's code is merged, or cherry-pick the commit] + // When scan() is called, on a scanner, it sends one message to blockchain bridge and a notify_later message, which in turn + // schedules another scan, in turn accountant sends another message to blockchain bridge. Make sure a scan() call to a scanner results in two messages + // to blockchain bridge (one received directly and other indirectly via notify_later). Make sure 6 messages are received in total. + // The second message is received after test defined scan intervals. + // Make sure to use a real database instead of using mock utilities. It'll require at least one row for each table of individual scanners. + let mut bootstrapper_config = make_bc_with_defaults(); + let payable_dao_factory = Box::new( + PayableDaoFactoryMock::new() + .make_result(PayableDaoMock::new()) // For Accountant + .make_result(PayableDaoMock::new()) // For Payable Scanner + .make_result(PayableDaoMock::new()), // For PendingPayable Scanner + ); + let pending_payable_dao_factory = Box::new( + PendingPayableDaoFactoryMock::new() + .make_result(PendingPayableDaoMock::new()) // For Accountant + .make_result(PendingPayableDaoMock::new()) // For Payable Scanner + .make_result(PendingPayableDaoMock::new()), // For PendingPayable Scanner + ); + let receivable_dao_factory = Box::new( + ReceivableDaoFactoryMock::new() + .make_result(ReceivableDaoMock::new()) // For Accountant + .make_result(ReceivableDaoMock::new()), // For Scanner + ); + let banned_dao_factory = + Box::new(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); let result = Accountant::new( - &bootstrapper_config, + &mut bootstrapper_config, payable_dao_factory, receivable_dao_factory, pending_payable_dao_factory, banned_dao_factory, ); - let transaction_confirmation_tools = result.confirmation_tools; - transaction_confirmation_tools - .notify_confirm_transaction - .as_any() - .downcast_ref::>() - .unwrap(); - transaction_confirmation_tools - .notify_cancel_failed_transaction - .as_any() - .downcast_ref::>() - .unwrap(); - transaction_confirmation_tools - .notify_later_scan_for_pending_payable + let financial_statistics = result.financial_statistics(); + let notify_later = result.notify_later; + notify_later + .scan_for_pending_payable .as_any() .downcast_ref::>() .unwrap(); - transaction_confirmation_tools - .notify_later_scan_for_payable + notify_later + .scan_for_payable .as_any() .downcast_ref::>() .unwrap(); - transaction_confirmation_tools - .notify_later_scan_for_receivable - .as_any() - .downcast_ref::>() - .unwrap(); - //testing presence of real scanners, there is a different test covering them all - result - .scanners - .receivables - .as_any() - .downcast_ref::() - .unwrap(); - result - .payable_threshold_tools + notify_later + .scan_for_receivable .as_any() - .downcast_ref::() - .unwrap(); - assert_eq!(result.crashable, false); - assert_eq!(result.financial_statistics.total_paid_receivable, 0); - assert_eq!(result.financial_statistics.total_paid_payable, 0); + .downcast_ref::>(); result .message_id_generator .as_any() .downcast_ref::() .unwrap(); + assert_eq!(result.crashable, false); + assert_eq!(financial_statistics.total_paid_receivable, 0); + assert_eq!(financial_statistics.total_paid_payable, 0); } #[test] fn scan_receivables_request() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: Default::default(), - }, - make_wallet("earning_wallet"), - ); + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let subject = AccountantBuilder::default() .bootstrapper_config(config) + .receivable_dao(ReceivableDaoMock::new()) .receivable_dao(receivable_dao) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -1564,7 +1037,7 @@ mod tests { recipient: make_wallet("earning_wallet"), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, - context_id: 4321 + context_id: 4321, }), } ); @@ -1572,19 +1045,13 @@ mod tests { #[test] fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("earning_wallet"), - ); + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); + config.suppress_initial_scans_opt = Some(true); let subject = AccountantBuilder::default() .bootstrapper_config(config) .build(); @@ -1618,19 +1085,7 @@ mod tests { #[test] fn scan_payables_request() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("some_wallet_address"), - ); + let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); let payable_account = PayableAccount { wallet: make_wallet("wallet"), balance: DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1, @@ -1643,7 +1098,9 @@ mod tests { PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let subject_addr = subject.start(); @@ -1671,7 +1128,7 @@ mod tests { accounts: vec![payable_account], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, - context_id: 4321 + context_id: 4321, }), } ); @@ -1679,19 +1136,7 @@ mod tests { #[test] fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("earning_wallet"), - ); + let config = bc_from_earning_wallet(make_wallet("earning_wallet")); let subject = AccountantBuilder::default() .bootstrapper_config(config) .build(); @@ -1725,19 +1170,13 @@ mod tests { #[test] fn scan_pending_payables_request() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("some_wallet_address"), - ); + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.suppress_initial_scans_opt = Some(true); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); let fingerprint = PendingPayableFingerprint { rowid_opt: Some(1234), timestamp: SystemTime::now(), @@ -1750,7 +1189,9 @@ mod tests { .return_all_fingerprints_result(vec![fingerprint.clone()]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .pending_payable_dao(pending_payable_dao) + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(PendingPayableDaoMock::new()) // For Payable Scanner + .pending_payable_dao(pending_payable_dao) // For PendingPayable Scanner .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let subject_addr = subject.start(); @@ -1778,27 +1219,83 @@ mod tests { pending_payable: vec![fingerprint], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, - context_id: 4321 + context_id: 4321, }), } ); } + #[test] + fn scan_request_from_ui_is_handled_in_case_the_scan_is_already_running() { + init_test_logging(); + let test_name = "scan_request_from_ui_is_handled_in_case_the_scan_is_already_running"; + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.suppress_initial_scans_opt = Some(true); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(1234), + timestamp: SystemTime::now(), + hash: Default::default(), + attempt_opt: Some(1), + amount: 1_000_000, + process_error: None, + }; + let pending_payable_dao = + PendingPayableDaoMock::default().return_all_fingerprints_result(vec![fingerprint]); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(config) + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(PendingPayableDaoMock::new()) // For Payable Scanner + .pending_payable_dao(pending_payable_dao) // For PendingPayable Scanner + .build(); + subject.logger = Logger::new(test_name); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: ScanType::PendingPayables, + } + .tmb(4321), + }; + subject_addr.try_send(ui_message).unwrap(); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: ScanType::PendingPayables, + } + .tmb(4321), + }; + + subject_addr.try_send(ui_message).unwrap(); + + System::current().stop(); + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {}: Pending Payable scan was already initiated", + test_name + )); + assert_eq!(blockchain_bridge_recording.len(), 1); + } + #[test] fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("earning_wallet"), - ); + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); let subject = AccountantBuilder::default() .bootstrapper_config(config) .build(); @@ -1846,12 +1343,13 @@ mod tests { .mark_pending_payable_rowid_result(Ok(())); let system = System::new("accountant_calls_payable_dao_to_mark_pending_payable"); let accountant = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(pending_payable_dao) // For Payable Scanner + .pending_payable_dao(PendingPayableDaoMock::new()) // For PendingPayable Scanner .build(); let expected_payable = Payable::new( expected_wallet.clone(), @@ -1887,7 +1385,9 @@ mod tests { let system = System::new("sent payable failure without backup"); let pending_payable_dao = PendingPayableDaoMock::default().fingerprint_rowid_result(None); let accountant = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(pending_payable_dao) // For Payable Scanner + .pending_payable_dao(PendingPayableDaoMock::new()) // For PendingPayable Scanner .build(); let hash = H256::from_uint(&U256::from(12345)); let sent_payable = SentPayable { @@ -1917,7 +1417,7 @@ mod tests { hash )); log_handler.exists_log_containing( - r#"WARN: Accountant: Failed transaction with a hash '0x0000000000000000000000000000000000000000000000000000000000003039' but without the record - thrown out"#, + r#"WARN: Accountant: Failed transaction with a hash '0x0000…3039' but without the record - thrown out"#, ); } @@ -1941,8 +1441,12 @@ mod tests { .delete_fingerprint_params(&delete_fingerprint_params_arc) .delete_fingerprint_result(Ok(())); let subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(pending_payable_dao) // For Payable Scanner + .pending_payable_dao(PendingPayableDaoMock::new()) // For Scanner .build(); let wallet = make_wallet("blah"); let hash_tx_1 = H256::from_uint(&U256::from(5555)); @@ -1991,7 +1495,7 @@ mod tests { log_handler.exists_log_containing("WARN: Accountant: Encountered transaction error at this end: \ 'TransactionFailed { msg: \"Attempt failed\", hash_opt: Some(0x0000000000000000000000000000000000000000000000000000000000003039)"); log_handler.exists_log_containing( - "DEBUG: Accountant: Deleting an existing fingerprint for a failed transaction 0x0000000000000000000000000000000000000000000000000000000000003039", + "DEBUG: Accountant: Deleting an existing backup for a failed transaction 0x0000…3039", ); } @@ -1999,39 +1503,21 @@ mod tests { fn accountant_sends_report_accounts_payable_to_blockchain_bridge_when_qualified_payable_found() { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let accounts = vec![ - PayableAccount { - wallet: make_wallet("blah"), - balance: DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 55, - last_paid_timestamp: from_time_t( - to_time_t(SystemTime::now()) - - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec - - 5, - ), - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("foo"), - balance: DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 66, - last_paid_timestamp: from_time_t( - to_time_t(SystemTime::now()) - - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec - - 500, - ), - pending_payable_opt: None, - }, - ]; - let payable_dao = PayableDaoMock::new().non_pending_payables_result(accounts.clone()); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let (qualified_payables, _, all_non_pending_payables) = + make_payables(now, &payment_thresholds); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let system = System::new("report_accounts_payable forwarded to blockchain_bridge"); let mut subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .payable_dao(payable_dao) + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.receivables = Box::new(NullScanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); + subject.scanners.receivable = Box::new(NullScanner::new()); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2045,18 +1531,13 @@ mod tests { system.run(); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recorder.len(), 1); - let report_accounts_payables_msgs: Vec<&ReportAccountsPayable> = (0 - ..blockchain_bridge_recorder.len()) - .flat_map(|index| { - blockchain_bridge_recorder.get_record_opt::(index) - }) - .collect(); + let message = blockchain_bridge_recorder.get_record::(0); assert_eq!( - report_accounts_payables_msgs, - vec![&ReportAccountsPayable { - accounts, - response_skeleton_opt: None - }] + message, + &ReportAccountsPayable { + accounts: qualified_payables, + response_skeleton_opt: None, + } ); } @@ -2068,20 +1549,16 @@ mod tests { let system = System::new( "accountant_sends_a_request_to_blockchain_bridge_to_scan_for_received_payments", ); - let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let mut subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - )) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) + .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) + .receivable_dao(ReceivableDaoMock::new()) // For Accountant + .receivable_dao(receivable_dao) // For Scanner .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.payables = Box::new(NullScanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); + subject.scanners.payable = Box::new(NullScanner::new()); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2121,15 +1598,16 @@ mod tests { gwei_amount: 10000, }; let more_money_received_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao = ReceivableDaoMock::new() .more_money_received_parameters(&more_money_received_params_arc) .more_money_received_result(Ok(())); let accountant = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - )) - .payable_dao(PayableDaoMock::new().non_pending_payables_result(vec![])) + .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) + .payable_dao(payable_dao) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) + .receivable_dao(ReceivableDaoMock::new()) .receivable_dao(receivable_dao) .build(); let system = System::new("accountant_receives_new_payments_to_the_receivables_dao"); @@ -2155,43 +1633,35 @@ mod tests { #[test] fn accountant_scans_after_startup() { init_test_logging(); - let return_all_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let payable_params_arc = Arc::new(Mutex::new(vec![])); let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); let paid_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, _) = make_recorder(); + let earning_wallet = make_wallet("earning"); let system = System::new("accountant_scans_after_startup"); - let config = bc_from_ac_plus_wallets( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(100), //making sure we cannot enter the first repeated scanning - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_millis(100), //except here, where we use it to stop the system - }, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - }, - make_wallet("buy"), - make_wallet("hi"), - ); - let mut pending_payable_dao = PendingPayableDaoMock::default() - .return_all_fingerprints_params(&return_all_fingerprints_params_arc) + let config = bc_from_wallets(make_wallet("buy"), earning_wallet.clone()); + let payable_dao = PayableDaoMock::new() + .non_pending_payables_params(&payable_params_arc) + .non_pending_payables_result(vec![]); + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_fingerprints_params(&pending_payable_params_arc) .return_all_fingerprints_result(vec![]); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_parameters(&new_delinquencies_params_arc) .new_delinquencies_result(vec![]) .paid_delinquencies_parameters(&paid_delinquencies_params_arc) .paid_delinquencies_result(vec![]); - let payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) - .pending_payable_dao(pending_payable_dao) + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(PendingPayableDaoMock::new()) // For Payable Scanner + .pending_payable_dao(pending_payable_dao) // For PendingPayable Scanner + .receivable_dao(ReceivableDaoMock::new()) // For Accountant + .receivable_dao(receivable_dao) // For Scanner .build(); let peer_actors = peer_actors_builder() .blockchain_bridge(blockchain_bridge) @@ -2202,129 +1672,78 @@ mod tests { send_start_message!(subject_subs); + System::current().stop(); system.run(); let tlh = TestLogHandler::new(); - tlh.await_log_containing("INFO: Accountant: Scanning for payables", 1000); + tlh.exists_log_containing("INFO: Accountant: Scanning for payables"); + tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); tlh.exists_log_containing(&format!( "INFO: Accountant: Scanning for receivables to {}", - make_wallet("hi") + earning_wallet )); tlh.exists_log_containing("INFO: Accountant: Scanning for delinquencies"); - tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); - //some more weak proofs but still good enough - //proof of calling a piece of scan_for_pending_payable - let return_all_fingerprints_params = return_all_fingerprints_params_arc.lock().unwrap(); - //the last ends this test calling System::current.stop() - assert_eq!(*return_all_fingerprints_params, vec![(), ()]); - //proof of calling a piece of scan_for_payable() - let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); - assert_eq!(*non_pending_payables_params, vec![()]); + let payable_params = payable_params_arc.lock().unwrap(); + let pending_payable_params = pending_payable_params_arc.lock().unwrap(); //proof of calling pieces of scan_for_delinquencies() let mut new_delinquencies_params = new_delinquencies_params_arc.lock().unwrap(); let (captured_timestamp, captured_curves) = new_delinquencies_params.remove(0); + let paid_delinquencies_params = paid_delinquencies_params_arc.lock().unwrap(); + assert_eq!(*payable_params, vec![()]); + assert_eq!(*pending_payable_params, vec![()]); assert!(new_delinquencies_params.is_empty()); assert!( captured_timestamp < SystemTime::now() && captured_timestamp >= from_time_t(to_time_t(SystemTime::now()) - 5) ); - assert_eq!(captured_curves, *DEFAULT_PAYMENT_THRESHOLDS); - let paid_delinquencies_params = paid_delinquencies_params_arc.lock().unwrap(); + assert_eq!(captured_curves, PaymentThresholds::default()); assert_eq!(paid_delinquencies_params.len(), 1); - assert_eq!(paid_delinquencies_params[0], *DEFAULT_PAYMENT_THRESHOLDS); + assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); } #[test] fn periodical_scanning_for_receivables_and_delinquencies_works() { init_test_logging(); - let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); - let ban_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_receivables_and_delinquencies_works"; + let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_receivable_params_arc = Arc::new(Mutex::new(vec![])); - let earning_wallet = make_wallet("earner3000"); - let wallet_to_be_banned = make_wallet("bad_luck"); - let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(100), - receivable_scan_interval: Duration::from_millis(99), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - earning_wallet.clone(), - ); - let new_delinquent_account = ReceivableAccount { - wallet: wallet_to_be_banned.clone(), - balance: 4567, - last_received_timestamp: from_time_t(200_000_000), - }; - let system = System::new("periodical_scanning_for_receivables_and_delinquencies_works"); - let banned_dao = BannedDaoMock::new().ban_parameters(&ban_params_arc); - let mut receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_parameters(&new_delinquencies_params_arc) - //this is the immediate try, not with our interval - .new_delinquencies_result(vec![]) - //after the interval we actually process data - .new_delinquencies_result(vec![new_delinquent_account]) - .paid_delinquencies_result(vec![]) - .paid_delinquencies_result(vec![]) - .paid_delinquencies_result(vec![]); - receivable_dao.have_new_delinquencies_shutdown_the_system = true; + let system = System::new(test_name); + SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let receivable_scanner = ScannerMock::new() + .begin_scan_params(&begin_scan_params_arc) + .begin_scan_result(Err(BeginScanError::NothingToProcess)) + .begin_scan_result(Ok(RetrieveTransactions { + recipient: make_wallet("some_recipient"), + response_skeleton_opt: None, + })) + .stop_the_system(); + let mut config = make_bc_with_defaults(); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_secs(100), + receivable_scan_interval: Duration::from_millis(99), + pending_payable_scan_interval: Duration::from_secs(100), + }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .receivable_dao(receivable_dao) - .banned_dao(banned_dao) .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.payables = Box::new(NullScanner); - subject.confirmation_tools.notify_later_scan_for_receivable = Box::new( + subject.scanners.payable = Box::new(NullScanner::new()); // Skipping + subject.scanners.pending_payable = Box::new(NullScanner::new()); // Skipping + subject.scanners.receivable = Box::new(receivable_scanner); + subject.notify_later.scan_for_receivable = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_receivable_params_arc) .permit_to_send_out(), ); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - let subject_addr: Addr = subject.start(); + let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); + let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); system.run(); - let retrieve_transactions_recording = blockchain_bridge_recording.lock().unwrap(); - assert_eq!(retrieve_transactions_recording.len(), 3); - let retrieve_transactions_msgs: Vec<&RetrieveTransactions> = (0 - ..retrieve_transactions_recording.len()) - .map(|index| retrieve_transactions_recording.get_record::(index)) - .collect(); - assert_eq!( - *retrieve_transactions_msgs, - vec![ - &RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt: None, - }, - &RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt: None, - }, - &RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt: None, - } - ] - ); - //sadly I cannot effectively assert on the exact params - //they are a) real timestamp of now, b) constant payment_thresholds - //the Rust type system gives me enough support to be okay with counting occurrences - let new_delinquencies_params = new_delinquencies_params_arc.lock().unwrap(); - assert_eq!(new_delinquencies_params.len(), 3); //the third one is the signal to shut the system down - let ban_params = ban_params_arc.lock().unwrap(); - assert_eq!(*ban_params, vec![wallet_to_be_banned]); + let begin_scan_params = begin_scan_params_arc.lock().unwrap(); let notify_later_receivable_params = notify_later_receivable_params_arc.lock().unwrap(); + assert_eq!(begin_scan_params.len(), 2); assert_eq!( *notify_later_receivable_params, vec![ @@ -2340,92 +1759,53 @@ mod tests { }, Duration::from_millis(99) ), - ( - ScanForReceivables { - response_skeleton_opt: None - }, - Duration::from_millis(99) - ) ] ) } #[test] fn periodical_scanning_for_pending_payable_works() { - //in the very first round we scan without waiting but we cannot find any pending payable init_test_logging(); - let return_all_pending_payable_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_pending_payable_works"; + let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let system = - System::new("accountant_payable_scan_timer_triggers_scanning_for_pending_payable"); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(100), - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_millis(98), - }, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - }, - make_wallet("hi"), - ); - // slightly above minimum balance, to the right of the curve (time intersection) - let pending_payable_fingerprint_record = PendingPayableFingerprint { - rowid_opt: Some(45454), - timestamp: SystemTime::now(), - hash: H256::from_uint(&U256::from(565)), - attempt_opt: Some(1), - amount: 4589, - process_error: None, - }; - let mut pending_payable_dao = PendingPayableDaoMock::default() - .return_all_fingerprints_params(&return_all_pending_payable_fingerprints_params_arc) - .return_all_fingerprints_result(vec![]) - .return_all_fingerprints_result(vec![pending_payable_fingerprint_record.clone()]); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); + let system = System::new(test_name); + SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let pending_payable_scanner = ScannerMock::new() + .begin_scan_params(&begin_scan_params_arc) + .begin_scan_result(Err(BeginScanError::NothingToProcess)) + .begin_scan_result(Ok(RequestTransactionReceipts { + pending_payable: vec![], + response_skeleton_opt: None, + })) + .stop_the_system(); + let mut config = make_bc_with_defaults(); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_secs(100), + receivable_scan_interval: Duration::from_secs(100), + pending_payable_scan_interval: Duration::from_millis(98), + }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .pending_payable_dao(pending_payable_dao) .build(); - subject.scanners.receivables = Box::new(NullScanner); //skipping - subject.scanners.payables = Box::new(NullScanner); //skipping - subject - .confirmation_tools - .notify_later_scan_for_pending_payable = Box::new( + subject.scanners.payable = Box::new(NullScanner::new()); //skipping + subject.scanners.pending_payable = Box::new(pending_payable_scanner); + subject.scanners.receivable = Box::new(NullScanner::new()); //skipping + subject.notify_later.scan_for_pending_payable = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_pending_payable_params_arc) .permit_to_send_out(), ); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); + let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); system.run(); - let return_all_pending_payable_fingerprints = - return_all_pending_payable_fingerprints_params_arc - .lock() - .unwrap(); - //the third attempt is the one where the queue is empty and System::current.stop() ends the cycle - assert_eq!(*return_all_pending_payable_fingerprints, vec![(), (), ()]); - let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recorder.len(), 1); - let request_transaction_receipt_msg = - blockchain_bridge_recorder.get_record::(0); - assert_eq!( - request_transaction_receipt_msg, - &RequestTransactionReceipts { - pending_payable: vec![pending_payable_fingerprint_record], - response_skeleton_opt: None, - } - ); + let begin_scan_params = begin_scan_params_arc.lock().unwrap(); + assert_eq!(begin_scan_params.len(), 2); let notify_later_pending_payable_params = notify_later_pending_payable_params_arc.lock().unwrap(); assert_eq!( @@ -2443,88 +1823,56 @@ mod tests { }, Duration::from_millis(98) ), - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ) ] ) } #[test] - fn accountant_payable_scan_timer_triggers_periodical_scanning_for_payables() { - //in the very first round we scan without waiting but we cannot find any payable records + fn periodical_scanning_for_payable_works() { init_test_logging(); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_payable_works"; + let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_payables_params_arc = Arc::new(Mutex::new(vec![])); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let system = System::new("accountant_payable_scan_timer_triggers_scanning_for_payables"); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(97), - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("hi"), - ); - let now = to_time_t(SystemTime::now()); - // slightly above minimum balance, to the right of the curve (time intersection) - let account = PayableAccount { - wallet: make_wallet("wallet"), - balance: DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 5, - last_paid_timestamp: from_time_t( - now - DEFAULT_PAYMENT_THRESHOLDS.threshold_interval_sec - 10, - ), - pending_payable_opt: None, - }; - let mut payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![]) - .non_pending_payables_result(vec![account.clone()]); - payable_dao.have_non_pending_payables_shut_down_the_system = true; - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); + let system = System::new(test_name); + SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let payable_scanner = ScannerMock::new() + .begin_scan_params(&begin_scan_params_arc) + .begin_scan_result(Err(BeginScanError::NothingToProcess)) + .begin_scan_result(Ok(ReportAccountsPayable { + accounts: vec![], + response_skeleton_opt: None, + })) + .stop_the_system(); + let mut config = bc_from_earning_wallet(make_wallet("hi")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(97), + receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + pending_payable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) .build(); - subject.scanners.pending_payables = Box::new(NullScanner); //skipping - subject.scanners.receivables = Box::new(NullScanner); //skipping - subject.confirmation_tools.notify_later_scan_for_payable = Box::new( + subject.logger = Logger::new(test_name); + subject.scanners.payable = Box::new(payable_scanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); //skipping + subject.scanners.receivable = Box::new(NullScanner::new()); //skipping + subject.notify_later.scan_for_payable = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_payables_params_arc) .permit_to_send_out(), ); let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); + let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); system.run(); - let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); - //the third attempt is the one where the queue is empty and System::current.stop() ends the cycle - assert_eq!(*non_pending_payables_params, vec![(), (), ()]); - let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recorder.len(), 1); - let report_accounts_payables_msg = - blockchain_bridge_recorder.get_record::(0); - assert_eq!( - report_accounts_payables_msg, - &ReportAccountsPayable { - accounts: vec![account], - response_skeleton_opt: None, - } - ); + //the second attempt is the one where the queue is empty and System::current.stop() ends the cycle + let begin_scan_params = begin_scan_params_arc.lock().unwrap(); let notify_later_payables_params = notify_later_payables_params_arc.lock().unwrap(); + assert_eq!(*begin_scan_params, vec![(), ()]); assert_eq!( *notify_later_payables_params, vec![ @@ -2540,12 +1888,6 @@ mod tests { }, Duration::from_millis(97) ), - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ) ] ) } @@ -2554,26 +1896,23 @@ mod tests { fn start_message_triggers_no_scans_in_suppress_mode() { init_test_logging(); let system = System::new("start_message_triggers_no_scans_in_suppress_mode"); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_millis(1), - receivable_scan_interval: Duration::from_millis(1), - pending_payable_scan_interval: Duration::from_secs(100), - }, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: true, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - }, - make_wallet("hi"), - ); + let mut config = bc_from_earning_wallet(make_wallet("hi")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(1), + receivable_scan_interval: Duration::from_millis(1), + pending_payable_scan_interval: Duration::from_secs(100), + }); + config.suppress_initial_scans_opt = Some(true); let payable_dao = PayableDaoMock::new(); // No payables: demanding one would cause a panic let receivable_dao = ReceivableDaoMock::new(); // No delinquencies: demanding one would cause a panic let peer_actors = peer_actors_builder().build(); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) + .payable_dao(payable_dao) // For Accountant + .payable_dao(PayableDaoMock::new()) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner + .receivable_dao(receivable_dao) // For Accountant + .receivable_dao(ReceivableDaoMock::new()) // For Scanner .build(); let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); @@ -2592,9 +1931,6 @@ mod tests { #[test] fn scan_for_payables_message_does_not_trigger_payment_for_balances_below_the_curve() { init_test_logging(); - let accountant_config = make_populated_accountant_config_with_defaults(); - let config = bc_from_ac_plus_earning_wallet(accountant_config, make_wallet("mine")); - let now = to_time_t(SystemTime::now()); let payment_thresholds = PaymentThresholds { threshold_interval_sec: 2_592_000, debt_threshold_gwei: 1_000_000_000, @@ -2603,6 +1939,9 @@ mod tests { permanent_debt_allowed_gwei: 10_000_000, unban_below_gwei: 10_000_000, }; + let config = bc_from_earning_wallet(make_wallet("mine")); + let now = to_time_t(SystemTime::now()); + let accounts = vec![ // below minimum balance, to the right of time intersection (inside buffer zone) PayableAccount { @@ -2644,12 +1983,16 @@ mod tests { blockchain_bridge_addr.recipient::(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner .build(); - subject.report_accounts_payable_sub = Some(report_accounts_payable_sub); - subject.config.payment_thresholds = payment_thresholds; + subject.report_accounts_payable_sub_opt = Some(report_accounts_payable_sub); - subject.scan_for_payables(None); + let _result = subject + .scanners + .payable + .begin_scan(SystemTime::now(), None, &subject.logger); System::current().stop_with_code(0); system.run(); @@ -2660,19 +2003,12 @@ mod tests { #[test] fn scan_for_payable_message_triggers_payment_for_balances_over_the_curve() { init_test_logging(); - let config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(50_000), - payable_scan_interval: Duration::from_millis(100), - receivable_scan_interval: Duration::from_secs(50_000), - }, - payment_thresholds: DEFAULT_PAYMENT_THRESHOLDS.clone(), - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - }, - make_wallet("mine"), - ); + let mut config = bc_from_earning_wallet(make_wallet("mine")); + config.scan_intervals_opt = Some(ScanIntervals { + pending_payable_scan_interval: Duration::from_secs(50_000), + payable_scan_interval: Duration::from_millis(100), + receivable_scan_interval: Duration::from_secs(50_000), + }); let now = to_time_t(SystemTime::now()); let accounts = vec![ // slightly above minimum balance, to the right of the curve (time intersection) @@ -2694,11 +2030,12 @@ mod tests { pending_payable_opt: None, }, ]; - let mut payable_dao = PayableDaoMock::default() - .non_pending_payables_result(accounts.clone()) - .non_pending_payables_result(vec![]); - payable_dao.have_non_pending_payables_shut_down_the_system = true; + let payable_dao = PayableDaoMock::default().non_pending_payables_result(accounts.clone()); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); + let mut expected_messages_by_type = HashMap::new(); + expected_messages_by_type.insert(TypeId::of::(), 1); + let blockchain_bridge = blockchain_bridge + .stop_after_messages_and_start_system_killer(expected_messages_by_type); let system = System::new("scan_for_payable_message_triggers_payment_for_balances_over_the_curve"); let peer_actors = peer_actors_builder() @@ -2706,10 +2043,12 @@ mod tests { .build(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao) + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner .build(); - subject.scanners.pending_payables = Box::new(NullScanner); - subject.scanners.receivables = Box::new(NullScanner); + subject.scanners.pending_payable = Box::new(NullScanner::new()); + subject.scanners.receivable = Box::new(NullScanner::new()); let subject_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&subject_addr); send_bind_message!(accountant_subs, peer_actors); @@ -2718,91 +2057,66 @@ mod tests { system.run(); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); + let message = blockchain_bridge_recordings.get_record::(0); assert_eq!( - blockchain_bridge_recordings.get_record::(0), + message, &ReportAccountsPayable { accounts, - response_skeleton_opt: None + response_skeleton_opt: None, } ); } #[test] - fn scan_for_delinquencies_triggers_bans_and_unbans() { + fn accountant_doesn_t_starts_another_scan_in_case_it_receives_the_message_and_the_scanner_is_running( + ) { init_test_logging(); - let accountant_config = make_populated_accountant_config_with_defaults(); - let config = bc_from_ac_plus_earning_wallet(accountant_config, make_wallet("mine")); - let newly_banned_1 = make_receivable_account(1234, true); - let newly_banned_2 = make_receivable_account(2345, true); - let newly_unbanned_1 = make_receivable_account(3456, false); - let newly_unbanned_2 = make_receivable_account(4567, false); - let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![]); - let new_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); - let paid_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); - let receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_parameters(&new_delinquencies_parameters_arc) - .new_delinquencies_result(vec![newly_banned_1.clone(), newly_banned_2.clone()]) - .paid_delinquencies_parameters(&paid_delinquencies_parameters_arc) - .paid_delinquencies_result(vec![newly_unbanned_1.clone(), newly_unbanned_2.clone()]); - let ban_parameters_arc = Arc::new(Mutex::new(vec![])); - let unban_parameters_arc = Arc::new(Mutex::new(vec![])); - let banned_dao = BannedDaoMock::new() - .ban_list_result(vec![]) - .ban_parameters(&ban_parameters_arc) - .unban_parameters(&unban_parameters_arc); - let subject = AccountantBuilder::default() + let test_name = "accountant_doesn_t_starts_another_scan_in_case_it_receives_the_message_and_the_scanner_is_running"; + let payable_dao = PayableDaoMock::default(); + let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); + let report_accounts_payable_sub = blockchain_bridge.start().recipient(); + let now = + to_time_t(SystemTime::now()) - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec - 1; + let payable_account = PayableAccount { + wallet: make_wallet("scan_for_payables"), + balance: DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1, + last_paid_timestamp: from_time_t(now), + pending_payable_opt: None, + }; + let mut payable_dao = + payable_dao.non_pending_payables_result(vec![payable_account.clone()]); + payable_dao.have_non_pending_payables_shut_down_the_system = true; + let config = bc_from_earning_wallet(make_wallet("mine")); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .payable_dao(PayableDaoMock::new()) // For Accountant + .payable_dao(payable_dao) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner .bootstrapper_config(config) - .payable_dao(payable_dao) - .receivable_dao(receivable_dao) - .banned_dao(banned_dao) - .build(); - - subject.scan_for_delinquencies(); - - let new_delinquencies_parameters: MutexGuard> = - new_delinquencies_parameters_arc.lock().unwrap(); - assert_eq!( - DEFAULT_PAYMENT_THRESHOLDS.clone(), - new_delinquencies_parameters[0].1 - ); - let paid_delinquencies_parameters: MutexGuard> = - paid_delinquencies_parameters_arc.lock().unwrap(); - assert_eq!( - DEFAULT_PAYMENT_THRESHOLDS.clone(), - paid_delinquencies_parameters[0] - ); - let ban_parameters = ban_parameters_arc.lock().unwrap(); - assert!(ban_parameters.contains(&newly_banned_1.wallet)); - assert!(ban_parameters.contains(&newly_banned_2.wallet)); - assert_eq!(2, ban_parameters.len()); - let unban_parameters = unban_parameters_arc.lock().unwrap(); - assert!(unban_parameters.contains(&newly_unbanned_1.wallet)); - assert!(unban_parameters.contains(&newly_unbanned_2.wallet)); - assert_eq!(2, unban_parameters.len()); - let tlh = TestLogHandler::new(); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c65743132333464 \\(balance: 1234 MASQ, age: \\d+ sec\\) banned for delinquency"); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c65743233343564 \\(balance: 2345 MASQ, age: \\d+ sec\\) banned for delinquency"); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c6574333435366e \\(balance: 3456 MASQ, age: \\d+ sec\\) is no longer delinquent: unbanned"); - tlh.exists_log_matching("INFO: Accountant: Wallet 0x00000000000000000077616c6c6574343536376e \\(balance: 4567 MASQ, age: \\d+ sec\\) is no longer delinquent: unbanned"); - } - - #[test] - fn scan_for_pending_payable_found_no_pending_payable() { - init_test_logging(); - let return_all_backup_records_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_fingerprints_params(&return_all_backup_records_params_arc) - .return_all_fingerprints_result(vec![]); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) .build(); + subject.report_accounts_payable_sub_opt = Some(report_accounts_payable_sub); + subject.scan_intervals.payable_scan_interval = Duration::from_millis(10); + subject.logger = Logger::new(test_name); + let addr = subject.start(); + addr.try_send(ScanForPayables { + response_skeleton_opt: None, + }) + .unwrap(); - let _ = subject.scan_for_pending_payable(None); + addr.try_send(ScanForPayables { + response_skeleton_opt: None, + }) + .unwrap(); - let return_all_backup_records_params = return_all_backup_records_params_arc.lock().unwrap(); - assert_eq!(*return_all_backup_records_params, vec![()]); - TestLogHandler::new() - .exists_log_containing("DEBUG: Accountant: No pending payable found during last scan"); + System::current().stop(); + system.run(); + let recording = blockchain_bridge_recording.lock().unwrap(); + let messages_received = recording.len(); + assert_eq!(messages_received, 0); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {}: Payable scan was already initiated", + test_name + )); } #[test] @@ -2830,19 +2144,16 @@ mod tests { payable_fingerprint_1.clone(), payable_fingerprint_2.clone(), ]); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("mine"), - ); + let config = bc_from_earning_wallet(make_wallet("mine")); let system = System::new("pending payable scan"); let mut subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(PendingPayableDaoMock::new()) // For Payable Scanner + .pending_payable_dao(pending_payable_dao) // For PendiingPayable Scanner .bootstrapper_config(config) .build(); let blockchain_bridge_addr = blockchain_bridge.start(); - subject - .confirmation_tools - .request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); + subject.request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); let _ = account_addr @@ -2872,9 +2183,7 @@ mod tests { fn report_routing_service_provided_message_is_received() { init_test_logging(); let now = SystemTime::now(); - let mut bootstrapper_config = BootstrapperConfig::default(); - bootstrapper_config.accountant_config_opt = Some(make_accountant_config_null()); - bootstrapper_config.earning_wallet = make_wallet("hi"); + let bootstrapper_config = bc_from_earning_wallet(make_wallet("hi")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() @@ -2883,7 +2192,10 @@ mod tests { let subject = AccountantBuilder::default() .bootstrapper_config(bootstrapper_config) .payable_dao(payable_dao_mock) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) .receivable_dao(receivable_dao_mock) + .receivable_dao(ReceivableDaoMock::new()) .build(); let system = System::new("report_routing_service_message_is_received"); let subject_addr: Addr = subject.start(); @@ -2921,19 +2233,18 @@ mod tests { fn report_routing_service_provided_message_is_received_from_our_consuming_wallet() { init_test_logging(); let consuming_wallet = make_wallet("our consuming wallet"); - let config = bc_from_ac_plus_wallets( - make_populated_accountant_config_with_defaults(), - consuming_wallet.clone(), - make_wallet("our earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("our earning wallet")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .receivable_dao(receivable_dao_mock) .payable_dao(payable_dao_mock) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) + .receivable_dao(receivable_dao_mock) + .receivable_dao(ReceivableDaoMock::new()) .build(); let system = System::new("report_routing_service_message_is_received"); let subject_addr: Addr = subject.start(); @@ -2970,10 +2281,7 @@ mod tests { fn report_routing_service_provided_message_is_received_from_our_earning_wallet() { init_test_logging(); let earning_wallet = make_wallet("our earning wallet"); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - ); + let config = bc_from_earning_wallet(earning_wallet.clone()); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() @@ -2981,7 +2289,10 @@ mod tests { let subject = AccountantBuilder::default() .bootstrapper_config(config) .payable_dao(payable_dao_mock) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) .receivable_dao(receivable_dao_mock) + .receivable_dao(ReceivableDaoMock::new()) .build(); let system = System::new("report_routing_service_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3018,10 +2329,7 @@ mod tests { fn report_exit_service_provided_message_is_received() { init_test_logging(); let now = SystemTime::now(); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("hi"), - ); + let config = bc_from_earning_wallet(make_wallet("hi")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() @@ -3029,8 +2337,11 @@ mod tests { .more_money_receivable_result(Ok(())); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .receivable_dao(receivable_dao_mock) .payable_dao(payable_dao_mock) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) + .receivable_dao(receivable_dao_mock) + .receivable_dao(ReceivableDaoMock::new()) .build(); let system = System::new("report_exit_service_provided_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3068,11 +2379,7 @@ mod tests { fn report_exit_service_provided_message_is_received_from_our_consuming_wallet() { init_test_logging(); let consuming_wallet = make_wallet("my consuming wallet"); - let config = bc_from_ac_plus_wallets( - make_accountant_config_null(), - consuming_wallet.clone(), - make_wallet("my earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("my earning wallet")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() @@ -3080,7 +2387,10 @@ mod tests { let subject = AccountantBuilder::default() .bootstrapper_config(config) .payable_dao(payable_dao_mock) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) .receivable_dao(receivable_dao_mock) + .receivable_dao(ReceivableDaoMock::new()) .build(); let system = System::new("report_exit_service_provided_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3117,16 +2427,18 @@ mod tests { fn report_exit_service_provided_message_is_received_from_our_earning_wallet() { init_test_logging(); let earning_wallet = make_wallet("my earning wallet"); - let config = - bc_from_ac_plus_earning_wallet(make_accountant_config_null(), earning_wallet.clone()); + let config = bc_from_earning_wallet(earning_wallet.clone()); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_dao(payable_dao_mock) - .receivable_dao(receivable_dao_mock) + .payable_dao(payable_dao_mock) // For Accountant + .payable_dao(PayableDaoMock::new()) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner + .receivable_dao(receivable_dao_mock) // For Accountant + .receivable_dao(ReceivableDaoMock::new()) // For Scanner .build(); let system = System::new("report_exit_service_provided_message_is_received"); let subject_addr: Addr = subject.start(); @@ -3162,10 +2474,7 @@ mod tests { #[test] fn report_services_consumed_message_is_received() { init_test_logging(); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("hi"), - ); + let config = make_bc_with_defaults(); let more_money_payable_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new() .more_money_payable_params(more_money_payable_params_arc.clone()) @@ -3175,6 +2484,8 @@ mod tests { let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .payable_dao(payable_dao_mock) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) .build(); subject.message_id_generator = Box::new(MessageIdGeneratorMock::default().id_result(123)); let system = System::new("report_services_consumed_message_is_received"); @@ -3264,6 +2575,8 @@ mod tests { let subject = AccountantBuilder::default() .bootstrapper_config(config) .payable_dao(payable_dao_mock) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) .build(); let system = System::new("test"); let subject_addr: Addr = subject.start(); @@ -3284,11 +2597,7 @@ mod tests { fn routing_service_consumed_is_reported_for_our_consuming_wallet() { init_test_logging(); let consuming_wallet = make_wallet("the consuming wallet"); - let config = bc_from_ac_plus_wallets( - make_populated_accountant_config_with_defaults(), - consuming_wallet.clone(), - make_wallet("the earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("the earning wallet")); let foreign_wallet = make_wallet("exit wallet"); let timestamp = SystemTime::now(); let report_message = ReportServicesConsumedMessage { @@ -3331,10 +2640,7 @@ mod tests { let earning_wallet = make_wallet("routing_service_consumed_is_reported_for_our_earning_wallet"); let foreign_wallet = make_wallet("exit wallet"); - let config = bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - earning_wallet.clone(), - ); + let config = bc_from_earning_wallet(earning_wallet.clone()); let timestamp = SystemTime::now(); let report_message = ReportServicesConsumedMessage { timestamp, @@ -3375,11 +2681,7 @@ mod tests { init_test_logging(); let consuming_wallet = make_wallet("exit_service_consumed_is_reported_for_our_consuming_wallet"); - let config = bc_from_ac_plus_wallets( - make_accountant_config_null(), - consuming_wallet.clone(), - make_wallet("own earning wallet"), - ); + let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("own earning wallet")); let report_message = ReportServicesConsumedMessage { timestamp: SystemTime::now(), exit: ExitServiceConsumed { @@ -3409,8 +2711,7 @@ mod tests { fn exit_service_consumed_is_reported_for_our_earning_wallet() { init_test_logging(); let earning_wallet = make_wallet("own earning wallet"); - let config = - bc_from_ac_plus_earning_wallet(make_accountant_config_null(), earning_wallet.clone()); + let config = bc_from_earning_wallet(earning_wallet.clone()); let report_message = ReportServicesConsumedMessage { timestamp: SystemTime::now(), exit: ExitServiceConsumed { @@ -3451,6 +2752,7 @@ mod tests { )); let subject = AccountantBuilder::default() .receivable_dao(receivable_dao) + .receivable_dao(ReceivableDaoMock::new()) .build(); let _ = subject.record_service_provided(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); @@ -3464,6 +2766,7 @@ mod tests { .more_money_receivable_result(Err(ReceivableDaoError::SignConversion(1234))); let subject = AccountantBuilder::default() .receivable_dao(receivable_dao) + .receivable_dao(ReceivableDaoMock::new()) .build(); subject.record_service_provided(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); @@ -3483,6 +2786,8 @@ mod tests { .more_money_payable_result(Err(PayableDaoError::SignConversion(1234))); let subject = AccountantBuilder::default() .payable_dao(payable_dao) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) .build(); let service_rate = i64::MAX as u64; @@ -3491,7 +2796,7 @@ mod tests { TestLogHandler::new().exists_log_containing(&format!( "ERROR: Accountant: Overflow error recording consumed services from {}: total charge {}, service rate {}, byte rate 1, payload size 2. Skipping", wallet, - i64::MAX as u64 +1*2, + i64::MAX as u64 + 1 * 2, i64::MAX as u64 )); } @@ -3509,41 +2814,20 @@ mod tests { )); let subject = AccountantBuilder::default() .payable_dao(payable_dao) + .payable_dao(PayableDaoMock::new()) + .payable_dao(PayableDaoMock::new()) .build(); let _ = subject.record_service_consumed(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); } - #[test] - #[should_panic( - expected = "Was unable to create a mark in payables for a new pending payable '0x000000000000000000000000000000000000000000000000000000000000007b' due to 'SignConversion(9999999999999)'" - )] - fn handle_sent_payable_fails_to_make_a_mark_in_payables_and_so_panics() { - let payable = Payable::new( - make_wallet("blah"), - 6789, - H256::from_uint(&U256::from(123)), - SystemTime::now(), - ); - let payable_dao = PayableDaoMock::new() - .mark_pending_payable_rowid_result(Err(PayableDaoError::SignConversion(9999999999999))); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprint_rowid_result(Some(7879)); - let subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - - let _ = subject.mark_pending_payable(vec![payable]); - } - #[test] #[should_panic( expected = "Database unmaintainable; payable fingerprint deletion for transaction 0x000000000000000000000000000000000000000000000000000000000000007b \ has stayed undone due to RecordDeletion(\"we slept over, sorry\")" )] - fn handle_sent_payable_dealing_with_failed_payment_fails_to_delete_the_existing_pending_payable_fingerprint_and_panics( - ) { + fn accountant_panics_in_case_it_receives_an_error_from_scanner_while_handling_sent_payable_msg() + { let rowid = 4; let hash = H256::from_uint(&U256::from(123)); let sent_payable = SentPayable { @@ -3559,402 +2843,38 @@ mod tests { .delete_fingerprint_result(Err(PendingPayableDaoError::RecordDeletion( "we slept over, sorry".to_string(), ))); + let system = System::new("test"); let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - - let _ = subject.handle_sent_payable(sent_payable); - } - - #[test] - fn handle_sent_payable_receives_two_payments_one_incorrect_and_one_correct() { - //the two failures differ in the logged messages - init_test_logging(); - let fingerprint_rowid_params_arc = Arc::new(Mutex::new(vec![])); - let now_system = SystemTime::now(); - let payable_1 = Err(BlockchainError::InvalidResponse); - let payable_2_rowid = 126; - let payable_hash_2 = H256::from_uint(&U256::from(166)); - let payable_2 = Payable::new(make_wallet("booga"), 6789, payable_hash_2, now_system); - let payable_3 = Err(BlockchainError::TransactionFailed { - msg: "closing hours, sorry".to_string(), - hash_opt: None, - }); - let sent_payable = SentPayable { - timestamp: SystemTime::now(), - payable: vec![payable_1, Ok(payable_2.clone()), payable_3], - response_skeleton_opt: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprint_rowid_params(&fingerprint_rowid_params_arc) - .fingerprint_rowid_result(Some(payable_2_rowid)); - let subject = AccountantBuilder::default() - .payable_dao(PayableDaoMock::new().mark_pending_payable_rowid_result(Ok(()))) - .pending_payable_dao(pending_payable_dao) + .pending_payable_dao(PendingPayableDaoMock::new()) // For Accountant + .pending_payable_dao(pending_payable_dao) // For Payable Scanner + .pending_payable_dao(PendingPayableDaoMock::new()) // For PendingPayable Scanner .build(); + let addr = subject.start(); - subject.handle_sent_payable(sent_payable); + let _ = addr.try_send(sent_payable); - let fingerprint_rowid_params = fingerprint_rowid_params_arc.lock().unwrap(); - assert_eq!(*fingerprint_rowid_params, vec![payable_hash_2]); //we know the other two errors are associated with an initiated transaction having a backup - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("WARN: Accountant: Outbound transaction failure due to 'InvalidResponse'. Please check your blockchain service URL configuration."); - log_handler.exists_log_containing("DEBUG: Accountant: Payable '0x00000000000000000000000000000000000000000000000000000000000000a6' has been marked as pending in the payable table"); - log_handler.exists_log_containing("WARN: Accountant: Encountered transaction error at this end: 'TransactionFailed { msg: \"closing hours, sorry\", hash_opt: None }'"); - log_handler.exists_log_containing("DEBUG: Accountant: Forgetting a transaction attempt that even did not reach the signing stage"); + System::current().stop(); + assert_eq!(system.run(), 0); } #[test] #[should_panic( - expected = "Payable fingerprint for 0x0000000000000000000000000000000000000000000000000000000000000315 doesn't exist but should by now; system unreliable" + expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" )] - fn handle_sent_payable_receives_proper_payment_but_fingerprint_not_found_so_it_panics() { - init_test_logging(); - let now_system = SystemTime::now(); - let payment_hash = H256::from_uint(&U256::from(789)); - let payment = Payable::new(make_wallet("booga"), 6789, payment_hash, now_system); - let pending_payable_dao = PendingPayableDaoMock::default().fingerprint_rowid_result(None); - let subject = AccountantBuilder::default() - .payable_dao(PayableDaoMock::new().mark_pending_payable_rowid_result(Ok(()))) - .pending_payable_dao(pending_payable_dao) + fn accountant_can_be_crashed_properly_but_not_improperly() { + let mut config = make_bc_with_defaults(); + config.crash_point = CrashPoint::Message; + let accountant = AccountantBuilder::default() + .bootstrapper_config(config) .build(); - let _ = subject.mark_pending_payable(vec![payment]); + prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); } #[test] - fn handle_confirm_transaction_works() { + fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { init_test_logging(); - let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_pending_payable_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transaction_confirmed_params(&transaction_confirmed_params_arc) - .transaction_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprint_params(&delete_pending_payable_fingerprint_params_arc) - .delete_fingerprint_result(Ok(())); - let mut subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let tx_hash = H256::from("sometransactionhash".keccak256()); - let amount = 4567; - let timestamp_from_time_of_payment = from_time_t(200_000_000); - let rowid = 2; - let pending_payable_fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: timestamp_from_time_of_payment, - hash: tx_hash, - attempt_opt: Some(1), - amount, - process_error: None, - }; - - let _ = subject.handle_confirm_pending_transaction(ConfirmPendingTransaction { - pending_payable_fingerprint: pending_payable_fingerprint.clone(), - }); - - let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!( - *transaction_confirmed_params, - vec![pending_payable_fingerprint] - ); - let delete_pending_payable_fingerprint_params = - delete_pending_payable_fingerprint_params_arc - .lock() - .unwrap(); - assert_eq!(*delete_pending_payable_fingerprint_params, vec![rowid]); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("DEBUG: Accountant: Confirmation of transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19; record for payable was modified"); - log_handler.exists_log_containing("INFO: Accountant: Transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 has gone through the whole confirmation process succeeding"); - } - - #[test] - #[should_panic( - expected = "Was unable to uncheck pending payable '0x0000000000000000000000000000000000000000000000000000000000000315' after confirmation due to 'RusqliteError(\"record change not successful\")" - )] - fn handle_confirm_pending_transaction_panics_on_unchecking_payable_table() { - init_test_logging(); - let hash = H256::from_uint(&U256::from(789)); - let rowid = 3; - let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Err( - PayableDaoError::RusqliteError("record change not successful".to_string()), - )); - let mut subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .build(); - let mut payment = make_pending_payable_fingerprint(); - payment.rowid_opt = Some(rowid); - payment.hash = hash; - let msg = ConfirmPendingTransaction { - pending_payable_fingerprint: payment.clone(), - }; - - let _ = subject.handle_confirm_pending_transaction(msg); - } - - #[test] - #[should_panic( - expected = "Was unable to delete payable fingerprint '0x0000000000000000000000000000000000000000000000000000000000000315' after successful transaction due to 'RecordDeletion(\"the database is fooling around with us\")'" - )] - fn handle_confirm_pending_transaction_panics_on_deleting_pending_payable_fingerprint() { - init_test_logging(); - let hash = H256::from_uint(&U256::from(789)); - let rowid = 3; - let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprint_result(Err( - PendingPayableDaoError::RecordDeletion( - "the database is fooling around with us".to_string(), - ), - )); - let mut subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - pending_payable_fingerprint.rowid_opt = Some(rowid); - pending_payable_fingerprint.hash = hash; - let msg = ConfirmPendingTransaction { - pending_payable_fingerprint: pending_payable_fingerprint.clone(), - }; - - let _ = subject.handle_confirm_pending_transaction(msg); - } - - #[test] - fn handle_cancel_pending_transaction_works() { - init_test_logging(); - let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failure_params(&mark_failure_params_arc) - .mark_failure_result(Ok(())); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - let tx_hash = H256::from("sometransactionhash".keccak256()); - let rowid = 2; - let transaction_id = PendingPayableId { - hash: tx_hash, - rowid, - }; - - let _ = subject.handle_cancel_pending_transaction(CancelFailedPendingTransaction { - id: transaction_id, - }); - - let mark_failure_params = mark_failure_params_arc.lock().unwrap(); - assert_eq!(*mark_failure_params, vec![rowid]); - TestLogHandler::new().exists_log_containing( - "WARN: Accountant: Broken transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 left with an error mark; you should take over \ - the care of this transaction to make sure your debts will be paid because there is no automated process that can fix this without you", - ); - } - - #[test] - #[should_panic( - expected = "Unsuccessful attempt for transaction 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\")" - )] - fn handle_cancel_pending_transaction_panics_on_its_inability_to_mark_failure() { - let payable_dao = PayableDaoMock::default().transaction_canceled_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().mark_failure_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), - )); - let subject = AccountantBuilder::default() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let rowid = 2; - let hash = H256::from("sometransactionhash".keccak256()); - - let _ = subject.handle_cancel_pending_transaction(CancelFailedPendingTransaction { - id: PendingPayableId { hash, rowid }, - }); - } - - #[test] - #[should_panic( - expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" - )] - fn accountant_can_be_crashed_properly_but_not_improperly() { - let mut config = BootstrapperConfig::default(); - config.crash_point = CrashPoint::Message; - config.accountant_config_opt = Some(make_accountant_config_null()); - let accountant = AccountantBuilder::default() - .bootstrapper_config(config) - .build(); - - prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); - } - - #[test] - fn investigate_debt_extremes_picks_the_most_relevant_records() { - let now = to_time_t(SystemTime::now()); - let same_amount_significance = 2_000_000; - let same_age_significance = from_time_t(now - 30000); - let payables = &[ - PayableAccount { - wallet: make_wallet("wallet0"), - balance: same_amount_significance, - last_paid_timestamp: from_time_t(now - 5000), - pending_payable_opt: None, - }, - //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked - PayableAccount { - wallet: make_wallet("wallet1"), - balance: same_amount_significance, - last_paid_timestamp: from_time_t(now - 10000), - pending_payable_opt: None, - }, - //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen - PayableAccount { - wallet: make_wallet("wallet3"), - balance: 100, - last_paid_timestamp: same_age_significance, - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("wallet2"), - balance: 330, - last_paid_timestamp: same_age_significance, - pending_payable_opt: None, - }, - ]; - - let result = Accountant::investigate_debt_extremes(payables); - - assert_eq!(result,"Payable scan found 4 debts; the biggest is 2000000 owed for 10000sec, the oldest is 330 owed for 30000sec") - } - - #[test] - fn payables_debug_summary_prints_pretty_summary() { - let now = to_time_t(SystemTime::now()); - let payment_thresholds = PaymentThresholds { - threshold_interval_sec: 2_592_000, - debt_threshold_gwei: 1_000_000_000, - payment_grace_period_sec: 86_400, - maturity_threshold_sec: 86_400, - permanent_debt_allowed_gwei: 10_000_000, - unban_below_gwei: 10_000_000, - }; - let qualified_payables = &[ - PayableAccount { - wallet: make_wallet("wallet0"), - balance: payment_thresholds.permanent_debt_allowed_gwei + 1000, - last_paid_timestamp: from_time_t( - now - payment_thresholds.threshold_interval_sec - 1234, - ), - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("wallet1"), - balance: payment_thresholds.permanent_debt_allowed_gwei + 1, - last_paid_timestamp: from_time_t( - now - payment_thresholds.threshold_interval_sec - 1, - ), - pending_payable_opt: None, - }, - ]; - let mut config = BootstrapperConfig::default(); - config.accountant_config_opt = Some(make_populated_accountant_config_with_defaults()); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(config) - .build(); - subject.config.payment_thresholds = payment_thresholds; - - let result = subject.payables_debug_summary(qualified_payables); - - assert_eq!(result, - "Paying qualified debts:\n\ - 10001000 owed for 2593234sec exceeds threshold: 9512428; creditor: 0x0000000000000000000000000077616c6c657430\n\ - 10000001 owed for 2592001sec exceeds threshold: 9999604; creditor: 0x0000000000000000000000000077616c6c657431" - ) - } - - #[test] - fn threshold_calculation_depends_on_user_defined_payment_thresholds() { - let safe_age_params_arc = Arc::new(Mutex::new(vec![])); - let safe_balance_params_arc = Arc::new(Mutex::new(vec![])); - let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); - let balance = 5555; - let how_far_in_past = Duration::from_secs(1111 + 1); - let last_paid_timestamp = SystemTime::now().sub(how_far_in_past); - let payable_account = PayableAccount { - wallet: make_wallet("hi"), - balance, - last_paid_timestamp, - pending_payable_opt: None, - }; - let custom_payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1111, - payment_grace_period_sec: 2222, - permanent_debt_allowed_gwei: 3333, - debt_threshold_gwei: 4444, - threshold_interval_sec: 5555, - unban_below_gwei: 3333, - }; - let mut bootstrapper_config = BootstrapperConfig::default(); - bootstrapper_config.accountant_config_opt = Some(AccountantConfig { - scan_intervals: Default::default(), - payment_thresholds: custom_payment_thresholds, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - }); - let payable_thresholds_tools = PayableThresholdToolsMock::default() - .is_innocent_age_params(&safe_age_params_arc) - .is_innocent_age_result( - how_far_in_past.as_secs() - <= custom_payment_thresholds.maturity_threshold_sec as u64, - ) - .is_innocent_balance_params(&safe_balance_params_arc) - .is_innocent_balance_result( - balance <= custom_payment_thresholds.permanent_debt_allowed_gwei, - ) - .calculate_payout_threshold_params(&calculate_payable_threshold_params_arc) - .calculate_payout_threshold_result(4567.0); //made up value - let mut subject = AccountantBuilder::default() - .bootstrapper_config(bootstrapper_config) - .build(); - subject.payable_threshold_tools = Box::new(payable_thresholds_tools); - - let result = subject.payable_exceeded_threshold(&payable_account); - - assert_eq!(result, Some(4567)); - let mut safe_age_params = safe_age_params_arc.lock().unwrap(); - let safe_age_single_params = safe_age_params.remove(0); - assert_eq!(*safe_age_params, vec![]); - let (time_elapsed, curve_derived_time) = safe_age_single_params; - assert!( - (how_far_in_past.as_secs() - 3) < time_elapsed - && time_elapsed < (how_far_in_past.as_secs() + 3) - ); - assert_eq!( - curve_derived_time, - custom_payment_thresholds.maturity_threshold_sec as u64 - ); - let safe_balance_params = safe_balance_params_arc.lock().unwrap(); - assert_eq!( - *safe_balance_params, - vec![( - payable_account.balance, - custom_payment_thresholds.permanent_debt_allowed_gwei - )] - ); - let mut calculate_payable_curves_params = - calculate_payable_threshold_params_arc.lock().unwrap(); - let calculate_payable_curves_single_params = calculate_payable_curves_params.remove(0); - assert_eq!(*calculate_payable_curves_params, vec![]); - let (payment_thresholds, time_elapsed) = calculate_payable_curves_single_params; - assert!( - (how_far_in_past.as_secs() - 3) < time_elapsed - && time_elapsed < (how_far_in_past.as_secs() + 3) - ); - assert_eq!(payment_thresholds, custom_payment_thresholds) - } - - #[test] - fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { - init_test_logging(); - let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); let get_transaction_receipt_params_arc = Arc::new(Mutex::new(vec![])); let return_all_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); @@ -3966,12 +2886,6 @@ mod tests { let notify_later_scan_for_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_scan_for_pending_payable_arc_cloned = notify_later_scan_for_pending_payable_params_arc.clone(); //because it moves into a closure - let notify_cancel_failed_transaction_params_arc = Arc::new(Mutex::new(vec![])); - let notify_cancel_failed_transaction_params_arc_cloned = - notify_cancel_failed_transaction_params_arc.clone(); //because it moves into a closure - let notify_confirm_transaction_params_arc = Arc::new(Mutex::new(vec![])); - let notify_confirm_transaction_params_arc_cloned = - notify_confirm_transaction_params_arc.clone(); //because it moves into a closure let pending_tx_hash_1 = H256::from_uint(&U256::from(123)); let pending_tx_hash_2 = H256::from_uint(&U256::from(567)); let rowid_for_account_1 = 3; @@ -4037,29 +2951,23 @@ mod tests { pending_payable_opt: None, }; let pending_payable_scan_interval = 200; //should be slightly less than 1/5 of the time until shutting the system - let payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![account_1, account_2]) + let payable_dao_for_accountant = PayableDaoMock::new(); + let payable_dao_for_payable_scanner = PayableDaoMock::new() .mark_pending_payable_rowid_params(&mark_pending_payable_params_arc) .mark_pending_payable_rowid_result(Ok(())) .mark_pending_payable_rowid_result(Ok(())) + .non_pending_payables_params(&non_pending_payables_params_arc) + .non_pending_payables_result(vec![account_1, account_2]); + let payable_dao_for_pending_payable_scanner = PayableDaoMock::new() .transaction_confirmed_params(&transaction_confirmed_params_arc) .transaction_confirmed_result(Ok(())); - let bootstrapper_config = bc_from_ac_plus_earning_wallet( - AccountantConfig { - scan_intervals: ScanIntervals { - payable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan - receivable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan - pending_payable_scan_interval: Duration::from_millis( - pending_payable_scan_interval, - ), - }, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - }, - make_wallet("some_wallet_address"), - ); + + let mut bootstrapper_config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + bootstrapper_config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan + receivable_scan_interval: Duration::from_secs(1_000_000), //we don't care about this scan + pending_payable_scan_interval: Duration::from_millis(pending_payable_scan_interval), + }); let fingerprint_1_first_round = PendingPayableFingerprint { rowid_opt: Some(rowid_for_account_1), timestamp: this_payable_timestamp_1, @@ -4096,7 +3004,14 @@ mod tests { attempt_opt: Some(4), ..fingerprint_2_first_round.clone() }; - let mut pending_payable_dao = PendingPayableDaoMock::default() + let pending_payable_dao_for_accountant = PendingPayableDaoMock::default(); + let pending_payable_dao_for_payable_scanner = PendingPayableDaoMock::default() + .fingerprint_rowid_result(Some(rowid_for_account_1)) + .fingerprint_rowid_result(Some(rowid_for_account_2)) + .insert_fingerprint_params(&insert_fingerprint_params_arc) + .insert_fingerprint_result(Ok(())) + .insert_fingerprint_result(Ok(())); + let mut pending_payable_dao_for_pending_payable_scanner = PendingPayableDaoMock::new() .return_all_fingerprints_params(&return_all_fingerprints_params_arc) .return_all_fingerprints_result(vec![]) .return_all_fingerprints_result(vec![ @@ -4112,11 +3027,6 @@ mod tests { fingerprint_2_third_round, ]) .return_all_fingerprints_result(vec![fingerprint_2_fourth_round.clone()]) - .insert_fingerprint_params(&insert_fingerprint_params_arc) - .insert_fingerprint_result(Ok(())) - .insert_fingerprint_result(Ok(())) - .fingerprint_rowid_result(Some(rowid_for_account_1)) - .fingerprint_rowid_result(Some(rowid_for_account_2)) .update_fingerprint_params(&update_fingerprint_params_arc) .update_fingerprint_results(Ok(())) .update_fingerprint_results(Ok(())) @@ -4130,31 +3040,25 @@ mod tests { .delete_fingerprint_params(&delete_record_params_arc) //this is used during confirmation of the successful one .delete_fingerprint_result(Ok(())); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; + pending_payable_dao_for_pending_payable_scanner + .have_return_all_fingerprints_shut_down_the_system = true; let accountant_addr = Arbiter::builder() .stop_system_on_panic(true) .start(move |_| { let mut subject = AccountantBuilder::default() .bootstrapper_config(bootstrapper_config) - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .payable_dao(payable_dao_for_accountant) + .payable_dao(payable_dao_for_payable_scanner) + .payable_dao(payable_dao_for_pending_payable_scanner) + .pending_payable_dao(pending_payable_dao_for_accountant) + .pending_payable_dao(pending_payable_dao_for_payable_scanner) + .pending_payable_dao(pending_payable_dao_for_pending_payable_scanner) .build(); - subject.scanners.receivables = Box::new(NullScanner); + subject.scanners.receivable = Box::new(NullScanner::new()); let notify_later_half_mock = NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_scan_for_pending_payable_arc_cloned) .permit_to_send_out(); - subject - .confirmation_tools - .notify_later_scan_for_pending_payable = Box::new(notify_later_half_mock); - let notify_half_mock = NotifyHandleMock::default() - .notify_params(¬ify_cancel_failed_transaction_params_arc_cloned) - .permit_to_send_out(); - subject.confirmation_tools.notify_cancel_failed_transaction = - Box::new(notify_half_mock); - let notify_half_mock = NotifyHandleMock::default() - .notify_params(¬ify_confirm_transaction_params_arc_cloned) - .permit_to_send_out(); - subject.confirmation_tools.notify_confirm_transaction = Box::new(notify_half_mock); + subject.notify_later.scan_for_pending_payable = Box::new(notify_later_half_mock); subject }); let mut peer_actors = peer_actors_builder().build(); @@ -4196,7 +3100,7 @@ mod tests { pending_tx_hash_2, pending_tx_hash_1, pending_tx_hash_2, - pending_tx_hash_2 + pending_tx_hash_2, ] ); let update_backup_after_cycle_params = update_fingerprint_params_arc.lock().unwrap(); @@ -4207,7 +3111,7 @@ mod tests { rowid_for_account_2, rowid_for_account_1, rowid_for_account_2, - rowid_for_account_2 + rowid_for_account_2, ] ); let mark_failure_params = mark_failure_params_arc.lock().unwrap(); @@ -4243,233 +3147,14 @@ mod tests { expected_scan_pending_payable_msg_and_interval, ] ); - let mut notify_confirm_transaction_params = - notify_confirm_transaction_params_arc.lock().unwrap(); - let actual_confirmed_payable: ConfirmPendingTransaction = - notify_confirm_transaction_params.remove(0); - assert!(notify_confirm_transaction_params.is_empty()); - let expected_confirmed_payable = ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint_2_fourth_round, - }; - assert_eq!(actual_confirmed_payable, expected_confirmed_payable); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing( - "WARN: Accountant: Broken transaction 0x000000000000000000000000000000000000000000000000000000000000007b left with an error mark; you should take over the care of this transaction to make sure your debts will be paid because there \ + "WARN: Accountant: Broken transaction 0x0000…007b left with an error mark; you should take over the care of this transaction to make sure your debts will be paid because there \ is no automated process that can fix this without you"); - log_handler.exists_log_matching("INFO: Accountant: Transaction '0x0000000000000000000000000000000000000000000000000000000000000237' has been added to the blockchain; detected locally at attempt 4 at \\d{2,}ms after its sending"); + log_handler.exists_log_matching("INFO: Accountant: Transaction '0x0000…0237' has been added to the blockchain; detected locally at attempt 4 at \\d{2,}ms after its sending"); log_handler.exists_log_containing("INFO: Accountant: Transaction 0x0000000000000000000000000000000000000000000000000000000000000237 has gone through the whole confirmation process succeeding"); } - #[test] - fn handle_pending_tx_handles_none_returned_for_transaction_receipt() { - init_test_logging(); - let subject = AccountantBuilder::default().build(); - let tx_receipt_opt = None; - let rowid = 455; - let hash = H256::from_uint(&U256::from(2323)); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt_opt: Some(3), - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![(tx_receipt_opt, fingerprint.clone())], - response_skeleton_opt: None, - }; - - let result = subject.handle_pending_transaction_with_its_receipt(&msg); - - assert_eq!( - result, - vec![PendingTransactionStatus::StillPending(PendingPayableId { - hash, - rowid - })] - ); - TestLogHandler::new().exists_log_matching("DEBUG: Accountant: Interpreting a receipt for transaction '0x0000000000000000000000000000000000000000000000000000000000000913' but none was given; attempt 3, 100\\d\\dms since sending"); - } - - #[test] - fn accountant_receives_reported_transaction_receipts_and_processes_them_all() { - let notify_handle_params_arc = Arc::new(Mutex::new(vec![])); - let mut subject = AccountantBuilder::default().build(); - subject.confirmation_tools.notify_confirm_transaction = - Box::new(NotifyHandleMock::default().notify_params(¬ify_handle_params_arc)); - let subject_addr = subject.start(); - let transaction_hash_1 = H256::from_uint(&U256::from(4545)); - let mut transaction_receipt_1 = TransactionReceipt::default(); - transaction_receipt_1.transaction_hash = transaction_hash_1; - transaction_receipt_1.status = Some(U64::from(1)); //success - let fingerprint_1 = PendingPayableFingerprint { - rowid_opt: Some(5), - timestamp: from_time_t(200_000_000), - hash: transaction_hash_1, - attempt_opt: Some(2), - amount: 444, - process_error: None, - }; - let transaction_hash_2 = H256::from_uint(&U256::from(3333333)); - let mut transaction_receipt_2 = TransactionReceipt::default(); - transaction_receipt_2.transaction_hash = transaction_hash_2; - transaction_receipt_2.status = Some(U64::from(1)); //success - let fingerprint_2 = PendingPayableFingerprint { - rowid_opt: Some(10), - timestamp: from_time_t(199_780_000), - hash: Default::default(), - attempt_opt: Some(15), - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - (Some(transaction_receipt_1), fingerprint_1.clone()), - (Some(transaction_receipt_2), fingerprint_2.clone()), - ], - response_skeleton_opt: None, - }; - - let _ = subject_addr.try_send(msg).unwrap(); - - let system = System::new("processing reported receipts"); - System::current().stop(); - system.run(); - let notify_handle_params = notify_handle_params_arc.lock().unwrap(); - assert_eq!( - *notify_handle_params, - vec![ - ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint_1 - }, - ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint_2 - } - ] - ); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { - init_test_logging(); - let subject = AccountantBuilder::default().build(); - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = H256::from_uint(&U256::from(4567)); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(777777), - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt_opt: Some(5), - amount: 2222, - process_error: None, - }; - - let result = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("receipt_check_logger"), - ); - - assert_eq!( - result, - PendingTransactionStatus::Failure(PendingPayableId { - hash, - rowid: 777777 - }) - ); - TestLogHandler::new().exists_log_matching("ERROR: receipt_check_logger: Pending \ - transaction '0x00000000000000000000000000000000000000000000000000000000000011d7' announced as a failure, interpreting attempt 5 after 1500\\d\\dms from the sending"); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - init_test_logging(); - let hash = H256::from_uint(&U256::from(567)); - let rowid = 466; - let tx_receipt = TransactionReceipt::default(); //status defaulted to None - let when_sent = SystemTime::now().sub(Duration::from_millis(100)); - let subject = AccountantBuilder::default().build(); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: when_sent, - hash, - attempt_opt: Some(1), - amount: 123, - process_error: None, - }; - - let result = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("none_within_waiting"), - ); - - assert_eq!( - result, - PendingTransactionStatus::StillPending(PendingPayableId { hash, rowid }) - ); - TestLogHandler::new().exists_log_containing( - "INFO: none_within_waiting: Pending \ - transaction '0x0000000000000000000000000000000000000000000000000000000000000237' couldn't be confirmed at attempt 1 at ", - ); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() - { - init_test_logging(); - let hash = H256::from_uint(&U256::from(567)); - let rowid = 466; - let tx_receipt = TransactionReceipt::default(); //status defaulted to None - let when_sent = - SystemTime::now().sub(Duration::from_secs(DEFAULT_PENDING_TOO_LONG_SEC + 5)); //old transaction - let subject = AccountantBuilder::default().build(); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(rowid), - timestamp: when_sent, - hash, - attempt_opt: Some(10), - amount: 123, - process_error: None, - }; - - let result = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("receipt_check_logger"), - ); - - assert_eq!( - result, - PendingTransactionStatus::Failure(PendingPayableId { hash, rowid }) - ); - TestLogHandler::new().exists_log_containing( - "ERROR: receipt_check_logger: Pending transaction '0x0000000000000000000000000000000000000000000000000000000000000237' has exceeded the maximum \ - pending time (21600sec) and the confirmation process is going to be aborted now at the final attempt 10; manual resolution is required from the user to \ - complete the transaction", - ); - } - - #[test] - #[should_panic( - expected = "tx receipt for pending '0x000000000000000000000000000000000000000000000000000000000000007b' - tx status: code other than 0 or 1 shouldn't be possible, but was 456" - )] - fn interpret_transaction_receipt_panics_at_undefined_status_code() { - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(456)); - let mut fingerprint = make_pending_payable_fingerprint(); - fingerprint.hash = H256::from_uint(&U256::from(123)); - let subject = AccountantBuilder::default().build(); - - let _ = subject.interpret_transaction_receipt( - &tx_receipt, - &fingerprint, - &Logger::new("receipt_check_logger"), - ); - } - #[test] fn accountant_handles_pending_payable_fingerprint() { init_test_logging(); @@ -4478,7 +3163,9 @@ mod tests { .insert_fingerprint_params(&insert_fingerprint_params_arc) .insert_fingerprint_result(Ok(())); let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payment_dao) + .pending_payable_dao(pending_payment_dao) // For Accountant + .pending_payable_dao(PendingPayableDaoMock::new()) // For Payable Scanner + .pending_payable_dao(PendingPayableDaoMock::new()) // For PendingPayable Scanner .build(); let accountant_addr = subject.start(); let tx_hash = H256::from_uint(&U256::from(55)); @@ -4526,6 +3213,8 @@ mod tests { let transaction_hash = H256::from_uint(&U256::from(456)); let subject = AccountantBuilder::default() .pending_payable_dao(pending_payable_dao) + .pending_payable_dao(PendingPayableDaoMock::new()) + .pending_payable_dao(PendingPayableDaoMock::new()) .build(); let timestamp_secs = 150_000_000; let fingerprint = PendingPayableFingerprint { @@ -4547,65 +3236,6 @@ mod tests { TestLogHandler::new().exists_log_containing("ERROR: Accountant: Failed to make a fingerprint for pending payable '0x00000000000000000000000000000000000000000000000000000000000001c8' due to 'InsertionFailed(\"Crashed\")'"); } - #[test] - fn separate_early_errors_works() { - let payable_ok = Payable { - to: make_wallet("blah"), - amount: 5555, - timestamp: SystemTime::now(), - tx_hash: Default::default(), - }; - let error = BlockchainError::SignedValueConversion(666); - let sent_payable = SentPayable { - timestamp: SystemTime::now(), - payable: vec![Ok(payable_ok.clone()), Err(error.clone())], - response_skeleton_opt: None, - }; - - let (ok, err) = Accountant::separate_early_errors(&sent_payable, &Logger::new("test")); - - assert_eq!(ok, vec![payable_ok]); - assert_eq!(err, vec![error]) - } - - #[test] - fn update_payable_fingerprint_happy_path() { - let update_after_cycle_params_arc = Arc::new(Mutex::new(vec![])); - let hash = H256::from_uint(&U256::from(444888)); - let rowid = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .update_fingerprint_params(&update_after_cycle_params_arc) - .update_fingerprint_results(Ok(())); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id = PendingPayableId { hash, rowid }; - - let _ = subject.update_payable_fingerprint(transaction_id); - - let update_after_cycle_params = update_after_cycle_params_arc.lock().unwrap(); - assert_eq!(*update_after_cycle_params, vec![rowid]) - } - - #[test] - #[should_panic( - expected = "Failure on updating payable fingerprint '0x000000000000000000000000000000000000000000000000000000000006c9d8' \ - due to UpdateFailed(\"yeah, bad\")" - )] - fn update_payable_fingerprint_sad_path() { - let hash = H256::from_uint(&U256::from(444888)); - let rowid = 3456; - let pending_payable_dao = PendingPayableDaoMock::default().update_fingerprint_results(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); - let subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id = PendingPayableId { hash, rowid }; - - let _ = subject.update_payable_fingerprint(transaction_id); - } - #[test] fn handles_scan_error() { let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); @@ -4639,8 +3269,8 @@ mod tests { payload: Err(( SCAN_ERROR, "Payables scan failed: 'My tummy hurts'".to_string() - )) - } + )), + }, } ); } @@ -4650,16 +3280,18 @@ mod tests { let payable_dao = PayableDaoMock::new().total_result(23456789); let receivable_dao = ReceivableDaoMock::new().total_result(98765432); let system = System::new("test"); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(bc_from_ac_plus_earning_wallet( - make_populated_accountant_config_with_defaults(), - make_wallet("some_wallet_address"), - )) - .receivable_dao(receivable_dao) - .payable_dao(payable_dao) + let subject = AccountantBuilder::default() + .bootstrapper_config(make_bc_with_defaults()) + .payable_dao(payable_dao) // For Accountant + .payable_dao(PayableDaoMock::new()) // For Payable Scanner + .payable_dao(PayableDaoMock::new()) // For PendingPayable Scanner + .receivable_dao(receivable_dao) // For Accountant + .receivable_dao(ReceivableDaoMock::new()) // For Scanner .build(); - subject.financial_statistics.total_paid_payable = 123456; - subject.financial_statistics.total_paid_receivable = 334455; + let mut financial_statistics = subject.financial_statistics(); + financial_statistics.total_paid_payable = 123456; + financial_statistics.total_paid_receivable = 334455; + subject.financial_statistics.replace(financial_statistics); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -4684,83 +3316,11 @@ mod tests { total_unpaid_and_pending_payable: 23456789, total_paid_payable: 123456, total_unpaid_receivable: 98765432, - total_paid_receivable: 334455 + total_paid_receivable: 334455, } ); } - #[test] - fn total_paid_payable_rises_with_each_bill_paid() { - let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let fingerprint = PendingPayableFingerprint { - rowid_opt: Some(5), - timestamp: from_time_t(189_999_888), - hash: H256::from_uint(&U256::from(56789)), - attempt_opt: Some(1), - amount: 5478, - process_error: None, - }; - let mut pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprint_result(Ok(())); - let payable_dao = PayableDaoMock::default() - .transaction_confirmed_params(&transaction_confirmed_params_arc) - .transaction_confirmed_result(Ok(())) - .transaction_confirmed_result(Ok(())); - pending_payable_dao.have_return_all_fingerprints_shut_down_the_system = true; - let mut subject = AccountantBuilder::default() - .pending_payable_dao(pending_payable_dao) - .payable_dao(payable_dao) - .build(); - subject.financial_statistics.total_paid_payable += 1111; - let msg = ConfirmPendingTransaction { - pending_payable_fingerprint: fingerprint.clone(), - }; - - subject.handle_confirm_pending_transaction(msg); - - assert_eq!(subject.financial_statistics.total_paid_payable, 1111 + 5478); - let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transaction_confirmed_params, vec![fingerprint]) - } - - #[test] - fn total_paid_receivable_rises_with_each_bill_paid() { - let more_money_received_params_arc = Arc::new(Mutex::new(vec![])); - let receivable_dao = ReceivableDaoMock::new() - .more_money_received_parameters(&more_money_received_params_arc) - .more_money_receivable_result(Ok(())); - let mut subject = AccountantBuilder::default() - .receivable_dao(receivable_dao) - .build(); - subject.financial_statistics.total_paid_receivable += 2222; - let receivables = vec![ - BlockchainTransaction { - block_number: 4578910, - from: make_wallet("wallet_1"), - gwei_amount: 45780, - }, - BlockchainTransaction { - block_number: 4569898, - from: make_wallet("wallet_2"), - gwei_amount: 33345, - }, - ]; - let now = SystemTime::now(); - - subject.handle_received_payments(ReceivedPayments { - timestamp: now, - payments: receivables.clone(), - response_skeleton_opt: None, - }); - - assert_eq!( - subject.financial_statistics.total_paid_receivable, - 2222 + 45780 + 33345 - ); - let more_money_received_params = more_money_received_params_arc.lock().unwrap(); - assert_eq!(*more_money_received_params, vec![(now, receivables)]); - } - #[test] #[cfg(not(feature = "no_test_share"))] fn msg_id_generates_numbers_only_if_debug_log_enabled() { diff --git a/node/src/accountant/scanners.rs b/node/src/accountant/scanners.rs new file mode 100644 index 000000000..6af36c822 --- /dev/null +++ b/node/src/accountant/scanners.rs @@ -0,0 +1,1852 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub(in crate::accountant) mod scanners { + use crate::accountant::payable_dao::{Payable, PayableDao, PayableDaoFactory}; + use crate::accountant::pending_payable_dao::{PendingPayableDao, PendingPayableDaoFactory}; + use crate::accountant::receivable_dao::ReceivableDao; + use crate::accountant::tools::payable_scanner_tools::{ + investigate_debt_extremes, qualified_payables_and_summary, separate_early_errors, + }; + use crate::accountant::tools::pending_payable_scanner_tools::{ + elapsed_in_ms, handle_none_status, handle_status_with_failure, handle_status_with_success, + }; + use crate::accountant::tools::receivable_scanner_tools::balance_and_age; + use crate::accountant::{ + Accountant, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, + ResponseSkeleton, ScanForPayables, ScanForPendingPayables, ScanForReceivables, SentPayable, + }; + use crate::accountant::{PendingPayableId, PendingTransactionStatus, ReportAccountsPayable}; + use crate::banned_dao::BannedDao; + use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; + use crate::blockchain::blockchain_interface::BlockchainError; + use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; + use crate::sub_lib::utils::NotifyLaterHandle; + use crate::sub_lib::wallet::Wallet; + use actix::{Message, System}; + use masq_lib::logger::Logger; + use masq_lib::messages::{ToMessageBody, UiScanResponse}; + use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; + use masq_lib::utils::ExpectValue; + use std::any::Any; + use std::cell::RefCell; + use std::rc::Rc; + use std::sync::{Arc, Mutex}; + use std::time::SystemTime; + use web3::types::TransactionReceipt; + + pub struct Scanners { + pub payable: Box>, + pub pending_payable: + Box>, + pub receivable: Box>, + } + + impl Scanners { + pub fn new( + payable_dao_factory: Box, + pending_payable_dao_factory: Box, + receivable_dao: Box, + banned_dao: Box, + payment_thresholds: Rc, + earning_wallet: Rc, + when_pending_too_long_sec: u64, + financial_statistics: Rc>, + ) -> Self { + Scanners { + payable: Box::new(PayableScanner::new( + payable_dao_factory.make(), + pending_payable_dao_factory.make(), + Rc::clone(&payment_thresholds), + )), + pending_payable: Box::new(PendingPayableScanner::new( + payable_dao_factory.make(), + pending_payable_dao_factory.make(), + Rc::clone(&payment_thresholds), + when_pending_too_long_sec, + Rc::clone(&financial_statistics), + )), + receivable: Box::new(ReceivableScanner::new( + receivable_dao, + banned_dao, + Rc::clone(&payment_thresholds), + earning_wallet, + financial_statistics, + )), + } + } + } + + pub trait Scanner + where + BeginMessage: Message, + EndMessage: Message, + { + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result; + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> Option; + fn scan_started_at(&self) -> Option; + fn mark_as_started(&mut self, timestamp: SystemTime); + fn mark_as_ended(&mut self, logger: &Logger); + as_any_dcl!(); + } + + #[derive(Debug, PartialEq, Eq)] + pub enum BeginScanError { + NothingToProcess, + ScanAlreadyRunning(SystemTime), + CalledFromNullScanner, // Exclusive for tests + } + + pub struct ScannerCommon { + initiated_at_opt: Option, + pub payment_thresholds: Rc, + } + + impl ScannerCommon { + fn new(payment_thresholds: Rc) -> Self { + Self { + initiated_at_opt: None, + payment_thresholds, + } + } + } + + pub struct PayableScanner { + pub common: ScannerCommon, + pub payable_dao: Box, + pub pending_payable_dao: Box, + } + + impl Scanner for PayableScanner { + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(timestamp) = self.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + } + self.mark_as_started(timestamp); + info!(logger, "Scanning for payables"); + let all_non_pending_payables = self.payable_dao.non_pending_payables(); + debug!( + logger, + "{}", + investigate_debt_extremes(timestamp, &all_non_pending_payables) + ); + let (qualified_payables, summary) = qualified_payables_and_summary( + timestamp, + all_non_pending_payables, + self.common.payment_thresholds.as_ref(), + ); + info!( + logger, + "Chose {} qualified debts to pay", + qualified_payables.len() + ); + debug!(logger, "{}", summary); + match qualified_payables.is_empty() { + true => { + self.mark_as_ended(logger); + Err(BeginScanError::NothingToProcess) + } + false => Ok(ReportAccountsPayable { + accounts: qualified_payables, + response_skeleton_opt, + }), + } + } + + fn finish_scan( + &mut self, + message: SentPayable, + logger: &Logger, + ) -> Option { + let (sent_payables, blockchain_errors) = separate_early_errors(&message, logger); + debug!( + logger, + "We gathered these errors at sending transactions for payable: {:?}, out of the \ + total of {} attempts", + blockchain_errors, + sent_payables.len() + blockchain_errors.len() + ); + + self.handle_sent_payables(sent_payables, logger); + self.handle_blockchain_errors(blockchain_errors, logger); + + self.mark_as_ended(logger); + match message.response_skeleton_opt { + Some(response_skeleton) => Some(NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }), + None => None, + } + } + + fn scan_started_at(&self) -> Option { + self.common.initiated_at_opt + } + + fn mark_as_started(&mut self, timestamp: SystemTime) { + self.common.initiated_at_opt = Some(timestamp); + } + + fn mark_as_ended(&mut self, logger: &Logger) { + match self.scan_started_at() { + Some(timestamp) => { + let elapsed_time = SystemTime::now() + .duration_since(timestamp) + .expect("Unable to calculate elapsed time for the scan.") + .as_millis(); + info!(logger, "The Payable scan ended in {elapsed_time}ms."); + self.common.initiated_at_opt = None; + } + None => error!(logger, "The scan_finished() was called for Payable scanner but timestamp was not found"), + }; + } + + as_any_impl!(); + } + + impl PayableScanner { + pub fn new( + payable_dao: Box, + pending_payable_dao: Box, + payment_thresholds: Rc, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + payable_dao, + pending_payable_dao, + } + } + + fn handle_sent_payables(&self, sent_payables: Vec, logger: &Logger) { + for payable in sent_payables { + if let Some(rowid) = self.pending_payable_dao.fingerprint_rowid(payable.tx_hash) { + if let Err(e) = self + .payable_dao + .as_ref() + .mark_pending_payable_rowid(&payable.to, rowid) + { + panic!( + "Was unable to create a mark in payables for a new pending payable \ + '{}' due to '{:?}'", + payable.tx_hash, e + ); + } + } else { + panic!( + "Payable fingerprint for {} doesn't exist but should by now; \ + system unreliable", + payable.tx_hash + ); + }; + + debug!( + logger, + "Payable '{}' has been marked as pending in the payable table", payable.tx_hash + ) + } + } + + fn handle_blockchain_errors( + &self, + blockchain_errors: Vec, + logger: &Logger, + ) { + for blockchain_error in blockchain_errors { + if let Some(hash) = blockchain_error.carries_transaction_hash() { + if let Some(rowid) = self.pending_payable_dao.fingerprint_rowid(hash) { + debug!( + logger, + "Deleting an existing backup for a failed transaction {}", hash + ); + if let Err(e) = self.pending_payable_dao.delete_fingerprint(rowid) { + panic!( + "Database unmaintainable; payable fingerprint deletion for \ + transaction {:?} has stayed undone due to {:?}", + hash, e + ); + }; + }; + + warning!( + logger, + "Failed transaction with a hash '{}' but without the record - thrown out", + hash + ) + } else { + debug!( + logger, + "Forgetting a transaction attempt that even did not reach the signing stage" + ) + }; + } + } + } + + pub struct PendingPayableScanner { + pub common: ScannerCommon, + pub payable_dao: Box, + pub pending_payable_dao: Box, + pub when_pending_too_long_sec: u64, + pub financial_statistics: Rc>, + } + + impl Scanner for PendingPayableScanner { + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(timestamp) = self.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + } + self.mark_as_started(timestamp); + info!(logger, "Scanning for pending payable"); + let filtered_pending_payable = self.pending_payable_dao.return_all_fingerprints(); + match filtered_pending_payable.is_empty() { + true => { + debug!( + logger, + "Pending payable scan ended. No pending payable found." + ); + self.mark_as_ended(logger); + Err(BeginScanError::NothingToProcess) + } + false => { + debug!( + logger, + "Found {} pending payables to process", + filtered_pending_payable.len() + ); + Ok(RequestTransactionReceipts { + pending_payable: filtered_pending_payable, + response_skeleton_opt, + }) + } + } + } + + fn finish_scan( + &mut self, + message: ReportTransactionReceipts, + logger: &Logger, + ) -> Option { + // TODO: Make accountant to handle empty vector. Maybe log it as an error. + debug!( + logger, + "Processing receipts for {} transactions", + message.fingerprints_with_receipts.len() + ); + let statuses = self.handle_pending_transaction_with_its_receipt(&message, logger); + self.process_transaction_by_status(statuses, logger); + + self.mark_as_ended(logger); + match message.response_skeleton_opt { + Some(response_skeleton) => Some(NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }), + None => None, + } + } + + fn scan_started_at(&self) -> Option { + self.common.initiated_at_opt + } + + fn mark_as_started(&mut self, timestamp: SystemTime) { + self.common.initiated_at_opt = Some(timestamp); + } + + fn mark_as_ended(&mut self, logger: &Logger) { + match self.scan_started_at() { + Some(timestamp) => { + let elapsed_time = SystemTime::now() + .duration_since(timestamp) + .expect("Unable to calculate elapsed time for the scan.") + .as_millis(); + info!( + logger, + "The Pending Payable scan ended in {elapsed_time}ms." + ); + self.common.initiated_at_opt = None; + } + None => error!(logger, "The scan_finished() was called for Pending Payable scanner but timestamp was not found"), + }; + } + + as_any_impl!(); + } + + impl PendingPayableScanner { + pub fn new( + payable_dao: Box, + pending_payable_dao: Box, + payment_thresholds: Rc, + when_pending_too_long_sec: u64, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + payable_dao, + pending_payable_dao, + when_pending_too_long_sec, + financial_statistics, + } + } + + pub(crate) fn handle_pending_transaction_with_its_receipt( + &self, + msg: &ReportTransactionReceipts, + logger: &Logger, + ) -> Vec { + msg.fingerprints_with_receipts + .iter() + .map(|(receipt_opt, fingerprint)| match receipt_opt { + Some(receipt) => { + self.interpret_transaction_receipt(receipt, fingerprint, logger) + } + None => { + debug!( + logger, + "Interpreting a receipt for transaction '{}' but none was given; \ + attempt {}, {}ms since sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + PendingTransactionStatus::StillPending(PendingPayableId { + hash: fingerprint.hash, + rowid: fingerprint.rowid_opt.expectv("initialized rowid"), + }) + } + }) + .collect() + } + + pub fn interpret_transaction_receipt( + &self, + receipt: &TransactionReceipt, + fingerprint: &PendingPayableFingerprint, + logger: &Logger, + ) -> PendingTransactionStatus { + match receipt.status { + None => handle_none_status(fingerprint, self.when_pending_too_long_sec, logger), + Some(status_code) => + match status_code.as_u64() { + 0 => handle_status_with_failure(fingerprint, logger), + 1 => handle_status_with_success(fingerprint, logger), + other => unreachable!("tx receipt for pending '{}' - tx status: code other than 0 or 1 shouldn't be possible, but was {}", fingerprint.hash, other) + } + } + } + + fn process_transaction_by_status( + &mut self, + statuses: Vec, + logger: &Logger, + ) { + for status in statuses { + match status { + PendingTransactionStatus::StillPending(transaction_id) => { + self.update_payable_fingerprint(transaction_id, logger); + } + PendingTransactionStatus::Failure(transaction_id) => { + self.order_cancel_failed_transaction(transaction_id, logger); + } + PendingTransactionStatus::Confirmed(fingerprint) => { + self.order_confirm_transaction(fingerprint, logger); + } + } + } + } + + pub(crate) fn update_payable_fingerprint( + &self, + pending_payable_id: PendingPayableId, + logger: &Logger, + ) { + if let Err(e) = self + .pending_payable_dao + .update_fingerprint(pending_payable_id.rowid) + { + panic!( + "Failure on updating payable fingerprint '{:?}' due to {:?}", + pending_payable_id.hash, e + ); + } else { + trace!( + logger, + "Updated record for rowid: {} ", + pending_payable_id.rowid + ); + } + } + + pub fn order_cancel_failed_transaction( + &self, + transaction_id: PendingPayableId, + logger: &Logger, + ) { + if let Err(e) = self.pending_payable_dao.mark_failure(transaction_id.rowid) { + panic!( + "Unsuccessful attempt for transaction {} to mark fatal error at payable \ + fingerprint due to {:?}; database unreliable", + transaction_id.hash, e + ) + } else { + warning!( + logger, + "Broken transaction {} left with an error mark; you should take over the care \ + of this transaction to make sure your debts will be paid because there is no \ + automated process that can fix this without you", transaction_id.hash + ); + } + } + + pub fn order_confirm_transaction( + &mut self, + pending_payable_fingerprint: PendingPayableFingerprint, + logger: &Logger, + ) { + let hash = pending_payable_fingerprint.hash; + let amount = pending_payable_fingerprint.amount; + let rowid = pending_payable_fingerprint + .rowid_opt + .expectv("initialized rowid"); + + if let Err(e) = self + .payable_dao + .transaction_confirmed(&pending_payable_fingerprint) + { + panic!( + "Was unable to uncheck pending payable '{}' after confirmation due to '{:?}'", + hash, e + ); + } else { + let mut financial_statistics = self.financial_statistics.as_ref().borrow().clone(); + financial_statistics.total_paid_payable += amount; + self.financial_statistics.replace(financial_statistics); + debug!( + logger, + "Confirmation of transaction {}; record for payable was modified", hash + ); + if let Err(e) = self.pending_payable_dao.delete_fingerprint(rowid) { + panic!( + "Was unable to delete payable fingerprint '{}' after successful transaction \ + due to '{:?}'", hash, e + ); + } else { + info!( + logger, + "Transaction {:?} has gone through the whole confirmation process succeeding", + hash + ); + } + } + } + + pub fn financial_statistics(&self) -> FinancialStatistics { + self.financial_statistics.as_ref().borrow().clone() + } + } + + pub struct ReceivableScanner { + pub common: ScannerCommon, + pub receivable_dao: Box, + pub banned_dao: Box, + pub earning_wallet: Rc, + pub financial_statistics: Rc>, + } + + impl Scanner for ReceivableScanner { + fn begin_scan( + &mut self, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(timestamp) = self.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + } + self.mark_as_started(timestamp); + info!( + logger, + "Scanning for receivables to {}", self.earning_wallet + ); + self.scan_for_delinquencies(timestamp, logger); + + Ok(RetrieveTransactions { + recipient: self.earning_wallet.as_ref().clone(), + response_skeleton_opt, + }) + } + + fn finish_scan( + &mut self, + message: ReceivedPayments, + logger: &Logger, + ) -> Option { + if message.payments.is_empty() { + warning!( + logger, + "Handling received payments we got zero payments but expected some, \ + skipping database operations" + ) + } else { + let total_newly_paid_receivable = message + .payments + .iter() + .fold(0, |so_far, now| so_far + now.gwei_amount); + self.receivable_dao + .as_mut() + .more_money_received(message.timestamp, message.payments); + let mut financial_statistics = self.financial_statistics(); + financial_statistics.total_paid_receivable += total_newly_paid_receivable; + self.financial_statistics.replace(financial_statistics); + } + + self.mark_as_ended(logger); + match message.response_skeleton_opt { + None => None, + Some(response_skeleton) => Some(NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }), + } + } + + fn scan_started_at(&self) -> Option { + self.common.initiated_at_opt + } + + fn mark_as_started(&mut self, timestamp: SystemTime) { + self.common.initiated_at_opt = Some(timestamp); + } + + fn mark_as_ended(&mut self, logger: &Logger) { + match self.scan_started_at() { + Some(timestamp) => { + let elapsed_time = SystemTime::now() + .duration_since(timestamp) + .expect("Unable to calculate elapsed time for the scan.") + .as_millis(); + info!(logger, "The Receivable scan ended in {elapsed_time}ms."); + self.common.initiated_at_opt = None; + } + None => error!(logger, "The scan_finished() was called for Receivable scanner but timestamp was not found"), + }; + } + + as_any_impl!(); + } + + impl ReceivableScanner { + pub fn new( + receivable_dao: Box, + banned_dao: Box, + payment_thresholds: Rc, + earning_wallet: Rc, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + earning_wallet, + receivable_dao, + banned_dao, + financial_statistics, + } + } + + pub fn scan_for_delinquencies(&self, timestamp: SystemTime, logger: &Logger) { + info!(logger, "Scanning for delinquencies"); + self.ban_nodes(timestamp, logger); + self.unban_nodes(timestamp, logger); + } + + pub fn ban_nodes(&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, age) = balance_and_age(timestamp, &account); + info!( + logger, + "Wallet {} (balance: {} MASQ, age: {} sec) banned for delinquency", + account.wallet, + balance, + age.as_secs() + ) + }); + } + + pub fn unban_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, age) = balance_and_age(timestamp, &account); + info!( + logger, + "Wallet {} (balance: {} MASQ, age: {} sec) is no longer delinquent: unbanned", + account.wallet, + balance, + age.as_secs() + ) + }); + } + + pub fn financial_statistics(&self) -> FinancialStatistics { + self.financial_statistics.as_ref().borrow().clone() + } + } + + pub struct NullScanner {} + + impl Scanner for NullScanner + where + BeginMessage: Message, + EndMessage: Message, + { + fn begin_scan( + &mut self, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + Err(BeginScanError::CalledFromNullScanner) + } + + fn finish_scan( + &mut self, + _message: EndMessage, + _logger: &Logger, + ) -> Option { + panic!("Called from NullScanner"); + } + + fn scan_started_at(&self) -> Option { + panic!("Called from NullScanner"); + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + panic!("Called from NullScanner"); + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + panic!("Called from NullScanner"); + } + + as_any_impl!(); + } + + impl NullScanner { + pub fn new() -> Self { + Self {} + } + } + + pub struct ScannerMock { + begin_scan_params: Arc>>, + begin_scan_results: RefCell>>, + end_scan_params: Arc>>, + end_scan_results: RefCell>>, + stop_system_after_last_message: RefCell, + } + + impl Scanner + for ScannerMock + where + BeginMessage: Message, + EndMessage: Message, + { + fn begin_scan( + &mut self, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + self.begin_scan_params.lock().unwrap().push(()); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.begin_scan_results.borrow_mut().remove(0) + } + + fn finish_scan( + &mut self, + message: EndMessage, + _logger: &Logger, + ) -> Option { + self.end_scan_params.lock().unwrap().push(message); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.end_scan_results.borrow_mut().remove(0) + } + + fn scan_started_at(&self) -> Option { + unimplemented!() + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + unimplemented!() + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + unimplemented!() + } + } + + impl ScannerMock { + pub fn new() -> Self { + Self { + begin_scan_params: Arc::new(Mutex::new(vec![])), + begin_scan_results: RefCell::new(vec![]), + end_scan_params: Arc::new(Mutex::new(vec![])), + end_scan_results: RefCell::new(vec![]), + stop_system_after_last_message: RefCell::new(false), + } + } + + pub fn begin_scan_params(mut self, params: &Arc>>) -> Self { + self.begin_scan_params = params.clone(); + self + } + + pub fn begin_scan_result(self, result: Result) -> Self { + self.begin_scan_results.borrow_mut().push(result); + self + } + + pub fn stop_the_system(self) -> Self { + self.stop_system_after_last_message.replace(true); + self + } + + pub fn is_allowed_to_stop_the_system(&self) -> bool { + self.stop_system_after_last_message.borrow().clone() + } + + pub fn is_last_message(&self) -> bool { + self.is_last_message_from_begin_scan() || self.is_last_message_from_end_scan() + } + + pub fn is_last_message_from_begin_scan(&self) -> bool { + self.begin_scan_results.borrow().len() == 1 && self.end_scan_results.borrow().is_empty() + } + + pub fn is_last_message_from_end_scan(&self) -> bool { + self.end_scan_results.borrow().len() == 1 && self.begin_scan_results.borrow().is_empty() + } + } + + #[derive(Default)] + pub struct NotifyLaterForScanners { + pub scan_for_pending_payable: + Box>, + pub scan_for_payable: Box>, + pub scan_for_receivable: Box>, + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::scanners::{ + BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, Scanner, Scanners, + }; + use crate::accountant::test_utils::{ + make_custom_payment_thresholds, make_payables, make_pending_payable_fingerprint, + make_receivable_account, BannedDaoMock, PayableDaoFactoryMock, PayableDaoMock, + PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoMock, + }; + use crate::accountant::{ + PendingPayableId, PendingTransactionStatus, ReceivedPayments, ReportTransactionReceipts, + RequestTransactionReceipts, SentPayable, DEFAULT_PENDING_TOO_LONG_SEC, + }; + use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; + use std::cell::RefCell; + use std::ops::Sub; + + use crate::accountant::payable_dao::{Payable, PayableDaoError}; + use crate::accountant::pending_payable_dao::PendingPayableDaoError; + use crate::blockchain::blockchain_interface::{BlockchainError, BlockchainTransaction}; + use crate::database::dao_utils::from_time_t; + use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; + use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; + use crate::test_utils::make_wallet; + use ethereum_types::{BigEndianHash, U64}; + use ethsign_crypto::Keccak256; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::rc::Rc; + use std::sync::{Arc, Mutex, MutexGuard}; + use std::time::{Duration, SystemTime}; + use web3::types::{TransactionReceipt, H256, U256}; + + #[test] + fn scanners_struct_can_be_constructed_with_the_respective_scanners() { + let payment_thresholds = Rc::new(PaymentThresholds::default()); + let payable_dao_factory = PayableDaoFactoryMock::new() + .make_result(PayableDaoMock::new()) + .make_result(PayableDaoMock::new()); + let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() + .make_result(PendingPayableDaoMock::new()) + .make_result(PendingPayableDaoMock::new()); + let scanners = Scanners::new( + Box::new(payable_dao_factory), + Box::new(pending_payable_dao_factory), + Box::new(ReceivableDaoMock::new()), + Box::new(BannedDaoMock::new()), + Rc::clone(&payment_thresholds), + Rc::new(make_wallet("earning")), + 0, + Rc::new(RefCell::new(FinancialStatistics::default())), + ); + + scanners + .payable + .as_any() + .downcast_ref::() + .unwrap(); + scanners + .pending_payable + .as_any() + .downcast_ref::() + .unwrap(); + scanners + .receivable + .as_any() + .downcast_ref::() + .unwrap(); + } + + #[test] + fn payable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "payable_scanner_can_initiate_a_scan"; + let now = SystemTime::now(); + let (qualified_payable_accounts, _, all_non_pending_payables) = + make_payables(now, &PaymentThresholds::default()); + let len_of_qualified_payables = qualified_payable_accounts.len(); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + + let mut subject = PayableScanner::default().payable_dao(payable_dao); + + let result = subject.begin_scan(now, None, &Logger::new(test_name)); + + let timestamp = subject.scan_started_at(); + assert_eq!(timestamp, Some(now)); + assert_eq!( + result, + Ok(ReportAccountsPayable { + accounts: qualified_payable_accounts.clone(), + response_skeleton_opt: None, + }) + ); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for payables"), + &format!("INFO: {test_name}: Chose {len_of_qualified_payables} qualified debts to pay"), + ]) + } + + #[test] + fn payable_scanner_throws_error_when_a_scan_is_already_running() { + let now = SystemTime::now(); + let (_, _, all_non_pending_payables) = make_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = PayableScanner::default().payable_dao(payable_dao); + let _result = subject.begin_scan(now, None, &Logger::new("test")); + + let run_again_result = subject.begin_scan(SystemTime::now(), None, &Logger::new("test")); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + run_again_result, + Err(BeginScanError::ScanAlreadyRunning(now)) + ); + } + + #[test] + fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + init_test_logging(); + let test_name = "payable_scanner_throws_error_in_case_no_qualified_payable_is_found"; + let now = SystemTime::now(); + let (_, unqualified_payable_accounts, _) = + make_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); + + let mut subject = PayableScanner::default().payable_dao(payable_dao); + + let result = subject.begin_scan(now, None, &Logger::new(test_name)); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(is_scan_running, false); + assert_eq!(result, Err(BeginScanError::NothingToProcess)); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for payables"), + "Chose 0 qualified debts to pay", + ]); + } + + #[test] + #[should_panic( + expected = "Payable fingerprint for 0x0000…0315 doesn't exist but should by now; system unreliable" + )] + fn payable_scanner_throws_error_when_fingerprint_is_not_found() { + let now = SystemTime::now(); + let payment_hash = H256::from_uint(&U256::from(789)); + let payable = Payable::new(make_wallet("booga"), 6789, payment_hash, now); + let payable_dao = PayableDaoMock::new().mark_pending_payable_rowid_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default().fingerprint_rowid_result(None); + let mut subject = PayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + let sent_payable = SentPayable { + timestamp: now, + payable: vec![Ok(payable)], + response_skeleton_opt: None, + }; + + let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + } + + #[test] + #[should_panic( + expected = "Database unmaintainable; payable fingerprint deletion for transaction \ + 0x000000000000000000000000000000000000000000000000000000000000007b has stayed \ + undone due to RecordDeletion(\"we slept over, sorry\")" + )] + fn payable_scanner_panics_when_failed_payment_fails_to_delete_the_existing_pending_payable_fingerprint( + ) { + let rowid = 4; + let hash = H256::from_uint(&U256::from(123)); + let sent_payable = SentPayable { + timestamp: SystemTime::now(), + payable: vec![Err(BlockchainError::TransactionFailed { + msg: "blah".to_string(), + hash_opt: Some(hash), + })], + response_skeleton_opt: None, + }; + let pending_payable_dao = PendingPayableDaoMock::default() + .fingerprint_rowid_result(Some(rowid)) + .delete_fingerprint_result(Err(PendingPayableDaoError::RecordDeletion( + "we slept over, sorry".to_string(), + ))); + let mut subject = PayableScanner::default().pending_payable_dao(pending_payable_dao); + + let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + } + + #[test] + #[should_panic( + expected = "Was unable to create a mark in payables for a new pending payable '0x0000…007b' \ + due to 'SignConversion(9999999999999)'" + )] + fn payable_scanner_panics_when_it_fails_to_make_a_mark_in_payables() { + let payable = Payable::new( + make_wallet("blah"), + 6789, + H256::from_uint(&U256::from(123)), + SystemTime::now(), + ); + let payable_dao = PayableDaoMock::new() + .mark_pending_payable_rowid_result(Err(PayableDaoError::SignConversion(9999999999999))); + let pending_payable_dao = + PendingPayableDaoMock::default().fingerprint_rowid_result(Some(7879)); + let mut subject = PayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + let sent_payable = SentPayable { + timestamp: SystemTime::now(), + payable: vec![Ok(payable)], + response_skeleton_opt: None, + }; + + let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + } + + #[test] + fn payable_scanner_handles_sent_payable_message() { + //the two failures differ in the logged messages + init_test_logging(); + let test_name = "payable_scanner_handles_sent_payable_message"; + let fingerprint_rowid_params_arc = Arc::new(Mutex::new(vec![])); + let now = SystemTime::now(); + let payable_1 = Err(BlockchainError::InvalidResponse); + let payable_2_rowid = 126; + let payable_2_hash = H256::from_uint(&U256::from(166)); + let payable_2 = Payable::new(make_wallet("booga"), 6789, payable_2_hash, now); + let payable_3 = Err(BlockchainError::TransactionFailed { + msg: "closing hours, sorry".to_string(), + hash_opt: None, + }); + let sent_payable = SentPayable { + timestamp: SystemTime::now(), + payable: vec![payable_1, Ok(payable_2.clone()), payable_3], + response_skeleton_opt: None, + }; + let payable_dao = PayableDaoMock::new().mark_pending_payable_rowid_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default() + .fingerprint_rowid_params(&fingerprint_rowid_params_arc) + .fingerprint_rowid_result(Some(payable_2_rowid)); + let mut subject = PayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + subject.mark_as_started(SystemTime::now()); + + let message_opt = subject.finish_scan(sent_payable, &Logger::new(test_name)); + + let is_scan_running = subject.scan_started_at().is_some(); + let fingerprint_rowid_params = fingerprint_rowid_params_arc.lock().unwrap(); + assert_eq!(message_opt, None); + assert_eq!(is_scan_running, false); + //we know the other two errors are associated with an initiated transaction having a backup + assert_eq!(*fingerprint_rowid_params, vec![payable_2_hash]); + let log_handler = TestLogHandler::new(); + log_handler.assert_logs_contain_in_order(vec![ + &format!( + "WARN: {test_name}: Outbound transaction failure due to 'InvalidResponse'. \ + Please check your blockchain service URL configuration." + ), + &format!( + "WARN: {test_name}: Encountered transaction error at this end: 'TransactionFailed \ + {{ msg: \"closing hours, sorry\", hash_opt: None }}'" + ), + &format!( + "DEBUG: {test_name}: Payable '0x0000…00a6' has been marked as pending in the payable table" + ), + &format!( + "DEBUG: {test_name}: Forgetting a transaction attempt that even did not reach the signing stage" + ), + ]); + log_handler.exists_log_matching( + "INFO: payable_scanner_handles_sent_payable_message: The Payable scan ended in \\d+ms.", + ); + } + + #[test] + fn pending_payable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "pending_payable_scanner_can_initiate_a_scan"; + let now = SystemTime::now(); + let fingerprints = vec![PendingPayableFingerprint { + rowid_opt: Some(1234), + timestamp: SystemTime::now(), + hash: Default::default(), + attempt_opt: Some(1), + amount: 1_000_000, + process_error: None, + }]; + let no_of_pending_payables = fingerprints.len(); + let pending_payable_dao = + PendingPayableDaoMock::new().return_all_fingerprints_result(fingerprints.clone()); + let mut pending_payable_scanner = + PendingPayableScanner::default().pending_payable_dao(pending_payable_dao); + + let result = pending_payable_scanner.begin_scan(now, None, &Logger::new(test_name)); + + let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Ok(RequestTransactionReceipts { + pending_payable: fingerprints, + response_skeleton_opt: None + }) + ); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for pending payable"), + &format!( + "DEBUG: {test_name}: Found {no_of_pending_payables} pending payables to process" + ), + ]) + } + + #[test] + fn pending_payable_scanner_throws_error_in_case_scan_is_already_running() { + let now = SystemTime::now(); + let pending_payable_dao = + PendingPayableDaoMock::new().return_all_fingerprints_result(vec![ + PendingPayableFingerprint { + rowid_opt: Some(1234), + timestamp: SystemTime::now(), + hash: Default::default(), + attempt_opt: Some(1), + amount: 1_000_000, + process_error: None, + }, + ]); + let mut subject = PendingPayableScanner::default().pending_payable_dao(pending_payable_dao); + let _ = subject.begin_scan(now, None, &Logger::new("test")); + + let result = subject.begin_scan(SystemTime::now(), None, &Logger::new("test")); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + } + + #[test] + fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { + init_test_logging(); + let test_name = "pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found"; + let now = SystemTime::now(); + let pending_payable_dao = + PendingPayableDaoMock::new().return_all_fingerprints_result(vec![]); + let mut pending_payable_scanner = + PendingPayableScanner::default().pending_payable_dao(pending_payable_dao); + + let result = pending_payable_scanner.begin_scan(now, None, &Logger::new(test_name)); + + let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); + assert_eq!(result, Err(BeginScanError::NothingToProcess)); + assert_eq!(is_scan_running, false); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for pending payable"), + &format!("DEBUG: {test_name}: Pending payable scan ended. No pending payable found."), + ]) + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() + { + init_test_logging(); + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; + let hash = H256::from_uint(&U256::from(567)); + let rowid = 466; + let tx_receipt = TransactionReceipt::default(); //status defaulted to None + let when_sent = + SystemTime::now().sub(Duration::from_secs(DEFAULT_PENDING_TOO_LONG_SEC + 5)); //old transaction + let subject = PendingPayableScanner::default(); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: when_sent, + hash, + attempt_opt: Some(10), + amount: 123, + process_error: None, + }; + + let result = subject.interpret_transaction_receipt( + &tx_receipt, + &fingerprint, + &Logger::new(test_name), + ); + + assert_eq!( + result, + PendingTransactionStatus::Failure(PendingPayableId { hash, rowid }) + ); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Pending transaction '0x0000…0237' has exceeded the maximum \ + pending time (21600sec) and the confirmation process is going to be aborted now \ + at the final attempt 10; manual resolution is required from the user to complete \ + the transaction" + )); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { + init_test_logging(); + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; + let subject = PendingPayableScanner::default(); + let hash = H256::from_uint(&U256::from(567)); + let rowid = 466; + let tx_receipt = TransactionReceipt::default(); //status defaulted to None + let when_sent = SystemTime::now().sub(Duration::from_millis(100)); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: when_sent, + hash, + attempt_opt: Some(1), + amount: 123, + process_error: None, + }; + + let result = subject.interpret_transaction_receipt( + &tx_receipt, + &fingerprint, + &Logger::new(test_name), + ); + + assert_eq!( + result, + PendingTransactionStatus::StillPending(PendingPayableId { hash, rowid }) + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Pending \ + transaction '0x0000…0237' couldn't be confirmed at attempt 1 at ", + )); + } + + #[test] + #[should_panic( + expected = "tx receipt for pending '0x0000…007b' - tx status: code other than 0 or 1 shouldn't be possible, but was 456" + )] + fn interpret_transaction_receipt_panics_at_undefined_status_code() { + let mut tx_receipt = TransactionReceipt::default(); + tx_receipt.status = Some(U64::from(456)); + let mut fingerprint = make_pending_payable_fingerprint(); + fingerprint.hash = H256::from_uint(&U256::from(123)); + let subject = PendingPayableScanner::default(); + + let _ = + subject.interpret_transaction_receipt(&tx_receipt, &fingerprint, &Logger::new("test")); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { + init_test_logging(); + let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; + let subject = PendingPayableScanner::default(); + let mut tx_receipt = TransactionReceipt::default(); + tx_receipt.status = Some(U64::from(0)); //failure + let hash = H256::from_uint(&U256::from(4567)); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(777777), + timestamp: SystemTime::now().sub(Duration::from_millis(150000)), + hash, + attempt_opt: Some(5), + amount: 2222, + process_error: None, + }; + + let result = subject.interpret_transaction_receipt( + &tx_receipt, + &fingerprint, + &Logger::new(test_name), + ); + + assert_eq!( + result, + PendingTransactionStatus::Failure(PendingPayableId { + hash, + rowid: 777777, + }) + ); + TestLogHandler::new().exists_log_matching(&format!( + "ERROR: {test_name}: Pending transaction '0x0000…11d7' announced as a failure, \ + interpreting attempt 5 after 1500\\d\\dms from the sending" + )); + } + + #[test] + fn handle_pending_tx_handles_none_returned_for_transaction_receipt() { + init_test_logging(); + let test_name = "handle_pending_tx_handles_none_returned_for_transaction_receipt"; + let subject = PendingPayableScanner::default(); + let tx_receipt_opt = None; + let rowid = 455; + let hash = H256::from_uint(&U256::from(2323)); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: SystemTime::now().sub(Duration::from_millis(10000)), + hash, + attempt_opt: Some(3), + amount: 111, + process_error: None, + }; + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: vec![(tx_receipt_opt, fingerprint.clone())], + response_skeleton_opt: None, + }; + + let result = + subject.handle_pending_transaction_with_its_receipt(&msg, &Logger::new(test_name)); + + assert_eq!( + result, + vec![PendingTransactionStatus::StillPending(PendingPayableId { + hash, + rowid, + })] + ); + TestLogHandler::new().exists_log_matching(&format!( + "DEBUG: {test_name}: Interpreting a receipt for transaction '0x0000…0913' \ + but none was given; attempt 3, 100\\d\\dms since sending" + )); + } + + #[test] + fn update_payable_fingerprint_happy_path() { + let update_after_cycle_params_arc = Arc::new(Mutex::new(vec![])); + let hash = H256::from_uint(&U256::from(444888)); + let rowid = 3456; + let pending_payable_dao = PendingPayableDaoMock::default() + .update_fingerprint_params(&update_after_cycle_params_arc) + .update_fingerprint_results(Ok(())); + let subject = PendingPayableScanner::default().pending_payable_dao(pending_payable_dao); + let transaction_id = PendingPayableId { hash, rowid }; + + subject.update_payable_fingerprint(transaction_id, &Logger::new("test")); + + let update_after_cycle_params = update_after_cycle_params_arc.lock().unwrap(); + assert_eq!(*update_after_cycle_params, vec![rowid]) + } + + #[test] + #[should_panic(expected = "Failure on updating payable fingerprint \ + '0x000000000000000000000000000000000000000000000000000000000006c9d8' \ + due to UpdateFailed(\"yeah, bad\")")] + fn update_payable_fingerprint_sad_path() { + let hash = H256::from_uint(&U256::from(444888)); + let rowid = 3456; + let pending_payable_dao = PendingPayableDaoMock::default().update_fingerprint_results(Err( + PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), + )); + let subject = PendingPayableScanner::default().pending_payable_dao(pending_payable_dao); + let transaction_id = PendingPayableId { hash, rowid }; + + subject.update_payable_fingerprint(transaction_id, &Logger::new("test")); + } + + #[test] + fn order_cancel_pending_transaction_works() { + init_test_logging(); + let test_name = "order_cancel_pending_transaction_works"; + let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_dao = PendingPayableDaoMock::default() + .mark_failure_params(&mark_failure_params_arc) + .mark_failure_result(Ok(())); + let subject = PendingPayableScanner::default().pending_payable_dao(pending_payable_dao); + let tx_hash = H256::from("sometransactionhash".keccak256()); + let rowid = 2; + let transaction_id = PendingPayableId { + hash: tx_hash, + rowid, + }; + + subject.order_cancel_failed_transaction(transaction_id, &Logger::new(test_name)); + + let mark_failure_params = mark_failure_params_arc.lock().unwrap(); + assert_eq!(*mark_failure_params, vec![rowid]); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Broken transaction 0x051a…8c19 left with an error \ + mark; you should take over the care of this transaction to make sure your debts will \ + be paid because there is no automated process that can fix this without you", + )); + } + + #[test] + #[should_panic( + expected = "Unsuccessful attempt for transaction 0x051a…8c19 to mark fatal error at payable \ + fingerprint due to UpdateFailed(\"no no no\"); database unreliable" + )] + fn order_cancel_pending_transaction_panics_when_it_fails_to_mark_failure() { + let payable_dao = PayableDaoMock::default().transaction_canceled_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default().mark_failure_result(Err( + PendingPayableDaoError::UpdateFailed("no no no".to_string()), + )); + let subject = PendingPayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + let rowid = 2; + let hash = H256::from("sometransactionhash".keccak256()); + let transaction_id = PendingPayableId { hash, rowid }; + + subject.order_cancel_failed_transaction(transaction_id, &Logger::new("test")); + } + + #[test] + #[should_panic( + expected = "Was unable to delete payable fingerprint '0x0000…0315' after successful \ + transaction due to 'RecordDeletion(\"the database is fooling around with us\")'" + )] + fn handle_confirm_pending_transaction_panics_while_deleting_pending_payable_fingerprint() { + let hash = H256::from_uint(&U256::from(789)); + let rowid = 3; + let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprint_result(Err( + PendingPayableDaoError::RecordDeletion( + "the database is fooling around with us".to_string(), + ), + )); + let mut subject = PendingPayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); + pending_payable_fingerprint.rowid_opt = Some(rowid); + pending_payable_fingerprint.hash = hash; + + subject.order_confirm_transaction(pending_payable_fingerprint, &Logger::new("test")); + } + + #[test] + fn order_confirm_transaction_works() { + init_test_logging(); + let test_name = "order_confirm_transaction_works"; + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let delete_pending_payable_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transaction_confirmed_params(&transaction_confirmed_params_arc) + .transaction_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default() + .delete_fingerprint_params(&delete_pending_payable_fingerprint_params_arc) + .delete_fingerprint_result(Ok(())); + let mut subject = PendingPayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + let tx_hash = H256::from("sometransactionhash".keccak256()); + let amount = 4567; + let timestamp_from_time_of_payment = from_time_t(200_000_000); + let rowid = 2; + let pending_payable_fingerprint = PendingPayableFingerprint { + rowid_opt: Some(rowid), + timestamp: timestamp_from_time_of_payment, + hash: tx_hash, + attempt_opt: Some(1), + amount, + process_error: None, + }; + + subject.order_confirm_transaction( + pending_payable_fingerprint.clone(), + &Logger::new(test_name), + ); + + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + let delete_pending_payable_fingerprint_params = + delete_pending_payable_fingerprint_params_arc + .lock() + .unwrap(); + assert_eq!( + *transaction_confirmed_params, + vec![pending_payable_fingerprint] + ); + assert_eq!(*delete_pending_payable_fingerprint_params, vec![rowid]); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!( + "DEBUG: {test_name}: Confirmation of transaction 0x051a…8c19; \ + record for payable was modified" + ), + &format!( + "INFO: {test_name}: Transaction \ + 0x051aae12b9595ccaa43c2eabfd5b86347c37fa0988167165b0b17b23fcaa8c19 \ + has gone through the whole confirmation process succeeding" + ), + ]); + } + + #[test] + fn total_paid_payable_rises_with_each_bill_paid() { + let test_name = "total_paid_payable_rises_with_each_bill_paid"; + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let fingerprint = PendingPayableFingerprint { + rowid_opt: Some(5), + timestamp: from_time_t(189_999_888), + hash: H256::from_uint(&U256::from(56789)), + attempt_opt: Some(1), + amount: 5478, + process_error: None, + }; + let payable_dao = PayableDaoMock::default() + .transaction_confirmed_params(&transaction_confirmed_params_arc) + .transaction_confirmed_result(Ok(())) + .transaction_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprint_result(Ok(())); + let mut subject = PendingPayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + let mut financial_statistics = subject.financial_statistics(); + financial_statistics.total_paid_payable += 1111; + subject.financial_statistics.replace(financial_statistics); + + subject.order_confirm_transaction(fingerprint.clone(), &Logger::new(test_name)); + + let total_paid_payable = subject.financial_statistics().total_paid_payable; + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + assert_eq!(total_paid_payable, 1111 + 5478); + assert_eq!(*transaction_confirmed_params, vec![fingerprint]) + } + + #[test] + #[should_panic( + expected = "Was unable to uncheck pending payable '0x0000…0315' after confirmation due to \ + 'RusqliteError(\"record change not successful\")'" + )] + fn order_confirm_transaction_panics_on_unchecking_payable_table() { + let hash = H256::from_uint(&U256::from(789)); + let rowid = 3; + let payable_dao = PayableDaoMock::new().transaction_confirmed_result(Err( + PayableDaoError::RusqliteError("record change not successful".to_string()), + )); + let mut subject = PendingPayableScanner::default().payable_dao(payable_dao); + let mut fingerprint = make_pending_payable_fingerprint(); + fingerprint.rowid_opt = Some(rowid); + fingerprint.hash = hash; + + subject.order_confirm_transaction(fingerprint, &Logger::new("test")); + } + + #[test] + fn pending_payable_scanner_handles_report_transaction_receipts_message() { + init_test_logging(); + let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message"; + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::new() + .transaction_confirmed_params(&transaction_confirmed_params_arc) + .transaction_confirmed_result(Ok(())) + .transaction_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::new() + .delete_fingerprint_result(Ok(())) + .delete_fingerprint_result(Ok(())); + let mut subject = PendingPayableScanner::default() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao); + let transaction_hash_1 = H256::from_uint(&U256::from(4545)); + let mut transaction_receipt_1 = TransactionReceipt::default(); + transaction_receipt_1.transaction_hash = transaction_hash_1; + transaction_receipt_1.status = Some(U64::from(1)); //success + let fingerprint_1 = PendingPayableFingerprint { + rowid_opt: Some(5), + timestamp: from_time_t(200_000_000), + hash: transaction_hash_1, + attempt_opt: Some(2), + amount: 444, + process_error: None, + }; + let transaction_hash_2 = H256::from_uint(&U256::from(1234)); + let mut transaction_receipt_2 = TransactionReceipt::default(); + transaction_receipt_2.transaction_hash = transaction_hash_2; + transaction_receipt_2.status = Some(U64::from(1)); //success + let fingerprint_2 = PendingPayableFingerprint { + rowid_opt: Some(10), + timestamp: from_time_t(199_780_000), + hash: transaction_hash_2, + attempt_opt: Some(15), + amount: 1212, + process_error: None, + }; + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: vec![ + (Some(transaction_receipt_1), fingerprint_1.clone()), + (Some(transaction_receipt_2), fingerprint_2.clone()), + ], + response_skeleton_opt: None, + }; + subject.mark_as_started(SystemTime::now()); + + let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + assert_eq!(message_opt, None); + assert_eq!( + *transaction_confirmed_params, + vec![fingerprint_1, fingerprint_2] + ); + assert_eq!(subject.scan_started_at(), None); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!( + "INFO: {}: Transaction {:?} has gone through the whole confirmation process succeeding", + test_name, transaction_hash_1 + ), + &format!( + "INFO: {}: Transaction {:?} has gone through the whole confirmation process succeeding", + test_name, transaction_hash_2 + ), + "INFO: pending_payable_scanner_handles_report_transaction_receipts_message: The \ + Pending Payable scan ended in \\d+ms.", + + ]); + } + + #[test] + fn receivable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "receivable_scanner_can_initiate_a_scan"; + let now = SystemTime::now(); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_result(vec![]) + .paid_delinquencies_result(vec![]); + let earning_wallet = make_wallet("earning"); + let mut receivable_scanner = ReceivableScanner::default() + .receivable_dao(receivable_dao) + .earning_wallet(earning_wallet.clone()); + + let result = receivable_scanner.begin_scan(now, None, &Logger::new(test_name)); + + let is_scan_running = receivable_scanner.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Ok(RetrieveTransactions { + recipient: earning_wallet.clone(), + response_skeleton_opt: None + }) + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Scanning for receivables to {earning_wallet}" + )); + } + + #[test] + fn receivable_scanner_throws_error_in_case_scan_is_already_running() { + let now = SystemTime::now(); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_result(vec![]) + .paid_delinquencies_result(vec![]); + let earning_wallet = make_wallet("earning"); + let mut receivable_scanner = ReceivableScanner::default() + .receivable_dao(receivable_dao) + .earning_wallet(earning_wallet.clone()); + let _ = receivable_scanner.begin_scan(now, None, &Logger::new("test")); + + let result = receivable_scanner.begin_scan(SystemTime::now(), None, &Logger::new("test")); + + let is_scan_running = receivable_scanner.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + } + + #[test] + fn receivable_scanner_scans_for_delinquencies() { + init_test_logging(); + let newly_banned_1 = make_receivable_account(1234, true); + let newly_banned_2 = make_receivable_account(2345, true); + let newly_unbanned_1 = make_receivable_account(3456, false); + let newly_unbanned_2 = make_receivable_account(4567, false); + let new_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); + let paid_delinquencies_parameters_arc = Arc::new(Mutex::new(vec![])); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_parameters(&new_delinquencies_parameters_arc) + .new_delinquencies_result(vec![newly_banned_1.clone(), newly_banned_2.clone()]) + .paid_delinquencies_parameters(&paid_delinquencies_parameters_arc) + .paid_delinquencies_result(vec![newly_unbanned_1.clone(), newly_unbanned_2.clone()]); + let ban_parameters_arc = Arc::new(Mutex::new(vec![])); + let unban_parameters_arc = Arc::new(Mutex::new(vec![])); + let payment_thresholds = make_custom_payment_thresholds(); + let banned_dao = BannedDaoMock::new() + .ban_list_result(vec![]) + .ban_parameters(&ban_parameters_arc) + .unban_parameters(&unban_parameters_arc); + let mut receivable_scanner = ReceivableScanner::default() + .receivable_dao(receivable_dao) + .banned_dao(banned_dao) + .payment_thresholds(payment_thresholds.clone()); + + let _result = receivable_scanner.begin_scan( + SystemTime::now(), + None, + &Logger::new("DELINQUENCY_TEST"), + ); + + let new_delinquencies_parameters: MutexGuard> = + new_delinquencies_parameters_arc.lock().unwrap(); + assert_eq!( + payment_thresholds.clone(), + new_delinquencies_parameters[0].1 + ); + let paid_delinquencies_parameters: MutexGuard> = + paid_delinquencies_parameters_arc.lock().unwrap(); + assert_eq!(payment_thresholds, paid_delinquencies_parameters[0]); + let ban_parameters = ban_parameters_arc.lock().unwrap(); + assert!(ban_parameters.contains(&newly_banned_1.wallet)); + assert!(ban_parameters.contains(&newly_banned_2.wallet)); + assert_eq!(2, ban_parameters.len()); + let unban_parameters = unban_parameters_arc.lock().unwrap(); + assert!(unban_parameters.contains(&newly_unbanned_1.wallet)); + assert!(unban_parameters.contains(&newly_unbanned_2.wallet)); + assert_eq!(2, unban_parameters.len()); + let tlh = TestLogHandler::new(); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c65743132333464 \ + \\(balance: 1234 MASQ, age: \\d+ sec\\) banned for delinquency", + ); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c65743233343564 \ + \\(balance: 2345 MASQ, age: \\d+ sec\\) banned for delinquency", + ); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c6574333435366e \ + \\(balance: 3456 MASQ, age: \\d+ sec\\) is no longer delinquent: unbanned", + ); + tlh.exists_log_matching( + "INFO: DELINQUENCY_TEST: Wallet 0x00000000000000000077616c6c6574343536376e \ + \\(balance: 4567 MASQ, age: \\d+ sec\\) is no longer delinquent: unbanned", + ); + } + + #[test] + fn receivable_scanner_aborts_scan_if_no_payments_were_supplied() { + init_test_logging(); + let test_name = "receivable_scanner_aborts_scan_if_no_payments_were_supplied"; + let mut subject = ReceivableScanner::default(); + let msg = ReceivedPayments { + timestamp: SystemTime::now(), + payments: vec![], + response_skeleton_opt: None, + }; + + let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + + assert_eq!(message_opt, None); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Handling received payments we got zero payments but \ + expected some, skipping database operations" + )); + } + + #[test] + fn receivable_scanner_handles_received_payments_message() { + init_test_logging(); + let test_name = "receivable_scanner_handles_received_payments_message"; + let now = SystemTime::now(); + let more_money_received_params_arc = Arc::new(Mutex::new(vec![])); + let receivable_dao = ReceivableDaoMock::new() + .more_money_received_parameters(&more_money_received_params_arc) + .more_money_receivable_result(Ok(())); + let mut subject = ReceivableScanner::default().receivable_dao(receivable_dao); + let mut financial_statistics = subject.financial_statistics(); + financial_statistics.total_paid_receivable += 2222; + subject.financial_statistics.replace(financial_statistics); + let receivables = vec![ + BlockchainTransaction { + block_number: 4578910, + from: make_wallet("wallet_1"), + gwei_amount: 45780, + }, + BlockchainTransaction { + block_number: 4569898, + from: make_wallet("wallet_2"), + gwei_amount: 33345, + }, + ]; + let msg = ReceivedPayments { + timestamp: now, + payments: receivables.clone(), + response_skeleton_opt: None, + }; + subject.mark_as_started(SystemTime::now()); + + let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + + let total_paid_receivable = subject.financial_statistics().total_paid_receivable; + let more_money_received_params = more_money_received_params_arc.lock().unwrap(); + assert_eq!(message_opt, None); + assert_eq!(subject.scan_started_at(), None); + assert_eq!(total_paid_receivable, 2222 + 45780 + 33345); + assert_eq!(*more_money_received_params, vec![(now, receivables)]); + TestLogHandler::new().exists_log_matching( + "INFO: receivable_scanner_handles_received_payments_message: The Receivable scan ended in \\d+ms.", + ); + } +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 33255d35b..dd330ff93 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -11,7 +11,10 @@ use crate::accountant::pending_payable_dao::{ use crate::accountant::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; -use crate::accountant::{Accountant, PendingPayableId}; +use crate::accountant::scanners::scanners::{ + PayableScanner, PendingPayableScanner, ReceivableScanner, +}; +use crate::accountant::{Accountant, PendingPayableId, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::banned_dao::{BannedDao, BannedDaoFactory}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::BlockchainTransaction; @@ -20,10 +23,11 @@ use crate::database::dao_utils; use crate::database::dao_utils::{from_time_t, to_time_t}; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; use crate::db_config::mocks::ConfigDaoMock; -use crate::sub_lib::accountant::{AccountantConfig, MessageIdGenerator, PaymentThresholds}; +use crate::sub_lib::accountant::FinancialStatistics; +use crate::sub_lib::accountant::{MessageIdGenerator, PaymentThresholds}; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; -use crate::test_utils::unshared_test_utils::make_populated_accountant_config_with_defaults; +use crate::test_utils::unshared_test_utils::make_bc_with_defaults; use actix::System; use ethereum_types::{BigEndianHash, H256, U256}; use rusqlite::{Connection, Error, OptionalExtension}; @@ -70,10 +74,10 @@ pub fn make_payable_account_with_recipient_and_balance_and_timestamp_opt( pub struct AccountantBuilder { config: Option, - payable_dao_factory: Option>, - receivable_dao_factory: Option>, - pending_payable_dao_factory: Option>, - banned_dao_factory: Option>, + payable_dao_factory: Option, + receivable_dao_factory: Option, + pending_payable_dao_factory: Option, + banned_dao_factory: Option, config_dao_factory: Option>, } @@ -97,24 +101,55 @@ impl AccountantBuilder { } pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> Self { - self.payable_dao_factory = Some(Box::new(PayableDaoFactoryMock::new(payable_dao))); + match self.payable_dao_factory { + None => { + self.payable_dao_factory = + Some(PayableDaoFactoryMock::new().make_result(payable_dao)) + } + Some(payable_dao_factory) => { + self.payable_dao_factory = Some(payable_dao_factory.make_result(payable_dao)) + } + } self } pub fn receivable_dao(mut self, receivable_dao: ReceivableDaoMock) -> Self { - self.receivable_dao_factory = Some(Box::new(ReceivableDaoFactoryMock::new(receivable_dao))); + match self.receivable_dao_factory { + None => { + self.receivable_dao_factory = + Some(ReceivableDaoFactoryMock::new().make_result(receivable_dao)) + } + Some(receivable_dao_factory) => { + self.receivable_dao_factory = + Some(receivable_dao_factory.make_result(receivable_dao)) + } + } self } pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { - self.pending_payable_dao_factory = Some(Box::new(PendingPayableDaoFactoryMock::new( - pending_payable_dao, - ))); + match self.pending_payable_dao_factory { + None => { + self.pending_payable_dao_factory = + Some(PendingPayableDaoFactoryMock::new().make_result(pending_payable_dao)) + } + Some(pending_payable_dao_factory) => { + self.pending_payable_dao_factory = + Some(pending_payable_dao_factory.make_result(pending_payable_dao)) + } + } self } pub fn banned_dao(mut self, banned_dao: BannedDaoMock) -> Self { - self.banned_dao_factory = Some(Box::new(BannedDaoFactoryMock::new(banned_dao))); + match self.banned_dao_factory { + None => { + self.banned_dao_factory = Some(BannedDaoFactoryMock::new().make_result(banned_dao)) + } + Some(banned_dao_factory) => { + self.banned_dao_factory = Some(banned_dao_factory.make_result(banned_dao)) + } + } self } @@ -124,110 +159,140 @@ impl AccountantBuilder { } pub fn build(self) -> Accountant { - let config = self.config.unwrap_or({ - let mut config = BootstrapperConfig::default(); - config.accountant_config_opt = Some(make_populated_accountant_config_with_defaults()); - config - }); - let payable_dao_factory = self - .payable_dao_factory - .unwrap_or(Box::new(PayableDaoFactoryMock::new(PayableDaoMock::new()))); - let receivable_dao_factory = - self.receivable_dao_factory - .unwrap_or(Box::new(ReceivableDaoFactoryMock::new( - ReceivableDaoMock::new(), - ))); - let pending_payable_dao_factory = self.pending_payable_dao_factory.unwrap_or(Box::new( - PendingPayableDaoFactoryMock::new(PendingPayableDaoMock::default()), - )); + let mut config = self.config.unwrap_or(make_bc_with_defaults()); + let payable_dao_factory = self.payable_dao_factory.unwrap_or( + PayableDaoFactoryMock::new() + .make_result(PayableDaoMock::new()) + .make_result(PayableDaoMock::new()) + .make_result(PayableDaoMock::new()), + ); + let receivable_dao_factory = self.receivable_dao_factory.unwrap_or( + ReceivableDaoFactoryMock::new() + .make_result(ReceivableDaoMock::new()) + .make_result(ReceivableDaoMock::new()), + ); + let pending_payable_dao_factory = self.pending_payable_dao_factory.unwrap_or( + PendingPayableDaoFactoryMock::new() + .make_result(PendingPayableDaoMock::new()) + .make_result(PendingPayableDaoMock::new()) + .make_result(PendingPayableDaoMock::new()), + ); let banned_dao_factory = self .banned_dao_factory - .unwrap_or(Box::new(BannedDaoFactoryMock::new(BannedDaoMock::new()))); + .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); let accountant = Accountant::new( - &config, - payable_dao_factory, - receivable_dao_factory, - pending_payable_dao_factory, - banned_dao_factory, + &mut config, + Box::new(payable_dao_factory), + Box::new(receivable_dao_factory), + Box::new(pending_payable_dao_factory), + Box::new(banned_dao_factory), ); accountant } } pub struct PayableDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl PayableDaoFactory for PayableDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().remove(0)) + if self.make_results.borrow().len() == 0 { + panic!( + "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + ) + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl PayableDaoFactoryMock { - pub fn new(mock: PayableDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(vec![mock]), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: PayableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } } pub struct ReceivableDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl ReceivableDaoFactory for ReceivableDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().remove(0)) + if self.make_results.borrow().len() == 0 { + panic!( + "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only supplied for Accountant and not for the Scanner while building Accountant." + ) + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl ReceivableDaoFactoryMock { - pub fn new(mock: ReceivableDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(vec![mock]), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: ReceivableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } } pub struct BannedDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl BannedDaoFactory for BannedDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().take().unwrap()) + if self.make_results.borrow().len() == 0 { + panic!("BannedDao Missing.") + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl BannedDaoFactoryMock { - pub fn new(mock: BannedDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(Some(mock)), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: BannedDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } } @@ -638,23 +703,14 @@ impl BannedDaoMock { } } -pub fn bc_from_ac_plus_earning_wallet( - ac: AccountantConfig, - earning_wallet: Wallet, -) -> BootstrapperConfig { - let mut bc = BootstrapperConfig::new(); - bc.accountant_config_opt = Some(ac); +pub fn bc_from_earning_wallet(earning_wallet: Wallet) -> BootstrapperConfig { + let mut bc = make_bc_with_defaults(); bc.earning_wallet = earning_wallet; bc } -pub fn bc_from_ac_plus_wallets( - ac: AccountantConfig, - consuming_wallet: Wallet, - earning_wallet: Wallet, -) -> BootstrapperConfig { - let mut bc = BootstrapperConfig::new(); - bc.accountant_config_opt = Some(ac); +pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> BootstrapperConfig { + let mut bc = make_bc_with_defaults(); bc.consuming_wallet_opt = Some(consuming_wallet); bc.earning_wallet = earning_wallet; bc @@ -727,6 +783,10 @@ impl PendingPayableDao for PendingPayableDaoMock { } impl PendingPayableDaoMock { + pub fn new() -> Self { + PendingPayableDaoMock::default() + } + pub fn fingerprint_rowid_params(mut self, params: &Arc>>) -> Self { self.fingerprint_rowid_params = params.clone(); self @@ -794,31 +854,132 @@ impl PendingPayableDaoMock { } pub struct PendingPayableDaoFactoryMock { - called: Rc>, - mock: RefCell>, + make_params: Arc>>, + make_results: RefCell>>, } impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { fn make(&self) -> Box { - *self.called.borrow_mut() = true; - Box::new(self.mock.borrow_mut().remove(0)) + if self.make_results.borrow().len() == 0 { + panic!( + "PendingPayableDao Missing. This problem mostly occurs when PendingPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + ) + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) } } impl PendingPayableDaoFactoryMock { - pub fn new(mock: PendingPayableDaoMock) -> Self { + pub fn new() -> Self { Self { - called: Rc::new(RefCell::new(false)), - mock: RefCell::new(vec![mock]), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn called(mut self, called: &Rc>) -> Self { - self.called = called.clone(); + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: PendingPayableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } } +impl Default for PayableScanner { + fn default() -> Self { + PayableScanner::new( + Box::new(PayableDaoMock::new()), + Box::new(PendingPayableDaoMock::new()), + Rc::new(PaymentThresholds::default()), + ) + } +} + +impl PayableScanner { + pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> Self { + self.payable_dao = Box::new(payable_dao); + self + } + + pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { + self.pending_payable_dao = Box::new(pending_payable_dao); + self + } +} + +impl Default for PendingPayableScanner { + fn default() -> Self { + PendingPayableScanner::new( + Box::new(PayableDaoMock::new()), + Box::new(PendingPayableDaoMock::new()), + Rc::new(PaymentThresholds::default()), + DEFAULT_PENDING_TOO_LONG_SEC, + Rc::new(RefCell::new(FinancialStatistics::default())), + ) + } +} + +impl PendingPayableScanner { + pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> Self { + self.payable_dao = Box::new(payable_dao); + self + } + + pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { + self.pending_payable_dao = Box::new(pending_payable_dao); + self + } +} + +impl Default for ReceivableScanner { + fn default() -> Self { + ReceivableScanner::new( + Box::new(ReceivableDaoMock::new()), + Box::new(BannedDaoMock::new()), + Rc::new(PaymentThresholds::default()), + Rc::new(make_wallet("earning")), + Rc::new(RefCell::new(FinancialStatistics::default())), + ) + } +} + +impl ReceivableScanner { + pub fn receivable_dao(mut self, receivable_dao: ReceivableDaoMock) -> Self { + self.receivable_dao = Box::new(receivable_dao); + self + } + + pub fn banned_dao(mut self, banned_dao: BannedDaoMock) -> Self { + self.banned_dao = Box::new(banned_dao); + self + } + + pub fn payment_thresholds(mut self, payment_thresholds: PaymentThresholds) -> Self { + self.common.payment_thresholds = Rc::new(payment_thresholds); + self + } + + pub fn earning_wallet(mut self, earning_wallet: Wallet) -> Self { + self.earning_wallet = Rc::new(earning_wallet); + self + } +} + +pub fn make_custom_payment_thresholds() -> PaymentThresholds { + PaymentThresholds { + threshold_interval_sec: 2_592_000, + debt_threshold_gwei: 1_000_000_000, + payment_grace_period_sec: 86_400, + maturity_threshold_sec: 86_400, + permanent_debt_allowed_gwei: 10_000_000, + unban_below_gwei: 10_000_000, + } +} + pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { PendingPayableFingerprint { rowid_opt: Some(33), @@ -830,6 +991,52 @@ pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { } } +pub fn make_payables( + now: SystemTime, + payment_thresholds: &PaymentThresholds, +) -> ( + Vec, + Vec, + Vec, +) { + let unqualified_payable_accounts = vec![PayableAccount { + wallet: make_wallet("wallet1"), + balance: payment_thresholds.permanent_debt_allowed_gwei + 1, + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec + 1, + ), + pending_payable_opt: None, + }]; + let qualified_payable_accounts = vec![ + PayableAccount { + wallet: make_wallet("wallet2"), + balance: payment_thresholds.permanent_debt_allowed_gwei + 1_000_000_000, + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec - 1, + ), + pending_payable_opt: None, + }, + PayableAccount { + wallet: make_wallet("wallet3"), + balance: payment_thresholds.permanent_debt_allowed_gwei + 1_200_000_000, + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec - 100, + ), + pending_payable_opt: None, + }, + ]; + + let mut all_non_pending_payables = Vec::new(); + all_non_pending_payables.extend(qualified_payable_accounts.clone()); + all_non_pending_payables.extend(unqualified_payable_accounts.clone()); + + ( + qualified_payable_accounts, + unqualified_payable_accounts, + all_non_pending_payables, + ) +} + #[derive(Default)] pub struct MessageIdGeneratorMock { ids: RefCell>, diff --git a/node/src/accountant/tools.rs b/node/src/accountant/tools.rs index 399d9e536..13270a190 100644 --- a/node/src/accountant/tools.rs +++ b/node/src/accountant/tools.rs @@ -1,165 +1,619 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub(in crate::accountant) mod accountant_tools { - use crate::accountant::{ - Accountant, CancelFailedPendingTransaction, ConfirmPendingTransaction, - RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, ScanForPendingPayables, - ScanForReceivables, - }; - use crate::sub_lib::utils::{NotifyHandle, NotifyLaterHandle}; - use actix::{Context, Recipient}; - #[cfg(test)] - use std::any::Any; - - pub struct Scanners { - pub pending_payables: Box, - pub payables: Box, - pub receivables: Box, - } - - impl Default for Scanners { - fn default() -> Self { - Scanners { - pending_payables: Box::new(PendingPayablesScanner), - payables: Box::new(PayablesScanner), - receivables: Box::new(ReceivablesScanner), +pub(crate) mod payable_scanner_tools { + use crate::accountant::payable_dao::{Payable, PayableAccount}; + use crate::accountant::SentPayable; + use crate::blockchain::blockchain_interface::BlockchainError; + use crate::sub_lib::accountant::PaymentThresholds; + use masq_lib::logger::Logger; + use masq_lib::utils::plus; + use std::time::SystemTime; + + //for debugging only + pub(crate) fn investigate_debt_extremes( + timestamp: SystemTime, + all_non_pending_payables: &[PayableAccount], + ) -> String { + if all_non_pending_payables.is_empty() { + return "Payable scan found no debts".to_string(); + } + #[derive(Clone, Copy, Default)] + struct PayableInfo { + balance: i64, + age: u64, + } + + fn bigger(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { + if payable_1.balance > payable_2.balance { + payable_1 + } else if payable_2.balance > payable_1.balance { + payable_2 + } else { + if payable_1.age != payable_2.age { + return older(payable_1, payable_2); + } + payable_1 } } - } - pub trait Scanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option); - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context); - as_any_dcl!(); + fn older(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { + if payable_1.age > payable_2.age { + payable_1 + } else if payable_2.age > payable_1.age { + payable_2 + } else { + if payable_1.balance != payable_2.balance { + return bigger(payable_1, payable_2); + } + payable_1 + } + } + + let init = (PayableInfo::default(), PayableInfo::default()); + let (biggest, oldest) = all_non_pending_payables + .iter() + .map(|payable| PayableInfo { + balance: payable.balance, + age: payable_time_diff(timestamp, payable), + }) + .fold(init, |so_far, payable| { + let (mut biggest, mut oldest) = so_far; + + biggest = bigger(biggest, payable); + oldest = older(oldest, payable); + + (biggest, oldest) + }); + format!("Payable scan found {} debts; the biggest is {} owed for {}sec, the oldest is {} owed for {}sec", + all_non_pending_payables.len(), biggest.balance, biggest.age, + oldest.balance, oldest.age) } - #[derive(Debug, PartialEq, Eq)] - pub struct PendingPayablesScanner; + pub(crate) fn is_payable_qualified( + time: SystemTime, + payable: &PayableAccount, + payment_thresholds: &PaymentThresholds, + ) -> Option { + // TODO: This calculation should be done in the database, if possible + let maturity_time_limit = payment_thresholds.maturity_threshold_sec as u64; + let permanent_allowed_debt = payment_thresholds.permanent_debt_allowed_gwei; + let time_since_last_paid = payable_time_diff(time, payable); + let payable_balance = payable.balance; - impl Scanner for PendingPayablesScanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option) { - accountant.scan_for_pending_payable(response_skeleton_opt) + if time_since_last_paid <= maturity_time_limit { + return None; } - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context) { - let _ = accountant - .confirmation_tools - .notify_later_scan_for_pending_payable - .notify_later( - ScanForPendingPayables { - response_skeleton_opt: None, // because scheduled scans don't respond - }, - accountant - .config - .scan_intervals - .pending_payable_scan_interval, - ctx, - ); + + if payable_balance <= permanent_allowed_debt { + return None; } - as_any_impl!(); + + let payout_threshold = calculate_payout_threshold(time_since_last_paid, payment_thresholds); + if payable_balance as f64 <= payout_threshold { + return None; + } + + Some(payout_threshold as u64) } - #[derive(Debug, PartialEq, Eq)] - pub struct PayablesScanner; + pub(crate) fn payable_time_diff(time: SystemTime, payable: &PayableAccount) -> u64 { + time.duration_since(payable.last_paid_timestamp) + .expect("Payable time is corrupt") + .as_secs() + } - impl Scanner for PayablesScanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option) { - accountant.scan_for_payables(response_skeleton_opt) - } + pub(crate) fn calculate_payout_threshold( + x: u64, + payment_thresholds: &PaymentThresholds, + ) -> f64 { + let m = -((payment_thresholds.debt_threshold_gwei as f64 + - payment_thresholds.permanent_debt_allowed_gwei as f64) + / (payment_thresholds.threshold_interval_sec as f64 + - payment_thresholds.maturity_threshold_sec as f64)); + let b = payment_thresholds.debt_threshold_gwei as f64 + - m * payment_thresholds.maturity_threshold_sec as f64; + m * x as f64 + b + } - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context) { - let _ = accountant - .confirmation_tools - .notify_later_scan_for_payable - .notify_later( - ScanForPayables { - response_skeleton_opt: None, - }, - accountant.config.scan_intervals.payable_scan_interval, - ctx, - ); + pub(crate) fn exceeded_summary( + time: SystemTime, + payable: &PayableAccount, + threshold: u64, + ) -> String { + format!( + "{} owed for {}sec exceeds threshold: {}; creditor: {}\n", + payable.balance, + payable_time_diff(time, payable), + threshold, + payable.wallet.clone(), + ) + } + + pub(crate) fn qualified_payables_and_summary( + time: SystemTime, + non_pending_payables: Vec, + payment_thresholds: &PaymentThresholds, + ) -> (Vec, String) { + let mut qualified_summary = String::from("Paying qualified debts:\n"); + let mut qualified_payables: Vec = vec![]; + + for payable in non_pending_payables { + if let Some(threshold) = is_payable_qualified(time, &payable, payment_thresholds) { + let payable_summary = exceeded_summary(time, &payable, threshold); + qualified_summary.push_str(&payable_summary); + qualified_payables.push(payable); + } } - as_any_impl!(); + let summary = match qualified_payables.is_empty() { + true => String::from("No Qualified Payables found."), + false => qualified_summary, + }; + + (qualified_payables, summary) + } + + pub(crate) fn separate_early_errors( + sent_payments: &SentPayable, + logger: &Logger, + ) -> (Vec, Vec) { + sent_payments + .payable + .iter() + .fold((vec![], vec![]), |so_far, payment| { + match payment { + Ok(payment_sent) => (plus(so_far.0, payment_sent.clone()), so_far.1), + Err(error) => { + logger.warning(|| match &error { + BlockchainError::TransactionFailed { .. } => format!("Encountered transaction error at this end: '{:?}'", error), + x => format!("Outbound transaction failure due to '{:?}'. Please check your blockchain service URL configuration.", x) + }); + (so_far.0, plus(so_far.1, error.clone())) + } + } + }) } +} - #[derive(Debug, PartialEq, Eq)] - pub struct ReceivablesScanner; +pub(crate) mod pending_payable_scanner_tools { + use crate::accountant::{PendingPayableId, PendingTransactionStatus}; + use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; + use masq_lib::logger::Logger; + use masq_lib::utils::ExpectValue; + use std::time::SystemTime; - impl Scanner for ReceivablesScanner { - fn scan(&self, accountant: &Accountant, response_skeleton_opt: Option) { - // TODO: Figure out how to combine the results of these two into a single response to the UI - accountant.scan_for_received_payments(response_skeleton_opt); - accountant.scan_for_delinquencies() - } + pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { + timestamp + .elapsed() + .expect("time calculation for elapsed failed") + .as_millis() + } - fn notify_later_assertable(&self, accountant: &Accountant, ctx: &mut Context) { - let _ = accountant - .confirmation_tools - .notify_later_scan_for_receivable - .notify_later( - ScanForReceivables { - response_skeleton_opt: None, - }, - accountant.config.scan_intervals.receivable_scan_interval, - ctx, - ); + pub fn handle_none_status( + fingerprint: &PendingPayableFingerprint, + max_pending_interval: u64, + logger: &Logger, + ) -> PendingTransactionStatus { + info!( + logger, + "Pending transaction '{}' couldn't be confirmed at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + let elapsed = fingerprint + .timestamp + .elapsed() + .expect("we should be older now"); + let transaction_id = PendingPayableId { + hash: fingerprint.hash, + rowid: fingerprint.rowid_opt.expectv("initialized rowid"), + }; + if max_pending_interval <= elapsed.as_secs() { + error!( + logger, + "Pending transaction '{}' has exceeded the maximum pending time \ + ({}sec) and the confirmation process is going to be aborted now \ + at the final attempt {}; manual resolution is required from the \ + user to complete the transaction.", + fingerprint.hash, + max_pending_interval, + fingerprint.attempt_opt.expectv("initialized attempt") + ); + PendingTransactionStatus::Failure(transaction_id) + } else { + PendingTransactionStatus::StillPending(transaction_id) } + } + + pub fn handle_status_with_success( + fingerprint: &PendingPayableFingerprint, + logger: &Logger, + ) -> PendingTransactionStatus { + info!( + logger, + "Transaction '{}' has been added to the blockchain; detected locally at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + PendingTransactionStatus::Confirmed(fingerprint.clone()) + } - as_any_impl!(); + pub fn handle_status_with_failure( + fingerprint: &PendingPayableFingerprint, + logger: &Logger, + ) -> PendingTransactionStatus { + error!( + logger, + "Pending transaction '{}' announced as a failure, interpreting attempt \ + {} after {}ms from the sending", + fingerprint.hash, + fingerprint.attempt_opt.expectv("initialized attempt"), + elapsed_in_ms(fingerprint.timestamp) + ); + PendingTransactionStatus::Failure(fingerprint.into()) } +} - //this is for turning off a certain scanner in testing to prevent it make "noise" - #[derive(Debug, PartialEq, Eq)] - pub struct NullScanner; +pub(crate) mod receivable_scanner_tools { + use crate::accountant::receivable_dao::ReceivableAccount; + use std::time::{Duration, SystemTime}; - impl Scanner for NullScanner { - fn scan(&self, _accountant: &Accountant, _response_skeleton_opt: Option) { - } - fn notify_later_assertable( - &self, - _accountant: &Accountant, - _ctx: &mut Context, - ) { - } - as_any_impl!(); - } - - #[derive(Default)] - pub struct TransactionConfirmationTools { - pub notify_later_scan_for_pending_payable: - Box>, - pub notify_later_scan_for_payable: Box>, - pub notify_later_scan_for_receivable: - Box>, - pub notify_confirm_transaction: - Box>, - pub notify_cancel_failed_transaction: - Box>, - pub request_transaction_receipts_subs_opt: Option>, + pub(crate) fn balance_and_age( + time: SystemTime, + account: &ReceivableAccount, + ) -> (String, Duration) { + let balance = format!("{}", (account.balance as f64) / 1_000_000_000.0); + let age = time + .duration_since(account.last_received_timestamp) + .unwrap_or_else(|_| Duration::new(0, 0)); + (balance, age) + } +} + +pub mod common_tools { + use std::time::SystemTime; + use time::format_description::parse; + use time::OffsetDateTime; + + const TIME_FORMATTING_STRING: &str = + "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"; + + pub fn timestamp_as_string(timestamp: &SystemTime) -> String { + let offset_date_time = OffsetDateTime::from(*timestamp); + offset_date_time + .format(&parse(TIME_FORMATTING_STRING).unwrap()) + .unwrap() } } #[cfg(test)] mod tests { - use crate::accountant::tools::accountant_tools::{ - PayablesScanner, PendingPayablesScanner, ReceivablesScanner, Scanners, + use crate::accountant::payable_dao::{Payable, PayableAccount}; + use crate::accountant::receivable_dao::ReceivableAccount; + use crate::accountant::tools::payable_scanner_tools::{ + calculate_payout_threshold, exceeded_summary, investigate_debt_extremes, + is_payable_qualified, payable_time_diff, qualified_payables_and_summary, + separate_early_errors, }; + use crate::accountant::tools::receivable_scanner_tools::balance_and_age; + use crate::accountant::SentPayable; + use crate::blockchain::blockchain_interface::BlockchainError; + use crate::database::dao_utils::{from_time_t, to_time_t}; + use crate::sub_lib::accountant::PaymentThresholds; + use crate::test_utils::make_wallet; + use masq_lib::logger::Logger; + use std::rc::Rc; + use std::time::SystemTime; #[test] - fn scanners_are_properly_defaulted() { - let subject = Scanners::default(); + fn payable_generated_before_maturity_time_limit_is_marked_unqualified() { + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let qualified_debt = payment_thresholds.permanent_debt_allowed_gwei + 1; + let unqualified_time = to_time_t(now) - payment_thresholds.maturity_threshold_sec + 1; + let unqualified_payable_account = PayableAccount { + wallet: make_wallet("wallet0"), + balance: qualified_debt, + last_paid_timestamp: from_time_t(unqualified_time), + pending_payable_opt: None, + }; - assert_eq!( - subject.pending_payables.as_any().downcast_ref(), - Some(&PendingPayablesScanner) - ); - assert_eq!( - subject.payables.as_any().downcast_ref(), - Some(&PayablesScanner) + let result = is_payable_qualified(now, &unqualified_payable_account, &payment_thresholds); + + assert_eq!(result, None); + } + + #[test] + fn payable_with_low_debt_is_marked_unqualified() { + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let unqualified_debt = payment_thresholds.permanent_debt_allowed_gwei - 1; + let qualified_time = to_time_t(now) - payment_thresholds.maturity_threshold_sec - 1; + let unqualified_payable_account = PayableAccount { + wallet: make_wallet("wallet0"), + balance: unqualified_debt, + last_paid_timestamp: from_time_t(qualified_time), + pending_payable_opt: None, + }; + + let result = is_payable_qualified(now, &unqualified_payable_account, &payment_thresholds); + + assert_eq!(result, None); + } + + #[test] + fn payable_with_low_payout_threshold_is_marked_unqualified() { + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let debt = payment_thresholds.permanent_debt_allowed_gwei + 1; + let time = to_time_t(now) - payment_thresholds.maturity_threshold_sec - 1; + let unqualified_payable_account = PayableAccount { + wallet: make_wallet("wallet0"), + balance: debt, + last_paid_timestamp: from_time_t(time), + pending_payable_opt: None, + }; + + let result = is_payable_qualified(now, &unqualified_payable_account, &payment_thresholds); + + assert_eq!(result, None); + } + + #[test] + fn payable_above_threshold_values_is_marked_qualified_and_returns_threshold() { + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let debt = payment_thresholds.permanent_debt_allowed_gwei + 1_000_000_000; + let time = to_time_t(now) - payment_thresholds.maturity_threshold_sec - 1; + let payment_thresholds_rc = Rc::new(payment_thresholds); + let qualified_payable = PayableAccount { + wallet: make_wallet("wallet0"), + balance: debt, + last_paid_timestamp: from_time_t(time), + pending_payable_opt: None, + }; + let threshold = calculate_payout_threshold( + payable_time_diff(now, &qualified_payable), + &payment_thresholds_rc, ); - assert_eq!( - subject.receivables.as_any().downcast_ref(), - Some(&ReceivablesScanner) - ) + eprintln!("Threshold: {}, Debt: {}", threshold, debt); + + let result = is_payable_qualified(now, &qualified_payable, &payment_thresholds_rc); + + assert_eq!(result, Some(threshold as u64)); + } + + #[test] + fn qualified_payables_can_be_filtered_out_from_non_pending_payables_along_with_their_summary() { + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let unqualified_payable_accounts = vec![PayableAccount { + wallet: make_wallet("wallet1"), + balance: payment_thresholds.permanent_debt_allowed_gwei + 1, + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec + 1, + ), + pending_payable_opt: None, + }]; + let qualified_payable_accounts = vec![ + PayableAccount { + wallet: make_wallet("wallet2"), + balance: payment_thresholds.permanent_debt_allowed_gwei + 1_000_000_000, + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec - 1, + ), + pending_payable_opt: None, + }, + PayableAccount { + wallet: make_wallet("wallet3"), + balance: payment_thresholds.permanent_debt_allowed_gwei + 1_200_000_000, + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec - 100, + ), + pending_payable_opt: None, + }, + ]; + let mut all_non_pending_payables = Vec::new(); + all_non_pending_payables.extend(qualified_payable_accounts.clone()); + all_non_pending_payables.extend(unqualified_payable_accounts.clone()); + + let (qualified_payables, summary) = + qualified_payables_and_summary(now, all_non_pending_payables, &payment_thresholds); + + let mut expected_summary = String::from("Paying qualified debts:\n"); + for payable in qualified_payable_accounts.iter() { + expected_summary.push_str(&exceeded_summary( + now, + &payable, + calculate_payout_threshold(payable_time_diff(now, &payable), &payment_thresholds) + as u64, + )) + } + + assert_eq!(qualified_payables, qualified_payable_accounts); + assert_eq!(summary, expected_summary); + } + + #[test] + fn returns_empty_array_and_summary_when_no_qualified_payables_are_found() { + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let unqualified_payable_accounts = vec![PayableAccount { + wallet: make_wallet("wallet1"), + balance: payment_thresholds.permanent_debt_allowed_gwei + 1, + last_paid_timestamp: from_time_t( + to_time_t(now) - payment_thresholds.maturity_threshold_sec + 1, + ), + pending_payable_opt: None, + }]; + + let (qualified_payables, summary) = + qualified_payables_and_summary(now, unqualified_payable_accounts, &payment_thresholds); + + assert_eq!(qualified_payables, vec![]); + assert_eq!(summary, String::from("No Qualified Payables found.")); + } + + #[test] + fn investigate_debt_extremes_picks_the_most_relevant_records() { + let now = SystemTime::now(); + let now_t = to_time_t(now); + let same_amount_significance = 2_000_000; + let same_age_significance = from_time_t(now_t - 30000); + let payables = &[ + PayableAccount { + wallet: make_wallet("wallet0"), + balance: same_amount_significance, + last_paid_timestamp: from_time_t(now_t - 5000), + pending_payable_opt: None, + }, + //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked + PayableAccount { + wallet: make_wallet("wallet1"), + balance: same_amount_significance, + last_paid_timestamp: from_time_t(now_t - 10000), + pending_payable_opt: None, + }, + //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen + PayableAccount { + wallet: make_wallet("wallet3"), + balance: 100, + last_paid_timestamp: same_age_significance, + pending_payable_opt: None, + }, + PayableAccount { + wallet: make_wallet("wallet2"), + balance: 330, + last_paid_timestamp: same_age_significance, + pending_payable_opt: None, + }, + ]; + + let result = investigate_debt_extremes(now, payables); + + assert_eq!(result, "Payable scan found 4 debts; the biggest is 2000000 owed for 10000sec, the oldest is 330 owed for 30000sec") } + + #[test] + fn balance_and_age_is_calculated_as_expected() { + let now = SystemTime::now(); + let offset = 1000; + let receivable_account = ReceivableAccount { + wallet: make_wallet("wallet0"), + balance: 10_000_000_000, + last_received_timestamp: from_time_t(to_time_t(now) - offset), + }; + + let (balance, age) = balance_and_age(now, &receivable_account); + + assert_eq!(balance, "10"); + assert_eq!(age.as_secs(), offset as u64); + } + + #[test] + fn separate_early_errors_works() { + let payable_ok = Payable { + to: make_wallet("blah"), + amount: 5555, + timestamp: SystemTime::now(), + tx_hash: Default::default(), + }; + let error = BlockchainError::SignedValueConversion(666); + let sent_payable = SentPayable { + timestamp: SystemTime::now(), + payable: vec![Ok(payable_ok.clone()), Err(error.clone())], + response_skeleton_opt: None, + }; + + let (ok, err) = separate_early_errors(&sent_payable, &Logger::new("test")); + + assert_eq!(ok, vec![payable_ok]); + assert_eq!(err, vec![error]) + } + + // TODO: Either make this test work or write an alternative test in the desired file + // #[test] + // fn threshold_calculation_depends_on_user_defined_payment_thresholds() { + // let safe_age_params_arc = Arc::new(Mutex::new(vec![])); + // let safe_balance_params_arc = Arc::new(Mutex::new(vec![])); + // let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); + // let balance = 5555; + // let how_far_in_past = Duration::from_secs(1111 + 1); + // let last_paid_timestamp = SystemTime::now().sub(how_far_in_past); + // let payable_account = PayableAccount { + // wallet: make_wallet("hi"), + // balance, + // last_paid_timestamp, + // pending_payable_opt: None, + // }; + // let custom_payment_thresholds = PaymentThresholds { + // maturity_threshold_sec: 1111, + // payment_grace_period_sec: 2222, + // permanent_debt_allowed_gwei: 3333, + // debt_threshold_gwei: 4444, + // threshold_interval_sec: 5555, + // unban_below_gwei: 3333, + // }; + // let mut bootstrapper_config = BootstrapperConfig::default(); + // bootstrapper_config.accountant_config_opt = Some(AccountantConfig { + // scan_intervals: Default::default(), + // payment_thresholds: custom_payment_thresholds, + // suppress_initial_scans: false, + // when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, + // }); + // let payable_thresholds_tools = PayableThresholdToolsMock::default() + // .is_innocent_age_params(&safe_age_params_arc) + // .is_innocent_age_result( + // how_far_in_past.as_secs() + // <= custom_payment_thresholds.maturity_threshold_sec as u64, + // ) + // .is_innocent_balance_params(&safe_balance_params_arc) + // .is_innocent_balance_result( + // balance <= custom_payment_thresholds.permanent_debt_allowed_gwei, + // ) + // .calculate_payout_threshold_params(&calculate_payable_threshold_params_arc) + // .calculate_payout_threshold_result(4567.0); //made up value + // let mut subject = AccountantBuilder::default() + // .bootstrapper_config(bootstrapper_config) + // .build(); + // subject.scanners.payables.payable_thresholds_tools = Box::new(payable_thresholds_tools); + // + // let result = subject.payable_exceeded_threshold(&payable_account); + // + // assert_eq!(result, Some(4567)); + // let mut safe_age_params = safe_age_params_arc.lock().unwrap(); + // let safe_age_single_params = safe_age_params.remove(0); + // assert_eq!(*safe_age_params, vec![]); + // let (time_elapsed, curve_derived_time) = safe_age_single_params; + // assert!( + // (how_far_in_past.as_secs() - 3) < time_elapsed + // && time_elapsed < (how_far_in_past.as_secs() + 3) + // ); + // assert_eq!( + // curve_derived_time, + // custom_payment_thresholds.maturity_threshold_sec as u64 + // ); + // let safe_balance_params = safe_balance_params_arc.lock().unwrap(); + // assert_eq!( + // *safe_balance_params, + // vec![( + // payable_account.balance, + // custom_payment_thresholds.permanent_debt_allowed_gwei + // )] + // ); + // let mut calculate_payable_curves_params = + // calculate_payable_threshold_params_arc.lock().unwrap(); + // let calculate_payable_curves_single_params = calculate_payable_curves_params.remove(0); + // assert_eq!(*calculate_payable_curves_params, vec![]); + // let (payment_thresholds, time_elapsed) = calculate_payable_curves_single_params; + // assert!( + // (how_far_in_past.as_secs() - 3) < time_elapsed + // && time_elapsed < (how_far_in_past.as_secs() + 3) + // ); + // assert_eq!(payment_thresholds, custom_payment_thresholds) + // } } diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index a8d1b0b36..68357e878 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -150,9 +150,10 @@ impl ActorSystemFactoryTools for ActorSystemFactoryToolsReal { }); let blockchain_bridge_subs = actor_factory.make_and_start_blockchain_bridge(&config); let neighborhood_subs = actor_factory.make_and_start_neighborhood(cryptdes.main, &config); + let data_directory = config.data_directory.clone(); let accountant_subs = actor_factory.make_and_start_accountant( - &config, - &config.data_directory.clone(), + &mut config.clone(), + &data_directory, &db_initializer, &BannedCacheLoaderReal {}, ); @@ -358,7 +359,7 @@ pub trait ActorFactory { ) -> NeighborhoodSubs; fn make_and_start_accountant( &self, - config: &BootstrapperConfig, + config: &mut BootstrapperConfig, data_directory: &Path, db_initializer: &dyn DbInitializer, banned_cache_loader: &dyn BannedCacheLoader, @@ -437,12 +438,12 @@ impl ActorFactory for ActorFactoryReal { fn make_and_start_accountant( &self, - config: &BootstrapperConfig, + config: &mut BootstrapperConfig, data_directory: &Path, db_initializer: &dyn DbInitializer, banned_cache_loader: &dyn BannedCacheLoader, ) -> AccountantSubs { - let cloned_config = config.clone(); + let mut cloned_config = config.clone(); let payable_dao_factory = Accountant::dao_factory(data_directory); let receivable_dao_factory = Accountant::dao_factory(data_directory); let pending_payable_dao_factory = Accountant::dao_factory(data_directory); @@ -456,7 +457,7 @@ impl ActorFactory for ActorFactoryReal { let arbiter = Arbiter::builder().stop_system_on_panic(true); let addr: Addr = arbiter.start(move |_| { Accountant::new( - &cloned_config, + &mut cloned_config, Box::new(payable_dao_factory), Box::new(receivable_dao_factory), Box::new(pending_payable_dao_factory), @@ -599,12 +600,14 @@ impl LogRecipientSetter for LogRecipientSetterReal { #[cfg(test)] mod tests { use super::*; + use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::bootstrapper::{Bootstrapper, RealUser}; use crate::database::connection_wrapper::ConnectionWrapper; use crate::node_test_utils::{ make_stream_handler_pool_subs_from, make_stream_handler_pool_subs_from_recorder, start_recorder_refcell_opt, }; + use crate::sub_lib::accountant::PaymentThresholds; use crate::sub_lib::blockchain_bridge::BlockchainBridgeConfig; use crate::sub_lib::cryptde::{PlainData, PublicKey}; use crate::sub_lib::cryptde_null::CryptDENull; @@ -627,7 +630,7 @@ mod tests { }; use crate::test_utils::recorder::{make_recorder, Recorder}; use crate::test_utils::unshared_test_utils::{ - make_populated_accountant_config_with_defaults, ArbitraryIdStamp, SystemKillerActor, + make_scan_intervals_with_defaults, ArbitraryIdStamp, SystemKillerActor, }; use crate::test_utils::{alias_cryptde, rate_pack}; use crate::test_utils::{main_cryptde, make_cryptde_pair}; @@ -853,7 +856,7 @@ mod tests { fn make_and_start_accountant( &self, - config: &BootstrapperConfig, + config: &mut BootstrapperConfig, data_directory: &Path, _db_initializer: &dyn DbInitializer, _banned_cache_loader: &dyn BannedCacheLoader, @@ -1026,7 +1029,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: Some(make_populated_accountant_config_with_defaults()), + scan_intervals_opt: Some(make_scan_intervals_with_defaults()), + suppress_initial_scans_opt: Some(false), clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1052,6 +1056,8 @@ mod tests { rate_pack(100), ), }, + payment_thresholds_opt: Some(PaymentThresholds::default()), + when_pending_too_long_opt: Some(DEFAULT_PENDING_TOO_LONG_SEC), }; let persistent_config = PersistentConfigurationMock::default().chain_name_result("eth-ropsten".to_string()); @@ -1096,7 +1102,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: None, + scan_intervals_opt: None, + suppress_initial_scans_opt: None, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1122,6 +1129,8 @@ mod tests { rate_pack(100), ), }, + payment_thresholds_opt: Default::default(), + when_pending_too_long_opt: None }; let add_mapping_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = make_subject_with_null_setter(); @@ -1392,7 +1401,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: None, + scan_intervals_opt: None, + suppress_initial_scans_opt: None, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1414,6 +1424,8 @@ mod tests { neighborhood_config: NeighborhoodConfig { mode: NeighborhoodMode::ConsumeOnly(vec![]), }, + payment_thresholds_opt: Default::default(), + when_pending_too_long_opt: None }; let system = System::new("MASQNode"); let mut subject = make_subject_with_null_setter(); @@ -1574,7 +1586,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - accountant_config_opt: None, + scan_intervals_opt: None, + suppress_initial_scans_opt: None, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1600,6 +1613,8 @@ mod tests { ), }, node_descriptor: Default::default(), + payment_thresholds_opt: Default::default(), + when_pending_too_long_opt: None, }; let subject = make_subject_with_null_setter(); let system = System::new("MASQNode"); diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index fb2892168..7d430e0cb 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -323,21 +323,19 @@ impl BlockchainBridge { _ => so_far, }); let (vector_of_results, error_opt) = short_circuit_result; - if !vector_of_results.is_empty() { - let pairs = vector_of_results - .into_iter() - .zip(msg.pending_payable.iter().cloned()) - .collect_vec(); - self.payment_confirmation - .report_transaction_receipts_sub_opt - .as_ref() - .expect("Accountant is unbound") - .try_send(ReportTransactionReceipts { - fingerprints_with_receipts: pairs, - response_skeleton_opt: msg.response_skeleton_opt, - }) - .expect("Accountant is dead"); - } + let pairs = vector_of_results + .into_iter() + .zip(msg.pending_payable.iter().cloned()) + .collect_vec(); + self.payment_confirmation + .report_transaction_receipts_sub_opt + .as_ref() + .expect("Accountant is unbound") + .try_send(ReportTransactionReceipts { + fingerprints_with_receipts: pairs, + response_skeleton_opt: msg.response_skeleton_opt, + }) + .expect("Accountant is dead"); if let Some((e, hash)) = error_opt { return Err (format! ( "Aborting scanning; request of a transaction receipt for '{:?}' failed due to '{:?}'", @@ -987,6 +985,41 @@ mod tests { for '0x000000000000000000000000000000000000000000000000000000000001348d' failed due to 'QueryFailed(\"bad bad bad\")'"); } + #[test] + fn blockchain_bridge_can_return_report_transaction_receipts_with_an_empty_vector() { + let (accountant, _, accountant_recording) = make_recorder(); + let recipient = accountant.start().recipient(); + let mut subject = BlockchainBridge::new( + Box::new(BlockchainInterfaceClandestine::new(Chain::Dev)), + Box::new(PersistentConfigurationMock::default()), + false, + Some(Wallet::new("mine")), + ); + subject + .payment_confirmation + .report_transaction_receipts_sub_opt = Some(recipient); + let msg = RequestTransactionReceipts { + pending_payable: vec![], + response_skeleton_opt: None, + }; + let system = System::new( + "blockchain_bridge_can_return_report_transaction_receipts_with_an_empty_vector", + ); + + let _ = subject.handle_request_transaction_receipts(&msg); + + System::current().stop(); + system.run(); + let recording = accountant_recording.lock().unwrap(); + assert_eq!( + recording.get_record::(0), + &ReportTransactionReceipts { + fingerprints_with_receipts: vec![], + response_skeleton_opt: None + } + ) + } + #[test] fn handle_request_transaction_receipts_short_circuits_on_failure_of_the_first_payment_and_it_does_not_send_any_message_just_aborts_and_logs( ) { diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index 47b0ad42d..0c6ca2227 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -21,7 +21,7 @@ use crate::node_configurator::{initialize_database, DirsWrapper, NodeConfigurato use crate::privilege_drop::{IdWrapper, IdWrapperReal}; use crate::server_initializer::LoggerInitializerWrapper; use crate::sub_lib::accountant; -use crate::sub_lib::accountant::AccountantConfig; +use crate::sub_lib::accountant::{PaymentThresholds, ScanIntervals}; use crate::sub_lib::blockchain_bridge::BlockchainBridgeConfig; use crate::sub_lib::cryptde::CryptDE; use crate::sub_lib::cryptde_null::CryptDENull; @@ -325,7 +325,9 @@ pub struct BootstrapperConfig { // These fields can be set while privileged without penalty pub log_level: LevelFilter, pub dns_servers: Vec, - pub accountant_config_opt: Option, + pub scan_intervals_opt: Option, + pub suppress_initial_scans_opt: Option, + pub when_pending_too_long_opt: Option, pub crash_point: CrashPoint, pub clandestine_discriminator_factories: Vec>, pub ui_gateway_config: UiGatewayConfig, @@ -337,6 +339,7 @@ pub struct BootstrapperConfig { pub alias_cryptde_null_opt: Option, pub mapping_protocol_opt: Option, pub real_user: RealUser, + pub payment_thresholds_opt: Option, // These fields must be set without privilege: otherwise the database will be created as root pub db_password_opt: Option, @@ -358,7 +361,8 @@ impl BootstrapperConfig { // These fields can be set while privileged without penalty log_level: LevelFilter::Off, dns_servers: vec![], - accountant_config_opt: Default::default(), + scan_intervals_opt: None, + suppress_initial_scans_opt: None, crash_point: CrashPoint::None, clandestine_discriminator_factories: vec![], ui_gateway_config: UiGatewayConfig { @@ -376,6 +380,7 @@ impl BootstrapperConfig { alias_cryptde_null_opt: None, mapping_protocol_opt: None, real_user: RealUser::new(None, None, None), + payment_thresholds_opt: Default::default(), // These fields must be set without privilege: otherwise the database will be created as root db_password_opt: None, @@ -385,6 +390,7 @@ impl BootstrapperConfig { neighborhood_config: NeighborhoodConfig { mode: NeighborhoodMode::ZeroHop, }, + when_pending_too_long_opt: None, } } @@ -398,7 +404,10 @@ impl BootstrapperConfig { self.earning_wallet = unprivileged.earning_wallet; self.consuming_wallet_opt = unprivileged.consuming_wallet_opt; self.db_password_opt = unprivileged.db_password_opt; - self.accountant_config_opt = unprivileged.accountant_config_opt; + self.scan_intervals_opt = unprivileged.scan_intervals_opt; + self.suppress_initial_scans_opt = unprivileged.suppress_initial_scans_opt; + self.payment_thresholds_opt = unprivileged.payment_thresholds_opt; + self.when_pending_too_long_opt = unprivileged.when_pending_too_long_opt; } pub fn exit_service_rate(&self) -> u64 { @@ -689,6 +698,7 @@ impl Bootstrapper { #[cfg(test)] mod tests { + use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::actor_system_factory::{ActorFactory, ActorSystemFactory}; use crate::bootstrapper::{ main_cryptde_ref, Bootstrapper, BootstrapperConfig, EnvironmentWrapper, PortConfiguration, @@ -724,7 +734,7 @@ mod tests { use crate::test_utils::tokio_wrapper_mocks::ReadHalfWrapperMock; use crate::test_utils::tokio_wrapper_mocks::WriteHalfWrapperMock; use crate::test_utils::unshared_test_utils::{ - make_populated_accountant_config_with_defaults, make_simplified_multi_config, + make_scan_intervals_with_defaults, make_simplified_multi_config, }; use crate::test_utils::{assert_contains, rate_pack}; use crate::test_utils::{main_cryptde, make_wallet}; @@ -1222,8 +1232,9 @@ mod tests { unprivileged_config.earning_wallet = earning_wallet.clone(); unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); - unprivileged_config.accountant_config_opt = - Some(make_populated_accountant_config_with_defaults()); + unprivileged_config.scan_intervals_opt = Some(make_scan_intervals_with_defaults()); + unprivileged_config.suppress_initial_scans_opt = Some(false); + unprivileged_config.when_pending_too_long_opt = Some(DEFAULT_PENDING_TOO_LONG_SEC); privileged_config.merge_unprivileged(unprivileged_config); @@ -1244,8 +1255,13 @@ mod tests { assert_eq!(privileged_config.consuming_wallet_opt, consuming_wallet_opt); assert_eq!(privileged_config.db_password_opt, db_password_opt); assert_eq!( - privileged_config.accountant_config_opt, - Some(make_populated_accountant_config_with_defaults()) + privileged_config.scan_intervals_opt, + Some(make_scan_intervals_with_defaults()) + ); + assert_eq!(privileged_config.suppress_initial_scans_opt, Some(false)); + assert_eq!( + privileged_config.when_pending_too_long_opt, + Some(DEFAULT_PENDING_TOO_LONG_SEC) ); //some values from the privileged config assert_eq!(privileged_config.log_level, Off); diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 1174c6ac2..a68a8b282 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -19,11 +19,13 @@ use crate::node_configurator::unprivileged_parse_args_configuration::{ use crate::node_configurator::{ data_directory_from_context, determine_config_file_path, DirsWrapper, DirsWrapperReal, }; +use crate::sub_lib::accountant::PaymentThresholds as PaymentThresholdsFromAccountant; use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; use crate::sub_lib::neighborhood::NodeDescriptor; use crate::sub_lib::neighborhood::{NeighborhoodMode as NeighborhoodModeEnum, DEFAULT_RATE_PACK}; use crate::sub_lib::utils::make_new_multi_config; use crate::test_utils::main_cryptde; +use crate::test_utils::unshared_test_utils::make_scan_intervals_with_defaults; use clap::value_t; use itertools::Itertools; use masq_lib::blockchains::chains::Chain as BlockChain; @@ -864,7 +866,10 @@ impl ValueRetriever for PaymentThresholds { _db_password_opt: &Option, ) -> Option<(String, UiSetupResponseValueStatus)> { let pc_value = pc.payment_thresholds().expectv("payment-thresholds"); - payment_thresholds_rate_pack_and_scan_intervals(pc_value, *DEFAULT_PAYMENT_THRESHOLDS) + payment_thresholds_rate_pack_and_scan_intervals( + pc_value, + PaymentThresholdsFromAccountant::default(), + ) } fn is_required(&self, _params: &SetupCluster) -> bool { @@ -915,7 +920,10 @@ impl ValueRetriever for ScanIntervals { _db_password_opt: &Option, ) -> Option<(String, UiSetupResponseValueStatus)> { let pc_value = pc.scan_intervals().expectv("scan-intervals"); - payment_thresholds_rate_pack_and_scan_intervals(pc_value, *DEFAULT_SCAN_INTERVALS) + payment_thresholds_rate_pack_and_scan_intervals( + pc_value, + make_scan_intervals_with_defaults(), + ) } fn is_required(&self, _params: &SetupCluster) -> bool { @@ -928,7 +936,7 @@ fn payment_thresholds_rate_pack_and_scan_intervals( default: T, ) -> Option<(String, UiSetupResponseValueStatus)> where - T: PartialEq + Display + Copy, + T: PartialEq + Display + Clone, { if persistent_config_value == default { Some((default.to_string(), Default)) @@ -1042,6 +1050,7 @@ mod tests { }; use crate::node_configurator::{DirsWrapper, DirsWrapperReal}; use crate::node_test_utils::DirsWrapperMock; + use crate::sub_lib::accountant::PaymentThresholds as PaymentThresholdsFromAccountant; use crate::sub_lib::cryptde::PublicKey; use crate::sub_lib::node_addr::NodeAddr; use crate::sub_lib::wallet::Wallet; @@ -3080,13 +3089,13 @@ mod tests { fn scan_intervals_computed_default_when_persistent_config_like_default() { assert_computed_default_when_persistent_config_like_default( &ScanIntervals {}, - *DEFAULT_SCAN_INTERVALS, + make_scan_intervals_with_defaults(), ) } #[test] fn scan_intervals_computed_default_persistent_config_unequal_to_default() { - let mut scan_intervals = *DEFAULT_SCAN_INTERVALS; + let mut scan_intervals = make_scan_intervals_with_defaults(); scan_intervals.pending_payable_scan_interval = scan_intervals .pending_payable_scan_interval .add(Duration::from_secs(15)); @@ -3113,7 +3122,7 @@ mod tests { #[test] fn payment_thresholds_computed_default_persistent_config_unequal_to_default() { - let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; + let mut payment_thresholds = PaymentThresholdsFromAccountant::default(); payment_thresholds.maturity_threshold_sec += 12; payment_thresholds.unban_below_gwei -= 11; payment_thresholds.debt_threshold_gwei += 1111; @@ -3151,14 +3160,16 @@ mod tests { pc_method_result_setter: &C, ) where C: Fn(PersistentConfigurationMock, T) -> PersistentConfigurationMock, - T: Display + PartialEq + Copy, + T: Display + PartialEq + Clone, { let mut bootstrapper_config = BootstrapperConfig::new(); //the rate_pack within the mode setting does not determine the result, so I just set a nonsense bootstrapper_config.neighborhood_config.mode = NeighborhoodModeEnum::OriginateOnly(vec![], rate_pack(0)); - let persistent_config = - pc_method_result_setter(PersistentConfigurationMock::new(), persistent_config_value); + let persistent_config = pc_method_result_setter( + PersistentConfigurationMock::new(), + persistent_config_value.clone(), + ); let result = subject.computed_default(&bootstrapper_config, &persistent_config, &None); diff --git a/node/src/lib.rs b/node/src/lib.rs index c7d99ce81..4850ed25c 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -6,6 +6,7 @@ pub mod sub_lib; #[macro_use] extern crate masq_lib; +extern crate core; #[cfg(test)] mod node_test_utils; diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index ab30645e5..7ff277dca 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -4,9 +4,7 @@ use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::blockchain::bip32::Bip32ECKeyProvider; use crate::bootstrapper::BootstrapperConfig; use crate::db_config::persistent_configuration::{PersistentConfigError, PersistentConfiguration}; -use crate::sub_lib::accountant::{ - AccountantConfig, PaymentThresholds, ScanIntervals, DEFAULT_EARNING_WALLET, -}; +use crate::sub_lib::accountant::{PaymentThresholds, ScanIntervals, DEFAULT_EARNING_WALLET}; use crate::sub_lib::cryptde::CryptDE; use crate::sub_lib::cryptde_null::CryptDENull; use crate::sub_lib::cryptde_real::CryptDEReal; @@ -479,30 +477,29 @@ fn configure_accountant_config( config: &mut BootstrapperConfig, persist_config: &mut dyn PersistentConfiguration, ) -> Result<(), ConfiguratorError> { + let payment_thresholds = process_combined_params( + "payment-thresholds", + multi_config, + persist_config, + |str: &str| PaymentThresholds::try_from(str), + |pc: &dyn PersistentConfiguration| pc.payment_thresholds(), + |pc: &mut dyn PersistentConfiguration, curves| pc.set_payment_thresholds(curves), + )?; + let scan_intervals = process_combined_params( + "scan-intervals", + multi_config, + persist_config, + |str: &str| ScanIntervals::try_from(str), + |pc: &dyn PersistentConfiguration| pc.scan_intervals(), + |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), + )?; let suppress_initial_scans = value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == *"off"; - - let accountant_config = AccountantConfig { - scan_intervals: process_combined_params( - "scan-intervals", - multi_config, - persist_config, - |str: &str| ScanIntervals::try_from(str), - |pc: &dyn PersistentConfiguration| pc.scan_intervals(), - |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), - )?, - payment_thresholds: process_combined_params( - "payment-thresholds", - multi_config, - persist_config, - |str: &str| PaymentThresholds::try_from(str), - |pc: &dyn PersistentConfiguration| pc.payment_thresholds(), - |pc: &mut dyn PersistentConfiguration, curves| pc.set_payment_thresholds(curves), - )?, - suppress_initial_scans, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - }; - config.accountant_config_opt = Some(accountant_config); + let when_pending_too_long = DEFAULT_PENDING_TOO_LONG_SEC; + config.payment_thresholds_opt = Some(payment_thresholds); + config.scan_intervals_opt = Some(scan_intervals); + config.suppress_initial_scans_opt = Some(suppress_initial_scans); + config.when_pending_too_long_opt = Some(when_pending_too_long); Ok(()) } @@ -1738,25 +1735,29 @@ mod tests { ) .unwrap(); - let actual_accountant_config = config.accountant_config_opt.unwrap(); - let expected_accountant_config = AccountantConfig { - scan_intervals: ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), - receivable_scan_interval: Duration::from_secs(130), - }, - payment_thresholds: PaymentThresholds { - threshold_interval_sec: 1000, - debt_threshold_gwei: 10000, - payment_grace_period_sec: 1000, - maturity_threshold_sec: 10000, - permanent_debt_allowed_gwei: 20000, - unban_below_gwei: 20000, - }, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, + let expected_scan_intervals = ScanIntervals { + pending_payable_scan_interval: Duration::from_secs(180), + payable_scan_interval: Duration::from_secs(150), + receivable_scan_interval: Duration::from_secs(130), + }; + let expected_payment_thresholds = PaymentThresholds { + threshold_interval_sec: 1000, + debt_threshold_gwei: 10000, + payment_grace_period_sec: 1000, + maturity_threshold_sec: 10000, + permanent_debt_allowed_gwei: 20000, + unban_below_gwei: 20000, }; - assert_eq!(actual_accountant_config, expected_accountant_config); + assert_eq!( + config.payment_thresholds_opt, + Some(expected_payment_thresholds) + ); + assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); + assert_eq!(config.suppress_initial_scans_opt, Some(false)); + assert_eq!( + config.when_pending_too_long_opt, + Some(DEFAULT_PENDING_TOO_LONG_SEC) + ); let set_scan_intervals_params = set_scan_intervals_params_arc.lock().unwrap(); assert_eq!(*set_scan_intervals_params, vec!["180|150|130".to_string()]); let set_payment_thresholds_params = set_payment_thresholds_params_arc.lock().unwrap(); @@ -1806,25 +1807,34 @@ mod tests { ) .unwrap(); - let actual_accountant_config = config.accountant_config_opt.unwrap(); - let expected_accountant_config = AccountantConfig { - scan_intervals: ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), - receivable_scan_interval: Duration::from_secs(130), - }, - payment_thresholds: PaymentThresholds { - threshold_interval_sec: 1000, - debt_threshold_gwei: 100000, - payment_grace_period_sec: 1000, - maturity_threshold_sec: 1000, - permanent_debt_allowed_gwei: 20000, - unban_below_gwei: 20000, - }, - suppress_initial_scans: false, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, + let expected_payment_thresholds = PaymentThresholds { + threshold_interval_sec: 1000, + debt_threshold_gwei: 100000, + payment_grace_period_sec: 1000, + maturity_threshold_sec: 1000, + permanent_debt_allowed_gwei: 20000, + unban_below_gwei: 20000, + }; + let expected_scan_intervals = ScanIntervals { + pending_payable_scan_interval: Duration::from_secs(180), + payable_scan_interval: Duration::from_secs(150), + receivable_scan_interval: Duration::from_secs(130), }; - assert_eq!(actual_accountant_config, expected_accountant_config); + let expected_suppress_initial_scans = false; + let expected_when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; + assert_eq!( + config.payment_thresholds_opt, + Some(expected_payment_thresholds) + ); + assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); + assert_eq!( + config.suppress_initial_scans_opt, + Some(expected_suppress_initial_scans) + ); + assert_eq!( + config.when_pending_too_long_opt, + Some(expected_when_pending_too_long_sec) + ); //no prepared results for the setter methods, that is they were uncalled } @@ -2356,13 +2366,7 @@ mod tests { ) .unwrap(); - assert_eq!( - bootstrapper_config - .accountant_config_opt - .unwrap() - .suppress_initial_scans, - true - ); + assert_eq!(bootstrapper_config.suppress_initial_scans_opt, Some(true)); } #[test] @@ -2383,13 +2387,7 @@ mod tests { ) .unwrap(); - assert_eq!( - bootstrapper_config - .accountant_config_opt - .unwrap() - .suppress_initial_scans, - false - ); + assert_eq!(bootstrapper_config.suppress_initial_scans_opt, Some(false)); } #[test] @@ -2410,13 +2408,7 @@ mod tests { ) .unwrap(); - assert_eq!( - bootstrapper_config - .accountant_config_opt - .unwrap() - .suppress_initial_scans, - false - ); + assert_eq!(bootstrapper_config.suppress_initial_scans_opt, Some(false)); } fn make_persistent_config( diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 965a1a828..de12fee70 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -36,7 +36,7 @@ lazy_static! { } //please, alphabetical order -#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct PaymentThresholds { pub debt_threshold_gwei: i64, pub maturity_threshold_sec: i64, @@ -46,6 +46,12 @@ pub struct PaymentThresholds { pub unban_below_gwei: i64, } +impl Default for PaymentThresholds { + fn default() -> Self { + DEFAULT_PAYMENT_THRESHOLDS.clone() + } +} + //this code is used in tests in Accountant impl PaymentThresholds { pub fn sugg_and_grace(&self, now: i64) -> i64 { @@ -64,13 +70,14 @@ pub struct ScanIntervals { pub receivable_scan_interval: Duration, } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct AccountantConfig { - pub scan_intervals: ScanIntervals, - pub payment_thresholds: PaymentThresholds, - pub suppress_initial_scans: bool, - pub when_pending_too_long_sec: u64, -} +// TODO: Remove it once you realise you don't want to know which fields was accountant config composed of +// #[derive(Clone, PartialEq, Debug)] +// pub struct AccountantConfig { +// pub scan_intervals: ScanIntervals, +// pub payment_thresholds: PaymentThresholds, +// pub suppress_initial_scans: bool, +// pub when_pending_too_long_sec: u64, +// } #[derive(Clone)] pub struct AccountantSubs { diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index 4d71805bd..ed88f290f 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -290,9 +290,9 @@ fn unreachable() -> ! { #[cfg(test)] mod tests { use super::*; - use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; use crate::sub_lib::combined_parameters::CombinedParamsDataTypes::U128; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; + use crate::test_utils::unshared_test_utils::make_scan_intervals_with_defaults; use std::panic::catch_unwind; #[test] @@ -425,7 +425,7 @@ mod tests { let panic_2 = catch_unwind(|| { let _: &[(&str, CombinedParamsDataTypes)] = - (&CombinedParams::PaymentThresholds(Some(*DEFAULT_PAYMENT_THRESHOLDS))).into(); + (&CombinedParams::PaymentThresholds(Some(PaymentThresholds::default()))).into(); }) .unwrap_err(); let panic_2_msg = panic_2.downcast_ref::().unwrap(); @@ -434,13 +434,13 @@ mod tests { panic_2_msg, &format!( "should be called only on uninitialized object, not: PaymentThresholds(Some({:?}))", - *DEFAULT_PAYMENT_THRESHOLDS + PaymentThresholds::default() ) ); let panic_3 = catch_unwind(|| { let _: &[(&str, CombinedParamsDataTypes)] = - (&CombinedParams::ScanIntervals(Some(*DEFAULT_SCAN_INTERVALS))).into(); + (&CombinedParams::ScanIntervals(Some(make_scan_intervals_with_defaults()))).into(); }) .unwrap_err(); let panic_3_msg = panic_3.downcast_ref::().unwrap(); @@ -449,7 +449,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Some({:?}))", - *DEFAULT_SCAN_INTERVALS + make_scan_intervals_with_defaults() ) ); } @@ -471,7 +471,7 @@ mod tests { ); let panic_2 = catch_unwind(|| { - (&CombinedParams::PaymentThresholds(Some(*DEFAULT_PAYMENT_THRESHOLDS))) + (&CombinedParams::PaymentThresholds(Some(PaymentThresholds::default()))) .initiate_objects(HashMap::new()); }) .unwrap_err(); @@ -481,12 +481,12 @@ mod tests { panic_2_msg, &format!( "should be called only on uninitialized object, not: PaymentThresholds(Some({:?}))", - *DEFAULT_PAYMENT_THRESHOLDS + PaymentThresholds::default() ) ); let panic_3 = catch_unwind(|| { - (&CombinedParams::ScanIntervals(Some(*DEFAULT_SCAN_INTERVALS))) + (&CombinedParams::ScanIntervals(Some(make_scan_intervals_with_defaults()))) .initiate_objects(HashMap::new()); }) .unwrap_err(); @@ -496,7 +496,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Some({:?}))", - *DEFAULT_SCAN_INTERVALS + make_scan_intervals_with_defaults() ) ); } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 256f66985..b01478433 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -513,13 +513,12 @@ pub struct TestRawTransaction { pub mod unshared_test_utils { use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; use crate::apps::app_node; + use crate::bootstrapper::BootstrapperConfig; use crate::daemon::{ChannelFactory, DaemonBindMessage}; use crate::db_config::config_dao_null::ConfigDaoNull; use crate::db_config::persistent_configuration::PersistentConfigurationReal; use crate::node_test_utils::DirsWrapperMock; - use crate::sub_lib::accountant::{ - AccountantConfig, DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS, - }; + use crate::sub_lib::accountant::{PaymentThresholds, ScanIntervals, DEFAULT_SCAN_INTERVALS}; use crate::sub_lib::neighborhood::{ConnectionProgressMessage, DEFAULT_RATE_PACK}; use crate::sub_lib::utils::{ NLSpawnHandleHolder, NLSpawnHandleHolderReal, NotifyHandle, NotifyLaterHandle, @@ -598,30 +597,25 @@ pub mod unshared_test_utils { persistent_config_mock: PersistentConfigurationMock, ) -> PersistentConfigurationMock { persistent_config_mock - .payment_thresholds_result(Ok(*DEFAULT_PAYMENT_THRESHOLDS)) - .scan_intervals_result(Ok(*DEFAULT_SCAN_INTERVALS)) + .payment_thresholds_result(Ok(PaymentThresholds::default())) + .scan_intervals_result(Ok(make_scan_intervals_with_defaults())) } pub fn make_persistent_config_real_with_config_dao_null() -> PersistentConfigurationReal { PersistentConfigurationReal::new(Box::new(ConfigDaoNull::default())) } - pub fn make_populated_accountant_config_with_defaults() -> AccountantConfig { - AccountantConfig { - scan_intervals: *DEFAULT_SCAN_INTERVALS, - payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, - suppress_initial_scans: false, - } + pub fn make_bc_with_defaults() -> BootstrapperConfig { + let mut config = BootstrapperConfig::new(); + config.scan_intervals_opt = Some(make_scan_intervals_with_defaults()); + config.suppress_initial_scans_opt = Some(false); + config.when_pending_too_long_opt = Some(DEFAULT_PENDING_TOO_LONG_SEC); + config.payment_thresholds_opt = Some(PaymentThresholds::default()); + config } - pub fn make_accountant_config_null() -> AccountantConfig { - AccountantConfig { - scan_intervals: Default::default(), - payment_thresholds: Default::default(), - when_pending_too_long_sec: Default::default(), - suppress_initial_scans: false, - } + pub fn make_scan_intervals_with_defaults() -> ScanIntervals { + DEFAULT_SCAN_INTERVALS.clone() } pub fn make_recipient_and_recording_arc( diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 0091ef7f4..01d2311f9 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -112,6 +112,7 @@ recorder_message_handler!(AddReturnRouteMessage); recorder_message_handler!(AddRouteMessage); recorder_message_handler!(AddStreamMsg); recorder_message_handler!(BindMessage); +recorder_message_handler!(ConnectionProgressMessage); recorder_message_handler!(CrashNotification); recorder_message_handler!(DaemonBindMessage); recorder_message_handler!(DispatcherNodeQueryMessage); @@ -155,7 +156,6 @@ recorder_message_handler!(ReportTransactionReceipts); recorder_message_handler!(ReportAccountsPayable); recorder_message_handler!(ScanForReceivables); recorder_message_handler!(ScanForPayables); -recorder_message_handler!(ConnectionProgressMessage); recorder_message_handler!(ScanForPendingPayables); impl Handler for Recorder {