diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index e1e4c0fe4..6beea5748 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -5,7 +5,7 @@ use crate::data_version::DataVersion; use const_format::concatcp; pub const DEFAULT_CHAIN: Chain = Chain::PolyMainnet; -pub const CURRENT_SCHEMA_VERSION: usize = 10; +pub const CURRENT_SCHEMA_VERSION: usize = 11; pub const HIGHEST_RANDOM_CLANDESTINE_PORT: u16 = 9999; pub const HTTP_PORT: u16 = 80; diff --git a/masq_lib/src/lib.rs b/masq_lib/src/lib.rs index 1fc5eb68d..ae638163e 100644 --- a/masq_lib/src/lib.rs +++ b/masq_lib/src/lib.rs @@ -25,6 +25,5 @@ pub mod data_version; pub mod exit_locations; pub mod shared_schema; pub mod test_utils; -pub mod type_obfuscation; pub mod ui_gateway; pub mod ui_traffic_converter; diff --git a/masq_lib/src/messages.rs b/masq_lib/src/messages.rs index 9641c5086..d93332c3d 100644 --- a/masq_lib/src/messages.rs +++ b/masq_lib/src/messages.rs @@ -527,10 +527,10 @@ pub struct UiRatePack { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct UiScanIntervals { - #[serde(rename = "pendingPayableSec")] - pub pending_payable_sec: u64, #[serde(rename = "payableSec")] pub payable_sec: u64, + #[serde(rename = "pendingPayableSec")] + pub pending_payable_sec: u64, #[serde(rename = "receivableSec")] pub receivable_sec: u64, } @@ -783,8 +783,8 @@ conversation_message!(UiRecoverWalletsResponse, "recoverWallets"); #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum ScanType { Payables, - Receivables, PendingPayables, + Receivables, } impl FromStr for ScanType { @@ -793,8 +793,8 @@ impl FromStr for ScanType { fn from_str(s: &str) -> Result { match s { s if &s.to_lowercase() == "payables" => Ok(ScanType::Payables), - s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), s if &s.to_lowercase() == "pendingpayables" => Ok(ScanType::PendingPayables), + s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), s => Err(format!("Unrecognized ScanType: '{}'", s)), } } @@ -1225,10 +1225,10 @@ mod tests { let result: Vec = vec![ "Payables", "pAYABLES", - "Receivables", - "rECEIVABLES", "PendingPayables", "pENDINGpAYABLES", + "Receivables", + "rECEIVABLES", ] .into_iter() .map(|s| ScanType::from_str(s).unwrap()) @@ -1239,10 +1239,10 @@ mod tests { vec![ ScanType::Payables, ScanType::Payables, - ScanType::Receivables, - ScanType::Receivables, ScanType::PendingPayables, ScanType::PendingPayables, + ScanType::Receivables, + ScanType::Receivables, ] ) } diff --git a/masq_lib/src/type_obfuscation.rs b/masq_lib/src/type_obfuscation.rs deleted file mode 100644 index 1f3c79258..000000000 --- a/masq_lib/src/type_obfuscation.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use std::any::TypeId; -use std::mem::transmute; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Obfuscated { - type_id: TypeId, - bytes: Vec, -} - -impl Obfuscated { - // Although we're asking the compiler for a cast between two types - // where one is generic and both could possibly be of a different - // size, which almost applies to an unsupported kind of operation, - // the compiler stays calm here. The use of vectors at the input as - // well as output lets us avoid the above depicted situation. - // - // If you wish to write an implementation allowing more arbitrary - // types on your own, instead of helping yourself by a library like - // 'bytemuck', consider these functions from the std library, - // 'mem::transmute_copy' or 'mem::forget()', which will renew - // the compiler's trust for you. However, the true adventure will - // begin when you are supposed to write code to realign the plain - // bytes backwards to your desired type... - - pub fn obfuscate_vector(data: Vec) -> Obfuscated { - let bytes = unsafe { transmute::, Vec>(data) }; - - Obfuscated { - type_id: TypeId::of::(), - bytes, - } - } - - pub fn expose_vector(self) -> Vec { - if self.type_id != TypeId::of::() { - panic!("Forbidden! You're trying to interpret obfuscated data as the wrong type.") - } - - unsafe { transmute::, Vec>(self.bytes) } - } - - // Proper casting from a non vec structure into a vector of bytes - // is difficult and ideally requires an involvement of a library - // like bytemuck. - // If you think we do need such cast, place other methods in here - // and don't remove the ones above because: - // a) bytemuck will force you to implement its 'Pod' trait which - // might imply an (at minimum) ugly implementation for a std - // type like a Vec because both the trait and the type have - // their definitions situated externally to our project, - // therefore you might need to solve it by introducing - // a super-trait from our code - // b) using our simple 'obfuscate_vector' function will always - // be fairly more efficient than if done with help of - // the other library -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn obfuscation_works() { - let data = vec!["I'm fearing of losing my entire identity".to_string()]; - - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let fenix_like_data: Vec = obfuscated_data.expose_vector(); - - assert_eq!(data, fenix_like_data) - } - - #[test] - #[should_panic( - expected = "Forbidden! You're trying to interpret obfuscated data as the wrong type." - )] - fn obfuscation_attempt_to_reinterpret_to_wrong_type() { - let data = vec![0_u64]; - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let _: Vec = obfuscated_data.expose_vector(); - } -} diff --git a/node/Cargo.toml b/node/Cargo.toml index 9933071d0..80d75b5ef 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -15,7 +15,7 @@ automap = { path = "../automap"} backtrace = "0.3.57" base64 = "0.13.0" bytes = "0.4.12" -time = {version = "0.3.11", features = [ "macros" ]} +time = {version = "0.3.11", features = [ "macros", "parsing" ]} clap = "2.33.3" crossbeam-channel = "0.5.1" dirs = "4.0.0" diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs new file mode 100644 index 000000000..ce93a1f17 --- /dev/null +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -0,0 +1,790 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCondition::UncheckedPendingTooLong; +use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers, VigilantRusqliteFlatten}; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::database::rusqlite_wrappers::ConnectionWrapper; +use masq_lib::utils::ExpectValue; +use std::collections::HashSet; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use web3::types::Address; + +#[derive(Debug, PartialEq, Eq)] +pub enum FailedPayableDaoError { + EmptyInput, + NoChange, + InvalidInput(String), + PartialExecution(String), + SqlExecutionFailed(String), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FailureReason { + PendingTooLong, + NonceIssue, +} + +impl FromStr for FailureReason { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "PendingTooLong" => Ok(FailureReason::PendingTooLong), + "NonceIssue" => Ok(FailureReason::NonceIssue), + _ => Err(format!("Invalid FailureReason: {}", s)), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FailedTx { + pub hash: TxHash, + pub receiver_address: Address, + pub amount: u128, + pub timestamp: i64, + pub gas_price_wei: u128, + pub nonce: u64, + pub reason: FailureReason, + pub rechecked: bool, +} + +pub enum FailureRetrieveCondition { + UncheckedPendingTooLong, +} + +impl Display for FailureRetrieveCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FailureRetrieveCondition::UncheckedPendingTooLong => { + write!(f, "WHERE reason = 'PendingTooLong' AND rechecked = 0",) + } + } + } +} + +pub trait FailedPayableDao { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> Vec; + fn mark_as_rechecked(&self) -> Result<(), FailedPayableDaoError>; + fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError>; +} + +#[derive(Debug)] +pub struct FailedPayableDaoReal<'a> { + conn: Box, +} + +impl<'a> FailedPayableDaoReal<'a> { + pub fn new(conn: Box) -> Self { + Self { conn } + } +} + +impl FailedPayableDao for FailedPayableDaoReal<'_> { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + let hashes_vec: Vec = hashes.iter().copied().collect(); + let sql = format!( + "SELECT tx_hash, rowid FROM failed_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)) + ); + + let mut stmt = self + .conn + .prepare(&sql) + .unwrap_or_else(|_| panic!("Failed to prepare SQL statement")); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let tx_hash = TxHash::from_str(&tx_hash_str[2..]).expect("Failed to parse TxHash"); + let row_id: u64 = row.get(1).expectv("row_id"); + + Ok((tx_hash, row_id)) + }) + .unwrap_or_else(|_| panic!("Failed to execute query")) + .vigilant_flatten() + .collect() + } + + fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError> { + if txs.is_empty() { + return Err(FailedPayableDaoError::EmptyInput); + } + + let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); + if unique_hashes.len() != txs.len() { + return Err(FailedPayableDaoError::InvalidInput(format!( + "Duplicate hashes found in the input. Input Transactions: {:?}", + txs + ))); + } + + let duplicates = self.get_tx_identifiers(&unique_hashes); + if !duplicates.is_empty() { + return Err(FailedPayableDaoError::InvalidInput(format!( + "Duplicates detected in the database: {:?}", + duplicates, + ))); + } + + if let Some(_rechecked_tx) = txs.iter().find(|tx| tx.rechecked) { + return Err(FailedPayableDaoError::InvalidInput(format!( + "Already rechecked transaction(s) provided: {:?}", + txs + ))); + } + + let sql = format!( + "INSERT INTO failed_payable (\ + tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + reason, \ + rechecked + ) VALUES {}", + comma_joined_stringifiable(txs, |tx| { + let amount_checked = checked_conversion::(tx.amount); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); + let (gas_price_wei_high_b, gas_price_wei_low_b) = + BigIntDivider::deconstruct(gas_price_wei_checked); + format!( + "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{:?}', {})", + tx.hash, + tx.receiver_address, + amount_high_b, + amount_low_b, + tx.timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + tx.nonce, + tx.reason, + tx.rechecked + ) + }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(inserted_rows) => { + if inserted_rows == txs.len() { + Ok(()) + } else { + Err(FailedPayableDaoError::PartialExecution(format!( + "Only {} out of {} records inserted", + inserted_rows, + txs.len() + ))) + } + } + Err(e) => Err(FailedPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn retrieve_txs(&self, condition: Option) -> Vec { + let raw_sql = "SELECT tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + reason, \ + rechecked \ + FROM failed_payable" + .to_string(); + let sql = match condition { + None => raw_sql, + Some(condition) => format!("{} {}", raw_sql, condition), + }; + + let mut stmt = self + .conn + .prepare(&sql) + .expect("Failed to prepare SQL statement"); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let hash = TxHash::from_str(&tx_hash_str[2..]).expect("Failed to parse TxHash"); + let receiver_address_str: String = row.get(1).expectv("receiver_address"); + let receiver_address = + Address::from_str(&receiver_address_str[2..]).expect("Failed to parse Address"); + let amount_high_b = row.get(2).expectv("amount_high_b"); + let amount_low_b = row.get(3).expectv("amount_low_b"); + let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let timestamp = row.get(4).expectv("timestamp"); + let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); + let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); + let gas_price_wei = + BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; + let nonce = row.get(7).expectv("nonce"); + let reason_str: String = row.get(8).expectv("reason"); + let reason = + FailureReason::from_str(&reason_str).expect("Failed to parse FailureReason"); + let rechecked_as_integer: u8 = row.get(9).expectv("rechecked"); + let rechecked = rechecked_as_integer == 1; + + Ok(FailedTx { + hash, + receiver_address, + amount, + timestamp, + gas_price_wei, + nonce, + reason, + rechecked, + }) + }) + .expect("Failed to execute query") + .vigilant_flatten() + .collect() + } + + fn mark_as_rechecked(&self) -> Result<(), FailedPayableDaoError> { + let txs = self.retrieve_txs(Some(UncheckedPendingTooLong)); + let hashes_vec: Vec = txs.iter().map(|tx| tx.hash).collect(); + let hashes_string = comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)); + + let sql = format!( + "UPDATE failed_payable SET rechecked = 1 WHERE tx_hash IN ({})", + hashes_string + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(rows_changed) => { + if rows_changed == txs.len() { + Ok(()) + } else { + // This should never occur because we retrieve transaction hashes + // under the condition that all retrieved transactions are unchecked. + Err(FailedPayableDaoError::PartialExecution(format!( + "Only {} of {} records has been marked as rechecked.", + rows_changed, + txs.len(), + ))) + } + } + Err(e) => Err(FailedPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError> { + if hashes.is_empty() { + return Err(FailedPayableDaoError::EmptyInput); + } + + let hashes_vec: Vec = hashes.iter().cloned().collect(); + let sql = format!( + "DELETE FROM failed_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| { format!("'{:?}'", hash) }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(deleted_rows) => { + if deleted_rows == hashes.len() { + Ok(()) + } else if deleted_rows == 0 { + Err(FailedPayableDaoError::NoChange) + } else { + Err(FailedPayableDaoError::PartialExecution(format!( + "Only {} of {} hashes has been deleted.", + deleted_rows, + hashes.len(), + ))) + } + } + Err(e) => Err(FailedPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::{ + NonceIssue, PendingTooLong, + }; + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, + FailureRetrieveCondition, + }; + use crate::accountant::db_access_objects::test_utils::{ + make_read_only_db_connection, FailedTxBuilder, + }; + use crate::accountant::db_access_objects::utils::current_unix_timestamp; + use crate::blockchain::test_utils::make_tx_hash; + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, + }; + use crate::database::test_utils::ConnectionWrapperMock; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::Connection; + use std::collections::HashSet; + use std::str::FromStr; + + #[test] + fn insert_new_records_works() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "insert_new_records_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .reason(NonceIssue) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(PendingTooLong) + .build(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let txs = vec![tx1, tx2]; + + let result = subject.insert_new_records(&txs); + + let retrieved_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(retrieved_txs, txs); + } + + #[test] + fn insert_new_records_throws_err_for_empty_input() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_err_for_empty_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let empty_input = vec![]; + + let result = subject.insert_new_records(&empty_input); + + assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); + } + + #[test] + fn insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(123); + let tx1 = FailedTxBuilder::default().hash(hash).build(); + let tx2 = FailedTxBuilder::default() + .hash(hash) + .rechecked(true) + .build(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + + let result = subject.insert_new_records(&vec![tx1, tx2]); + + assert_eq!( + result, + Err(FailedPayableDaoError::InvalidInput( + "Duplicate hashes found in the input. Input Transactions: \ + [FailedTx { \ + hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 0, gas_price_wei: 0, \ + nonce: 0, reason: PendingTooLong, rechecked: false }, \ + FailedTx { \ + hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 0, gas_price_wei: 0, \ + nonce: 0, reason: PendingTooLong, rechecked: true }]" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(123); + let tx1 = FailedTxBuilder::default().hash(hash).build(); + let tx2 = FailedTxBuilder::default() + .hash(hash) + .rechecked(true) + .build(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let initial_insertion_result = subject.insert_new_records(&vec![tx1]); + + let result = subject.insert_new_records(&vec![tx2]); + + assert_eq!(initial_insertion_result, Ok(())); + assert_eq!( + result, + Err(FailedPayableDaoError::InvalidInput( + "Duplicates detected in the database: \ + {0x000000000000000000000000000000000000000000000000000000000000007b: 1}" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_throws_err_if_an_already_rechecked_tx_is_supplied() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_err_if_an_already_rechecked_tx_is_supplied", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .rechecked(true) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .rechecked(false) + .build(); + let input = vec![tx1, tx2]; + + let result = subject.insert_new_records(&input); + + assert_eq!( + result, + Err(FailedPayableDaoError::InvalidInput(format!( + "Already rechecked transaction(s) provided: {:?}", + input + ))) + ); + } + + #[test] + fn insert_new_records_returns_err_if_partially_executed() { + let setup_conn = Connection::open_in_memory().unwrap(); + setup_conn + .execute("CREATE TABLE example (id integer)", []) + .unwrap(); + let get_tx_identifiers_stmt = setup_conn.prepare("SELECT id FROM example").unwrap(); + let faulty_insert_stmt = { setup_conn.prepare("SELECT id FROM example").unwrap() }; + let wrapped_conn = ConnectionWrapperMock::default() + .prepare_result(Ok(get_tx_identifiers_stmt)) + .prepare_result(Ok(faulty_insert_stmt)); + let tx = FailedTxBuilder::default().build(); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(FailedPayableDaoError::PartialExecution( + "Only 0 out of 1 records inserted".to_string() + )) + ); + } + + #[test] + fn insert_new_records_can_throw_error() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_can_throw_error", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let tx = FailedTxBuilder::default().build(); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(FailedPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn get_tx_identifiers_works() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "get_tx_identifiers_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let another_present_hash = make_tx_hash(3); + let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); + let present_tx = FailedTxBuilder::default().hash(present_hash).build(); + let another_present_tx = FailedTxBuilder::default() + .hash(another_present_hash) + .build(); + subject + .insert_new_records(&vec![present_tx, another_present_tx]) + .unwrap(); + + let result = subject.get_tx_identifiers(&hashset); + + assert_eq!(result.get(&present_hash), Some(&1u64)); + assert_eq!(result.get(&absent_hash), None); + assert_eq!(result.get(&another_present_hash), Some(&2u64)); + } + + #[test] + fn failure_reason_from_str_works() { + assert_eq!( + FailureReason::from_str("PendingTooLong"), + Ok(PendingTooLong) + ); + assert_eq!(FailureReason::from_str("NonceIssue"), Ok(NonceIssue)); + assert_eq!( + FailureReason::from_str("InvalidReason"), + Err("Invalid FailureReason: InvalidReason".to_string()) + ); + } + + #[test] + fn retrieve_condition_display_works() { + let expected_condition = "WHERE reason = 'PendingTooLong' AND rechecked = 0"; + assert_eq!( + FailureRetrieveCondition::UncheckedPendingTooLong.to_string(), + expected_condition + ); + } + + #[test] + fn can_retrieve_all_txs() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "can_retrieve_all_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .nonce(1) + .build(); + let tx3 = FailedTxBuilder::default().hash(make_tx_hash(3)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + subject.insert_new_records(&vec![tx3.clone()]).unwrap(); + + let result = subject.retrieve_txs(None); + + assert_eq!(result, vec![tx1, tx2, tx3]); + } + + #[test] + fn can_retrieve_unchecked_pending_too_long_txs() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "can_retrieve_unchecked_pending_too_long_txs", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let now = current_unix_timestamp(); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .reason(PendingTooLong) + .timestamp(now - 3600) + .rechecked(false) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(NonceIssue) + .rechecked(false) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .reason(PendingTooLong) + .rechecked(false) + .timestamp(now - 3000) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(FailureRetrieveCondition::UncheckedPendingTooLong)); + + assert_eq!(result, vec![tx1, tx3]); + } + + #[test] + fn mark_as_rechecked_works() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "mark_as_rechecked_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .reason(NonceIssue) + .rechecked(false) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(PendingTooLong) + .rechecked(false) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .reason(PendingTooLong) + .rechecked(false) + .build(); + let tx1_pre_checked_state = tx1.rechecked; + let tx2_pre_checked_state = tx2.rechecked; + let tx3_pre_checked_state = tx3.rechecked; + subject + .insert_new_records(&vec![tx1, tx2.clone(), tx3.clone()]) + .unwrap(); + + let result = subject.mark_as_rechecked(); + + let updated_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(tx1_pre_checked_state, false); + assert_eq!(tx2_pre_checked_state, false); + assert_eq!(tx3_pre_checked_state, false); + assert_eq!(updated_txs[0].rechecked, false); + assert_eq!(updated_txs[1].rechecked, true); + assert_eq!(updated_txs[2].rechecked, true); + } + + #[test] + fn mark_as_rechecked_handles_sql_error() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "mark_as_rechecked_handles_sql_error", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.mark_as_rechecked(); + + assert_eq!( + result, + Err(FailedPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ); + } + + #[test] + fn txs_can_be_deleted() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "txs_can_be_deleted"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = FailedTxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = FailedTxBuilder::default().hash(make_tx_hash(3)).build(); + let tx4 = FailedTxBuilder::default().hash(make_tx_hash(4)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .unwrap(); + let hashset = HashSet::from([tx1.hash, tx3.hash]); + + let result = subject.delete_records(&hashset); + + let remaining_records = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(remaining_records, vec![tx2, tx4]); + } + + #[test] + fn delete_records_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + + let result = subject.delete_records(&HashSet::new()); + + assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); + } + + #[test] + fn delete_records_returns_error_when_no_records_are_deleted() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_error_when_no_records_are_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let non_existent_hash = make_tx_hash(999); + let hashset = HashSet::from([non_existent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!(result, Err(FailedPayableDaoError::NoChange)); + } + + #[test] + fn delete_records_returns_error_when_not_all_input_records_were_deleted() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_error_when_not_all_input_records_were_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let tx = FailedTxBuilder::default().hash(present_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hashset = HashSet::from([present_hash, absent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!( + result, + Err(FailedPayableDaoError::PartialExecution( + "Only 1 of 2 hashes has been deleted.".to_string() + )) + ); + } + + #[test] + fn delete_records_returns_a_general_error_from_sql() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_a_general_error_from_sql", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + let hashes = HashSet::from([make_tx_hash(1)]); + + let result = subject.delete_records(&hashes); + + assert_eq!( + result, + Err(FailedPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } +} diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index a350148ab..ae165909a 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -1,7 +1,10 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod banned_dao; +pub mod failed_payable_dao; pub mod payable_dao; pub mod pending_payable_dao; pub mod receivable_dao; +pub mod sent_payable_dao; +mod test_utils; pub mod utils; diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 88897281b..c7d438a41 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -7,7 +7,7 @@ use crate::accountant::db_big_integer::big_int_db_processor::{BigIntDbProcessor, use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::db_access_objects::utils; use crate::accountant::db_access_objects::utils::{ - sum_i128_values_from_table, to_time_t, AssemblerFeeder, CustomQuery, DaoFactoryReal, + sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten, }; use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::{ @@ -100,7 +100,7 @@ impl PayableDao for PayableDaoReal { let update_clause_with_compensated_overflow = "update payable set \ balance_high_b = :balance_high_b, balance_low_b = :balance_low_b where wallet_address = :wallet"; - let last_paid_timestamp = to_time_t(timestamp); + let last_paid_timestamp = to_unix_timestamp(timestamp); let params = SQLParamsBuilder::default() .key(WalletAddress(wallet)) .wei_change(WeiChange::new( @@ -158,7 +158,7 @@ impl PayableDao for PayableDaoReal { pending_payable_rowid = null where pending_payable_rowid = :rowid"; let i64_rowid = checked_conversion::(pending_payable_fingerprint.rowid); - let last_paid = to_time_t(pending_payable_fingerprint.timestamp); + let last_paid = to_unix_timestamp(pending_payable_fingerprint.timestamp); let params = SQLParamsBuilder::default() .key( PendingPayableRowid(&i64_rowid)) .wei_change(WeiChange::new( "balance", pending_payable_fingerprint.amount, WeiChangeDirection::Subtraction)) @@ -196,7 +196,7 @@ impl PayableDao for PayableDaoReal { balance_wei: checked_conversion::(BigIntDivider::reconstitute( high_b, low_b, )), - last_paid_timestamp: utils::from_time_t(last_paid_timestamp), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), pending_payable_opt: None, }) } @@ -282,7 +282,7 @@ impl PayableDao for PayableDaoReal { balance_wei: checked_conversion::(BigIntDivider::reconstitute( high_bytes, low_bytes, )), - last_paid_timestamp: utils::from_time_t(last_paid_timestamp), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), pending_payable_opt: match rowid { Some(rowid) => Some(PendingPayableId::new( u64::try_from(rowid).unwrap(), @@ -338,7 +338,7 @@ impl PayableDaoReal { balance_wei: checked_conversion::(BigIntDivider::reconstitute( high_bytes, low_bytes, )), - last_paid_timestamp: utils::from_time_t(last_paid_timestamp), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), pending_payable_opt: rowid_opt.map(|rowid| { let hash_str = hash_opt.expect("database corrupt; missing hash but existing rowid"); @@ -541,7 +541,7 @@ mod mark_pending_payable_associated_functions { #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::{from_time_t, now_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, current_unix_timestamp, to_unix_timestamp}; use crate::accountant::gwei_to_wei; use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::explanatory_extension; use crate::accountant::test_utils::{assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_pending_payable_fingerprint, trick_rusqlite_with_read_only_conn}; @@ -577,7 +577,10 @@ mod tests { let status = subject.account_status(&wallet).unwrap(); assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, 1234); - assert_eq!(to_time_t(status.last_paid_timestamp), to_time_t(now)); + assert_eq!( + to_unix_timestamp(status.last_paid_timestamp), + to_unix_timestamp(now) + ); } #[test] @@ -616,8 +619,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, expected_balance); assert_eq!( - to_time_t(status.last_paid_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_paid_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); }; assert_account(wallet, initial_value + balance_change); @@ -653,8 +656,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, initial_value + balance_change); assert_eq!( - to_time_t(status.last_paid_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_paid_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); } @@ -746,13 +749,13 @@ mod tests { PayableAccount { wallet: wallet_0, balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), - last_paid_timestamp: from_time_t(45678), + last_paid_timestamp: from_unix_timestamp(45678), pending_payable_opt: None, }, PayableAccount { wallet: wallet_1, balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), - last_paid_timestamp: from_time_t(150_000_000), + last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: Some(PendingPayableId::new( pending_payable_rowid_1, make_tx_hash(0) @@ -762,7 +765,7 @@ mod tests { PayableAccount { wallet: wallet_2, balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), - last_paid_timestamp: from_time_t(151_000_000), + last_paid_timestamp: from_unix_timestamp(151_000_000), pending_payable_opt: Some(PendingPayableId::new( pending_payable_rowid_2, make_tx_hash(0) @@ -907,12 +910,12 @@ mod tests { let hash_1 = make_tx_hash(12345); let rowid_1 = 789; let previous_timestamp_1_s = 190_000_000; - let new_payable_timestamp_1 = from_time_t(199_000_000); + let new_payable_timestamp_1 = from_unix_timestamp(199_000_000); let wallet_1 = make_wallet("bobble"); let hash_2 = make_tx_hash(54321); let rowid_2 = 792; let previous_timestamp_2_s = 187_100_000; - let new_payable_timestamp_2 = from_time_t(191_333_000); + let new_payable_timestamp_2 = from_unix_timestamp(191_333_000); let wallet_2 = make_wallet("booble bobble"); { insert_payable_record_fn( @@ -946,8 +949,8 @@ mod tests { amount: balance_change_2, process_error: None, }; - let previous_timestamp_1 = from_time_t(previous_timestamp_1_s); - let previous_timestamp_2 = from_time_t(previous_timestamp_2_s); + let previous_timestamp_1 = from_unix_timestamp(previous_timestamp_1_s); + let previous_timestamp_2 = from_unix_timestamp(previous_timestamp_2_s); TestSetupValuesHolder { fingerprint_1, fingerprint_2, @@ -1203,13 +1206,13 @@ mod tests { PayableAccount { wallet: make_wallet("foobar"), balance_wei: 1234567890123456 as u128, - last_paid_timestamp: from_time_t(111_111_111), + last_paid_timestamp: from_unix_timestamp(111_111_111), pending_payable_opt: None }, PayableAccount { wallet: make_wallet("barfoo"), balance_wei: 1234567890123456 as u128, - last_paid_timestamp: from_time_t(111_111_111), + last_paid_timestamp: from_unix_timestamp(111_111_111), pending_payable_opt: None }, ] @@ -1304,7 +1307,7 @@ mod tests { //Accounts of balances smaller than one gwei don't qualify. //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, //here by balance and then by age. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( "custom_query_in_top_records_mode_with_default_ordering", @@ -1324,13 +1327,13 @@ mod tests { PayableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 7_562_000_300_000, - last_paid_timestamp: from_time_t(now - 86_001), + last_paid_timestamp: from_unix_timestamp(now - 86_001), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_401), + last_paid_timestamp: from_unix_timestamp(now - 86_401), pending_payable_opt: Some(PendingPayableId::new( 1, H256::from_str( @@ -1342,7 +1345,7 @@ mod tests { PayableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_300), + last_paid_timestamp: from_unix_timestamp(now - 86_300), pending_payable_opt: None }, ] @@ -1354,7 +1357,7 @@ mod tests { //Accounts of balances smaller than one gwei don't qualify. //Two accounts differ only in balance but not in the debt's age which allows to check doubled ordering, //here by age and then by balance. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( "custom_query_in_top_records_mode_ordered_by_age", @@ -1374,7 +1377,7 @@ mod tests { PayableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_401), + last_paid_timestamp: from_unix_timestamp(now - 86_401), pending_payable_opt: Some(PendingPayableId::new( 1, H256::from_str( @@ -1386,13 +1389,13 @@ mod tests { PayableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), balance_wei: 1_000_000_002, - last_paid_timestamp: from_time_t(now - 86_401), + last_paid_timestamp: from_unix_timestamp(now - 86_401), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_300), + last_paid_timestamp: from_unix_timestamp(now - 86_300), pending_payable_opt: None }, ] @@ -1422,7 +1425,7 @@ mod tests { fn custom_query_in_range_mode() { //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, //by balance and then by age. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { insert( conn, @@ -1482,7 +1485,7 @@ mod tests { max_age_s: 200000, min_amount_gwei: 500_000_000, max_amount_gwei: 35_000_000_000, - timestamp: from_time_t(now), + timestamp: from_unix_timestamp(now), }) .unwrap(); @@ -1492,19 +1495,19 @@ mod tests { PayableAccount { wallet: Wallet::new("0x7777777777777777777777777777777777777777"), balance_wei: gwei_to_wei(2_500_647_000_u32), - last_paid_timestamp: from_time_t(now - 80_333), + last_paid_timestamp: from_unix_timestamp(now - 80_333), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x6666666666666666666666666666666666666666"), balance_wei: gwei_to_wei(1_800_456_000_u32), - last_paid_timestamp: from_time_t(now - 100_401), + last_paid_timestamp: from_unix_timestamp(now - 100_401), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: gwei_to_wei(1_800_456_000_u32), - last_paid_timestamp: from_time_t(now - 55_120), + last_paid_timestamp: from_unix_timestamp(now - 55_120), pending_payable_opt: Some(PendingPayableId::new( 1, H256::from_str( @@ -1519,7 +1522,7 @@ mod tests { #[test] fn range_query_does_not_display_values_from_below_1_gwei() { - let now = now_time_t(); + let now = current_unix_timestamp(); let timestamp_1 = now - 11_001; let timestamp_2 = now - 5000; let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { @@ -1558,7 +1561,7 @@ mod tests { vec![PayableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 30_000_300_000, - last_paid_timestamp: from_time_t(timestamp_2), + last_paid_timestamp: from_unix_timestamp(timestamp_2), pending_payable_opt: None },] ) @@ -1570,7 +1573,7 @@ mod tests { let conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let timestamp = utils::now_time_t(); + let timestamp = utils::current_unix_timestamp(); insert_payable_record_fn( &*conn, "0x1111111111111111111111111111111111111111", diff --git a/node/src/accountant/db_access_objects/pending_payable_dao.rs b/node/src/accountant/db_access_objects/pending_payable_dao.rs index 67c779ce0..e555fcc9a 100644 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ b/node/src/accountant/db_access_objects/pending_payable_dao.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::utils::{ - from_time_t, to_time_t, DaoFactoryReal, VigilantRusqliteFlatten, + from_unix_timestamp, to_unix_timestamp, DaoFactoryReal, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; @@ -104,7 +104,7 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { let attempt: u16 = Self::get_with_expect(row, 5); Ok(PendingPayableFingerprint { rowid, - timestamp: from_time_t(timestamp), + timestamp: from_unix_timestamp(timestamp), hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { panic!( "Invalid hash format (\"{}\": {:?}) - database corrupt", @@ -133,7 +133,7 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { hashes_and_amounts: &[HashAndAmount], batch_wide_timestamp: SystemTime, ) -> String { - let time_t = to_time_t(batch_wide_timestamp); + let time_t = to_unix_timestamp(batch_wide_timestamp); comma_joined_stringifiable(hashes_and_amounts, |hash_and_amount| { let amount_checked = checked_conversion::(hash_and_amount.amount); let (high_bytes, low_bytes) = BigIntDivider::deconstruct(amount_checked); @@ -275,7 +275,7 @@ mod tests { use crate::accountant::db_access_objects::pending_payable_dao::{ PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, }; - use crate::accountant::db_access_objects::utils::from_time_t; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; @@ -304,7 +304,7 @@ mod tests { let amount_1 = 55556; let hash_2 = make_tx_hash(6789); let amount_2 = 44445; - let batch_wide_timestamp = from_time_t(200_000_000); + let batch_wide_timestamp = from_unix_timestamp(200_000_000); let subject = PendingPayableDaoReal::new(wrapped_conn); let hash_and_amount_1 = HashAndAmount { hash: hash_1, @@ -365,7 +365,7 @@ mod tests { let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); let hash = make_tx_hash(45466); let amount = 55556; - let timestamp = from_time_t(200_000_000); + let timestamp = from_unix_timestamp(200_000_000); let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); let hash_and_amount = HashAndAmount { hash, amount }; @@ -394,7 +394,7 @@ mod tests { let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); let hash_1 = make_tx_hash(4546); let amount_1 = 55556; - let batch_wide_timestamp = from_time_t(200_000_000); + let batch_wide_timestamp = from_unix_timestamp(200_000_000); let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); let hash_and_amount = HashAndAmount { hash: hash_1, @@ -414,7 +414,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_time_t(195_000_000); + let timestamp = from_unix_timestamp(195_000_000); // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, // then complain about its excessive size if supplied in unquoted strings let hash_1 = @@ -510,7 +510,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = PendingPayableDaoReal::new(wrapped_conn); - let batch_wide_timestamp = from_time_t(195_000_000); + let batch_wide_timestamp = from_unix_timestamp(195_000_000); let hash_1 = make_tx_hash(11119); let amount_1 = 787; let hash_2 = make_tx_hash(10000); @@ -568,7 +568,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_time_t(198_000_000); + let timestamp = from_unix_timestamp(198_000_000); let hash = make_tx_hash(10000); let amount = 333; let hash_and_amount_1 = HashAndAmount { @@ -750,7 +750,7 @@ mod tests { hash: hash_3, amount: 3344, }; - let timestamp = from_time_t(190_000_000); + let timestamp = from_unix_timestamp(190_000_000); let subject = PendingPayableDaoReal::new(conn); { subject @@ -842,7 +842,7 @@ mod tests { hash: hash_2, amount: amount_2, }; - let timestamp = from_time_t(190_000_000); + let timestamp = from_unix_timestamp(190_000_000); let subject = PendingPayableDaoReal::new(conn); { subject @@ -868,7 +868,7 @@ mod tests { let process_error: Option = row.get(6).unwrap(); Ok(PendingPayableFingerprint { rowid, - timestamp: from_time_t(timestamp), + timestamp: from_unix_timestamp(timestamp), hash: H256::from_str(&transaction_hash[2..]).unwrap(), attempt, amount: checked_conversion::(BigIntDivider::reconstitute( diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index 9b71a3939..ad8f52462 100644 --- a/node/src/accountant/db_access_objects/receivable_dao.rs +++ b/node/src/accountant/db_access_objects/receivable_dao.rs @@ -4,7 +4,7 @@ use crate::accountant::checked_conversion; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoError::RusqliteError; use crate::accountant::db_access_objects::utils; use crate::accountant::db_access_objects::utils::{ - sum_i128_values_from_table, to_time_t, AssemblerFeeder, CustomQuery, DaoFactoryReal, + sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, RangeStmConfig, ThresholdUtils, TopStmConfig, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; @@ -120,7 +120,7 @@ impl ReceivableDao for ReceivableDaoReal { let update_clause_with_compensated_overflow = "update receivable set balance_high_b = :balance_high_b, balance_low_b = :balance_low_b \ where wallet_address = :wallet"; - let last_received_timestamp = to_time_t(timestamp); + let last_received_timestamp = to_unix_timestamp(timestamp); let params = SQLParamsBuilder::default() .key(WalletAddress(wallet)) .wei_change(WeiChange::new( @@ -216,7 +216,7 @@ impl ReceivableDao for ReceivableDaoReal { named_params! { ":debt_threshold": checked_conversion::(payment_thresholds.debt_threshold_gwei), ":slope": slope, - ":sugg_and_grace": payment_thresholds.sugg_and_grace(to_time_t(now)), + ":sugg_and_grace": payment_thresholds.sugg_and_grace(to_unix_timestamp(now)), ":permanent_debt_allowed_high_b": permanent_debt_allowed_high_b, ":permanent_debt_allowed_low_b": permanent_debt_allowed_low_b }, @@ -337,7 +337,7 @@ impl ReceivableDaoReal { where wallet_address = :wallet"; match received_payments.iter().try_for_each(|received_payment| { - let last_received_timestamp = to_time_t(timestamp); + let last_received_timestamp = to_unix_timestamp(timestamp); let params = SQLParamsBuilder::default() .key(WalletAddress(&received_payment.from)) .wei_change(WeiChange::new( @@ -414,7 +414,7 @@ impl ReceivableDaoReal { Ok(ReceivableAccount { wallet, balance_wei: BigIntDivider::reconstitute(high_bytes, low_bytes), - last_received_timestamp: utils::from_time_t(last_received_timestamp), + last_received_timestamp: utils::from_unix_timestamp(last_received_timestamp), }) } e => panic!( @@ -493,7 +493,7 @@ impl TableNameDAO for ReceivableDaoReal { mod tests { use super::*; use crate::accountant::db_access_objects::utils::{ - from_time_t, now_time_t, to_time_t, CustomQuery, + current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, CustomQuery, }; use crate::accountant::gwei_to_wei; use crate::accountant::test_utils::{ @@ -609,8 +609,8 @@ mod tests { "receivable_dao", "more_money_receivable_works_for_new_address", ); - let payment_time_t = to_time_t(SystemTime::now()) - 1111; - let payment_time = from_time_t(payment_time_t); + let payment_time_t = to_unix_timestamp(SystemTime::now()) - 1111; + let payment_time = from_unix_timestamp(payment_time_t); let wallet = make_wallet("booga"); let subject = ReceivableDaoReal::new( DbInitializerReal::default() @@ -625,7 +625,10 @@ mod tests { let status = subject.account_status(&wallet).unwrap(); assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, 1234); - assert_eq!(to_time_t(status.last_received_timestamp), payment_time_t); + assert_eq!( + to_unix_timestamp(status.last_received_timestamp), + payment_time_t + ); } #[test] @@ -661,8 +664,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, expected_balance); assert_eq!( - to_time_t(status.last_received_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_received_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); }; assert_account(wallet, 1234 + 2345); @@ -695,8 +698,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, 1234 + i64::MAX as i128); assert_eq!( - to_time_t(status.last_received_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_received_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); } @@ -812,15 +815,15 @@ mod tests { assert_eq!(status1.wallet, debtor1); assert_eq!(status1.balance_wei, first_expected_result); assert_eq!( - to_time_t(status1.last_received_timestamp), - to_time_t(payment_time) + to_unix_timestamp(status1.last_received_timestamp), + to_unix_timestamp(payment_time) ); let status2 = subject.account_status(&debtor2).unwrap(); assert_eq!(status2.wallet, debtor2); assert_eq!(status2.balance_wei, second_expected_result); assert_eq!( - to_time_t(status2.last_received_timestamp), - to_time_t(payment_time) + to_unix_timestamp(status2.last_received_timestamp), + to_unix_timestamp(payment_time) ); } @@ -887,8 +890,8 @@ mod tests { first_initial_balance as i128 - 1111 ); assert_eq!( - to_time_t(actual_record_1.last_received_timestamp), - to_time_t(time_of_change) + to_unix_timestamp(actual_record_1.last_received_timestamp), + to_unix_timestamp(time_of_change) ); let actual_record_2 = subject.account_status(&unknown_wallet); assert!(actual_record_2.is_none()); @@ -899,8 +902,8 @@ mod tests { second_initial_balance as i128 - 9999 ); assert_eq!( - to_time_t(actual_record_3.last_received_timestamp), - to_time_t(time_of_change) + to_unix_timestamp(actual_record_3.last_received_timestamp), + to_unix_timestamp(time_of_change) ); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( @@ -1202,37 +1205,37 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut not_delinquent_inside_grace_period = make_receivable_account(1234, false); not_delinquent_inside_grace_period.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei + 1); not_delinquent_inside_grace_period.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) + 2); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) + 2); let mut not_delinquent_after_grace_below_slope = make_receivable_account(2345, false); not_delinquent_after_grace_below_slope.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 2); not_delinquent_after_grace_below_slope.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 1); let mut delinquent_above_slope_after_grace = make_receivable_account(3456, true); delinquent_above_slope_after_grace.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); delinquent_above_slope_after_grace.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 2); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 2); let mut not_delinquent_below_slope_before_stop = make_receivable_account(4567, false); not_delinquent_below_slope_before_stop.balance_wei = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); not_delinquent_below_slope_before_stop.last_received_timestamp = - from_time_t(payment_thresholds.sugg_thru_decreasing(now) + 2); + from_unix_timestamp(payment_thresholds.sugg_thru_decreasing(now) + 2); let mut delinquent_above_slope_before_stop = make_receivable_account(5678, true); delinquent_above_slope_before_stop.balance_wei = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2); delinquent_above_slope_before_stop.last_received_timestamp = - from_time_t(payment_thresholds.sugg_thru_decreasing(now) + 1); + from_unix_timestamp(payment_thresholds.sugg_thru_decreasing(now) + 1); let mut not_delinquent_above_slope_after_stop = make_receivable_account(6789, false); not_delinquent_above_slope_after_stop.balance_wei = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei - 1); not_delinquent_above_slope_after_stop.last_received_timestamp = - from_time_t(payment_thresholds.sugg_thru_decreasing(now) - 2); + from_unix_timestamp(payment_thresholds.sugg_thru_decreasing(now) - 2); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); add_receivable_account(&conn, ¬_delinquent_inside_grace_period); @@ -1243,7 +1246,7 @@ mod tests { add_receivable_account(&conn, ¬_delinquent_above_slope_after_stop); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &delinquent_above_slope_after_grace); assert_contains(&result, &delinquent_above_slope_before_stop); @@ -1260,15 +1263,15 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut not_delinquent = make_receivable_account(1234, false); not_delinquent.balance_wei = gwei_to_wei(105); not_delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 25); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 25); let mut delinquent = make_receivable_account(2345, true); delinquent.balance_wei = gwei_to_wei(105); delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 75); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 75); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies_shallow_slope"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); @@ -1276,7 +1279,7 @@ mod tests { add_receivable_account(&conn, &delinquent); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &delinquent); assert_eq!(result.len(), 1); @@ -1292,15 +1295,15 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut not_delinquent = make_receivable_account(1234, false); not_delinquent.balance_wei = gwei_to_wei(600); not_delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 25); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 25); let mut delinquent = make_receivable_account(2345, true); delinquent.balance_wei = gwei_to_wei(600); delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 75); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 75); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies_steep_slope"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); @@ -1308,7 +1311,7 @@ mod tests { add_receivable_account(&conn, &delinquent); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &delinquent); assert_eq!(result.len(), 1); @@ -1324,15 +1327,15 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut existing_delinquency = make_receivable_account(1234, true); existing_delinquency.balance_wei = gwei_to_wei(250); existing_delinquency.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 1); let mut new_delinquency = make_receivable_account(2345, true); new_delinquency.balance_wei = gwei_to_wei(250); new_delinquency.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 1); let home_dir = ensure_node_home_directory_exists( "receivable_dao", "new_delinquencies_does_not_find_existing_delinquencies", @@ -1343,7 +1346,7 @@ mod tests { add_banned_account(&conn, &existing_delinquency); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &new_delinquency); assert_eq!(result.len(), 1); @@ -1359,7 +1362,7 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let home_dir = ensure_node_home_directory_exists( "receivable_dao", "new_delinquencies_work_for_still_empty_tables", @@ -1367,7 +1370,7 @@ mod tests { let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert!(result.is_empty()) } @@ -1387,24 +1390,24 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let sugg_and_grace = payment_thresholds.sugg_and_grace(now); let too_young_new_delinquency = ReceivableAccount { wallet: make_wallet("abc123"), balance_wei: 123_456_789_101_112, - last_received_timestamp: from_time_t(sugg_and_grace + 1), + last_received_timestamp: from_unix_timestamp(sugg_and_grace + 1), }; let ok_new_delinquency = ReceivableAccount { wallet: make_wallet("aaa999"), balance_wei: 123_456_789_101_112, - last_received_timestamp: from_time_t(sugg_and_grace - 1), + last_received_timestamp: from_unix_timestamp(sugg_and_grace - 1), }; let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); add_receivable_account(&conn, &too_young_new_delinquency); add_receivable_account(&conn, &ok_new_delinquency.clone()); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_eq!(result, vec![ok_new_delinquency]) } @@ -1535,7 +1538,7 @@ mod tests { #[test] fn custom_query_in_top_records_mode_default_ordering() { - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = common_setup_of_accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_receivable( "custom_query_in_top_records_mode_default_ordering", @@ -1555,17 +1558,17 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 32_000_000_200, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ReceivableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 222_000), + last_received_timestamp: from_unix_timestamp(now - 222_000), }, ReceivableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ] ); @@ -1573,7 +1576,7 @@ mod tests { #[test] fn custom_query_in_top_records_mode_ordered_by_age() { - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = common_setup_of_accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_receivable( "custom_query_in_top_records_mode_ordered_by_age", @@ -1593,17 +1596,17 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 222_000), + last_received_timestamp: from_unix_timestamp(now - 222_000), }, ReceivableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 32_000_000_200, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ReceivableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ] ); @@ -1632,7 +1635,7 @@ mod tests { fn custom_query_in_range_mode() { //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, //by balance and then by age. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = |conn: &dyn ConnectionWrapper, insert: InsertReceivableHelperFn| { insert( conn, @@ -1692,7 +1695,7 @@ mod tests { max_age_s: 99000, min_amount_gwei: -560000, max_amount_gwei: 1_100_000_000, - timestamp: from_time_t(now), + timestamp: from_unix_timestamp(now), }) .unwrap(); @@ -1702,22 +1705,22 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x6666666666666666666666666666666666666666"), balance_wei: gwei_to_wei(1_050_444_230), - last_received_timestamp: from_time_t(now - 66_244), + last_received_timestamp: from_unix_timestamp(now - 66_244), }, ReceivableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: gwei_to_wei(1_000_000_230), - last_received_timestamp: from_time_t(now - 86_000), + last_received_timestamp: from_unix_timestamp(now - 86_000), }, ReceivableAccount { wallet: Wallet::new("0x3333333333333333333333333333333333333333"), balance_wei: gwei_to_wei(1_000_000_230), - last_received_timestamp: from_time_t(now - 70_000), + last_received_timestamp: from_unix_timestamp(now - 70_000), }, ReceivableAccount { wallet: Wallet::new("0x8888888888888888888888888888888888888888"), balance_wei: gwei_to_wei(-90), - last_received_timestamp: from_time_t(now - 66_000), + last_received_timestamp: from_unix_timestamp(now - 66_000), } ] ); @@ -1725,20 +1728,20 @@ mod tests { #[test] fn range_query_does_not_display_values_from_below_1_gwei() { - let timestamp1 = now_time_t() - 5000; - let timestamp2 = now_time_t() - 3232; + let timestamp1 = current_unix_timestamp() - 5000; + let timestamp2 = current_unix_timestamp() - 3232; let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertReceivableHelperFn| { insert( conn, "0x1111111111111111111111111111111111111111", 999_999_999, //smaller than 1 gwei - now_time_t() - 11_001, + current_unix_timestamp() - 11_001, ); insert( conn, "0x2222222222222222222222222222222222222222", -999_999_999, //smaller than -1 gwei - now_time_t() - 5_606, + current_unix_timestamp() - 5_606, ); insert( conn, @@ -1774,12 +1777,12 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x3333333333333333333333333333333333333333"), balance_wei: 30_000_300_000, - last_received_timestamp: from_time_t(timestamp1), + last_received_timestamp: from_unix_timestamp(timestamp1), }, ReceivableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), balance_wei: -2_000_300_000, - last_received_timestamp: from_time_t(timestamp2), + last_received_timestamp: from_unix_timestamp(timestamp2), } ] ) @@ -1793,7 +1796,7 @@ mod tests { .unwrap(); let insert = insert_account_by_separate_values; - let timestamp = utils::now_time_t(); + let timestamp = utils::current_unix_timestamp(); insert( &*conn, "0x1111111111111111111111111111111111111111", @@ -1855,7 +1858,7 @@ mod tests { &account.wallet, &high_bytes, &low_bytes, - &to_time_t(account.last_received_timestamp), + &to_unix_timestamp(account.last_received_timestamp), ]; stmt.execute(params).unwrap(); } diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs new file mode 100644 index 000000000..5cdc59047 --- /dev/null +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -0,0 +1,1140 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use ethereum_types::{H256, U64}; +use web3::types::Address; +use masq_lib::utils::ExpectValue; +use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers}; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; +use crate::database::rusqlite_wrappers::ConnectionWrapper; +use itertools::Itertools; + +#[derive(Debug, PartialEq, Eq)] +pub enum SentPayableDaoError { + EmptyInput, + NoChange, + InvalidInput(String), + PartialExecution(String), + SqlExecutionFailed(String), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Tx { + pub hash: TxHash, + pub receiver_address: Address, + pub amount: u128, + pub timestamp: i64, + pub gas_price_wei: u128, + pub nonce: u64, + pub block_opt: Option, +} + +pub enum RetrieveCondition { + IsPending, + ByHash(Vec), +} + +impl Display for RetrieveCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RetrieveCondition::IsPending => { + write!(f, "WHERE block_hash IS NULL") + } + RetrieveCondition::ByHash(tx_hashes) => { + write!( + f, + "WHERE tx_hash IN ({})", + comma_joined_stringifiable(tx_hashes, |hash| format!("'{:?}'", hash)) + ) + } + } + } +} + +pub trait SentPayableDao { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> Vec; + fn update_tx_blocks( + &self, + hash_map: &HashMap, + ) -> Result<(), SentPayableDaoError>; + fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError>; + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; +} + +#[derive(Debug)] +pub struct SentPayableDaoReal<'a> { + conn: Box, +} + +impl<'a> SentPayableDaoReal<'a> { + pub fn new(conn: Box) -> Self { + Self { conn } + } +} + +impl SentPayableDao for SentPayableDaoReal<'_> { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + let hashes_vec: Vec = hashes.iter().copied().collect(); + let sql = format!( + "SELECT tx_hash, rowid FROM sent_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)) + ); + + let mut stmt = self + .conn + .prepare(&sql) + .expect("Failed to prepare SQL statement"); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let tx_hash = H256::from_str(&tx_hash_str[2..]).expect("Failed to parse H256"); + let row_id: u64 = row.get(1).expectv("rowid"); + + Ok((tx_hash, row_id)) + }) + .expect("Failed to execute query") + .filter_map(Result::ok) + .collect() + } + + fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError> { + if txs.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); + if unique_hashes.len() != txs.len() { + return Err(SentPayableDaoError::InvalidInput(format!( + "Duplicate hashes found in the input. Input Transactions: {:?}", + txs + ))); + } + + let duplicates = self.get_tx_identifiers(&unique_hashes); + if !duplicates.is_empty() { + return Err(SentPayableDaoError::InvalidInput(format!( + "Duplicates detected in the database: {:?}", + duplicates, + ))); + } + + let sql = format!( + "INSERT INTO sent_payable (\ + tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + block_hash, \ + block_number + ) VALUES {}", + comma_joined_stringifiable(txs, |tx| { + let amount_checked = checked_conversion::(tx.amount); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); + let (gas_price_wei_high_b, gas_price_wei_low_b) = + BigIntDivider::deconstruct(gas_price_wei_checked); + let block_details = match &tx.block_opt { + Some(block) => format!("'{:?}', {}", block.block_hash, block.block_number), + None => "null, null".to_string(), + }; + format!( + "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, {})", + tx.hash, + tx.receiver_address, + amount_high_b, + amount_low_b, + tx.timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + tx.nonce, + block_details + ) + }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(inserted_rows) => { + if inserted_rows == txs.len() { + Ok(()) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} out of {} records inserted", + inserted_rows, + txs.len() + ))) + } + } + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn retrieve_txs(&self, condition_opt: Option) -> Vec { + let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ + timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, block_hash, block_number FROM sent_payable" + .to_string(); + let sql = match condition_opt { + None => raw_sql, + Some(condition) => format!("{} {}", raw_sql, condition), + }; + + let mut stmt = self + .conn + .prepare(&sql) + .expect("Failed to prepare SQL statement"); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let hash = H256::from_str(&tx_hash_str[2..]).expect("Failed to parse H256"); + let receiver_address_str: String = row.get(1).expectv("receivable_address"); + let receiver_address = + Address::from_str(&receiver_address_str[2..]).expect("Failed to parse H160"); + let amount_high_b = row.get(2).expectv("amount_high_b"); + let amount_low_b = row.get(3).expectv("amount_low_b"); + let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let timestamp = row.get(4).expectv("timestamp"); + let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); + let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); + let gas_price_wei = + BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; + let nonce = row.get(7).expectv("nonce"); + let block_hash_opt: Option = { + let block_hash_str_opt: Option = row.get(8).expectv("block_hash"); + block_hash_str_opt + .map(|string| H256::from_str(&string[2..]).expect("Failed to parse H256")) + }; + let block_number_opt: Option = { + let block_number_i64_opt: Option = row.get(9).expectv("block_number"); + block_number_i64_opt.map(|v| u64::try_from(v).expect("Failed to parse u64")) + }; + + let block_opt = match (block_hash_opt, block_number_opt) { + (Some(block_hash), Some(block_number)) => Some(TransactionBlock { + block_hash, + block_number: U64::from(block_number), + }), + (None, None) => None, + _ => panic!("Invalid block details"), + }; + + Ok(Tx { + hash, + receiver_address, + amount, + timestamp, + gas_price_wei, + nonce, + block_opt, + }) + }) + .expect("Failed to execute query") + .filter_map(Result::ok) + .collect() + } + + fn update_tx_blocks( + &self, + hash_map: &HashMap, + ) -> Result<(), SentPayableDaoError> { + if hash_map.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + for (hash, transaction_block) in hash_map { + let sql = format!( + "UPDATE sent_payable SET block_hash = '{:?}', block_number = {} WHERE tx_hash = '{:?}'", + transaction_block.block_hash, transaction_block.block_number, hash + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(updated_rows) => { + if updated_rows == 1 { + continue; + } else { + return Err(SentPayableDaoError::PartialExecution(format!( + "Failed to update status for hash {:?}", + hash + ))); + } + } + Err(e) => { + return Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())); + } + } + } + + Ok(()) + } + + fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError> { + if new_txs.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let build_case = |value_fn: fn(&Tx) -> String| { + new_txs + .iter() + .map(|tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx))) + .join(" ") + }; + + let tx_hash_cases = build_case(|tx| format!("'{:?}'", tx.hash)); + let receiver_address_cases = build_case(|tx| format!("'{:?}'", tx.receiver_address)); + let amount_high_b_cases = build_case(|tx| { + let amount_checked = checked_conversion::(tx.amount); + let (high, _) = BigIntDivider::deconstruct(amount_checked); + high.to_string() + }); + let amount_low_b_cases = build_case(|tx| { + let amount_checked = checked_conversion::(tx.amount); + let (_, low) = BigIntDivider::deconstruct(amount_checked); + low.to_string() + }); + let timestamp_cases = build_case(|tx| tx.timestamp.to_string()); + let gas_price_wei_high_b_cases = build_case(|tx| { + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (high, _) = BigIntDivider::deconstruct(gas_price_wei_checked); + high.to_string() + }); + let gas_price_wei_low_b_cases = build_case(|tx| { + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); + low.to_string() + }); + let block_hash_cases = build_case(|tx| match &tx.block_opt { + Some(block) => format!("'{:?}'", block.block_hash), + None => "NULL".to_string(), + }); + let block_number_cases = build_case(|tx| match &tx.block_opt { + Some(block) => block.block_number.as_u64().to_string(), + None => "NULL".to_string(), + }); + + let nonces = comma_joined_stringifiable(new_txs, |tx| tx.nonce.to_string()); + + let sql = format!( + "UPDATE sent_payable \ + SET \ + tx_hash = CASE \ + {tx_hash_cases} \ + END, \ + receiver_address = CASE \ + {receiver_address_cases} \ + END, \ + amount_high_b = CASE \ + {amount_high_b_cases} \ + END, \ + amount_low_b = CASE \ + {amount_low_b_cases} \ + END, \ + timestamp = CASE \ + {timestamp_cases} \ + END, \ + gas_price_wei_high_b = CASE \ + {gas_price_wei_high_b_cases} \ + END, \ + gas_price_wei_low_b = CASE \ + {gas_price_wei_low_b_cases} \ + END, \ + block_hash = CASE \ + {block_hash_cases} \ + END, \ + block_number = CASE \ + {block_number_cases} \ + END \ + WHERE nonce IN ({nonces})", + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(updated_rows) => match updated_rows { + 0 => Err(SentPayableDaoError::NoChange), + count if count == new_txs.len() => Ok(()), + _ => Err(SentPayableDaoError::PartialExecution(format!( + "Only {} out of {} records updated", + updated_rows, + new_txs.len() + ))), + }, + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + if hashes.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let hashes_vec: Vec = hashes.iter().cloned().collect(); + let sql = format!( + "DELETE FROM sent_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| { format!("'{:?}'", hash) }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(deleted_rows) => { + if deleted_rows == hashes.len() { + Ok(()) + } else if deleted_rows == 0 { + Err(SentPayableDaoError::NoChange) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} of the {} hashes has been deleted.", + deleted_rows, + hashes.len(), + ))) + } + } + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + use std::sync::{Arc, Mutex}; + use crate::accountant::db_access_objects::sent_payable_dao::{RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal}; + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, + }; + use crate::database::test_utils::ConnectionWrapperMock; + use ethereum_types::{ H256, U64}; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::{Connection}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending}; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; + use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; + + #[test] + fn insert_new_records_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "insert_new_records_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(2)) + .block(Default::default()) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let txs = vec![tx1, tx2]; + + let result = subject.insert_new_records(&txs); + + let retrieved_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(retrieved_txs.len(), 2); + assert_eq!(retrieved_txs, txs); + } + + #[test] + fn insert_new_records_throws_err_for_empty_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_err_for_empty_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let empty_input = vec![]; + + let result = subject.insert_new_records(&empty_input); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(1234); + let tx1 = TxBuilder::default() + .hash(hash) + .timestamp(1749204017) + .build(); + let tx2 = TxBuilder::default() + .hash(hash) + .timestamp(1749204020) + .block(Default::default()) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.insert_new_records(&vec![tx1, tx2]); + + assert_eq!( + result, + Err(SentPayableDaoError::InvalidInput( + "Duplicate hashes found in the input. Input Transactions: \ + [Tx { \ + hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ + nonce: 0, block_opt: None }, \ + Tx { \ + hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 1749204020, gas_price_wei: 0, \ + nonce: 0, block_opt: Some(TransactionBlock { \ + block_hash: 0x0000000000000000000000000000000000000000000000000000000000000000, \ + block_number: 0 }) }]" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(1234); + let tx1 = TxBuilder::default().hash(hash).build(); + let tx2 = TxBuilder::default() + .hash(hash) + .block(Default::default()) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let initial_insertion_result = subject.insert_new_records(&vec![tx1]); + + let result = subject.insert_new_records(&vec![tx2]); + + assert_eq!(initial_insertion_result, Ok(())); + assert_eq!( + result, + Err(SentPayableDaoError::InvalidInput( + "Duplicates detected in the database: \ + {0x00000000000000000000000000000000000000000000000000000000000004d2: 1}" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_returns_err_if_partially_executed() { + let setup_conn = Connection::open_in_memory().unwrap(); + setup_conn + .execute("CREATE TABLE example (id integer)", []) + .unwrap(); + let get_tx_identifiers_stmt = setup_conn.prepare("SELECT id FROM example").unwrap(); + let faulty_insert_stmt = { setup_conn.prepare("SELECT id FROM example").unwrap() }; + let wrapped_conn = ConnectionWrapperMock::default() + .prepare_result(Ok(get_tx_identifiers_stmt)) + .prepare_result(Ok(faulty_insert_stmt)); + let tx = TxBuilder::default().build(); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution( + "Only 0 out of 1 records inserted".to_string() + )) + ); + } + + #[test] + fn insert_new_records_can_throw_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_can_throw_error", + ); + let tx = TxBuilder::default().build(); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn get_tx_identifiers_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "get_tx_identifiers_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let another_present_hash = make_tx_hash(3); + let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); + let present_tx = TxBuilder::default().hash(present_hash).build(); + let another_present_tx = TxBuilder::default().hash(another_present_hash).build(); + subject + .insert_new_records(&vec![present_tx, another_present_tx]) + .unwrap(); + + let result = subject.get_tx_identifiers(&hashset); + + assert_eq!(result.get(&present_hash), Some(&1u64)); + assert_eq!(result.get(&absent_hash), None); + assert_eq!(result.get(&another_present_hash), Some(&2u64)); + } + + #[test] + fn retrieve_condition_display_works() { + assert_eq!(IsPending.to_string(), "WHERE block_hash IS NULL"); + assert_eq!( + ByHash(vec![ + H256::from_low_u64_be(0x123456789), + H256::from_low_u64_be(0x987654321), + ]) + .to_string(), + "WHERE tx_hash IN (\ + '0x0000000000000000000000000000000000000000000000000000000123456789', \ + '0x0000000000000000000000000000000000000000000000000000000987654321'\ + )" + .to_string() + ); + } + + #[test] + fn can_retrieve_all_txs() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_all_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(2)) + .block(Default::default()) + .build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + subject.insert_new_records(&vec![tx3.clone()]).unwrap(); + + let result = subject.retrieve_txs(None); + + assert_eq!(result, vec![tx1, tx2, tx3]); + } + + #[test] + fn can_retrieve_pending_txs() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_pending_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default() + .hash(make_tx_hash(3)) + .block(Default::default()) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3]) + .unwrap(); + + let result = subject.retrieve_txs(Some(RetrieveCondition::IsPending)); + + assert_eq!(result, vec![tx1, tx2]); + } + + #[test] + fn tx_can_be_retrieved_by_hash() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_hash"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx3.hash]))); + + assert_eq!(result, vec![tx1, tx3]); + } + + #[test] + #[should_panic(expected = "Invalid block details")] + fn retrieve_txs_enforces_complete_block_details() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "retrieve_txs_enforces_complete_block_details", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + // Insert a record with block_hash but no block_number + { + let sql = "INSERT INTO sent_payable (\ + tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + block_hash, \ + block_number\ + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"; + let mut stmt = wrapped_conn.prepare(sql).unwrap(); + stmt.execute(rusqlite::params![ + "0x1234567890123456789012345678901234567890123456789012345678901234", + "0x1234567890123456789012345678901234567890", + 0, + 100, + 1234567890, + 0, + 1000000000, + 1, + "0x2345678901234567890123456789012345678901234567890123456789012345", + rusqlite::types::Null, + ]) + .unwrap(); + } + let subject = SentPayableDaoReal::new(wrapped_conn); + + // This should panic due to invalid block details + let _ = subject.retrieve_txs(None); + } + + #[test] + fn update_tx_blocks_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "update_tx_blocks_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let pre_assert_is_block_details_present_tx1 = tx1.block_opt.is_some(); + let pre_assert_is_block_details_present_tx2 = tx2.block_opt.is_some(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + let tx_block_1 = TransactionBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), + }; + let tx_block_2 = TransactionBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), + }; + let hash_map = HashMap::from([ + (tx1.hash, tx_block_1.clone()), + (tx2.hash, tx_block_2.clone()), + ]); + + let result = subject.update_tx_blocks(&hash_map); + + let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); + assert_eq!(result, Ok(())); + assert_eq!(pre_assert_is_block_details_present_tx1, false); + assert_eq!(updated_txs[0].block_opt, Some(tx_block_1)); + assert_eq!(pre_assert_is_block_details_present_tx2, false); + assert_eq!(updated_txs[1].block_opt, Some(tx_block_2)); + } + + #[test] + fn update_tx_blocks_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_tx_blocks_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let existent_hash = make_tx_hash(1); + let tx = TxBuilder::default().hash(existent_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hash_map = HashMap::new(); + + let result = subject.update_tx_blocks(&hash_map); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn update_tx_blocks_returns_error_during_partial_execution() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_tx_blocks_returns_error_during_partial_execution", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let existent_hash = make_tx_hash(1); + let non_existent_hash = make_tx_hash(999); + let tx = TxBuilder::default().hash(existent_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hash_map = HashMap::from([ + ( + existent_hash, + TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }, + ), + ( + non_existent_hash, + TransactionBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), + }, + ), + ]); + + let result = subject.update_tx_blocks(&hash_map); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution(format!( + "Failed to update status for hash {:?}", + non_existent_hash + ))) + ); + } + + #[test] + fn update_tx_blocks_returns_error_when_an_error_occurs_while_executing_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_tx_blocks_returns_error_when_an_error_occurs_while_executing_sql", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let hash = make_tx_hash(1); + let hash_map = HashMap::from([( + hash, + TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), + }, + )]); + + let result = subject.update_tx_blocks(&hash_map); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn txs_can_be_deleted() { + let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "txs_can_be_deleted"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); + let tx4 = TxBuilder::default() + .hash(make_tx_hash(4)) + .block(Default::default()) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .unwrap(); + let hashset = HashSet::from([tx1.hash, tx3.hash]); + + let result = subject.delete_records(&hashset); + + let remaining_records = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(remaining_records, vec![tx2, tx4]); + } + + #[test] + fn delete_records_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.delete_records(&HashSet::new()); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn delete_records_returns_error_when_no_records_are_deleted() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_no_records_are_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let non_existent_hash = make_tx_hash(999); + let hashset = HashSet::from([non_existent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!(result, Err(SentPayableDaoError::NoChange)); + } + + #[test] + fn delete_records_returns_error_when_not_all_input_records_were_deleted() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_not_all_input_records_were_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let tx = TxBuilder::default().hash(present_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hashset = HashSet::from([present_hash, absent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution( + "Only 1 of the 2 hashes has been deleted.".to_string() + )) + ); + } + + #[test] + fn delete_records_returns_a_general_error_from_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_a_general_error_from_sql", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let hashes = HashSet::from([make_tx_hash(1)]); + + let result = subject.delete_records(&hashes); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn replace_records_works_as_expected() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_works_as_expected", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).nonce(3).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3]) + .unwrap(); + let new_tx2 = TxBuilder::default() + .hash(make_tx_hash(22)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(2) + .build(); + let new_tx3 = TxBuilder::default() + .hash(make_tx_hash(33)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(3) + .build(); + + let result = subject.replace_records(&[new_tx2.clone(), new_tx3.clone()]); + + let retrieved_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(retrieved_txs, vec![tx1, new_tx2, new_tx3]); + } + + #[test] + fn replace_records_uses_single_sql_statement() { + let prepare_params = Arc::new(Mutex::new(vec![])); + let setup_conn = Connection::open_in_memory().unwrap(); + setup_conn + .execute("CREATE TABLE example (id integer)", []) + .unwrap(); + let stmt = setup_conn.prepare("SELECT id FROM example").unwrap(); + let wrapped_conn = ConnectionWrapperMock::default() + .prepare_params(&prepare_params) + .prepare_result(Ok(stmt)); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).nonce(3).build(); + + let _ = subject.replace_records(&[tx1, tx2, tx3]); + + let captured_params = prepare_params.lock().unwrap(); + let sql = &captured_params[0]; + assert!(sql.starts_with("UPDATE sent_payable SET")); + assert!(sql.contains("tx_hash = CASE")); + assert!(sql.contains("receiver_address = CASE")); + assert!(sql.contains("amount_high_b = CASE")); + assert!(sql.contains("amount_low_b = CASE")); + assert!(sql.contains("timestamp = CASE")); + assert!(sql.contains("gas_price_wei_high_b = CASE")); + assert!(sql.contains("gas_price_wei_low_b = CASE")); + assert!(sql.contains("block_hash = CASE")); + assert!(sql.contains("block_number = CASE")); + assert!(sql.contains("WHERE nonce IN (1, 2, 3)")); + assert!(sql.contains("WHEN nonce = 1 THEN '0x0000000000000000000000000000000000000000000000000000000000000001'")); + assert!(sql.contains("WHEN nonce = 2 THEN '0x0000000000000000000000000000000000000000000000000000000000000002'")); + assert!(sql.contains("WHEN nonce = 3 THEN '0x0000000000000000000000000000000000000000000000000000000000000003'")); + assert_eq!(captured_params.len(), 1); + } + + #[test] + fn replace_records_throws_error_for_empty_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_throws_error_for_empty_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + subject.insert_new_records(&vec![tx1, tx2]).unwrap(); + + let result = subject.replace_records(&[]); + + assert_eq!(result, Err(EmptyInput)); + } + + #[test] + fn replace_records_throws_partial_execution_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_throws_partial_execution_error", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + let new_tx2 = TxBuilder::default() + .hash(make_tx_hash(22)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(2) + .build(); + let new_tx3 = TxBuilder::default() + .hash(make_tx_hash(33)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(3) + .build(); + + let result = subject.replace_records(&[new_tx2, new_tx3]); + + assert_eq!( + result, + Err(PartialExecution( + "Only 1 out of 2 records updated".to_string() + )) + ); + } + + #[test] + fn replace_records_returns_no_change_error_when_no_rows_updated() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_returns_no_change_error_when_no_rows_updated", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx = TxBuilder::default().hash(make_tx_hash(1)).nonce(42).build(); + + let result = subject.replace_records(&[tx]); + + assert_eq!(result, Err(SentPayableDaoError::NoChange)); + } + + #[test] + fn replace_records_returns_a_general_error_from_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_returns_a_general_error_from_sql", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let tx = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + + let result = subject.replace_records(&[tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } +} diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs new file mode 100644 index 000000000..598a4121d --- /dev/null +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -0,0 +1,134 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +#![cfg(test)] + +use std::path::PathBuf; +use rusqlite::{Connection, OpenFlags}; +use crate::accountant::db_access_objects::sent_payable_dao::{ Tx}; +use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use web3::types::{Address}; +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionBlock; +use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE}; +use crate::database::rusqlite_wrappers::ConnectionWrapperReal; + +#[derive(Default)] +pub struct TxBuilder { + hash_opt: Option, + receiver_address_opt: Option
, + amount_opt: Option, + timestamp_opt: Option, + gas_price_wei_opt: Option, + nonce_opt: Option, + block_opt: Option, +} + +impl TxBuilder { + pub fn default() -> Self { + Default::default() + } + + pub fn hash(mut self, hash: TxHash) -> Self { + self.hash_opt = Some(hash); + self + } + + pub fn timestamp(mut self, timestamp: i64) -> Self { + self.timestamp_opt = Some(timestamp); + self + } + + pub fn nonce(mut self, nonce: u64) -> Self { + self.nonce_opt = Some(nonce); + self + } + + pub fn block(mut self, block: TransactionBlock) -> Self { + self.block_opt = Some(block); + self + } + + pub fn build(self) -> Tx { + Tx { + hash: self.hash_opt.unwrap_or_default(), + receiver_address: self.receiver_address_opt.unwrap_or_default(), + amount: self.amount_opt.unwrap_or_default(), + timestamp: self.timestamp_opt.unwrap_or_else(current_unix_timestamp), + gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + nonce: self.nonce_opt.unwrap_or_default(), + block_opt: self.block_opt, + } + } +} + +#[derive(Default)] +pub struct FailedTxBuilder { + hash_opt: Option, + receiver_address_opt: Option
, + amount_opt: Option, + timestamp_opt: Option, + gas_price_wei_opt: Option, + nonce_opt: Option, + reason_opt: Option, + rechecked_opt: Option, +} + +impl FailedTxBuilder { + pub fn default() -> Self { + Default::default() + } + + pub fn hash(mut self, hash: TxHash) -> Self { + self.hash_opt = Some(hash); + self + } + + pub fn timestamp(mut self, timestamp: i64) -> Self { + self.timestamp_opt = Some(timestamp); + self + } + + pub fn nonce(mut self, nonce: u64) -> Self { + self.nonce_opt = Some(nonce); + self + } + + pub fn reason(mut self, reason: FailureReason) -> Self { + self.reason_opt = Some(reason); + self + } + + pub fn rechecked(mut self, rechecked: bool) -> Self { + self.rechecked_opt = Some(rechecked); + self + } + + pub fn build(self) -> FailedTx { + FailedTx { + hash: self.hash_opt.unwrap_or_default(), + receiver_address: self.receiver_address_opt.unwrap_or_default(), + amount: self.amount_opt.unwrap_or_default(), + timestamp: self.timestamp_opt.unwrap_or_default(), + gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + nonce: self.nonce_opt.unwrap_or_default(), + reason: self + .reason_opt + .unwrap_or_else(|| FailureReason::PendingTooLong), + rechecked: self.rechecked_opt.unwrap_or_else(|| false), + } + } +} + +pub fn make_read_only_db_connection(home_dir: PathBuf) -> ConnectionWrapperReal { + { + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + } + let read_only_conn = Connection::open_with_flags( + home_dir.join(DATABASE_FILE), + OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .unwrap(); + + ConnectionWrapperReal::new(read_only_conn) +} diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 8b78bb5f4..8fbc875c2 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -9,11 +9,13 @@ use crate::database::db_initializer::{ }; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::accountant::PaymentThresholds; +use ethereum_types::H256; use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::messages::{ RangeQuery, TopRecordsConfig, TopRecordsOrdering, UiPayableAccount, UiReceivableAccount, }; use rusqlite::{Row, Statement, ToSql}; +use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::iter::FlatMap; use std::path::{Path, PathBuf}; @@ -21,7 +23,11 @@ use std::string::ToString; use std::time::Duration; use std::time::SystemTime; -pub fn to_time_t(system_time: SystemTime) -> i64 { +pub type TxHash = H256; +pub type RowId = u64; +pub type TxIdentifiers = HashMap; + +pub fn to_unix_timestamp(system_time: SystemTime) -> i64 { match system_time.duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => sign_conversion::(d.as_secs()).expect("MASQNode has expired"), Err(e) => panic!( @@ -31,12 +37,12 @@ pub fn to_time_t(system_time: SystemTime) -> i64 { } } -pub fn now_time_t() -> i64 { - to_time_t(SystemTime::now()) +pub fn current_unix_timestamp() -> i64 { + to_unix_timestamp(SystemTime::now()) } -pub fn from_time_t(time_t: i64) -> SystemTime { - let interval = Duration::from_secs(time_t as u64); +pub fn from_unix_timestamp(unix_timestamp: i64) -> SystemTime { + let interval = Duration::from_secs(unix_timestamp as u64); SystemTime::UNIX_EPOCH + interval } @@ -193,11 +199,11 @@ impl CustomQuery { max_age: u64, timestamp: SystemTime, ) -> RusqliteParamsWithOwnedToSql { - let now = to_time_t(timestamp); - let age_to_time_t = |age_limit| now - checked_conversion::(age_limit); + let now = to_unix_timestamp(timestamp); + let age_to_unix_timestamp = |age_limit| now - checked_conversion::(age_limit); vec![ - (":min_timestamp", Box::new(age_to_time_t(max_age))), - (":max_timestamp", Box::new(age_to_time_t(min_age))), + (":min_timestamp", Box::new(age_to_unix_timestamp(max_age))), + (":max_timestamp", Box::new(age_to_unix_timestamp(min_age))), ] } @@ -299,7 +305,7 @@ pub fn remap_receivable_accounts(accounts: Vec) -> Vec u64 { - (to_time_t(SystemTime::now()) - to_time_t(timestamp)) as u64 + (to_unix_timestamp(SystemTime::now()) - to_unix_timestamp(timestamp)) as u64 } #[allow(clippy::type_complexity)] @@ -466,8 +472,8 @@ mod tests { }; let assigned_value_1 = get_assigned_value(param_pair_1.1.to_sql().unwrap()); let assigned_value_2 = get_assigned_value(param_pair_2.1.to_sql().unwrap()); - assert_eq!(assigned_value_1, to_time_t(now) - 10000); - assert_eq!(assigned_value_2, to_time_t(now) - 5555) + assert_eq!(assigned_value_1, to_unix_timestamp(now) - 10000); + assert_eq!(assigned_value_2, to_unix_timestamp(now) - 5555) } #[test] @@ -608,10 +614,10 @@ mod tests { #[test] #[should_panic(expected = "Must be wrong, moment way far in the past")] - fn to_time_t_does_not_like_time_traveling() { + fn to_unix_timestamp_does_not_like_time_traveling() { let far_far_before = UNIX_EPOCH.checked_sub(Duration::from_secs(1)).unwrap(); - let _ = to_time_t(far_far_before); + let _ = to_unix_timestamp(far_far_before); } #[test] diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 0a74d076c..24dbdcc68 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -22,10 +22,10 @@ use crate::accountant::db_access_objects::utils::{ use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, }; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, ScanSchedulers, Scanners}; +use crate::accountant::scanners::{StartScanError, Scanners}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -57,14 +57,14 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::UiFinancialsResponse; +use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, ScanType, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, + QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; use masq_lib::ui_gateway::MessageTarget::ClientId; -use masq_lib::ui_gateway::{MessageBody, MessagePath}; +use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; @@ -76,13 +76,15 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; +use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours pub struct Accountant { - suppress_initial_scans: bool, consuming_wallet_opt: Option, earning_wallet: Wallet, payable_dao: Box, @@ -95,7 +97,7 @@ pub struct Accountant { outbound_payments_instructions_sub_opt: Option>, qualified_payables_sub_opt: Option>, retrieve_transactions_sub_opt: Option>, - request_transaction_receipts_subs_opt: Option>, + request_transaction_receipts_sub_opt: Option>, report_inbound_payments_sub_opt: Option>, report_sent_payables_sub_opt: Option>, ui_message_sub_opt: Option>, @@ -134,24 +136,35 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } -#[derive(Debug, Message, PartialEq)] +#[derive(Debug, PartialEq, Eq, Message, Clone)] +pub struct ReportTransactionReceipts { + pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, + pub response_skeleton_opt: Option, +} + +#[derive(Debug, Message, PartialEq, Clone)] pub struct SentPayables { pub payment_procedure_result: Result, PayableTransactionError>, pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForPayables { +pub struct ScanForPendingPayables { pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForReceivables { +pub struct ScanForNewPayables { pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForPendingPayables { +pub struct ScanForRetryPayables { + pub response_skeleton_opt: Option, +} + +#[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] +pub struct ScanForReceivables { pub response_skeleton_opt: Option, } @@ -183,94 +196,202 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, _msg: StartMessage, ctx: &mut Self::Context) -> Self::Result { - if self.suppress_initial_scans { - info!( - &self.logger, - "Started with --scans off; declining to begin database and blockchain scans" - ); - } else { + if self.scan_schedulers.automatic_scans_enabled { debug!( &self.logger, "Started with --scans on; starting database and blockchain scans" ); - ctx.notify(ScanForPendingPayables { response_skeleton_opt: None, }); - ctx.notify(ScanForPayables { - response_skeleton_opt: None, - }); ctx.notify(ScanForReceivables { response_skeleton_opt: None, }); + } else { + info!( + &self.logger, + "Started with --scans off; declining to begin database and blockchain scans" + ); } } } -impl Handler for Accountant { +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_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); + fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { + // By now we know this is an automatic scan process. The scan may be or may not be + // rescheduled. It depends on the findings. Any failed transaction will lead to the launch + // of the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled + // to run again. However, not from here. + let response_skeleton_opt = msg.response_skeleton_opt; + + let scheduling_hint = + self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); + + match scheduling_hint { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + ScanRescheduleAfterEarlyStop::Schedule(scan_type) => unreachable!( + "Early stopped pending payable scan was suggested to be followed up \ + by the scan for {:?}, which is not supported though", + scan_type + ), + ScanRescheduleAfterEarlyStop::DoNotSchedule => { + trace!( + self.logger, + "No early rescheduling, as the pending payable scan did find results" + ); + } } } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle( - &mut self, - msg: BlockchainAgentWithContextMessage, - _ctx: &mut Self::Context, - ) -> Self::Result { - self.handle_payable_payment_setup(msg) + fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { + // We know this must be a scheduled scan, but are yet clueless where it's going to be + // rescheduled. If no payable qualifies for a payment, we do it here right away. If some + // transactions made it out, the next scheduling of this scanner is going to be decided by + // the PendingPayableScanner whose job is to evaluate if it has seen every pending payable + // complete. That's the moment when another run of the NewPayableScanner makes sense again. + let response_skeleton = msg.response_skeleton_opt; + + let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); + + match scheduling_hint { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + ScanRescheduleAfterEarlyStop::Schedule(other_scan_type) => unreachable!( + "Early stopped new payable scan was suggested to be followed up by the scan \ + for {:?}, which is not supported though", + other_scan_type + ), + ScanRescheduleAfterEarlyStop::DoNotSchedule => { + trace!( + self.logger, + "No early rescheduling, as the new payable scan did find results" + ) + } + } } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: SentPayables, _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_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); - } + fn handle(&mut self, msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { + // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes discovering + // that there have been some failed payables. No place for that here. + let response_skeleton = msg.response_skeleton_opt; + self.handle_request_of_scan_for_retry_payable(response_skeleton); } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_payable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::Payables, ctx); + fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { + // By now we know it is an automatic scan. The ReceivableScanner is independent of other + // scanners and rescheduled regularly, just here. + self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); + self.scan_schedulers.receivable.schedule(ctx, &self.logger); } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_pending_payable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::PendingPayables, ctx); + fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { + let response_skeleton_opt = msg.response_skeleton_opt; + match self.scanners.finish_pending_payable_scan(msg, &self.logger) { + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) => { + if let Some(node_to_ui_msg) = ui_msg_opt { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + // Externally triggered scan should never be allowed to spark a procedure that + // would bring over payables with fresh nonces. The job's done. + } else { + self.scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger) + } + } + PendingPayableScanResult::PaymentRetryRequired => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + }; } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::Receivables, ctx); + fn handle( + &mut self, + msg: BlockchainAgentWithContextMessage, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_payable_payment_setup(msg) + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { + let scan_result = self.scanners.finish_payable_scan(msg, &self.logger); + + match scan_result.ui_response_opt { + None => match scan_result.result { + OperationOutcome::NewPendingPayable => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + OperationOutcome::Failure => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + }, + Some(node_to_ui_msg) => { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + + // Externally triggered scans are not allowed to provoke an unwinding scan sequence + // with intervals. The only exception is the PendingPayableScanner and retry- + // payable scanner, which are ever meant to run in a tight tandem. + } + } + } +} + +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.finish_receivable_scan(msg, &self.logger) { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } } } @@ -279,17 +400,8 @@ impl Handler for Accountant { fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); - match scan_error.scan_type { - ScanType::Payables => { - self.scanners.payable.mark_as_ended(&self.logger); - } - ScanType::PendingPayables => { - self.scanners.pending_payable.mark_as_ended(&self.logger); - } - ScanType::Receivables => { - self.scanners.receivable.mark_as_ended(&self.logger); - } - }; + self.scanners + .acknowledge_scan_error(&scan_error, &self.logger); if let Some(response_skeleton) = scan_error.response_skeleton_opt { let error_msg = NodeToUiMessage { target: ClientId(response_skeleton.client_id), @@ -357,7 +469,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable: Vec, + pub pending_payable_fingerprints: Vec, pub response_skeleton_opt: Option, } @@ -367,26 +479,6 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -#[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, - pub response_skeleton_opt: Option, -} - -impl Handler for Accountant { - type Result = (); - - 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_opt - .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( @@ -429,6 +521,7 @@ impl Accountant { let payable_dao = dao_factories.payable_dao_factory.make(); let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); + let scan_schedulers = ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), @@ -437,7 +530,6 @@ impl Accountant { ); Accountant { - suppress_initial_scans: config.suppress_initial_scans, consuming_wallet_opt: config.consuming_wallet_opt.clone(), earning_wallet, payable_dao, @@ -445,14 +537,14 @@ impl Accountant { pending_payable_dao, scanners, crashable: config.crash_point == CrashPoint::Message, - scan_schedulers: ScanSchedulers::new(scan_intervals), + scan_schedulers, financial_statistics: Rc::clone(&financial_statistics), outbound_payments_instructions_sub_opt: None, qualified_payables_sub_opt: None, report_sent_payables_sub_opt: None, retrieve_transactions_sub_opt: None, report_inbound_payments_sub_opt: None, - request_transaction_receipts_subs_opt: None, + request_transaction_receipts_sub_opt: None, ui_message_sub_opt: None, message_id_generator: Box::new(MessageIdGeneratorReal::default()), logger: Logger::new("Accountant"), @@ -571,7 +663,7 @@ impl Accountant { Some(msg.peer_actors.blockchain_bridge.qualified_payables); self.report_sent_payables_sub_opt = Some(msg.peer_actors.accountant.report_sent_payments); self.ui_message_sub_opt = Some(msg.peer_actors.ui_gateway.node_to_ui_message_sub); - self.request_transaction_receipts_subs_opt = Some( + self.request_transaction_receipts_sub_opt = Some( msg.peer_actors .blockchain_bridge .request_transaction_receipts, @@ -600,14 +692,6 @@ impl Accountant { } } - fn schedule_next_scan(&self, scan_type: ScanType, ctx: &mut Context) { - self.scan_schedulers - .schedulers - .get(&scan_type) - .unwrap_or_else(|| panic!("Scan Scheduler {:?} not properly prepared", scan_type)) - .schedule(ctx) - } - fn handle_report_routing_service_provided_message( &mut self, msg: ReportRoutingServiceProvidedMessage, @@ -691,15 +775,13 @@ impl Accountant { fn handle_payable_payment_setup(&mut self, msg: BlockchainAgentWithContextMessage) { let blockchain_bridge_instructions = match self .scanners - .payable - .try_skipping_payment_adjustment(msg, &self.logger) + .try_skipping_payable_adjustment(msg, &self.logger) { Ok(Either::Left(finalized_msg)) => finalized_msg, Ok(Either::Right(unaccepted_msg)) => { //TODO we will eventually query info from Neighborhood before the adjustment, according to GH-699 self.scanners - .payable - .perform_payment_adjustment(unaccepted_msg, &self.logger) + .perform_payable_adjustment(unaccepted_msg, &self.logger) } Err(_e) => todo!("be completed by GH-711"), }; @@ -839,19 +921,53 @@ impl Accountant { } } - fn handle_request_of_scan_for_payable( + fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) { - let result = match self.consuming_wallet_opt.clone() { - Some(consuming_wallet) => self.scanners.payable.begin_scan( - consuming_wallet, - SystemTime::now(), + ) -> ScanRescheduleAfterEarlyStop { + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( + consuming_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + self.scan_schedulers.automatic_scans_enabled, + ), + None => Err(StartScanError::NoConsumingWalletFound), + }; + + match result { + Ok(scan_message) => { + self.qualified_payables_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"); + ScanRescheduleAfterEarlyStop::DoNotSchedule + } + Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::NewPayables, + e, response_skeleton_opt, - &self.logger, ), - None => Err(BeginScanError::NoConsumingWalletFound), - }; + } + } + + fn handle_request_of_scan_for_retry_payable( + &mut self, + response_skeleton_opt: Option, + ) { + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( + consuming_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ), + None => Err(StartScanError::NoConsumingWalletFound), + }; match result { Ok(scan_message) => { @@ -861,65 +977,125 @@ impl Accountant { .try_send(scan_message) .expect("BlockchainBridge is dead"); } - Err(e) => e.handle_error( - &self.logger, - ScanType::Payables, - response_skeleton_opt.is_some(), - ), + Err(e) => { + let _ = self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::RetryPayables, + e, + response_skeleton_opt, + ); + } } } fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) { - let result = match self.consuming_wallet_opt.clone() { - Some(consuming_wallet) => self.scanners.pending_payable.begin_scan( - consuming_wallet, // This argument is not used and is therefore irrelevant - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ), - None => Err(BeginScanError::NoConsumingWalletFound), + ) -> ScanRescheduleAfterEarlyStop { + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( + consuming_wallet, // This argument is not used and is therefore irrelevant + SystemTime::now(), + response_skeleton_opt, + &self.logger, + self.scan_schedulers.automatic_scans_enabled, + ), + None => Err(StartScanError::NoConsumingWalletFound), + }; + + let hint: ScanRescheduleAfterEarlyStop = match result { + Ok(scan_message) => { + self.request_transaction_receipts_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"); + ScanRescheduleAfterEarlyStop::DoNotSchedule + } + Err(e) => { + let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); + self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan, + }, + e, + response_skeleton_opt, + ) + } }; - match result { - Ok(scan_message) => self - .request_transaction_receipts_subs_opt - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(scan_message) - .expect("BlockchainBridge is dead"), - Err(e) => e.handle_error( - &self.logger, - ScanType::PendingPayables, - response_skeleton_opt.is_some(), - ), + if self.scanners.initial_pending_payable_scan() { + self.scanners.unset_initial_pending_payable_scan() } + + hint + } + + fn handle_start_scan_error_and_prevent_scan_stall_point( + &self, + scanner: PayableSequenceScanner, + e: StartScanError, + response_skeleton_opt: Option, + ) -> ScanRescheduleAfterEarlyStop { + let is_externally_triggered = response_skeleton_opt.is_some(); + + e.log_error(&self.logger, scanner.into(), is_externally_triggered); + + if let Some(skeleton) = response_skeleton_opt { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }; + + self.scan_schedulers + .reschedule_on_error_resolver + .resolve_rescheduling_on_error(scanner, &e, is_externally_triggered, &self.logger) } fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, ) { - match self.scanners.receivable.begin_scan( - self.earning_wallet.clone(), - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ) { + let result: Result = + self.scanners.start_receivable_scan_guarded( + &self.earning_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + self.scan_schedulers.automatic_scans_enabled, + ); + + match result { Ok(scan_message) => self .retrieve_transactions_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"), - Err(e) => e.handle_error( - &self.logger, - ScanType::Receivables, - response_skeleton_opt.is_some(), - ), - }; + Err(e) => { + e.log_error( + &self.logger, + ScanType::Receivables, + response_skeleton_opt.is_some(), + ); + + if let Some(skeleton) = response_skeleton_opt { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }; + } + } } fn handle_externally_triggered_scan( @@ -928,13 +1104,17 @@ impl Accountant { scan_type: ScanType, response_skeleton: ResponseSkeleton, ) { + // Each of these scans runs only once per request, they do not go on into a sequence under + // any circumstances match scan_type { - ScanType::Payables => self.handle_request_of_scan_for_payable(Some(response_skeleton)), + ScanType::Payables => { + self.handle_request_of_scan_for_new_payable(Some(response_skeleton)); + } ScanType::PendingPayables => { self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)); } ScanType::Receivables => { - self.handle_request_of_scan_for_receivable(Some(response_skeleton)) + self.handle_request_of_scan_for_receivable(Some(response_skeleton)); } } } @@ -1043,33 +1223,26 @@ mod tests { PendingPayable, PendingPayableDaoError, TransactionHashes, }; use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::BeginScanError; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement}; + use crate::accountant::scanners::{StartScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_payables, - BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, NullScanner, - PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, - PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, - ReceivableDaoMock, ScannerMock, - }; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; - use crate::blockchain::blockchain_bridge::BlockchainBridge; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::test_utils::{ - make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, + make_tx_hash }; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::config_dao::ConfigDaoRecord; use crate::db_config::mocks::ConfigDaoMock; - use crate::match_every_type_id; + use crate::{match_lazily_every_type_id, setup_for_counter_msg_triggered_via_type_id}; use crate::sub_lib::accountant::{ ExitServiceConsumed, PaymentThresholds, RoutingServiceConsumed, ScanIntervals, DEFAULT_EARNING_WALLET, DEFAULT_PAYMENT_THRESHOLDS, @@ -1077,47 +1250,46 @@ mod tests { use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::neighborhood::ConfigChange; use crate::sub_lib::neighborhood::{Hops, WalletPair}; - use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; - use crate::test_utils::recorder::make_recorder; + use crate::test_utils::recorder::{make_recorder, PeerActorsBuilder, SetUpCounterMsgs}; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use crate::test_utils::unshared_test_utils::notify_handlers::NotifyLaterHandleMock; + use crate::test_utils::unshared_test_utils::notify_handlers::{NotifyHandleMock, NotifyLaterHandleMock}; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; use crate::test_utils::unshared_test_utils::{ assert_on_initialization_with_panic_on_migration, make_bc_with_defaults, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; use crate::test_utils::{make_paying_wallet, make_wallet}; - use actix::{Arbiter, System}; + use actix::{System}; use ethereum_types::U64; use ethsign_crypto::Keccak256; - use log::Level; + use log::{Level}; use masq_lib::constants::{ REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, VALUE_EXCEEDS_ALLOWED_LIMIT, }; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; use masq_lib::messages::{ - CustomQueries, RangeQuery, ScanType, TopRecordsConfig, UiFinancialStatistics, + CustomQueries, RangeQuery, TopRecordsConfig, UiFinancialStatistics, UiMessageError, UiPayableAccount, UiReceivableAccount, UiScanRequest, UiScanResponse, }; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; - use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; - use masq_lib::utils::find_free_port; - use std::any::TypeId; - use std::ops::{Add, Sub}; - use std::str::FromStr; + use std::any::{TypeId}; + use std::ops::{Sub}; use std::sync::Arc; use std::sync::Mutex; - use std::time::Duration; + use std::time::{Duration, UNIX_EPOCH}; use std::vec; + use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::test_utils::recorder_counter_msgs::SingleTypeCounterMsgSetup; impl Handler> for Accountant { type Result = (); @@ -1230,33 +1402,26 @@ mod tests { ); let financial_statistics = result.financial_statistics().clone(); - let assert_scan_scheduler = |scan_type: ScanType, expected_scan_interval: Duration| { - assert_eq!( - result - .scan_schedulers - .schedulers - .get(&scan_type) - .unwrap() - .interval(), - expected_scan_interval - ) - }; let default_scan_intervals = ScanIntervals::default(); - assert_scan_scheduler( - ScanType::Payables, - default_scan_intervals.payable_scan_interval, + assert_eq!( + result.scan_schedulers.payable.new_payable_interval, + default_scan_intervals.payable_scan_interval ); - assert_scan_scheduler( - ScanType::PendingPayables, + assert_eq!( + result.scan_schedulers.pending_payable.interval, default_scan_intervals.pending_payable_scan_interval, ); - assert_scan_scheduler( - ScanType::Receivables, + assert_eq!( + result.scan_schedulers.receivable.interval, default_scan_intervals.receivable_scan_interval, ); + assert_eq!(result.scan_schedulers.automatic_scans_enabled, true); + assert_eq!( + result.scanners.aware_of_unresolved_pending_payables(), + false + ); assert_eq!(result.consuming_wallet_opt, None); assert_eq!(result.earning_wallet, *DEFAULT_EARNING_WALLET); - assert_eq!(result.suppress_initial_scans, false); result .message_id_generator .as_any() @@ -1330,100 +1495,7 @@ mod tests { } #[test] - fn scan_receivables_request() { - 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_daos(vec![ForReceivableScanner(receivable_dao)]) - .build(); - 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::Receivables, - } - .tmb(4321), - }; - - subject_addr.try_send(ui_message).unwrap(); - - System::current().stop(); - system.run(); - let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!( - blockchain_bridge_recording.get_record::(0), - &RetrieveTransactions { - recipient: make_wallet("earning_wallet"), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - } - ); - } - - #[test] - fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }); - config.suppress_initial_scans = true; - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .config_dao( - ConfigDaoMock::new() - .get_result(Ok(ConfigDaoRecord::new("start_block", None, false))) - .set_result(Ok(())), - ) - .build(); - let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let received_payments = ReceivedPayments { - timestamp: SystemTime::now(), - new_start_block: BlockMarker::Value(0), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - transactions: vec![], - }; - - subject_addr.try_send(received_payments).unwrap(); - - System::current().stop(); - system.run(); - let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); - assert_eq!( - ui_gateway_recording.get_record::(0), - &NodeToUiMessage { - target: ClientId(1234), - body: UiScanResponse {}.tmb(4321), - } - ); - } - - #[test] - fn scan_payables_request() { + fn externally_triggered_scan_payables_request() { let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); let consuming_wallet = make_paying_wallet(b"consuming"); let payable_account = PayableAccount { @@ -1436,18 +1508,24 @@ mod tests { }; let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(QualifiedPayablesMessage)); + let blockchain_bridge_addr = blockchain_bridge.start(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); 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 { @@ -1458,13 +1536,12 @@ mod tests { subject_addr.try_send(ui_message).unwrap(); - System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( blockchain_bridge_recording.get_record::(0), &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable_account]), + qualified_payables: vec![payable_account], consuming_wallet, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1483,17 +1560,18 @@ mod tests { no_rowid_results: vec![], }); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let sent_payable = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: make_wallet("blah"), @@ -1504,6 +1582,7 @@ mod tests { context_id: 4321, }), }; + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); subject_addr.try_send(sent_payable).unwrap(); @@ -1520,17 +1599,16 @@ mod tests { } #[test] - fn received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge( - ) { - // the numbers for balances don't do real math, they need not to match either the condition for + fn qualified_payables_under_our_money_limit_are_forwarded_to_blockchain_bridge_right_away() { + // The numbers in balances don't do real math, they don't need to match either the condition for // the payment adjustment or the actual values that come from the payable size reducing algorithm; // all that is mocked in this test init_test_logging(); - let test_name = "received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge"; + let test_name = "qualified_payables_under_our_money_limit_are_forwarded_to_blockchain_bridge_right_away"; let is_adjustment_required_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let instructions_recipient = blockchain_bridge - .system_stop_conditions(match_every_type_id!(OutboundPaymentsInstructions)) + .system_stop_conditions(match_lazily_every_type_id!(OutboundPaymentsInstructions)) .start() .recipient(); let mut subject = AccountantBuilder::default().build(); @@ -1540,7 +1618,11 @@ mod tests { let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); - subject.scanners.payable = Box::new(payable_scanner); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); subject.outbound_payments_instructions_sub_opt = Some(instructions_recipient); subject.logger = Logger::new(test_name); let subject_addr = subject.start(); @@ -1551,7 +1633,7 @@ mod tests { let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp); let accounts = vec![account_1, account_2]; let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_payables_in_test(accounts.clone()), + qualified_payables: accounts.clone(), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1566,8 +1648,8 @@ mod tests { let (blockchain_agent_with_context_msg_actual, logger_clone) = is_adjustment_required_params.remove(0); assert_eq!( - blockchain_agent_with_context_msg_actual.protected_qualified_payables, - protect_payables_in_test(accounts.clone()) + blockchain_agent_with_context_msg_actual.qualified_payables, + accounts.clone() ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -1599,31 +1681,37 @@ mod tests { agent_id_stamp ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) - // adjust_payments() did not need a prepared result which means it wasn't reached + assert_using_the_same_logger(&logger_clone, test_name, None) + // adjust_payments() did not need a prepared result, which means it wasn't reached // because otherwise this test would've panicked } - fn test_use_of_the_same_logger(logger_clone: &Logger, test_name: &str) { - let experiment_msg = format!("DEBUG: {test_name}: hello world"); + fn assert_using_the_same_logger( + logger_clone: &Logger, + test_name: &str, + differentiation_opt: Option<&str>, + ) { let log_handler = TestLogHandler::default(); + let experiment_msg = format!("DEBUG: {test_name}: hello world: {:?}", differentiation_opt); log_handler.exists_no_log_containing(&experiment_msg); - debug!(logger_clone, "hello world"); + + debug!(logger_clone, "hello world: {:?}", differentiation_opt); + log_handler.exists_log_containing(&experiment_msg); } #[test] - fn received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge( - ) { - // the numbers for balances don't do real math, they need not to match either the condition for + fn qualified_payables_over_masq_balance_are_adjusted_before_sending_to_blockchain_bridge() { + // The numbers in balances don't do real math, they don't need to match either the condition for // the payment adjustment or the actual values that come from the payable size reducing algorithm; // all that is mocked in this test init_test_logging(); - let test_name = "received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge"; + let test_name = + "qualified_payables_over_masq_balance_are_adjusted_before_sending_to_blockchain_bridge"; let adjust_payments_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let report_recipient = blockchain_bridge - .system_stop_conditions(match_every_type_id!(OutboundPaymentsInstructions)) + .system_stop_conditions(match_lazily_every_type_id!(OutboundPaymentsInstructions)) .start() .recipient(); let mut subject = AccountantBuilder::default().build(); @@ -1644,12 +1732,10 @@ mod tests { let agent_id_stamp_first_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_first_phase); - let initial_unadjusted_accounts = protect_payables_in_test(vec![ - unadjusted_account_1.clone(), - unadjusted_account_2.clone(), - ]); + let initial_unadjusted_accounts = + vec![unadjusted_account_1.clone(), unadjusted_account_2.clone()]; let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: initial_unadjusted_accounts.clone(), + qualified_payables: initial_unadjusted_accounts.clone(), agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; @@ -1671,7 +1757,11 @@ mod tests { let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); - subject.scanners.payable = Box::new(payable_scanner); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); subject.outbound_payments_instructions_sub_opt = Some(report_recipient); subject.logger = Logger::new(test_name); let subject_addr = subject.start(); @@ -1689,7 +1779,7 @@ mod tests { assert_eq!( actual_prepared_adjustment .original_setup_msg - .protected_qualified_payables, + .qualified_payables, initial_unadjusted_accounts ); assert_eq!( @@ -1723,13 +1813,12 @@ mod tests { Some(response_skeleton) ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) + assert_using_the_same_logger(&logger_clone, test_name, None) } #[test] - fn scan_pending_payables_request() { + fn externally_triggered_scan_pending_payables_request() { let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), receivable_scan_interval: Duration::from_millis(10_000), @@ -1745,18 +1834,24 @@ mod tests { }; let pending_payable_dao = PendingPayableDaoMock::default() .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let subject_addr = subject.start(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)); + let blockchain_bridge_addr = blockchain_bridge.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(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); + let subject_addr = subject.start(); let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { @@ -1767,13 +1862,12 @@ mod tests { subject_addr.try_send(ui_message).unwrap(); - System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable: vec![fingerprint], + pending_payable_fingerprints: vec![fingerprint], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1783,46 +1877,106 @@ mod tests { } #[test] - fn scan_request_from_ui_is_handled_in_case_the_scan_is_already_running() { - init_test_logging(); - let test_name = "scan_request_from_ui_is_handled_in_case_the_scan_is_already_running"; - let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), + fn externally_triggered_scan_identifies_all_pending_payables_as_complete() { + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 565, + context_id: 112233, }); - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transaction_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let ui_gateway_addr = ui_gateway.start(); + let system = System::new("test"); + subject.scan_schedulers.automatic_scans_enabled = false; + // Making sure we would kill the test if any sort of scan was scheduled + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); + let subject_addr = subject.start(); + let tx_fingerprint = make_pending_payable_fingerprint(); + let report_tx_receipts = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(777), + status: TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(456), + block_number: 78901234.into(), + }), + }), + tx_fingerprint.clone(), + )], + response_skeleton_opt, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint]); + + subject_addr.try_send(report_tx_receipts).unwrap(); + + system.run(); + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transaction_confirmed_params, vec![vec![tx_fingerprint]]); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + assert_eq!( + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton_opt.unwrap().client_id), + body: UiScanResponse {}.tmb(response_skeleton_opt.unwrap().context_id), + } + ); + assert_eq!(ui_gateway_recording.len(), 1); + } + + #[test] + fn externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running() { + init_test_logging(); + let test_name = + "externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running"; + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.automatic_scans_enabled = false; + let now_unix = to_unix_timestamp(SystemTime::now()); + let payment_thresholds = PaymentThresholds::default(); + let past_timestamp_unix = now_unix + - (payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec) as i64; + let mut payable_account = make_payable_account(123); + payable_account.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei); + payable_account.last_paid_timestamp = from_unix_timestamp(past_timestamp_unix); + let payable_dao = + PayableDaoMock::default().non_pending_payables_result(vec![payable_account]); let subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(make_paying_wallet(b"consuming")) .logger(Logger::new(test_name)) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); - let system = System::new("test"); + let system = System::new(test_name); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .ui_gateway(ui_gateway) + .build(); let first_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: ScanType::PendingPayables, + scan_type: ScanType::Payables, } .tmb(4321), }; let second_message = first_message.clone(); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); subject_addr.try_send(BindMessage { peer_actors }).unwrap(); subject_addr.try_send(first_message).unwrap(); @@ -1832,117 +1986,222 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {}: PendingPayables scan was already initiated", + "INFO: {}: Payables scan was already initiated", test_name )); assert_eq!(blockchain_bridge_recording.len(), 1); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let msg = ui_gateway_recording.get_record::(0); + assert_eq!(msg.body, UiScanResponse {}.tmb(4321)); + } + + fn test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled( + test_name: &str, + scan_type: ScanType, + ) { + let expected_log_msg = format!( + "WARN: {test_name}: User requested {:?} scan was denied. Automatic mode \ + prevents manual triggers.", + scan_type + ); + + test_externally_triggered_scan_is_prevented_if( + true, + true, + test_name, + scan_type, + &expected_log_msg, + ) + } + + fn test_externally_triggered_scan_is_prevented_if( + automatic_scans_enabled: bool, + aware_of_unresolved_pending_payables: bool, + test_name: &str, + scan_type: ScanType, + expected_log_message: &str, + ) { + init_test_logging(); + let (blockchain_bridge, _, blockchain_bridge_recorder_arc) = make_recorder(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .consuming_wallet(make_wallet("abc")) + .build(); + subject.scan_schedulers.automatic_scans_enabled = automatic_scans_enabled; + subject + .scanners + .set_aware_of_unresolved_pending_payables(aware_of_unresolved_pending_payables); + subject.scanners.unset_initial_pending_payable_scan(); + let subject_addr = subject.start(); + let system = System::new(test_name); + let peer_actors = PeerActorsBuilder::default() + .ui_gateway(ui_gateway) + .blockchain_bridge(blockchain_bridge) + .build(); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { scan_type }.tmb(6789), + }; + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + + subject_addr.try_send(ui_message).unwrap(); + + assert_eq!(system.run(), 0); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let msg = ui_gateway_recording.get_record::(0); + assert_eq!(msg.body, UiScanResponse {}.tmb(6789)); + assert_eq!(ui_gateway_recording.len(), 1); + let blockchain_bridge_recorder = blockchain_bridge_recorder_arc.lock().unwrap(); + assert_eq!(blockchain_bridge_recorder.len(), 0); + TestLogHandler::new().exists_log_containing(expected_log_message); } #[test] - fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { - 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), + fn externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled() { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled("externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled", ScanType::Payables) + } + + #[test] + fn externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled() + { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled("externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled", ScanType::PendingPayables) + } + + #[test] + fn externally_triggered_scan_for_receivables_is_prevented_if_automatic_scans_are_enabled() { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled( + "externally_triggered_scan_for_receivables_is_prevented_if_automatic_scans_are_enabled", + ScanType::Receivables, + ) + } + + #[test] + fn externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete( + ) { + let test_name = "externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete"; + let expected_log_msg = format!( + "INFO: {test_name}: User requested PendingPayables scan was denied expecting zero \ + findings. Run the Payable scanner first." + ); + + test_externally_triggered_scan_is_prevented_if( + false, + false, + test_name, + ScanType::PendingPayables, + &expected_log_msg, + ) + } + + #[test] + fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( + ) { + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 4555, + context_id: 5566, }); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) + // TODO when we have more logic in place with the other cards taken in, we'll need to configure these + // accordingly + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]) + .mark_failures_result(Ok(())); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable)]) .build(); + subject.scan_schedulers.automatic_scans_enabled = false; + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let (peer_actors, peer_addresses) = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .ui_gateway(ui_gateway) + .build_and_provide_addresses(); let subject_addr = subject.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(234), + status: TxStatus::Failed + }), + make_pending_payable_fingerprint() + )], + response_skeleton_opt + }, + &subject_addr + ); + let second_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable { + recipient_wallet: make_wallet("abc"), + hash: make_tx_hash(789) + } + )]), + response_skeleton_opt + }, + &subject_addr + ); + peer_addresses + .blockchain_bridge_addr + .try_send(SetUpCounterMsgs::new(vec![ + first_counter_msg_setup, + second_counter_msg_setup, + ])) + .unwrap(); subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), + let pending_payable_request = ScanForPendingPayables { + response_skeleton_opt, }; - subject_addr.try_send(report_transaction_receipts).unwrap(); + subject_addr.try_send(pending_payable_request).unwrap(); - System::current().stop(); system.run(); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), &NodeToUiMessage { - target: ClientId(1234), - body: UiScanResponse {}.tmb(4321), + target: ClientId(4555), + body: UiScanResponse {}.tmb(5566), } ); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!(blockchain_bridge_recording.len(), 2); } #[test] - fn accountant_calls_payable_dao_to_mark_pending_payable() { - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let expected_wallet = make_wallet("paying_you"); - let expected_hash = H256::from("transaction_hash".keccak256()); - let expected_rowid = 45623; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(expected_rowid, expected_hash)], - no_rowid_results: vec![], - }); - let payable_dao = PayableDaoMock::new() - .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) - .mark_pending_payables_rowids_result(Ok(())); - let system = System::new("accountant_calls_payable_dao_to_mark_pending_payable"); - let accountant = AccountantBuilder::default() - .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) - .payable_daos(vec![ForPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) - .build(); - let expected_payable = PendingPayable::new(expected_wallet.clone(), expected_hash.clone()); - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( - expected_payable.clone(), - )]), - response_skeleton_opt: None, - }; - let subject = accountant.start(); - - subject - .try_send(sent_payable) - .expect("unexpected actix error"); - - System::current().stop(); - system.run(); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); - assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); - let mark_pending_payables_rowids_params = - mark_pending_payables_rowids_params_arc.lock().unwrap(); - assert_eq!( - *mark_pending_payables_rowids_params, - vec![vec![(expected_wallet, expected_rowid)]] - ); - } - - #[test] - fn accountant_sends_initial_payable_payments_msg_when_qualified_payable_found() { + fn accountant_sends_qualified_payable_msg_when_qualified_payable_found() { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); let (qualified_payables, _, all_non_pending_payables) = - make_payables(now, &payment_thresholds); + make_qualified_and_unqualified_payables(now, &payment_thresholds); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let system = System::new( - "accountant_sends_initial_payable_payments_msg_when_qualified_payable_found", - ); + let system = + System::new("accountant_sends_qualified_payable_msg_when_qualified_payable_found"); let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.receivable = Box::new(NullScanner::new()); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -1950,7 +2209,11 @@ mod tests { .build(); send_bind_message!(accountant_subs, peer_actors); - send_start_message!(accountant_subs); + accountant_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); System::current().stop(); system.run(); @@ -1960,17 +2223,188 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + qualified_payables, consuming_wallet, response_skeleton_opt: None, } ); } + #[test] + fn automatic_scan_for_new_payables_schedules_another_one_immediately_if_no_qualified_payables_found( + ) { + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let system = + System::new("automatic_scan_for_new_payables_schedules_another_one_immediately_if_no_qualified_payables_found"); + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = AccountantBuilder::default() + .consuming_wallet(consuming_wallet) + .build(); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + subject.scan_schedulers.payable.dyn_interval_computer = Box::new( + NewPayableScanDynIntervalComputerMock::default() + .compute_interval_result(Some(Duration::from_secs(500))), + ); + let payable_scanner = ScannerMock::default() + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_result(Err(StartScanError::NothingToProcess)); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + let accountant_addr = subject.start(); + + accountant_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + System::current().stop(); + assert_eq!(system.run(), 0); + let mut notify_later_params = notify_later_params_arc.lock().unwrap(); + let (msg, interval) = notify_later_params.remove(0); + assert_eq!( + msg, + ScanForNewPayables { + response_skeleton_opt: None + } + ); + assert_eq!(interval, Duration::from_secs(500)); + assert_eq!(notify_later_params.len(), 0); + // Accountant is unbound; therefore, it is guaranteed that sending a message to + // the BlockchainBridge wasn't attempted. It would've panicked otherwise. + } + + #[test] + fn accountant_handles_scan_for_retry_payables() { + init_test_logging(); + let test_name = "accountant_handles_scan_for_retry_payables"; + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let consuming_wallet = make_wallet("abc"); + subject.consuming_wallet_opt = Some(consuming_wallet.clone()); + let qualified_payables_msg = QualifiedPayablesMessage { + qualified_payables: vec![make_payable_account(789)], + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + }; + let payable_scanner_mock = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner_mock, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + let accountant_addr = subject.start(); + let accountant_subs = Accountant::make_subs_from(&accountant_addr); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); + send_bind_message!(accountant_subs, peer_actors); + + accountant_addr + .try_send(ScanForRetryPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + System::current().stop(); + let before = SystemTime::now(); + system.run(); + let after = SystemTime::now(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let (actual_wallet, actual_now, actual_response_skeleton_opt, actual_logger, _) = + start_scan_params.remove(0); + assert_eq!(actual_wallet, consuming_wallet); + assert_eq!(actual_response_skeleton_opt, None); + assert!(before <= actual_now && actual_now <= after); + assert!( + start_scan_params.is_empty(), + "should be empty but was {:?}", + start_scan_params + ); + let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); + let message = blockchain_bridge_recorder.get_record::(0); + assert_eq!(message, &qualified_payables_msg); + assert_eq!(blockchain_bridge_recorder.len(), 1); + assert_using_the_same_logger(&actual_logger, test_name, None) + } + + #[test] + fn scan_for_retry_payables_if_consuming_wallet_is_not_present() { + init_test_logging(); + let test_name = "scan_for_retry_payables_if_consuming_wallet_is_not_present"; + let system = System::new(test_name); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let ui_gateway_addr = ui_gateway.start(); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let payable_scanner_mock = ScannerMock::new(); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner_mock, + ))); + subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); + // It must be populated because no errors are tolerated at the RetryPayableScanner + // if automatic scans are on + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 789, + context_id: 111, + }); + let accountant_addr = subject.start(); + + accountant_addr + .try_send(ScanForRetryPayables { + response_skeleton_opt, + }) + .unwrap(); + + system.run(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let message = ui_gateway_recording.get_record::(0); + assert_eq!( + message, + &NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton_opt.unwrap().client_id), + body: UiScanResponse {}.tmb(response_skeleton_opt.unwrap().context_id) + } + ); + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found")); + } + #[test] fn accountant_requests_blockchain_bridge_to_scan_for_received_payments() { init_test_logging(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RetrieveTransactions)); let earning_wallet = make_wallet("someearningwallet"); let system = System::new("accountant_requests_blockchain_bridge_to_scan_for_received_payments"); @@ -1981,8 +2415,12 @@ mod tests { .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.payable = Box::new(NullScanner::new()); + // Important. Preventing the possibly endless sequence of + // PendingPayableScanner -> NewPayableScanner -> NewPayableScanner... + subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default()); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -1992,10 +2430,8 @@ mod tests { send_start_message!(accountant_subs); - System::current().stop(); system.run(); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recorder.len(), 1); let retrieve_transactions_msg = blockchain_bridge_recorder.get_record::(0); assert_eq!( @@ -2005,6 +2441,101 @@ mod tests { response_skeleton_opt: None, } ); + assert_eq!(blockchain_bridge_recorder.len(), 1); + } + + #[test] + fn externally_triggered_scan_receivables_request() { + 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), + pending_payable_scan_interval: Duration::from_millis(2_000), + receivable_scan_interval: Duration::from_millis(10_000), + }); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_result(vec![]) + .paid_delinquencies_result(vec![]); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(config) + .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) + .build(); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RetrieveTransactions)); + let blockchain_bridge_addr = blockchain_bridge.start(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.retrieve_transactions_sub_opt = Some(blockchain_bridge_addr.recipient()); + let subject_addr = subject.start(); + let system = System::new("test"); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: ScanType::Receivables, + } + .tmb(4321), + }; + + subject_addr.try_send(ui_message).unwrap(); + + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!( + blockchain_bridge_recording.get_record::(0), + &RetrieveTransactions { + recipient: make_wallet("earning_wallet"), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + } + ); + } + + #[test] + fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { + 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), + pending_payable_scan_interval: Duration::from_millis(2_000), + receivable_scan_interval: Duration::from_millis(10_000), + }); + config.automatic_scans_enabled = false; + let subject = AccountantBuilder::default() + .bootstrapper_config(config) + .config_dao( + ConfigDaoMock::new() + .get_result(Ok(ConfigDaoRecord::new("start_block", None, false))) + .set_result(Ok(())), + ) + .build(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + let received_payments = ReceivedPayments { + timestamp: SystemTime::now(), + new_start_block: BlockMarker::Value(0), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + transactions: vec![], + }; + + subject_addr.try_send(received_payments).unwrap(); + + System::current().stop(); + system.run(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + assert_eq!( + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + target: ClientId(1234), + body: UiScanResponse {}.tmb(4321), + } + ); } #[test] @@ -2077,82 +2608,747 @@ mod tests { } #[test] - fn accountant_scans_after_startup() { + fn accountant_scans_after_startup_and_does_not_detect_any_pending_payables() { + // We will want to prove that the PendingPayableScanner runs before the NewPayableScanner. + // Their relationship towards the ReceivableScanner isn't important. init_test_logging(); - 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 test_name = "accountant_scans_after_startup_and_does_not_detect_any_pending_payables"; + let scan_params = ScanParams::default(); + let notify_and_notify_later_params = NotifyAndNotifyLaterParams::default(); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let earning_wallet = make_wallet("earning"); - let system = System::new("accountant_scans_after_startup"); - 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_errorless_fingerprints_params(&pending_payable_params_arc) - .return_all_errorless_fingerprints_result(vec![]); - 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 subject = AccountantBuilder::default() - .bootstrapper_config(config) - .payable_daos(vec![ForPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) - .build(); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); + let consuming_wallet = make_wallet("consuming"); + let system = System::new(test_name); + let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); + let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.payable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.pending_payable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let receivable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.receivable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let (subject, new_payable_expected_computed_interval, receivable_scan_interval) = + set_up_subject_for_no_pending_payables_found_startup_test( + test_name, + ¬ify_and_notify_later_params, + &compute_interval_params_arc, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let peer_actors = peer_actors_builder().build(); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); - System::current().stop(); + // The system is stopped by the NotifyLaterHandleMock for the Receivable scanner + let before = SystemTime::now(); system.run(); - 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()); + let after = SystemTime::now(); + assert_pending_payable_scanner_for_no_pending_payable_found( + test_name, + consuming_wallet, + &scan_params.pending_payable_start_scan, + ¬ify_and_notify_later_params.pending_payables_notify_later, + before, + after, + ); + assert_payable_scanner_for_no_pending_payable_found( + ¬ify_and_notify_later_params, + compute_interval_params_arc, + new_payable_expected_computed_interval, + before, + after, + ); + assert_receivable_scanner( + test_name, + earning_wallet, + &scan_params.receivable_start_scan, + ¬ify_and_notify_later_params.receivables_notify_later, + receivable_scan_interval, + ); + // The test lays down evidences that the NewPayableScanner couldn't run before + // the PendingPayableScanner, which is an intention. + // To interpret the evidence, we have to notice that the PendingPayableScanner ran + // certainly, while it wasn't attempted to schedule in the whole test. That points out that + // the scanning sequence started spontaneously, not requiring any prior scheduling. Most + // importantly, regarding the payable scanner, it ran not even once. We know, though, + // that its scheduling did take place, specifically an urgent call of the new payable mode. + // That totally corresponds with the expected behavior where the PendingPayableScanner + // should first search for any stray pending payables; if no findings, the NewPayableScanner + // is supposed to go next, and it shouldn't have to undertake the standard new-payable + // interval, but here, at the beginning, it comes immediately. + } + + #[test] + fn accountant_scans_after_startup_and_detects_pending_payable_from_before() { + // We do ensure the PendingPayableScanner runs before the NewPayableScanner. Not interested + // in an exact placing of the ReceivableScanner so much. + init_test_logging(); + let test_name = "accountant_scans_after_startup_and_detects_pending_payable_from_before"; + let scan_params = ScanParams::default(); + let notify_and_notify_later_params = NotifyAndNotifyLaterParams::default(); + let earning_wallet = make_wallet("earning"); + let consuming_wallet = make_wallet("consuming"); + let system = System::new(test_name); + let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); + let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); + let pp_fingerprint = make_pending_payable_fingerprint(); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_params(&scan_params.payable_start_scan) + .start_scan_result(Ok(QualifiedPayablesMessage { + qualified_payables: vec![make_payable_account(123)], + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + })) + .finish_scan_params(&scan_params.payable_finish_scan) + // Important + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.pending_payable_start_scan) + .start_scan_result(Ok(RequestTransactionReceipts { + pending_payable_fingerprints: vec![pp_fingerprint], + response_skeleton_opt: None, + })) + .finish_scan_params(&scan_params.pending_payable_finish_scan) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + let receivable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.receivable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let (subject, pending_payable_expected_notify_later_interval, receivable_scan_interval) = + set_up_subject_for_some_pending_payable_found_startup_test( + test_name, + ¬ify_and_notify_later_params, + config, + payable_scanner, + pending_payable_scanner, + receivable_scanner, + ); + let (peer_actors, addresses) = peer_actors_builder().build_and_provide_addresses(); + let subject_addr: Addr = subject.start(); + let subject_subs = Accountant::make_subs_from(&subject_addr); + let expected_report_transaction_receipts = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(789), + status: TxStatus::Failed, + }), + make_pending_payable_fingerprint(), + )], + response_skeleton_opt: None, + }; + let expected_sent_payables = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { + recipient_wallet: make_wallet("bcd"), + hash: make_tx_hash(890), + })]), + response_skeleton_opt: None, + }; + let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + expected_report_transaction_receipts.clone(), + &subject_addr + ); + let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + expected_sent_payables.clone(), + &subject_addr + ); + send_bind_message!(subject_subs, peer_actors); + addresses + .blockchain_bridge_addr + .try_send(SetUpCounterMsgs::new(vec![ + blockchain_bridge_counter_msg_setup_for_pending_payable_scanner, + blockchain_bridge_counter_msg_setup_for_payable_scanner, + ])) + .unwrap(); + + send_start_message!(subject_subs); + + // The system is stopped by the NotifyHandleLaterMock for the PendingPayable scanner + let before = SystemTime::now(); + system.run(); + let after = SystemTime::now(); + assert_pending_payable_scanner_for_some_pending_payable_found( + test_name, + consuming_wallet.clone(), + &scan_params, + ¬ify_and_notify_later_params.pending_payables_notify_later, + pending_payable_expected_notify_later_interval, + expected_report_transaction_receipts, + before, + after, + ); + assert_payable_scanner_for_some_pending_payable_found( + test_name, + consuming_wallet, + &scan_params, + ¬ify_and_notify_later_params, + expected_sent_payables, + ); + assert_receivable_scanner( + test_name, + earning_wallet, + &scan_params.receivable_start_scan, + ¬ify_and_notify_later_params.receivables_notify_later, + receivable_scan_interval, + ); + // Given the assertions prove that the pending payable scanner has run multiple times + // before the new payable scanner started or was scheduled, the front position belongs to + // the one first mentioned, no doubts. + } + + #[derive(Default)] + struct ScanParams { + payable_start_scan: + Arc, Logger, String)>>>, + payable_finish_scan: Arc>>, + pending_payable_start_scan: + Arc, Logger, String)>>>, + pending_payable_finish_scan: Arc>>, + receivable_start_scan: + Arc, Logger, String)>>>, + // receivable_finish_scan ... not needed + } + + #[derive(Default)] + struct NotifyAndNotifyLaterParams { + new_payables_notify_later: Arc>>, + new_payables_notify: Arc>>, + retry_payables_notify: Arc>>, + pending_payables_notify_later: Arc>>, + receivables_notify_later: Arc>>, + } + + fn set_up_subject_for_no_pending_payables_found_startup_test( + test_name: &str, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + compute_interval_params_arc: &Arc>>, + config: BootstrapperConfig, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + payable_scanner: ScannerMock, + ) -> (Accountant, Duration, Duration) { + let mut subject = make_subject_and_inject_scanners( + test_name, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let new_payable_expected_computed_interval = Duration::from_secs(3600); + // Important that this is made short because the test relies on it with the system stop. + let receivable_scan_interval = Duration::from_millis(50); + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later), + ); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), + ); + subject.scan_schedulers.payable.retry_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.retry_payables_notify), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.new_payables_notify), + ); + let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later) + .stop_system_on_count_received(1); + subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); + subject.scan_schedulers.receivable.interval = receivable_scan_interval; + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_result(Some(new_payable_expected_computed_interval)); + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + ( + subject, + new_payable_expected_computed_interval, + receivable_scan_interval, + ) + } + + fn set_up_subject_for_some_pending_payable_found_startup_test( + test_name: &str, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + config: BootstrapperConfig, + payable_scanner: ScannerMock, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + ) -> (Accountant, Duration, Duration) { + let mut subject = make_subject_and_inject_scanners( + test_name, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let pending_payable_scan_interval = Duration::from_secs(3600); + let receivable_scan_interval = Duration::from_secs(3600); + let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later) + // This should stop the system + .stop_system_on_count_received(1); + subject.scan_schedulers.pending_payable.handle = + Box::new(pending_payable_notify_later_handle_mock); + subject.scan_schedulers.pending_payable.interval = pending_payable_scan_interval; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), + ); + subject.scan_schedulers.payable.retry_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.retry_payables_notify) + .capture_msg_and_let_it_fly_on(), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.new_payables_notify), + ); + let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later); + subject.scan_schedulers.receivable.interval = receivable_scan_interval; + subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); + ( + subject, + pending_payable_scan_interval, + receivable_scan_interval, + ) + } + + fn make_subject_and_inject_scanners( + test_name: &str, + config: BootstrapperConfig, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + payable_scanner: ScannerMock, + ) -> Accountant { + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .bootstrapper_config(config) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + } + + fn assert_pending_payable_scanner_for_no_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + pending_payable_start_scan_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + scan_for_pending_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) { + let pp_logger = pending_payable_common( + consuming_wallet, + pending_payable_start_scan_params_arc, + act_started_at, + act_finished_at, + ); + let scan_for_pending_payables_notify_later_params = + scan_for_pending_payables_notify_later_params_arc + .lock() + .unwrap(); + // PendingPayableScanner can only start after NewPayableScanner finishes and makes at least + // one transaction. The test stops before running NewPayableScanner, missing both + // the second PendingPayableScanner run and its scheduling event. assert!( - captured_timestamp < SystemTime::now() - && captured_timestamp >= from_time_t(to_time_t(SystemTime::now()) - 5) + scan_for_pending_payables_notify_later_params.is_empty(), + "We did not expect to see another schedule for pending payables, but it happened {:?}", + scan_for_pending_payables_notify_later_params ); - assert_eq!(captured_curves, PaymentThresholds::default()); - assert_eq!(paid_delinquencies_params.len(), 1); - assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing("INFO: Accountant: Scanning for payables"); - tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); - tlh.exists_log_containing(&format!( - "INFO: Accountant: Scanning for receivables to {}", - earning_wallet - )); - tlh.exists_log_containing("INFO: Accountant: Scanning for delinquencies"); + assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); + } + + fn assert_pending_payable_scanner_for_some_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + scan_params: &ScanParams, + scan_for_pending_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + pending_payable_expected_notify_later_interval: Duration, + expected_report_tx_receipts_msg: ReportTransactionReceipts, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) { + let pp_start_scan_logger = pending_payable_common( + consuming_wallet, + &scan_params.pending_payable_start_scan, + act_started_at, + act_finished_at, + ); + assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); + let mut pending_payable_finish_scan_params = + scan_params.pending_payable_finish_scan.lock().unwrap(); + let (actual_report_tx_receipts_msg, pp_finish_scan_logger) = + pending_payable_finish_scan_params.remove(0); + assert_eq!( + actual_report_tx_receipts_msg, + expected_report_tx_receipts_msg + ); + assert_using_the_same_logger(&pp_finish_scan_logger, test_name, Some("pp finish scan")); + let scan_for_pending_payables_notify_later_params = + scan_for_pending_payables_notify_later_params_arc + .lock() + .unwrap(); + // This is the moment when the test ends. It says that we went the way of the pending payable + // sequence, instead of calling the NewPayableScan just after the initial pending payable + // scan. + assert_eq!( + *scan_for_pending_payables_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + pending_payable_expected_notify_later_interval + )], + ); + } + + fn pending_payable_common( + consuming_wallet: Wallet, + pending_payable_start_scan_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) -> Logger { + let mut pending_payable_params = pending_payable_start_scan_params_arc.lock().unwrap(); + let ( + pp_wallet, + pp_scan_started_at, + pp_response_skeleton_opt, + pp_logger, + pp_trigger_msg_type_str, + ) = pending_payable_params.remove(0); + assert_eq!(pp_wallet, consuming_wallet); + assert_eq!(pp_response_skeleton_opt, None); + assert!( + pp_trigger_msg_type_str.contains("PendingPayable"), + "Should contain PendingPayable but {}", + pp_trigger_msg_type_str + ); + assert!( + pending_payable_params.is_empty(), + "Should be empty but was {:?}", + pending_payable_params + ); + assert!( + act_started_at <= pp_scan_started_at && pp_scan_started_at <= act_finished_at, + "The scanner was supposed to run between {:?} and {:?} but it was {:?}", + act_started_at, + act_finished_at, + pp_scan_started_at + ); + pp_logger + } + + fn assert_payable_scanner_for_no_pending_payable_found( + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + compute_interval_params_arc: Arc>>, + new_payable_expected_computed_interval: Duration, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) { + // Note that there is no functionality from the payable scanner actually running. + // We only witness it to be scheduled. + let scan_for_new_payables_notify_later_params = notify_and_notify_later_params + .new_payables_notify_later + .lock() + .unwrap(); + assert_eq!( + *scan_for_new_payables_notify_later_params, + vec![( + ScanForNewPayables { + response_skeleton_opt: None + }, + new_payable_expected_computed_interval + )] + ); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (p_scheduling_now, last_new_payable_scan_timestamp, _) = + compute_interval_params.remove(0); + assert_eq!(last_new_payable_scan_timestamp, UNIX_EPOCH); + let scan_for_new_payables_notify_params = notify_and_notify_later_params + .new_payables_notify + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_params.is_empty(), + "We did not expect any immediate scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_params + ); + let scan_for_retry_payables_notify_params = notify_and_notify_later_params + .retry_payables_notify + .lock() + .unwrap(); + assert!( + scan_for_retry_payables_notify_params.is_empty(), + "We did not expect any scheduling of retry payables, but it happened {:?}", + scan_for_retry_payables_notify_params + ); + assert!( + act_started_at <= p_scheduling_now && p_scheduling_now <= act_finished_at, + "The payable scan scheduling was supposed to take place between {:?} and {:?} \ + but it was {:?}", + act_started_at, + act_finished_at, + p_scheduling_now + ); + } + + fn assert_payable_scanner_for_some_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + scan_params: &ScanParams, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + expected_sent_payables: SentPayables, + ) { + let mut payable_start_scan_params = scan_params.payable_start_scan.lock().unwrap(); + let (p_wallet, _, p_response_skeleton_opt, p_start_scan_logger, p_trigger_msg_type_str) = + payable_start_scan_params.remove(0); + assert_eq!(p_wallet, consuming_wallet); + assert_eq!(p_response_skeleton_opt, None); + // Important: it's the proof that we're dealing with the RetryPayableScanner not NewPayableScanner + assert!( + p_trigger_msg_type_str.contains("RetryPayable"), + "Should contain RetryPayable but {}", + p_trigger_msg_type_str + ); + assert!( + payable_start_scan_params.is_empty(), + "Should be empty but was {:?}", + payable_start_scan_params + ); + assert_using_the_same_logger(&p_start_scan_logger, test_name, Some("retry payable start")); + let mut payable_finish_scan_params = scan_params.payable_finish_scan.lock().unwrap(); + let (actual_sent_payable, p_finish_scan_logger) = payable_finish_scan_params.remove(0); + assert_eq!(actual_sent_payable, expected_sent_payables,); + assert!( + payable_finish_scan_params.is_empty(), + "Should be empty but was {:?}", + payable_finish_scan_params + ); + assert_using_the_same_logger( + &p_finish_scan_logger, + test_name, + Some("retry payable finish"), + ); + let scan_for_new_payables_notify_later_params = notify_and_notify_later_params + .new_payables_notify_later + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_later_params.is_empty(), + "We did not expect any later scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_later_params + ); + let scan_for_new_payables_notify_params = notify_and_notify_later_params + .new_payables_notify + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_params.is_empty(), + "We did not expect any immediate scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_params + ); + let scan_for_retry_payables_notify_params = notify_and_notify_later_params + .retry_payables_notify + .lock() + .unwrap(); + assert_eq!( + *scan_for_retry_payables_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt: None + }], + ); + } + + fn assert_receivable_scanner( + test_name: &str, + earning_wallet: Wallet, + receivable_start_scan_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + scan_for_receivables_notify_later_params_arc: &Arc< + Mutex>, + >, + receivable_scan_interval: Duration, + ) { + let mut receivable_start_scan_params = receivable_start_scan_params_arc.lock().unwrap(); + let (r_wallet, _r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = + receivable_start_scan_params.remove(0); + assert_eq!(r_wallet, earning_wallet); + assert_eq!(r_response_skeleton_opt, None); + assert!( + r_trigger_msg_name_str.contains("Receivable"), + "Should contain Receivable but {}", + r_trigger_msg_name_str + ); + assert!( + receivable_start_scan_params.is_empty(), + "Should be already empty but was {:?}", + receivable_start_scan_params + ); + assert_using_the_same_logger(&r_logger, test_name, Some("r")); + let scan_for_receivables_notify_later_params = + scan_for_receivables_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *scan_for_receivables_notify_later_params, + vec![( + ScanForReceivables { + response_skeleton_opt: None + }, + receivable_scan_interval + )] + ); + } + + #[test] + fn initial_pending_payable_scan_if_some_payables_found() { + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let system = System::new("test"); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_addr = blockchain_bridge.start(); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); + let flag_before = subject.scanners.initial_pending_payable_scan(); + + let hint = subject.handle_request_of_scan_for_pending_payable(None); + + System::current().stop(); + system.run(); + let flag_after = subject.scanners.initial_pending_payable_scan(); + assert_eq!(hint, ScanRescheduleAfterEarlyStop::DoNotSchedule); + assert_eq!(flag_before, true); + assert_eq!(flag_after, false); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let _ = blockchain_bridge_recording.get_record::(0); + } + + #[test] + fn initial_pending_payable_scan_if_no_payables_found() { + let pending_payable_dao = + PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let flag_before = subject.scanners.initial_pending_payable_scan(); + + let hint = subject.handle_request_of_scan_for_pending_payable(None); + + let flag_after = subject.scanners.initial_pending_payable_scan(); + assert_eq!( + hint, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ); + assert_eq!(flag_before, true); + assert_eq!(flag_after, false); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ + cross_scan_cause_opt: None, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ + should be impossible with PendingPayableScanner in automatic mode" + )] + fn initial_pending_payable_scan_hits_unexpected_error() { + init_test_logging(); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("abc")) + .build(); + let pending_payable_scanner = + ScannerMock::default().scan_started_at_result(Some(UNIX_EPOCH)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + + let _ = subject.handle_request_of_scan_for_pending_payable(None); } #[test] fn periodical_scanning_for_receivables_and_delinquencies_works() { init_test_logging(); let test_name = "periodical_scanning_for_receivables_and_delinquencies_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_receivable_params_arc = Arc::new(Mutex::new(vec![])); 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 { + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Err(StartScanError::NothingToProcess)) + .start_scan_result(Ok(RetrieveTransactions { recipient: make_wallet("some_recipient"), response_skeleton_opt: None, })) @@ -2161,64 +3357,70 @@ mod tests { let mut config = bc_from_earning_wallet(earning_wallet.clone()); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(100), + pending_payable_scan_interval: Duration::from_secs(10), receivable_scan_interval: Duration::from_millis(99), - pending_payable_scan_interval: Duration::from_secs(100), }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .logger(Logger::new(test_name)) .build(); - 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.scan_schedulers.update_scheduler( - ScanType::Receivables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_receivable_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject.scan_schedulers.receivable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_later_receivable_params_arc) + .capture_msg_and_let_it_fly_on(), ); 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); + subject_addr + .try_send(ScanForReceivables { + response_skeleton_opt: None, + }) + .unwrap(); let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); let notify_later_receivable_params = notify_later_receivable_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Receivables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); + let tlh = TestLogHandler::new(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); let ( first_attempt_wallet, first_attempt_timestamp, first_attempt_response_skeleton_opt, first_attempt_logger, - ) = begin_scan_params.remove(0); + _, + ) = start_scan_params.remove(0); let ( second_attempt_wallet, second_attempt_timestamp, second_attempt_response_skeleton_opt, second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); + _, + ) = start_scan_params.remove(0); + assert_eq!(first_attempt_wallet, earning_wallet); assert_eq!(second_attempt_wallet, earning_wallet); assert!(time_before <= first_attempt_timestamp); assert!(first_attempt_timestamp <= second_attempt_timestamp); assert!(second_attempt_timestamp <= time_after); assert_eq!(first_attempt_response_skeleton_opt, None); assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); + assert!(start_scan_params.is_empty()); + debug!( + first_attempt_logger, + "first attempt verifying receivable scanner" + ); + debug!( + second_attempt_logger, + "second attempt verifying receivable scanner" + ); assert_eq!( *notify_later_receivable_params, vec![ @@ -2235,208 +3437,241 @@ mod tests { Duration::from_millis(99) ), ] - ) + ); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: There was nothing to process during Receivables scan." + )); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: first attempt verifying receivable scanner", + )); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: second attempt verifying receivable scanner", + )); } + // This test begins with the new payable scan, continues over the retry payable scan and ends + // with another attempt for new payables which proves one complete cycle. #[test] - fn periodical_scanning_for_pending_payable_works() { + fn periodical_scanning_for_payables_works() { init_test_logging(); - 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 test_name = "periodical_scanning_for_payables_works"; + let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); + let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); - SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_addr = blockchain_bridge.start(); + let payable_account = make_payable_account(123); + let qualified_payable = vec![payable_account.clone()]; let consuming_wallet = make_paying_wallet(b"consuming"); - 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_after_last_msg(); - 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() - .consuming_wallet(consuming_wallet.clone()) - .bootstrapper_config(config) - .logger(Logger::new(test_name)) - .build(); - 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.scan_schedulers.update_scheduler( - ScanType::PendingPayables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_pending_payable_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + let counter_msg_1 = BlockchainAgentWithContextMessage { + qualified_payables: qualified_payable.clone(), + agent: Box::new(BlockchainAgentMock::default()), + response_skeleton_opt: None, + }; + let transaction_hash = make_tx_hash(789); + let creditor_wallet = make_wallet("blah"); + let counter_msg_2 = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable::new(creditor_wallet, transaction_hash), + )]), + response_skeleton_opt: None, + }; + let tx_receipt = TxReceipt { + transaction_hash, + status: TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), + }), + }; + let pending_payable_fingerprint = make_pending_payable_fingerprint(); + let counter_msg_3 = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(tx_receipt), + pending_payable_fingerprint.clone(), + )], + response_skeleton_opt: None, + }; + let request_transaction_receipts_msg = RequestTransactionReceipts { + pending_payable_fingerprints: vec![pending_payable_fingerprint], + response_skeleton_opt: None, + }; + let qualified_payables_msg = QualifiedPayablesMessage { + qualified_payables: qualified_payable.clone(), + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + }; + let subject = set_up_subject_to_prove_periodical_payable_scan( + test_name, + &blockchain_bridge_addr, + &consuming_wallet, + &qualified_payables_msg, + &request_transaction_receipts_msg, + &start_scan_pending_payable_params_arc, + &start_scan_payable_params_arc, + ¬ify_later_pending_payables_params_arc, + ¬ify_payable_params_arc, ); - 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); + let subject_addr = subject.start(); + let set_up_counter_msgs = SetUpCounterMsgs::new(vec![ + setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + counter_msg_1, + &subject_addr + ), + setup_for_counter_msg_triggered_via_type_id!( + OutboundPaymentsInstructions, + counter_msg_2, + &subject_addr + ), + setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + counter_msg_3, + &subject_addr + ), + ]); + blockchain_bridge_addr + .try_send(set_up_counter_msgs) + .unwrap(); - send_start_message!(subject_subs); + subject_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); - let notify_later_pending_payable_params = - notify_later_pending_payable_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = begin_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, consuming_wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); - assert_eq!( - *notify_later_pending_payable_params, - vec![ - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ), - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ), - ] - ) + let mut start_scan_payable_params = start_scan_payable_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = + start_scan_payable_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(time_before <= timestamp && timestamp <= time_after); + assert_eq!(response_skeleton_opt, None); + assert!(start_scan_payable_params.is_empty()); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); + let mut start_scan_pending_payable_params = + start_scan_pending_payable_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = + start_scan_pending_payable_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(time_before <= timestamp && timestamp <= time_after); + assert_eq!(response_skeleton_opt, None); + assert!(start_scan_pending_payable_params.is_empty()); + assert_using_the_same_logger(&logger, test_name, Some("start scan pending payable")); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let actual_qualified_payables_msg = + blockchain_bridge_recording.get_record::(0); + assert_eq!(actual_qualified_payables_msg, &qualified_payables_msg); + let actual_outbound_payment_instructions_msg = + blockchain_bridge_recording.get_record::(1); + assert_eq!( + actual_outbound_payment_instructions_msg.affordable_accounts, + vec![payable_account] + ); + let actual_requested_receipts_1 = + blockchain_bridge_recording.get_record::(2); + assert_eq!( + actual_requested_receipts_1, + &request_transaction_receipts_msg + ); + let notify_later_pending_payables_params = + notify_later_pending_payables_params_arc.lock().unwrap(); + assert_eq!( + *notify_later_pending_payables_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_millis(50) + ),] + ); + let notify_payables_params = notify_payable_params_arc.lock().unwrap(); + assert_eq!( + *notify_payables_params, + vec![ScanForNewPayables { + response_skeleton_opt: None + },] + ); } - #[test] - fn periodical_scanning_for_payable_works() { - init_test_logging(); - 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 system = System::new(test_name); - SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions - let consuming_wallet = make_paying_wallet(b"consuming"); + fn set_up_subject_to_prove_periodical_payable_scan( + test_name: &str, + blockchain_bridge_addr: &Addr, + consuming_wallet: &Wallet, + qualified_payables_msg: &QualifiedPayablesMessage, + request_transaction_receipts: &RequestTransactionReceipts, + start_scan_pending_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + start_scan_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + notify_later_pending_payables_params_arc: &Arc< + Mutex>, + >, + notify_payable_params_arc: &Arc>>, + ) -> Accountant { + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_pending_payable_params_arc) + .start_scan_result(Ok(request_transaction_receipts.clone())) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); let payable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( - 123, - )]), - consuming_wallet: consuming_wallet.clone(), - response_skeleton_opt: None, - })) - .stop_the_system_after_last_msg(); + .scan_started_at_result(None) + // Always checking also on the payable scanner when handling ScanForPendingPayable + .scan_started_at_result(None) + .start_scan_params(&start_scan_payable_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(97), + // This simply means that we're gonna surplus this value (it abides by how many pending + // payable cycles have to go in between before the lastly submitted txs are confirmed), + payable_scan_interval: Duration::from_millis(10), + pending_payable_scan_interval: Duration::from_millis(50), 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) .consuming_wallet(consuming_wallet.clone()) .logger(Logger::new(test_name)) .build(); - 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.scan_schedulers.update_scheduler( - ScanType::Payables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_payables_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::::default() + .notify_later_params(¬ify_later_pending_payables_params_arc) + .capture_msg_and_let_it_fly_on(), ); - 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); - - let time_before = SystemTime::now(); - system.run(); - let time_after = SystemTime::now(); - //the second attempt is the one where the queue is empty and System::current.stop() ends the cycle - let notify_later_payables_params = notify_later_payables_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Payables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = begin_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, consuming_wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); - assert_eq!( - *notify_later_payables_params, - vec![ - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ] - ) + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::::default() + .notify_params(¬ify_payable_params_arc) + // This should stop the system. If anything goes wrong, the SystemKillerActor will. + .stop_system_on_count_received(1), + ); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); + subject.outbound_payments_instructions_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject.request_transaction_receipts_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject } #[test] @@ -2447,12 +3682,15 @@ mod tests { subject.consuming_wallet_opt = None; subject.logger = Logger::new(test_name); - subject.handle_request_of_scan_for_payable(None); + subject.handle_request_of_scan_for_new_payable(None); - let has_scan_started = subject.scanners.payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::Payables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." )); } @@ -2466,10 +3704,13 @@ mod tests { subject.handle_request_of_scan_for_pending_payable(None); - let has_scan_started = subject.scanners.pending_payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::PendingPayables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." )); } @@ -2481,10 +3722,10 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(100), + pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_millis(100), - pending_payable_scan_interval: Duration::from_millis(100), }); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; let peer_actors = peer_actors_builder().build(); let subject = AccountantBuilder::default() .bootstrapper_config(config) @@ -2498,14 +3739,14 @@ mod tests { System::current().stop(); assert_eq!(system.run(), 0); - // no panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes + // No panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes TestLogHandler::new().exists_log_containing( &format!("{test_name}: Started with --scans off; declining to begin database and blockchain scans"), ); } #[test] - fn scan_for_payables_message_does_not_trigger_payment_for_balances_below_the_curve() { + fn scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve() { init_test_logging(); let consuming_wallet = make_paying_wallet(b"consuming wallet"); let payment_thresholds = PaymentThresholds { @@ -2517,15 +3758,16 @@ mod tests { unban_below_gwei: 10_000_000, }; let config = bc_from_earning_wallet(make_wallet("mine")); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let accounts = vec![ // below minimum balance, to the right of time intersection (inside buffer zone) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei - 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( - payment_thresholds.threshold_interval_sec + 10, + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec, ), ), pending_payable_opt: None, @@ -2534,21 +3776,21 @@ mod tests { PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec - 10, - ), + last_paid_timestamp: from_unix_timestamp( + now - checked_conversion::(payment_thresholds.maturity_threshold_sec) + + 1, ), pending_payable_opt: None, }, // above minimum balance, to the right of minimum time (not in buffer zone, below the curve) PayableAccount { wallet: make_wallet("wallet2"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 55), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec + 15, - ), + balance_wei: gwei_to_wei::( + payment_thresholds.permanent_debt_allowed_gwei, + ) + 1, + last_paid_timestamp: from_unix_timestamp( + now - checked_conversion::(payment_thresholds.threshold_interval_sec) + + 1, ), pending_payable_opt: None, }, @@ -2558,49 +3800,62 @@ mod tests { .non_pending_payables_result(vec![]); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); let system = System::new( - "scan_for_payable_message_does_not_trigger_payment_for_balances_below_the_curve", + "scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve", ); let blockchain_bridge_addr: Addr = blockchain_bridge.start(); - let outbound_payments_instructions_sub = - blockchain_bridge_addr.recipient::(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_daos(vec![ForPayableScanner(payable_dao)]) + .consuming_wallet(consuming_wallet.clone()) + .build(); + let payable_scanner = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .payable_dao(payable_dao) .build(); - subject.outbound_payments_instructions_sub_opt = Some(outbound_payments_instructions_sub); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + bind_ui_gateway_unasserted(&mut subject); - let _result = subject.scanners.payable.begin_scan( - consuming_wallet, - SystemTime::now(), - None, - &subject.logger, - ); + let result = subject.handle_request_of_scan_for_new_payable(None); System::current().stop(); system.run(); + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); } #[test] - fn scan_for_payable_message_triggers_payment_for_balances_over_the_curve() { + fn scan_for_new_payables_triggers_payment_for_balances_over_the_curve() { init_test_logging(); let mut config = bc_from_earning_wallet(make_wallet("mine")); let consuming_wallet = make_paying_wallet(b"consuming"); config.scan_intervals_opt = Some(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(50_000), payable_scan_interval: Duration::from_secs(50_000), + pending_payable_scan_interval: Duration::from_secs(10_000), receivable_scan_interval: Duration::from_secs(50_000), }); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let qualified_payables = vec![ - // slightly above minimum balance, to the right of the curve (time intersection) + // slightly above the minimum balance, to the right of the curve (time intersection) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei( DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei + 1, ), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( DEFAULT_PAYMENT_THRESHOLDS.threshold_interval_sec + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec @@ -2613,7 +3868,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 10, ), @@ -2624,65 +3879,87 @@ mod tests { let payable_dao = PayableDaoMock::default().non_pending_payables_result(qualified_payables.clone()); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); - let blockchain_bridge = blockchain_bridge - .system_stop_conditions(match_every_type_id!(QualifiedPayablesMessage)); + let blockchain_bridge_addr = blockchain_bridge.start(); let system = System::new("scan_for_payable_message_triggers_payment_for_balances_over_the_curve"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); - 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); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + bind_ui_gateway_unasserted(&mut subject); - send_start_message!(accountant_subs); + subject.handle_request_of_scan_for_new_payable(None); + System::current().stop(); system.run(); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); let message = blockchain_bridge_recordings.get_record::(0); assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + qualified_payables, consuming_wallet, response_skeleton_opt: None, } ); } + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Early stopped new payable scan \ + was suggested to be followed up by the scan for Receivables, which is not supported though" + )] + fn start_scan_error_in_new_payables_and_unexpected_reaction_by_receivable_scan_scheduling() { + let mut subject = AccountantBuilder::default().build(); + let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() + .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + ScanType::Receivables, + )); + subject.scan_schedulers.reschedule_on_error_resolver = + Box::new(reschedule_on_error_resolver); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + } + #[test] fn accountant_does_not_initiate_another_scan_if_one_is_already_running() { init_test_logging(); let test_name = "accountant_does_not_initiate_another_scan_if_one_is_already_running"; - let payable_dao = PayableDaoMock::default(); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( QualifiedPayablesMessage, QualifiedPayablesMessage )) .start(); - let pps_for_blockchain_bridge_sub = blockchain_bridge_addr.clone().recipient(); - let last_paid_timestamp = to_time_t(SystemTime::now()) - - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec as i64 - - 1; - let payable_account = PayableAccount { - wallet: make_wallet("scan_for_payables"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t(last_paid_timestamp), - pending_payable_opt: None, - }; - let payable_dao = payable_dao - .non_pending_payables_result(vec![payable_account.clone()]) - .non_pending_payables_result(vec![payable_account]); - let config = bc_from_earning_wallet(make_wallet("mine")); + let qualified_payables_sub = blockchain_bridge_addr.clone().recipient(); + let (mut qualified_payables, _, _) = + make_qualified_and_unqualified_payables(now, &payment_thresholds); + let payable_1 = qualified_payables.remove(0); + let payable_2 = qualified_payables.remove(0); + let payable_dao = PayableDaoMock::new() + .non_pending_payables_result(vec![payable_1.clone()]) + .non_pending_payables_result(vec![payable_2.clone()]); + let mut config = bc_from_earning_wallet(make_wallet("mine")); + config.payment_thresholds_opt = Some(payment_thresholds); let system = System::new(test_name); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) @@ -2690,56 +3967,63 @@ mod tests { .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); - let message_before = ScanForPayables { + let message_before = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 111, context_id: 222, }), }; - let message_after = ScanForPayables { + let message_simultaneous = ScanForNewPayables { + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 999, + context_id: 888, + }), + }; + let message_after = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 333, context_id: 444, }), }; - subject.qualified_payables_sub_opt = Some(pps_for_blockchain_bridge_sub); + subject.qualified_payables_sub_opt = Some(qualified_payables_sub); + bind_ui_gateway_unasserted(&mut subject); + // important + subject.scan_schedulers.automatic_scans_enabled = false; let addr = subject.start(); addr.try_send(message_before.clone()).unwrap(); - addr.try_send(ScanForPayables { - response_skeleton_opt: None, - }) - .unwrap(); + addr.try_send(message_simultaneous).unwrap(); - // We ignored the second ScanForPayables message because the first message meant a scan - // was already in progress; now let's make it look like that scan has ended so that we - // can prove the next message will start another one. - addr.try_send(AssertionsMessage { - assertions: Box::new(|accountant: &mut Accountant| { - accountant - .scanners - .payable - .mark_as_ended(&Logger::new("irrelevant")) + // We ignored the second ScanForNewPayables message as there was already in progress from + // the first message. Now we reset the state by ending the first scan by a failure and see + // that the third scan request is going to be accepted willingly again. + addr.try_send(SentPayables { + payment_procedure_result: Err(PayableTransactionError::Signing("bluh".to_string())), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1122, + context_id: 7788, }), }) .unwrap(); addr.try_send(message_after.clone()).unwrap(); system.run(); - let recording = blockchain_bridge_recording.lock().unwrap(); - let messages_received = recording.len(); - assert_eq!(messages_received, 2); - let first_message: &QualifiedPayablesMessage = recording.get_record(0); + let blockchain_bridge_recording = blockchain_bridge_recording.lock().unwrap(); + let first_message_actual: &QualifiedPayablesMessage = + blockchain_bridge_recording.get_record(0); assert_eq!( - first_message.response_skeleton_opt, + first_message_actual.response_skeleton_opt, message_before.response_skeleton_opt ); - let second_message: &QualifiedPayablesMessage = recording.get_record(1); + let second_message_actual: &QualifiedPayablesMessage = + blockchain_bridge_recording.get_record(1); assert_eq!( - second_message.response_skeleton_opt, + second_message_actual.response_skeleton_opt, message_after.response_skeleton_opt ); + let messages_received = blockchain_bridge_recording.len(); + assert_eq!(messages_received, 2); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {}: Payables scan was already initiated", + "INFO: {}: Payables scan was already initiated", test_name )); } @@ -2749,11 +4033,11 @@ mod tests { init_test_logging(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge - .system_stop_conditions(match_every_type_id!(RequestTransactionReceipts)) + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); let payable_fingerprint_1 = PendingPayableFingerprint { rowid: 555, - timestamp: from_time_t(210_000_000), + timestamp: from_unix_timestamp(210_000_000), hash: make_tx_hash(45678), attempt: 1, amount: 4444, @@ -2761,7 +4045,7 @@ mod tests { }; let payable_fingerprint_2 = PendingPayableFingerprint { rowid: 550, - timestamp: from_time_t(210_000_100), + timestamp: from_unix_timestamp(210_000_100), hash: make_tx_hash(112233), attempt: 2, amount: 7999, @@ -2780,7 +4064,7 @@ mod tests { .bootstrapper_config(config) .build(); - subject.request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); let _ = account_addr @@ -2791,19 +4075,89 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recording.len(), 1); let received_msg = blockchain_bridge_recording.get_record::(0); assert_eq!( received_msg, &RequestTransactionReceipts { - pending_payable: vec![payable_fingerprint_1, payable_fingerprint_2], + pending_payable_fingerprints: vec![payable_fingerprint_1, payable_fingerprint_2], response_skeleton_opt: None, } ); + assert_eq!(blockchain_bridge_recording.len(), 1); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); } + #[test] + fn start_scan_error_in_pending_payables_if_initial_scan_is_true_and_no_consuming_wallet_found() + { + let pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default().build(); + subject.consuming_wallet_opt = None; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payables_notify_later_params_arc) + .stop_system_on_count_received(1), + ); + subject.scan_schedulers.pending_payable.interval = Duration::from_secs(60); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payables_notify_params_arc)); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForPendingPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + let pending_payables_notify_later_params = + pending_payables_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payables_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_secs(60) + )] + ); + let new_payables_notify_params = new_payables_notify_params_arc.lock().unwrap(); + assert_eq!( + new_payables_notify_params.len(), + 0, + "Did not expect the new payables request" + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Early stopped pending payable scan \ + was suggested to be followed up by the scan for Receivables, which is not supported though" + )] + fn start_scan_error_in_pending_payables_and_unexpected_reaction_by_receivable_scan_scheduling() + { + let mut subject = AccountantBuilder::default().build(); + let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() + .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + ScanType::Receivables, + )); + subject.scan_schedulers.reschedule_on_error_resolver = + Box::new(reschedule_on_error_resolver); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForPendingPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + } + #[test] fn report_routing_service_provided_message_is_received() { init_test_logging(); @@ -3401,459 +4755,462 @@ mod tests { } #[test] - #[should_panic( - expected = "Recording services consumed from 0x000000000000000000000000000000626f6f6761 but \ - has hit fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" - )] - fn record_service_consumed_panics_on_fatal_errors() { + #[should_panic( + expected = "Recording services consumed from 0x000000000000000000000000000000626f6f6761 but \ + has hit fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" + )] + fn record_service_consumed_panics_on_fatal_errors() { + init_test_logging(); + let wallet = make_wallet("booga"); + let payable_dao = PayableDaoMock::new().more_money_payable_result(Err( + PayableDaoError::RusqliteError("we cannot help ourselves; this is baaad".to_string()), + )); + let subject = AccountantBuilder::default() + .payable_daos(vec![ForAccountantBody(payable_dao)]) + .build(); + + let _ = subject.record_service_consumed(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); + } + + #[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 = make_bc_with_defaults(); + config.crash_point = CrashPoint::Message; + let accountant = AccountantBuilder::default() + .bootstrapper_config(config) + .build(); + + prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); + } + + #[test] + fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { + let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let expected_wallet = make_wallet("paying_you"); + let expected_hash = H256::from("transaction_hash".keccak256()); + let expected_rowid = 45623; + let pending_payable_dao = PendingPayableDaoMock::default() + .fingerprints_rowids_params(&fingerprints_rowids_params_arc) + .fingerprints_rowids_result(TransactionHashes { + rowid_results: vec![(expected_rowid, expected_hash)], + no_rowid_results: vec![], + }); + let payable_dao = PayableDaoMock::new() + .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) + .mark_pending_payables_rowids_result(Ok(())); + let system = + System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![ForPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .build(); + let pending_payable_interval = Duration::from_millis(55); + subject.scan_schedulers.pending_payable.interval = pending_payable_interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), + ); + let expected_payable = PendingPayable::new(expected_wallet.clone(), expected_hash.clone()); + let sent_payable = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + expected_payable.clone(), + )]), + response_skeleton_opt: None, + }; + let addr = subject.start(); + + addr.try_send(sent_payable).expect("unexpected actix error"); + + System::current().stop(); + system.run(); + let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); + assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); + let mark_pending_payables_rowids_params = + mark_pending_payables_rowids_params_arc.lock().unwrap(); + assert_eq!( + *mark_pending_payables_rowids_params, + vec![vec![(expected_wallet, expected_rowid)]] + ); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payable_notify_later_params, + vec![(ScanForPendingPayables::default(), pending_payable_interval)] + ); + // The accountant is unbound here. We don't use the bind message. It means we can prove + // none of those other scan requests could have been sent (especially ScanForNewPayables, + // ScanForRetryPayables) + } + + #[test] + fn no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted( + ) { init_test_logging(); - let wallet = make_wallet("booga"); - let payable_dao = PayableDaoMock::new().more_money_payable_result(Err( - PayableDaoError::RusqliteError("we cannot help ourselves; this is baaad".to_string()), - )); - let subject = AccountantBuilder::default() - .payable_daos(vec![ForAccountantBody(payable_dao)]) + let test_name = "no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + ScannerMock::default() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::Failure, + }), + ))); + // Important. Otherwise, the scan would've been handled through a different endpoint and + // gone for a very long time + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = SystemTime::now(); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&payable_notify_later_params_arc), + ); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let sent_payable = SentPayables { + payment_procedure_result: Err(PayableTransactionError::Sending { + msg: "booga".to_string(), + hashes: vec![make_tx_hash(456)], + }), + response_skeleton_opt: None, + }; + let addr = subject.start(); - let _ = subject.record_service_consumed(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); + addr.try_send(sent_payable.clone()) + .expect("unexpected actix error"); + + System::current().stop(); + assert_eq!(system.run(), 0); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (actual_sent_payable, logger) = finish_scan_params.remove(0); + assert_eq!(actual_sent_payable, sent_payable,); + assert_using_the_same_logger(&logger, test_name, None); + let mut payable_notify_later_params = payable_notify_later_params_arc.lock().unwrap(); + let (scheduled_msg, _interval) = payable_notify_later_params.remove(0); + assert_eq!(scheduled_msg, ScanForNewPayables::default()); + assert!( + payable_notify_later_params.is_empty(), + "Should be empty but {:?}", + payable_notify_later_params + ); } #[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 = make_bc_with_defaults(); - config.crash_point = CrashPoint::Message; - let accountant = AccountantBuilder::default() - .bootstrapper_config(config) + fn accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed() { + init_test_logging(); + let test_name = + "accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + let system = System::new(test_name); + let (mut msg, _) = + make_report_transaction_receipts_msg(vec![TxStatus::Pending, TxStatus::Failed]); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 45, + context_id: 7, + }); + msg.response_skeleton_opt = response_skeleton_opt; + let subject_addr = subject.start(); - prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); + subject_addr.try_send(msg.clone()).unwrap(); + + System::current().stop(); + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let retry_payable_notify_params = retry_payable_notify_params_arc.lock().unwrap(); + assert_eq!( + *retry_payable_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt + }] + ); + assert_using_the_same_logger(&logger, test_name, None) } #[test] - fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { - init_test_logging(); - let port = find_free_port(); - let pending_tx_hash_1 = - H256::from_str("e66814b2812a80d619813f51aa999c0df84eb79d10f4923b2b7667b30d6b33d3") - .unwrap(); - let pending_tx_hash_2 = - H256::from_str("0288ef000581b3bca8a2017eac9aea696366f8f1b7437f18d1aad57bccb7032c") - .unwrap(); - let _blockchain_client_server = MBCSBuilder::new(port) - // Blockchain Agent Gas Price - .ok_response("0x3B9ACA00".to_string(), 0) // 1000000000 - // Blockchain Agent transaction fee balance - .ok_response("0xFFF0".to_string(), 0) // 65520 - // Blockchain Agent masq balance - .ok_response( - "0x000000000000000000000000000000000000000000000000000000000000FFFF".to_string(), - 0, - ) - // Submit payments to blockchain - .ok_response("0xFFF0".to_string(), 1) - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 1 - handle_request_transaction_receipts - .begin_batch() - .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) // Null response - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 2 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 3 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .status(U64::from(0)) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 4 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .status(U64::from(1)) - .block_number(U64::from(1234)) - .block_hash(Default::default()) - .build(), - ) - .end_batch() - .start(); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let return_all_errorless_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let update_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); + fn accountant_confirms_payable_txs_and_schedules_the_new_payable_scanner_timely() { let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_record_params_arc = Arc::new(Mutex::new(vec![])); - 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 rowid_for_account_1 = 3; - let rowid_for_account_2 = 5; - let now = SystemTime::now(); - let past_payable_timestamp_1 = now.sub(Duration::from_secs( - (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 555) as u64, - )); - let past_payable_timestamp_2 = now.sub(Duration::from_secs( - (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 50) as u64, - )); - let this_payable_timestamp_1 = now; - let this_payable_timestamp_2 = now.add(Duration::from_millis(50)); - let payable_account_balance_1 = - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 10); - let payable_account_balance_2 = - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 666); - let wallet_account_1 = make_wallet("creditor1"); - let wallet_account_2 = make_wallet("creditor2"); - let blockchain_interface = make_blockchain_interface_web3(port); - let consuming_wallet = make_paying_wallet(b"wallet"); - let system = System::new("pending_transaction"); - let persistent_config_id_stamp = ArbitraryIdStamp::new(); - let persistent_config = PersistentConfigurationMock::default() - .set_arbitrary_id_stamp(persistent_config_id_stamp); - let blockchain_bridge = BlockchainBridge::new( - Box::new(blockchain_interface), - Arc::new(Mutex::new(persistent_config)), - false, - ); - let account_1 = PayableAccount { - wallet: wallet_account_1.clone(), - balance_wei: payable_account_balance_1, - last_paid_timestamp: past_payable_timestamp_1, - pending_payable_opt: None, - }; - let account_2 = PayableAccount { - wallet: wallet_account_2.clone(), - balance_wei: payable_account_balance_2, - last_paid_timestamp: past_payable_timestamp_2, - pending_payable_opt: None, - }; - let pending_payable_scan_interval = 1000; // should be slightly less than 1/5 of the time until shutting the system - let payable_dao_for_payable_scanner = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![account_1, account_2]) - .mark_pending_payables_rowids_params(&mark_pending_payable_params_arc) - .mark_pending_payables_rowids_result(Ok(())); - let payable_dao_for_pending_payable_scanner = PayableDaoMock::new() + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - 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: rowid_for_account_1, - timestamp: this_payable_timestamp_1, - hash: pending_tx_hash_1, - attempt: 1, - amount: payable_account_balance_1, - process_error: None, - }; - let fingerprint_2_first_round = PendingPayableFingerprint { - rowid: rowid_for_account_2, - timestamp: this_payable_timestamp_2, - hash: pending_tx_hash_2, - attempt: 1, - amount: payable_account_balance_2, - process_error: None, - }; - let fingerprint_1_second_round = PendingPayableFingerprint { - attempt: 2, - ..fingerprint_1_first_round.clone() - }; - let fingerprint_2_second_round = PendingPayableFingerprint { - attempt: 2, - ..fingerprint_2_first_round.clone() - }; - let fingerprint_1_third_round = PendingPayableFingerprint { - attempt: 3, - ..fingerprint_1_first_round.clone() - }; - let fingerprint_2_third_round = PendingPayableFingerprint { - attempt: 3, - ..fingerprint_2_first_round.clone() - }; - let fingerprint_2_fourth_round = PendingPayableFingerprint { - attempt: 4, - ..fingerprint_2_first_round.clone() - }; - let pending_payable_dao_for_payable_scanner = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }); - let mut pending_payable_dao_for_pending_payable_scanner = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_params(&return_all_errorless_fingerprints_params_arc) - .return_all_errorless_fingerprints_result(vec![]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_first_round, - fingerprint_2_first_round, - ]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_second_round, - fingerprint_2_second_round, - ]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_third_round, - fingerprint_2_third_round, - ]) - .return_all_errorless_fingerprints_result(vec![fingerprint_2_fourth_round.clone()]) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }) - .increment_scan_attempts_params(&update_fingerprint_params_arc) - .increment_scan_attempts_result(Ok(())) - .increment_scan_attempts_result(Ok(())) - .increment_scan_attempts_result(Ok(())) - .mark_failures_params(&mark_failure_params_arc) - // we don't have a better solution yet, so we mark this down - .mark_failures_result(Ok(())) - .delete_fingerprints_params(&delete_record_params_arc) - // this is used during confirmation of the successful one - .delete_fingerprints_result(Ok(())); - pending_payable_dao_for_pending_payable_scanner - .have_return_all_errorless_fingerprints_shut_down_the_system = true; - let pending_payable_dao_for_accountant = - PendingPayableDaoMock::new().insert_fingerprints_result(Ok(())); - let accountant_addr = Arbiter::builder() - .stop_system_on_panic(true) - .start(move |_| { - let mut subject = AccountantBuilder::default() - .consuming_wallet(consuming_wallet) - .bootstrapper_config(bootstrapper_config) - .payable_daos(vec![ - ForPayableScanner(payable_dao_for_payable_scanner), - ForPendingPayableScanner(payable_dao_for_pending_payable_scanner), - ]) - .pending_payable_daos(vec![ - ForAccountantBody(pending_payable_dao_for_accountant), - ForPayableScanner(pending_payable_dao_for_payable_scanner), - ForPendingPayableScanner(pending_payable_dao_for_pending_payable_scanner), - ]) - .build(); - 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) - .capture_msg_and_let_it_fly_on(); - subject.scan_schedulers.update_scheduler( - ScanType::PendingPayables, - Some(Box::new(notify_later_half_mock)), - None, - ); - subject - }); - let mut peer_actors = peer_actors_builder().build(); - let accountant_subs = Accountant::make_subs_from(&accountant_addr); - peer_actors.accountant = accountant_subs.clone(); - let blockchain_bridge_addr = blockchain_bridge.start(); - let blockchain_bridge_subs = BlockchainBridge::make_subs_from(&blockchain_bridge_addr); - peer_actors.blockchain_bridge = blockchain_bridge_subs.clone(); - send_bind_message!(accountant_subs, peer_actors); - send_bind_message!(blockchain_bridge_subs, peer_actors); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let system = System::new("new_payable_scanner_timely"); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(3)) + .unwrap(); + let nominal_interval = Duration::from_secs(6); + let expected_computed_interval = Duration::from_secs(3); + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_result(Some(expected_computed_interval)); + subject.scan_schedulers.payable.new_payable_interval = nominal_interval; + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let subject_addr = subject.start(); + let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + ]); - send_start_message!(accountant_subs); + subject_addr.try_send(msg).unwrap(); - assert_eq!(system.run(), 0); - let mut mark_pending_payable_params = mark_pending_payable_params_arc.lock().unwrap(); - let mut one_set_of_mark_pending_payable_params = mark_pending_payable_params.remove(0); - assert!(mark_pending_payable_params.is_empty()); - let first_payable = one_set_of_mark_pending_payable_params.remove(0); - assert_eq!(first_payable.0, wallet_account_1); - assert_eq!(first_payable.1, rowid_for_account_1); - let second_payable = one_set_of_mark_pending_payable_params.remove(0); - assert!( - one_set_of_mark_pending_payable_params.is_empty(), - "{:?}", - one_set_of_mark_pending_payable_params - ); - assert_eq!(second_payable.0, wallet_account_2); - assert_eq!(second_payable.1, rowid_for_account_2); - let return_all_errorless_fingerprints_params = - return_all_errorless_fingerprints_params_arc.lock().unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary - assert!(return_all_errorless_fingerprints_params.len() >= 5); - let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); - assert_eq!(*non_pending_payables_params, vec![()]); // because we disabled further scanning for payables - let update_fingerprints_params = update_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *update_fingerprints_params, - vec![ - vec![rowid_for_account_1, rowid_for_account_2], - vec![rowid_for_account_1, rowid_for_account_2], - vec![rowid_for_account_2], - ] - ); - let mark_failure_params = mark_failure_params_arc.lock().unwrap(); - assert_eq!(*mark_failure_params, vec![vec![rowid_for_account_1]]); - let delete_record_params = delete_record_params_arc.lock().unwrap(); - assert_eq!(*delete_record_params, vec![vec![rowid_for_account_2]]); - let transaction_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + System::current().stop(); + system.run(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (_, last_new_payable_timestamp_actual, scan_interval_actual) = + compute_interval_params.remove(0); assert_eq!( - *transaction_confirmed_params, - vec![vec![fingerprint_2_fourth_round.clone()]] - ); - let expected_scan_pending_payable_msg_and_interval = ( - ScanForPendingPayables { - response_skeleton_opt: None, - }, - Duration::from_millis(pending_payable_scan_interval), + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp ); - let mut notify_later_check_for_confirmation = - notify_later_scan_for_pending_payable_params_arc - .lock() - .unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary - let vector_of_first_five_cycles = notify_later_check_for_confirmation - .drain(0..=4) - .collect_vec(); + assert_eq!(scan_interval_actual, nominal_interval); + assert!(compute_interval_params.is_empty()); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); assert_eq!( - vector_of_first_five_cycles, - vec![ - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval, - ] - ); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "WARN: Accountant: Broken transactions 0xe66814b2812a80d619813f51aa999c0df84eb79d10f\ - 4923b2b7667b30d6b33d3 marked as an error. You should take over the care of those to make sure \ - your debts are going to be settled properly. At the moment, there is no automated process \ - fixing that without your assistance"); - log_handler.exists_log_matching("INFO: Accountant: Transaction 0x0288ef000581b3bca8a2017eac9\ - aea696366f8f1b7437f18d1aad57bccb7032c 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: Transactions 0x0288ef000581b3bca8a2017eac9aea696366f8f1b7437f18d1aad5\ - 7bccb7032c completed their confirmation process succeeding", + *new_payable_notify_later, + vec![(ScanForNewPayables::default(), expected_computed_interval)] ); + let new_payable_notify = new_payable_notify_arc.lock().unwrap(); + assert!( + new_payable_notify.is_empty(), + "should be empty but was: {:?}", + new_payable_notify + ) } #[test] - fn accountant_receives_reported_transaction_receipts_and_processes_them_all() { + fn accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(8)) + .unwrap(); + let nominal_interval = Duration::from_secs(6); + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_result(None); + subject.scan_schedulers.payable.new_payable_interval = nominal_interval; + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); let subject_addr = subject.start(); - let transaction_hash_1 = make_tx_hash(4545); - let transaction_receipt_1 = TxReceipt { - transaction_hash: transaction_hash_1, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), + let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), block_number: U64::from(100), }), - }; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_time_t(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, - }; - let transaction_hash_2 = make_tx_hash(3333333); - let transaction_receipt_2 = TxReceipt { - transaction_hash: transaction_hash_2, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), block_number: U64::from(200), }), - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 10, - timestamp: from_time_t(199_780_000), - hash: Default::default(), - attempt: 15, - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_1), - fingerprint_1.clone(), - ), - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_2), - fingerprint_2.clone(), - ), - ], - response_skeleton_opt: None, - }; + ]); subject_addr.try_send(msg).unwrap(); - let system = System::new("processing reported receipts"); + let system = System::new("new_payable_scanner_asap"); System::current().stop(); system.run(); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (_, last_new_payable_timestamp_actual, scan_interval_actual) = + compute_interval_params.remove(0); assert_eq!( - *transactions_confirmed_params, - vec![vec![fingerprint_1, fingerprint_2]] + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp + ); + assert_eq!(scan_interval_actual, nominal_interval); + assert!(compute_interval_params.is_empty()); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); + assert!( + new_payable_notify_later.is_empty(), + "should be empty but was: {:?}", + new_payable_notify_later + ); + let new_payable_notify = new_payable_notify_arc.lock().unwrap(); + assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]) + } + + #[test] + fn scheduler_for_new_payables_operates_with_proper_now_timestamp() { + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let system = System::new("scheduler_for_new_payables_operates_with_proper_now_timestamp"); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_millis(3500)) + .unwrap(); + let new_payable_interval = Duration::from_secs(6); + subject.scan_schedulers.payable.new_payable_interval = new_payable_interval; + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); + let subject_addr = subject.start(); + let (msg, _) = make_report_transaction_receipts_msg(vec![ + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + ]); + + subject_addr.try_send(msg).unwrap(); + + let before = SystemTime::now(); + System::current().stop(); + system.run(); + let after = SystemTime::now(); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); + let (_, actual_interval) = new_payable_notify_later[0]; + let interval_computer = NewPayableScanDynIntervalComputerReal::default(); + let left_side_bound = interval_computer + .compute_interval( + before, + last_new_payable_scan_timestamp, + new_payable_interval, + ) + .unwrap(); + let right_side_bound = interval_computer + .compute_interval(after, last_new_payable_scan_timestamp, new_payable_interval) + .unwrap(); + assert!( + left_side_bound >= actual_interval && actual_interval >= right_side_bound, + "expected actual {:?} to be between {:?} and {:?}", + actual_interval, + left_side_bound, + right_side_bound ); } + fn make_report_transaction_receipts_msg( + status_txs: Vec, + ) -> (ReportTransactionReceipts, Vec) { + let (receipt_result_fingerprint_pairs, fingerprints): (Vec<_>, Vec<_>) = status_txs + .into_iter() + .enumerate() + .map(|(idx, status)| { + let transaction_hash = make_tx_hash(idx as u32); + let transaction_receipt_result = TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash, + status, + }); + let fingerprint = PendingPayableFingerprint { + rowid: idx as u64, + timestamp: from_unix_timestamp(1_000_000_000 * idx as i64), + hash: transaction_hash, + attempt: 2, + amount: 1_000_000 * idx as u128 * idx as u128, + process_error: None, + }; + ( + (transaction_receipt_result, fingerprint.clone()), + fingerprint, + ) + }) + .unzip(); + + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: receipt_result_fingerprint_pairs, + response_skeleton_opt: None, + }; + + (msg, fingerprints) + } + #[test] fn accountant_handles_inserting_new_fingerprints() { init_test_logging(); @@ -4771,17 +6128,9 @@ mod tests { let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); - match message.scan_type { - ScanType::Payables => subject.scanners.payable.mark_as_started(SystemTime::now()), - ScanType::PendingPayables => subject - .scanners - .pending_payable - .mark_as_started(SystemTime::now()), - ScanType::Receivables => subject - .scanners - .receivable - .mark_as_started(SystemTime::now()), - } + subject + .scanners + .reset_scan_started(message.scan_type, MarkScanner::Started(SystemTime::now())); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -4792,19 +6141,13 @@ mod tests { subject_addr .try_send(AssertionsMessage { assertions: Box::new(move |actor: &mut Accountant| { - let scan_started_at_opt = match message.scan_type { - ScanType::Payables => actor.scanners.payable.scan_started_at(), - ScanType::PendingPayables => { - actor.scanners.pending_payable.scan_started_at() - } - ScanType::Receivables => actor.scanners.receivable.scan_started_at(), - }; + let scan_started_at_opt = actor.scanners.scan_started_at(message.scan_type); assert_eq!(scan_started_at_opt, None); }), }) .unwrap(); System::current().stop(); - system.run(); + assert_eq!(system.run(), 0); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); match message.response_skeleton_opt { Some(response_skeleton) => { @@ -4860,6 +6203,10 @@ mod tests { assert_on_initialization_with_panic_on_migration(&data_dir, &act); } + + fn bind_ui_gateway_unasserted(accountant: &mut Accountant) { + accountant.ui_message_sub_opt = Some(make_recorder().0.start().recipient()); + } } #[cfg(test)] diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index 88ee13e74..74c88690b 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use masq_lib::logger::Logger; use std::time::SystemTime; @@ -71,9 +71,8 @@ pub enum AnalysisError {} #[cfg(test)] mod tests { use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; + use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::test_utils::make_payable_account; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; @@ -86,7 +85,7 @@ mod tests { payable.balance_wei = 100_000_000; let agent = BlockchainAgentMock::default(); let setup_msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable]), + qualified_payables: vec![payable], agent: Box::new(agent), response_skeleton_opt: None, }; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs b/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs deleted file mode 100644 index 16331e4bf..000000000 --- a/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -pub mod payable_scanner; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 1307cb006..349ffe3df 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod mid_scan_msg_handling; +pub mod payable_scanner_extension; +pub mod scan_schedulers; pub mod scanners_utils; pub mod test_utils; @@ -11,31 +12,23 @@ use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ - debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, - investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, - separate_errors, separate_rowids_and_hashes, PayableThresholdsGauge, - PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata, -}; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::PendingPayableId; +use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ - comma_joined_stringifiable, gwei_to_wei, Accountant, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, - ScanForPendingPayables, ScanForReceivables, SentPayables, + comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, + ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, + ScanForReceivables, SentPayables, }; use crate::accountant::db_access_objects::banned_dao::BannedDao; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, -}; -use crate::sub_lib::blockchain_bridge::{ - OutboundPaymentsInstructions, + DaoFactories, FinancialStatistics, PaymentThresholds, }; -use crate::sub_lib::utils::{NotifyLaterHandle, NotifyLaterHandleReal}; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; -use actix::{Context, Message}; +use actix::{Message}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; @@ -44,22 +37,40 @@ use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; use std::rc::Rc; -use std::time::{Duration, SystemTime}; +use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; +use variant_count::VariantCount; use web3::types::H256; -use masq_lib::type_obfuscation::Obfuscated; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{PreparedAdjustment, MultistagePayableScanner, SolvencySensitivePaymentInstructor}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; +use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; +// Leave the individual scanner objects private! pub struct Scanners { - pub payable: Box>, - pub pending_payable: Box>, - pub receivable: Box>, + payable: Box, + aware_of_unresolved_pending_payable: bool, + initial_pending_payable_scan: bool, + pending_payable: Box< + dyn PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + >, + receivable: Box< + dyn PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + >, + >, } impl Scanners { @@ -86,6 +97,7 @@ impl Scanners { let persistent_configuration = PersistentConfigurationReal::from(dao_factories.config_dao_factory.make()); + let receivable = Box::new(ReceivableScanner::new( dao_factories.receivable_dao_factory.make(), dao_factories.banned_dao_factory.make(), @@ -96,25 +108,290 @@ impl Scanners { Scanners { payable, + aware_of_unresolved_pending_payable: false, + initial_pending_payable_scan: true, pending_payable, receivable, } } + + pub fn start_new_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + automatic_scans_enabled: bool, + ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + if triggered_manually && automatic_scans_enabled { + return Err(StartScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); + } + if let Some(started_at) = self.payable.scan_started_at() { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at, + }); + } + + Self::start_correct_payable_scanner::( + &mut *self.payable, + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } + + // Note: This scanner cannot be started on its own. It always runs after the pending payable + // scan, but only if it is clear that a retry is needed. + pub fn start_retry_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(started_at) = self.payable.scan_started_at() { + unreachable!( + "Guards should ensure that no payable scanner can run if the pending payable \ + repetitive sequence is still ongoing. However, some other payable scan intruded \ + at {} and is still running at {}", + StartScanError::timestamp_as_string(started_at), + StartScanError::timestamp_as_string(SystemTime::now()) + ) + } + + Self::start_correct_payable_scanner::( + &mut *self.payable, + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } + + pub fn start_pending_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + automatic_scans_enabled: bool, + ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + self.check_general_conditions_for_pending_payable_scan( + triggered_manually, + automatic_scans_enabled, + )?; + match ( + self.pending_payable.scan_started_at(), + self.payable.scan_started_at(), + ) { + (Some(pp_timestamp), Some(p_timestamp)) => + // If you're wondering, then yes, this condition should be the sacred truth between + // PendingPayableScanner and NewPayableScanner. + { + unreachable!( + "Any payable-related scanners should never be allowed to run in parallel. \ + Scan for pending payables started at: {}, scan for payables started at: {}", + StartScanError::timestamp_as_string(pp_timestamp), + StartScanError::timestamp_as_string(p_timestamp) + ) + } + (Some(started_at), None) => { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at, + }) + } + (None, Some(started_at)) => { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: Some(ScanType::Payables), + started_at, + }) + } + (None, None) => (), + } + self.pending_payable + .start_scan(wallet, timestamp, response_skeleton_opt, logger) + } + + pub fn start_receivable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + automatic_scans_enabled: bool, + ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + if triggered_manually && automatic_scans_enabled { + return Err(StartScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); + } + if let Some(started_at) = self.receivable.scan_started_at() { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at, + }); + } + + self.receivable + .start_scan(wallet, timestamp, response_skeleton_opt, logger) + } + + pub fn finish_payable_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { + let scan_result = self.payable.finish_scan(msg, logger); + match scan_result.result { + OperationOutcome::NewPendingPayable => self.aware_of_unresolved_pending_payable = true, + OperationOutcome::Failure => (), + }; + scan_result + } + + pub fn finish_pending_payable_scan( + &mut self, + msg: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanResult { + self.pending_payable.finish_scan(msg, logger) + } + + pub fn finish_receivable_scan( + &mut self, + msg: ReceivedPayments, + logger: &Logger, + ) -> Option { + self.receivable.finish_scan(msg, logger) + } + + pub fn acknowledge_scan_error(&mut self, error: &ScanError, logger: &Logger) { + match error.scan_type { + ScanType::Payables => { + self.payable.mark_as_ended(logger); + } + ScanType::PendingPayables => { + self.pending_payable.mark_as_ended(logger); + } + ScanType::Receivables => { + self.receivable.mark_as_ended(logger); + } + }; + } + + pub fn try_skipping_payable_adjustment( + &self, + msg: BlockchainAgentWithContextMessage, + logger: &Logger, + ) -> Result, String> { + self.payable.try_skipping_payment_adjustment(msg, logger) + } + + pub fn perform_payable_adjustment( + &self, + setup: PreparedAdjustment, + logger: &Logger, + ) -> OutboundPaymentsInstructions { + self.payable.perform_payment_adjustment(setup, logger) + } + + pub fn initial_pending_payable_scan(&self) -> bool { + self.initial_pending_payable_scan + } + + pub fn unset_initial_pending_payable_scan(&mut self) { + self.initial_pending_payable_scan = false + } + + // This is a helper function reducing a boilerplate of complex trait resolving where + // the compiler requires to specify which trigger message distinguish the scan to run. + // The payable scanner offers two modes through doubled implementations of StartableScanner + // which uses the trigger message type as the only distinction between them. + fn start_correct_payable_scanner<'a, TriggerMessage>( + scanner: &'a mut (dyn MultistageDualPayableScanner + 'a), + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result + where + TriggerMessage: Message, + (dyn MultistageDualPayableScanner + 'a): + StartableScanner, + { + <(dyn MultistageDualPayableScanner + 'a) as StartableScanner< + TriggerMessage, + QualifiedPayablesMessage, + >>::start_scan(scanner, wallet, timestamp, response_skeleton_opt, logger) + } + + fn check_general_conditions_for_pending_payable_scan( + &mut self, + triggered_manually: bool, + automatic_scans_enabled: bool, + ) -> Result<(), StartScanError> { + if triggered_manually && automatic_scans_enabled { + return Err(StartScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); + } + if self.initial_pending_payable_scan { + return Ok(()); + } + if triggered_manually && !self.aware_of_unresolved_pending_payable { + return Err(StartScanError::ManualTriggerError( + MTError::UnnecessaryRequest { + hint_opt: Some("Run the Payable scanner first.".to_string()), + }, + )); + } + if !self.aware_of_unresolved_pending_payable { + unreachable!( + "Automatic pending payable scan should never start if there are no pending \ + payables to process." + ) + } + + Ok(()) + } } -pub trait Scanner -where - BeginMessage: Message, +pub(in crate::accountant::scanners) trait PrivateScanner< + TriggerMessage, + StartMessage, + EndMessage, + ScanResult, +>: + StartableScanner + Scanner where + TriggerMessage: Message, + StartMessage: Message, EndMessage: Message, { - fn begin_scan( +} + +trait StartableScanner +where + TriggerMessage: Message, + StartMessage: Message, +{ + fn start_scan( &mut self, - wallet: Wallet, + wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result; - fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> Option; + ) -> Result; +} + +trait Scanner +where + EndMessage: Message, +{ + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); fn mark_as_ended(&mut self, logger: &Logger); @@ -125,7 +402,7 @@ where pub struct ScannerCommon { initiated_at_opt: Option, - pub payment_thresholds: Rc, + payment_thresholds: Rc, } impl ScannerCommon { @@ -159,6 +436,7 @@ impl ScannerCommon { } } +#[macro_export] macro_rules! time_marking_methods { ($scan_type_variant: ident) => { fn scan_started_at(&self) -> Option { @@ -180,26 +458,25 @@ macro_rules! time_marking_methods { } pub struct PayableScanner { + pub payable_threshold_gauge: Box, pub common: ScannerCommon, pub payable_dao: Box, pub pending_payable_dao: Box, - pub payable_threshold_gauge: Box, pub payment_adjuster: Box, } -impl Scanner for PayableScanner { - fn begin_scan( +impl MultistageDualPayableScanner for PayableScanner {} + +impl StartableScanner for PayableScanner { + fn start_scan( &mut self, - consuming_wallet: Wallet, + consuming_wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } + ) -> Result { self.mark_as_started(timestamp); - info!(logger, "Scanning for payables"); + info!(logger, "Scanning for new payables"); let all_non_pending_payables = self.payable_dao.non_pending_payables(); debug!( @@ -214,7 +491,7 @@ impl Scanner for PayableScanner { match qualified_payables.is_empty() { true => { self.mark_as_ended(logger); - Err(BeginScanError::NothingToProcess) + Err(StartScanError::NothingToProcess) } false => { info!( @@ -222,18 +499,32 @@ impl Scanner for PayableScanner { "Chose {} qualified debts to pay", qualified_payables.len() ); - let protected_payables = self.protect_payables(qualified_payables); + let outgoing_msg = QualifiedPayablesMessage::new( - protected_payables, - consuming_wallet, + qualified_payables, + consuming_wallet.clone(), response_skeleton_opt, ); Ok(outgoing_msg) } } } +} - fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { +impl StartableScanner for PayableScanner { + fn start_scan( + &mut self, + _consuming_wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + todo!("Complete me under GH-605") + } +} + +impl Scanner for PayableScanner { + fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> PayableScanResult { let (sent_payables, err_opt) = separate_errors(&message, logger); debug!( logger, @@ -247,12 +538,25 @@ impl Scanner for PayableScanner { self.handle_sent_payable_errors(err_opt, logger); self.mark_as_ended(logger); - message - .response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + + let ui_response_opt = + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + + let result = if !sent_payables.is_empty() { + OperationOutcome::NewPendingPayable + } else { + OperationOutcome::Failure + }; + + PayableScanResult { + ui_response_opt, + result, + } } time_marking_methods!(Payables); @@ -270,15 +574,11 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { .payment_adjuster .search_for_indispensable_adjustment(&msg, logger) { - Ok(None) => { - let protected = msg.protected_qualified_payables; - let unprotected = self.expose_payables(protected); - Ok(Either::Left(OutboundPaymentsInstructions::new( - unprotected, - msg.agent, - msg.response_skeleton_opt, - ))) - } + Ok(None) => Ok(Either::Left(OutboundPaymentsInstructions::new( + msg.qualified_payables, + msg.agent, + msg.response_skeleton_opt, + ))), Ok(Some(adjustment)) => Ok(Either::Right(PreparedAdjustment::new(msg, adjustment))), Err(_e) => todo!("be implemented with GH-711"), } @@ -294,8 +594,6 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { } } -impl MultistagePayableScanner for PayableScanner {} - impl PayableScanner { pub fn new( payable_dao: Box, @@ -398,7 +696,7 @@ impl PayableScanner { } let sent_payables_hashes = hashes.iter().copied().collect::>(); - if !PayableScanner::is_symmetrical(sent_payables_hashes, hashes_from_db) { + if !Self::is_symmetrical(sent_payables_hashes, hashes_from_db) { panic!( "Inconsistency in two maps, they cannot be matched by hashes. Data set directly \ sent from BlockchainBridge: {:?}, set derived from the DB: {:?}", @@ -546,14 +844,6 @@ impl PayableScanner { panic!("{}", msg) }; } - - fn protect_payables(&self, payables: Vec) -> Obfuscated { - Obfuscated::obfuscate_vector(payables) - } - - fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { - obfuscated.expose_vector() - } } pub struct PendingPayableScanner { @@ -564,24 +854,33 @@ pub struct PendingPayableScanner { pub financial_statistics: Rc>, } -impl Scanner for PendingPayableScanner { - fn begin_scan( +impl + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + > for PendingPayableScanner +{ +} + +impl StartableScanner + for PendingPayableScanner +{ + fn start_scan( &mut self, - _irrelevant_wallet: Wallet, + _wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } + ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for pending payable"); let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); match filtered_pending_payable.is_empty() { true => { self.mark_as_ended(logger); - Err(BeginScanError::NothingToProcess) + Err(StartScanError::NothingToProcess) } false => { debug!( @@ -590,22 +889,27 @@ impl Scanner for PendingP filtered_pending_payable.len() ); Ok(RequestTransactionReceipts { - pending_payable: filtered_pending_payable, + pending_payable_fingerprints: filtered_pending_payable, response_skeleton_opt, }) } } } +} +impl Scanner for PendingPayableScanner { fn finish_scan( &mut self, message: ReportTransactionReceipts, logger: &Logger, - ) -> Option { + ) -> PendingPayableScanResult { let response_skeleton_opt = message.response_skeleton_opt; - match message.fingerprints_with_receipts.is_empty() { - true => debug!(logger, "No transaction receipts found."), + let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { + true => { + warning!(logger, "No transaction receipts found."); + todo!("This requires the payment retry. GH-631 must be completed first"); + } false => { debug!( logger, @@ -613,15 +917,24 @@ impl Scanner for PendingP message.fingerprints_with_receipts.len() ); let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - self.process_transactions_by_reported_state(scan_report, logger); + let requires_payment_retry = + self.process_transactions_by_reported_state(scan_report, logger); + + self.mark_as_ended(logger); + + requires_payment_retry } - } + }; - self.mark_as_ended(logger); - response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + if requires_payment_retry { + PendingPayableScanResult::PaymentRetryRequired + } else { + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) + } } time_marking_methods!(PendingPayables); @@ -683,10 +996,14 @@ impl PendingPayableScanner { &mut self, scan_report: PendingPayableScanReport, logger: &Logger, - ) { + ) -> bool { + let requires_payments_retry = scan_report.requires_payments_retry(); + self.confirm_transactions(scan_report.confirmed, logger); self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger) + self.update_remaining_fingerprints(scan_report.still_pending, logger); + + requires_payments_retry } fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { @@ -791,30 +1108,40 @@ pub struct ReceivableScanner { pub financial_statistics: Rc>, } -impl Scanner for ReceivableScanner { - fn begin_scan( +impl + PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + > for ReceivableScanner +{ +} + +impl StartableScanner for ReceivableScanner { + fn start_scan( &mut self, - earning_wallet: Wallet, + earning_wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } + ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for receivables to {}", earning_wallet); self.scan_for_delinquencies(timestamp, logger); Ok(RetrieveTransactions { - recipient: earning_wallet, + recipient: earning_wallet.clone(), response_skeleton_opt, }) } +} +impl Scanner> for ReceivableScanner { fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { self.handle_new_received_payments(&msg, logger); self.mark_as_ended(logger); + msg.response_skeleton_opt .map(|response_skeleton| NodeToUiMessage { target: MessageTarget::ClientId(response_skeleton.client_id), @@ -946,52 +1273,76 @@ impl ReceivableScanner { } } -#[derive(Debug, PartialEq, Eq)] -pub enum BeginScanError { +#[derive(Debug, PartialEq, Eq, Clone, VariantCount)] +pub enum StartScanError { NothingToProcess, NoConsumingWalletFound, - ScanAlreadyRunning(SystemTime), + ScanAlreadyRunning { + cross_scan_cause_opt: Option, + started_at: SystemTime, + }, CalledFromNullScanner, // Exclusive for tests + ManualTriggerError(MTError), } -impl BeginScanError { - pub fn handle_error( - &self, - logger: &Logger, - scan_type: ScanType, - is_externally_triggered: bool, - ) { - let log_message_opt = match self { - BeginScanError::NothingToProcess => Some(format!( +impl StartScanError { + pub fn log_error(&self, logger: &Logger, scan_type: ScanType, is_externally_triggered: bool) { + enum ErrorType { + Temporary(String), + Permanent(String), + } + + let log_message = match self { + StartScanError::NothingToProcess => ErrorType::Temporary(format!( "There was nothing to process during {:?} scan.", scan_type )), - BeginScanError::ScanAlreadyRunning(timestamp) => Some(format!( - "{:?} scan was already initiated at {}. \ - Hence, this scan request will be ignored.", + StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt, + started_at, + } => ErrorType::Temporary(Self::scan_already_running_msg( scan_type, - BeginScanError::timestamp_as_string(timestamp) + *cross_scan_cause_opt, + *started_at, )), - BeginScanError::NoConsumingWalletFound => Some(format!( + StartScanError::NoConsumingWalletFound => ErrorType::Permanent(format!( "Cannot initiate {:?} scan because no consuming wallet was found.", scan_type )), - BeginScanError::CalledFromNullScanner => match cfg!(test) { - true => None, + StartScanError::CalledFromNullScanner => match cfg!(test) { + true => ErrorType::Permanent(format!( + "Called from NullScanner, not the {:?} scanner.", + scan_type + )), false => panic!("Null Scanner shouldn't be running inside production code."), }, + StartScanError::ManualTriggerError(e) => match e { + MTError::AutomaticScanConflict => ErrorType::Permanent(format!( + "User requested {:?} scan was denied. Automatic mode prevents manual triggers.", + scan_type + )), + MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( + "User requested {:?} scan was denied expecting zero findings.{}", + scan_type, + match hint_opt { + Some(hint) => format!(" {}", hint), + None => "".to_string(), + } + )), + }, }; - if let Some(log_message) = log_message_opt { - match is_externally_triggered { - true => info!(logger, "{}", log_message), - false => debug!(logger, "{}", log_message), - } + match log_message { + ErrorType::Temporary(msg) => match is_externally_triggered { + true => info!(logger, "{}", msg), + false => debug!(logger, "{}", msg), + }, + ErrorType::Permanent(msg) => warning!(logger, "{}", msg), } } - fn timestamp_as_string(timestamp: &SystemTime) -> String { - let offset_date_time = OffsetDateTime::from(*timestamp); + fn timestamp_as_string(timestamp: SystemTime) -> String { + let offset_date_time = OffsetDateTime::from(timestamp); offset_date_time .format( &parse(TIME_FORMATTING_STRING) @@ -999,93 +1350,57 @@ impl BeginScanError { ) .expect("Error while formatting timestamp as string.") } -} -pub struct ScanSchedulers { - pub schedulers: HashMap>, -} + fn scan_already_running_msg( + request_of: ScanType, + cross_scan_cause_opt: Option, + scan_started: SystemTime, + ) -> String { + let (blocking_scanner, request_spec) = if let Some(cross_scan_cause) = cross_scan_cause_opt + { + (cross_scan_cause, format!("the {:?}", request_of)) + } else { + (request_of, "this".to_string()) + }; -impl ScanSchedulers { - pub fn new(scan_intervals: ScanIntervals) -> Self { - let schedulers = HashMap::from_iter([ - ( - ScanType::Payables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.payable_scan_interval, - }) as Box, - ), - ( - ScanType::PendingPayables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.pending_payable_scan_interval, - }), - ), - ( - ScanType::Receivables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.receivable_scan_interval, - }), - ), - ]); - ScanSchedulers { schedulers } + format!( + "{:?} scan was already initiated at {}. Hence, {} scan request will be ignored.", + blocking_scanner, + StartScanError::timestamp_as_string(scan_started), + request_spec + ) } } -pub struct PeriodicalScanScheduler { - pub handle: Box>, - pub interval: Duration, +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum MTError { + AutomaticScanConflict, + UnnecessaryRequest { hint_opt: Option }, } -pub trait ScanScheduler { - fn schedule(&self, ctx: &mut Context); - fn interval(&self) -> Duration { - intentionally_blank!() - } +pub trait RealScannerMarker {} - as_any_ref_in_trait!(); - as_any_mut_in_trait!(); +macro_rules! impl_real_scanner_marker { + ($($t:ty),*) => { + $(impl RealScannerMarker for $t {})* + } } -impl ScanScheduler for PeriodicalScanScheduler { - fn schedule(&self, ctx: &mut Context) { - // the default of the message implies response_skeleton_opt to be None - // because scheduled scans don't respond - let _ = self.handle.notify_later(T::default(), self.interval, ctx); - } - fn interval(&self) -> Duration { - self.interval - } +impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScanner); - as_any_ref_in_trait_impl!(); - as_any_mut_in_trait_impl!(); -} #[cfg(test)] mod tests { use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; use crate::accountant::db_access_objects::pending_payable_dao::{ PendingPayable, PendingPayableDaoError, TransactionHashes, }; - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; - use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, ScanSchedulers, - Scanner, ScannerCommon, Scanners, - }; - use crate::accountant::test_utils::{ - make_custom_payment_thresholds, make_payable_account, make_payables, - make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, - BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, - PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, - PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, - ReceivableDaoMock, ReceivableScannerBuilder, - }; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; + use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; + use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; + use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1095,9 +1410,9 @@ mod tests { use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::mocks::ConfigDaoMock; - use crate::db_config::persistent_configuration::{PersistentConfigError}; + use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, + DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; @@ -1106,9 +1421,8 @@ mod tests { use actix::{Message, System}; use ethereum_types::U64; use masq_lib::logger::Logger; - use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::Regex; + use regex::{Regex}; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; use std::collections::HashSet; @@ -1119,8 +1433,88 @@ mod tests { use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H256}; use web3::Error; + use masq_lib::messages::ScanType; + use masq_lib::ui_gateway::NodeToUiMessage; + use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; + impl Scanners { + pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { + match replacement { + ScannerReplacement::Payable(ReplacementType::Real(scanner)) => { + self.payable = Box::new(scanner) + } + ScannerReplacement::Payable(ReplacementType::Mock(scanner)) => { + self.payable = Box::new(scanner) + } + ScannerReplacement::Payable(ReplacementType::Null) => { + self.payable = Box::new(NullScanner::default()) + } + ScannerReplacement::PendingPayable(ReplacementType::Real(scanner)) => { + self.pending_payable = Box::new(scanner) + } + ScannerReplacement::PendingPayable(ReplacementType::Mock(scanner)) => { + self.pending_payable = Box::new(scanner) + } + ScannerReplacement::PendingPayable(ReplacementType::Null) => { + self.pending_payable = Box::new(NullScanner::default()) + } + ScannerReplacement::Receivable(ReplacementType::Real(scanner)) => { + self.receivable = Box::new(scanner) + } + ScannerReplacement::Receivable(ReplacementType::Mock(scanner)) => { + self.receivable = Box::new(scanner) + } + ScannerReplacement::Receivable(ReplacementType::Null) => { + self.receivable = Box::new(NullScanner::default()) + } + } + } + + pub fn reset_scan_started(&mut self, scan_type: ScanType, value: MarkScanner) { + match scan_type { + ScanType::Payables => { + Self::simple_scanner_timestamp_treatment(&mut *self.payable, value) + } + ScanType::PendingPayables => { + Self::simple_scanner_timestamp_treatment(&mut *self.pending_payable, value) + } + ScanType::Receivables => { + Self::simple_scanner_timestamp_treatment(&mut *self.receivable, value) + } + } + } + + pub fn aware_of_unresolved_pending_payables(&self) -> bool { + self.aware_of_unresolved_pending_payable + } + + pub fn set_aware_of_unresolved_pending_payables(&mut self, value: bool) { + self.aware_of_unresolved_pending_payable = value + } + + fn simple_scanner_timestamp_treatment( + scanner: &mut Scanner, + value: MarkScanner, + ) where + Scanner: self::Scanner + ?Sized, + EndMessage: actix::Message, + { + match value { + MarkScanner::Ended(logger) => scanner.mark_as_ended(logger), + MarkScanner::Started(timestamp) => scanner.mark_as_started(timestamp), + } + } + + pub fn scan_started_at(&self, scan_type: ScanType) -> Option { + match scan_type { + ScanType::Payables => self.payable.scan_started_at(), + ScanType::PendingPayables => self.pending_payable.scan_started_at(), + ScanType::Receivables => self.receivable.scan_started_at(), + } + } + } + #[test] fn scanners_struct_can_be_constructed_with_the_respective_scanners() { let payable_dao_factory = PayableDaoFactoryMock::new() @@ -1179,6 +1573,8 @@ mod tests { &payment_thresholds ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); + assert_eq!(scanners.aware_of_unresolved_pending_payable, false); + assert_eq!(scanners.initial_pending_payable_scan, true); assert_eq!( pending_payable_scanner.when_pending_too_long_sec, when_pending_too_long_sec @@ -1214,54 +1610,159 @@ mod tests { vec![("start_block".to_string(), Some("136890".to_string()))] ); assert_eq!( - Rc::strong_count(&payment_thresholds_rc), - initial_rc_count + 3 + Rc::strong_count(&payment_thresholds_rc), + initial_rc_count + 3 + ); + } + + #[test] + fn new_payable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "new_payable_scanner_can_initiate_a_scan"; + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + let (qualified_payable_accounts, _, all_non_pending_payables) = + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + subject.payable = Box::new(payable_scanner); + + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + true, + ); + + let timestamp = subject.payable.scan_started_at(); + assert_eq!(timestamp, Some(now)); + assert_eq!( + result, + Ok(QualifiedPayablesMessage { + qualified_payables: qualified_payable_accounts.clone(), + consuming_wallet, + response_skeleton_opt: None, + }) + ); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for new payables"), + &format!( + "INFO: {test_name}: Chose {} qualified debts to pay", + qualified_payable_accounts.len() + ), + ]) + } + + #[test] + fn new_payable_scanner_cannot_be_initiated_if_it_is_already_running() { + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + SystemTime::now(), + &PaymentThresholds::default(), + ); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + subject.payable = Box::new(payable_scanner); + let previous_scan_started_at = SystemTime::now(); + let _ = subject.start_new_payable_scan_guarded( + &consuming_wallet, + previous_scan_started_at, + None, + &Logger::new("test"), + true, + ); + + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); + + let is_scan_running = subject.payable.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: previous_scan_started_at + }) ); } #[test] - fn protected_payables_can_be_cast_from_and_back_to_vec_of_payable_accounts_by_payable_scanner() - { - let initial_unprotected = vec![make_payable_account(123), make_payable_account(456)]; - let subject = PayableScannerBuilder::new().build(); + fn new_payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + let (_, unqualified_payable_accounts, _) = + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); + let mut subject = make_dull_subject(); + subject.payable = Box::new( + PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(), + ); - let protected = subject.protect_payables(initial_unprotected.clone()); - let again_unprotected: Vec = subject.expose_payables(protected); + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); - assert_eq!(initial_unprotected, again_unprotected) + let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); + assert_eq!(is_scan_running, false); + assert_eq!(result, Err(StartScanError::NothingToProcess)); } #[test] - fn payable_scanner_can_initiate_a_scan() { + fn retry_payable_scanner_can_initiate_a_scan() { init_test_logging(); - let test_name = "payable_scanner_can_initiate_a_scan"; + let test_name = "retry_payable_scanner_can_initiate_a_scan"; let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (qualified_payable_accounts, _, all_non_pending_payables) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let mut subject = PayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); + subject.payable = Box::new(payable_scanner); - let result = - subject.begin_scan(consuming_wallet.clone(), now, None, &Logger::new(test_name)); + let result = subject.start_retry_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + ); - let timestamp = subject.scan_started_at(); + let timestamp = subject.payable.scan_started_at(); assert_eq!(timestamp, Some(now)); assert_eq!( result, Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test( - qualified_payable_accounts.clone() - ), + qualified_payables: qualified_payable_accounts.clone(), consuming_wallet, response_skeleton_opt: None, }) ); TestLogHandler::new().assert_logs_match_in_order(vec![ - &format!("INFO: {test_name}: Scanning for payables"), + &format!("INFO: {test_name}: Scanning for retry-required payables"), &format!( "INFO: {test_name}: Chose {} qualified debts to pay", qualified_payable_accounts.len() @@ -1270,49 +1771,103 @@ mod tests { } #[test] - fn payable_scanner_throws_error_when_a_scan_is_already_running() { + fn retry_payable_scanner_panics_in_case_scan_is_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let now = SystemTime::now(); - let (_, _, all_non_pending_payables) = make_payables(now, &PaymentThresholds::default()); + let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + SystemTime::now(), + &PaymentThresholds::default(), + ); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let mut subject = PayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let _result = subject.begin_scan(consuming_wallet.clone(), now, None, &Logger::new("test")); - - let run_again_result = subject.begin_scan( - consuming_wallet, + subject.payable = Box::new(payable_scanner); + let before = SystemTime::now(); + let _ = subject.start_retry_payable_scan_guarded( + &consuming_wallet, 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)) + let caught_panic = catch_unwind(AssertUnwindSafe(|| { + let _: Result = subject + .start_retry_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + ); + })) + .unwrap_err(); + + let after = SystemTime::now(); + let panic_msg = caught_panic.downcast_ref::().unwrap(); + let expected_needle_1 = "internal error: entered unreachable code: Guard for pending \ + payables should've prevented running the tandem of scanners if the payable scanner was \ + still running. It started "; + assert!( + panic_msg.contains(expected_needle_1), + "We looked for {} but the actual string doesn't contain it: {}", + expected_needle_1, + panic_msg + ); + let expected_needle_2 = "and is still running at "; + assert!( + panic_msg.contains(expected_needle_2), + "We looked for {} but the actual string doesn't contain it: {}", + expected_needle_2, + panic_msg + ); + check_timestamps_in_panic_for_already_running_retry_payable_scanner( + &panic_msg, before, after, + ) + } + + fn check_timestamps_in_panic_for_already_running_retry_payable_scanner( + panic_msg: &str, + before: SystemTime, + after: SystemTime, + ) { + let system_times = parse_system_time_from_str(panic_msg); + let first_actual = system_times[0]; + let second_actual = system_times[1]; + + assert!( + before <= first_actual + && first_actual <= second_actual + && second_actual <= after, + "We expected this relationship before({:?}) <= first_actual({:?}) <= second_actual({:?}) \ + <= after({:?}), but it does not hold true", + before, + first_actual, + second_actual, + after ); } #[test] - fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + #[should_panic(expected = "Complete me with GH-605")] + fn retry_payable_scanner_panics_in_case_no_qualified_payable_is_found() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (_, unqualified_payable_accounts, _) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let result = subject.begin_scan(consuming_wallet, now, None, &Logger::new("test")); - - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(is_scan_running, false); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + let _ = Scanners::start_correct_payable_scanner::( + &mut subject, + &consuming_wallet, + now, + None, + &Logger::new("test"), + ); } #[test] @@ -1361,7 +1916,7 @@ mod tests { .mark_pending_payables_rowids_params(&mark_pending_payables_params_arc) .mark_pending_payables_rowids_result(Ok(())) .mark_pending_payables_rowids_result(Ok(())); - let mut subject = PayableScannerBuilder::new() + let mut payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .pending_payable_dao(pending_payable_dao) .build(); @@ -1374,13 +1929,26 @@ mod tests { ]), response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; - let message_opt = subject.finish_scan(sent_payable, &logger); + let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(message_opt, None); + let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; + assert_eq!( + payable_scan_result, + PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable + } + ); assert_eq!(is_scan_running, false); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, true); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -1748,7 +2316,7 @@ mod tests { }) .delete_fingerprints_params(&delete_fingerprints_params_arc) .delete_fingerprints_result(Ok(())); - let mut subject = PayableScannerBuilder::new() + let payable_scanner = PayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); let logger = Logger::new(test_name); @@ -1759,12 +2327,25 @@ mod tests { }), response_skeleton_opt: None, }; + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; - let result = subject.finish_scan(sent_payable, &logger); + let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; System::current().stop(); system.run(); - assert_eq!(result, None); + assert_eq!( + payable_scan_result, + PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::Failure + } + ); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -1798,10 +2379,17 @@ mod tests { )), response_skeleton_opt: None, }; - let mut subject = PayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; - subject.finish_scan(sent_payable, &Logger::new(test_name)); + subject.finish_payable_scan(sent_payable, &Logger::new(test_name)); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" @@ -2103,11 +2691,11 @@ mod tests { let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); let debt = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); - let time = to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; + let time = to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; let unqualified_payable_account = vec![PayableAccount { wallet: make_wallet("wallet0"), balance_wei: debt, - last_paid_timestamp: from_time_t(time), + last_paid_timestamp: from_unix_timestamp(time), pending_payable_opt: None, }]; let subject = PayableScannerBuilder::new() @@ -2136,7 +2724,7 @@ mod tests { let qualified_payable = PayableAccount { wallet: make_wallet("wallet0"), balance_wei: debt, - last_paid_timestamp: from_time_t(time), + last_paid_timestamp: from_unix_timestamp(time), pending_payable_opt: None, }; let subject = PayableScannerBuilder::new() @@ -2168,8 +2756,8 @@ mod tests { let unqualified_payable_account = vec![PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, ), pending_payable_opt: None, }]; @@ -2194,7 +2782,7 @@ mod tests { let now = SystemTime::now(); let payable_fingerprint_1 = PendingPayableFingerprint { rowid: 555, - timestamp: from_time_t(210_000_000), + timestamp: from_unix_timestamp(210_000_000), hash: make_tx_hash(45678), attempt: 1, amount: 4444, @@ -2202,7 +2790,7 @@ mod tests { }; let payable_fingerprint_2 = PendingPayableFingerprint { rowid: 550, - timestamp: from_time_t(210_000_100), + timestamp: from_unix_timestamp(210_000_100), hash: make_tx_hash(112233), attempt: 1, amount: 7999, @@ -2211,24 +2799,31 @@ mod tests { let fingerprints = vec![payable_fingerprint_1, payable_fingerprint_2]; let pending_payable_dao = PendingPayableDaoMock::new() .return_all_errorless_fingerprints_result(fingerprints.clone()); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); - - let result = pending_payable_scanner.begin_scan( - consuming_wallet, + // Important + subject.aware_of_unresolved_pending_payable = true; + subject.pending_payable = Box::new(pending_payable_scanner); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.payable = Box::new(payable_scanner); + + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, now, None, &Logger::new(test_name), + true, ); let no_of_pending_payables = fingerprints.len(); - let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, Ok(RequestTransactionReceipts { - pending_payable: fingerprints, + pending_payable_fingerprints: fingerprints, response_skeleton_opt: None }) ); @@ -2241,29 +2836,154 @@ mod tests { } #[test] - fn pending_payable_scanner_throws_error_in_case_scan_is_already_running() { + fn pending_payable_scanner_cannot_be_initiated_if_it_itself_is_already_running() { let now = SystemTime::now(); let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(vec![PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: make_tx_hash(1), - attempt: 1, - amount: 1_000_000, - process_error: None, - }]); - let mut subject = PendingPayableScannerBuilder::new() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); + // Important + subject.aware_of_unresolved_pending_payable = true; + subject.pending_payable = Box::new(pending_payable_scanner); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); - let _ = subject.begin_scan(consuming_wallet.clone(), now, None, &logger); + let _ = + subject.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger, true); - let result = subject.begin_scan(consuming_wallet, SystemTime::now(), None, &logger); + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + true, + ); - let is_scan_running = subject.scan_started_at().is_some(); + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); - assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: now + }) + ); + } + + #[test] + fn pending_payable_scanner_cannot_be_initiated_if_payable_scanner_is_still_running() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + // Important + subject.aware_of_unresolved_pending_payable = true; + subject.pending_payable = Box::new(pending_payable_scanner); + subject.payable = Box::new(payable_scanner); + let logger = Logger::new("test"); + let previous_scan_started_at = SystemTime::now(); + subject.payable.mark_as_started(previous_scan_started_at); + + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + true, + ); + + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); + assert_eq!(is_scan_running, false); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: Some(ScanType::Payables), + started_at: previous_scan_started_at + }) + ); + } + + #[test] + fn both_payable_scanners_cannot_be_detected_in_progress_at_the_same_time() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.pending_payable = Box::new(pending_payable_scanner); + subject.payable = Box::new(payable_scanner); + let timestamp_pending_payable_start = SystemTime::now() + .checked_sub(Duration::from_millis(12)) + .unwrap(); + let timestamp_payable_scanner_start = SystemTime::now(); + subject.aware_of_unresolved_pending_payable = true; + subject + .pending_payable + .mark_as_started(timestamp_pending_payable_start); + subject + .payable + .mark_as_started(timestamp_payable_scanner_start); + + let caught_panic = catch_unwind(AssertUnwindSafe(|| { + let _ = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); + })) + .unwrap_err(); + + let panic_msg = caught_panic.downcast_ref::().unwrap(); + let expected_msg_fragment_1 = "internal error: entered unreachable code: Any payable-\ + related scanners should never be allowed to run in parallel. Scan for pending payables \ + started at: "; + assert!( + panic_msg.contains(expected_msg_fragment_1), + "This fragment '{}' wasn't found in \ + '{}'", + expected_msg_fragment_1, + panic_msg + ); + let expected_msg_fragment_2 = ", scan for payables started at: "; + assert!( + panic_msg.contains(expected_msg_fragment_2), + "This fragment '{}' wasn't found in \ + '{}'", + expected_msg_fragment_2, + panic_msg + ); + assert_timestamps_from_str( + panic_msg, + vec![ + timestamp_pending_payable_start, + timestamp_payable_scanner_start, + ], + ) + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Automatic pending payable \ + scan should never start if there are no pending payables to process." + )] + fn pending_payable_scanner_bumps_into_zero_pending_payable_awareness_in_the_automatic_mode() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + subject.pending_payable = Box::new(pending_payable_scanner); + subject.aware_of_unresolved_pending_payable = false; + + let _ = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); } #[test] @@ -2277,13 +2997,24 @@ mod tests { .build(); let result = - pending_payable_scanner.begin_scan(consuming_wallet, now, None, &Logger::new("test")); + pending_payable_scanner.start_scan(&consuming_wallet, now, None, &Logger::new("test")); let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + assert_eq!(result, Err(StartScanError::NothingToProcess)); assert_eq!(is_scan_running, false); } + #[test] + fn check_general_conditions_for_pending_payable_scan_if_it_is_initial_pending_payable_scan() { + let mut subject = make_dull_subject(); + subject.initial_pending_payable_scan = true; + + let result = subject.check_general_conditions_for_pending_payable_scan(false, true); + + assert_eq!(result, Ok(())); + assert_eq!(subject.initial_pending_payable_scan, true); + } + fn assert_interpreting_none_status_for_pending_payable( test_name: &str, when_pending_too_long_sec: u64, @@ -2682,7 +3413,7 @@ mod tests { let rowid_2 = 5; let pending_payable_fingerprint_1 = PendingPayableFingerprint { rowid: rowid_1, - timestamp: from_time_t(199_000_000), + timestamp: from_unix_timestamp(199_000_000), hash: make_tx_hash(0x123), attempt: 1, amount: 4567, @@ -2690,7 +3421,7 @@ mod tests { }; let pending_payable_fingerprint_2 = PendingPayableFingerprint { rowid: rowid_2, - timestamp: from_time_t(200_000_000), + timestamp: from_unix_timestamp(200_000_000), hash: make_tx_hash(0x567), attempt: 1, amount: 5555, @@ -2759,7 +3490,7 @@ mod tests { let test_name = "total_paid_payable_rises_with_each_bill_paid"; let fingerprint_1 = PendingPayableFingerprint { rowid: 5, - timestamp: from_time_t(189_999_888), + timestamp: from_unix_timestamp(189_999_888), hash: make_tx_hash(56789), attempt: 1, amount: 5478, @@ -2767,7 +3498,7 @@ mod tests { }; let fingerprint_2 = PendingPayableFingerprint { rowid: 6, - timestamp: from_time_t(200_000_011), + timestamp: from_unix_timestamp(200_000_011), hash: make_tx_hash(33333), attempt: 1, amount: 6543, @@ -2802,7 +3533,7 @@ mod tests { .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); let pending_payable_dao = PendingPayableDaoMock::new().delete_fingerprints_result(Ok(())); - let mut subject = PendingPayableScannerBuilder::new() + let mut pending_payable_scanner = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) .pending_payable_dao(pending_payable_dao) .build(); @@ -2816,7 +3547,7 @@ mod tests { }; let fingerprint_1 = PendingPayableFingerprint { rowid: 5, - timestamp: from_time_t(200_000_000), + timestamp: from_unix_timestamp(200_000_000), hash: transaction_hash_1, attempt: 2, amount: 444, @@ -2832,7 +3563,7 @@ mod tests { }; let fingerprint_2 = PendingPayableFingerprint { rowid: 10, - timestamp: from_time_t(199_780_000), + timestamp: from_unix_timestamp(199_780_000), hash: transaction_hash_2, attempt: 15, amount: 1212, @@ -2851,17 +3582,22 @@ mod tests { ], response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(message_opt, None); + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ); assert_eq!( *transactions_confirmed_params, vec![vec![fingerprint_1, fingerprint_2]] ); - assert_eq!(subject.scan_started_at(), None); + assert_eq!(subject.scan_started_at(ScanType::PendingPayables), None); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!( "INFO: {}: Transactions {:?}, {:?} completed their confirmation process succeeding", @@ -2876,21 +3612,26 @@ mod tests { init_test_logging(); let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; - let mut subject = PendingPayableScannerBuilder::new().build(); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); let msg = ReportTransactionReceipts { fingerprints_with_receipts: vec![], response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(message_opt, None); + let is_scan_running = subject.scan_started_at(ScanType::PendingPayables).is_some(); + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ); assert_eq!(is_scan_running, false); let tlh = TestLogHandler::new(); tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: No transaction receipts found." + "WARN: {test_name}: No transaction receipts found." )); tlh.exists_log_matching(&format!( "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." @@ -2906,18 +3647,21 @@ mod tests { .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let earning_wallet = make_wallet("earning"); - let mut receivable_scanner = ReceivableScannerBuilder::new() + let mut subject = make_dull_subject(); + let receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .build(); + subject.receivable = Box::new(receivable_scanner); - let result = receivable_scanner.begin_scan( - earning_wallet.clone(), + let result = subject.start_receivable_scan_guarded( + &earning_wallet, now, None, &Logger::new(test_name), + true, ); - let is_scan_running = receivable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.receivable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, @@ -2938,22 +3682,36 @@ mod tests { .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let earning_wallet = make_wallet("earning"); - let mut receivable_scanner = ReceivableScannerBuilder::new() + let mut subject = make_dull_subject(); + let receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .build(); - let _ = - receivable_scanner.begin_scan(earning_wallet.clone(), now, None, &Logger::new("test")); + subject.receivable = Box::new(receivable_scanner); + let _ = subject.start_receivable_scan_guarded( + &earning_wallet, + now, + None, + &Logger::new("test"), + true, + ); - let result = receivable_scanner.begin_scan( - earning_wallet, + let result = subject.start_receivable_scan_guarded( + &earning_wallet, SystemTime::now(), None, &Logger::new("test"), + true, ); - let is_scan_running = receivable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.receivable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); - assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: now + }) + ); } #[test] @@ -2986,7 +3744,7 @@ mod tests { let logger = Logger::new("DELINQUENCY_TEST"); let now = SystemTime::now(); - let result = receivable_scanner.begin_scan(earning_wallet.clone(), now, None, &logger); + let result = receivable_scanner.start_scan(&earning_wallet, now, None, &logger); assert_eq!( result, @@ -3040,7 +3798,7 @@ mod tests { .start_block_result(Ok(None)) .set_start_block_params(&set_start_block_params_arc) .set_start_block_result(Ok(())); - let mut subject = ReceivableScannerBuilder::new() + let receivable_scanner = ReceivableScannerBuilder::new() .persistent_configuration(persistent_config) .build(); let msg = ReceivedPayments { @@ -3049,10 +3807,12 @@ mod tests { response_skeleton_opt: None, transactions: vec![], }; + let mut subject = make_dull_subject(); + subject.receivable = Box::new(receivable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_receivable_scan(msg, &Logger::new(test_name)); - assert_eq!(message_opt, None); + assert_eq!(ui_msg_opt, None); let set_start_block_params = set_start_block_params_arc.lock().unwrap(); assert_eq!(*set_start_block_params, vec![Some(4321)]); TestLogHandler::new().exists_log_containing(&format!( @@ -3084,7 +3844,6 @@ mod tests { response_skeleton_opt: None, transactions: vec![], }; - // Not necessary, rather for preciseness subject.mark_as_started(SystemTime::now()); @@ -3112,13 +3871,15 @@ mod tests { let receivable_dao = ReceivableDaoMock::new() .more_money_received_params(&more_money_received_params_arc) .more_money_received_result(transaction); - let mut subject = ReceivableScannerBuilder::new() + let mut receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .persistent_configuration(persistent_config) .build(); - let mut financial_statistics = subject.financial_statistics.borrow().clone(); + let mut financial_statistics = receivable_scanner.financial_statistics.borrow().clone(); financial_statistics.total_paid_receivable_wei += 2_222_123_123; - subject.financial_statistics.replace(financial_statistics); + receivable_scanner + .financial_statistics + .replace(financial_statistics); let receivables = vec![ BlockchainTransaction { block_number: 4578910, @@ -3137,16 +3898,23 @@ mod tests { response_skeleton_opt: None, transactions: receivables.clone(), }; - subject.mark_as_started(SystemTime::now()); + receivable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.receivable = Box::new(receivable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_receivable_scan(msg, &Logger::new(test_name)); - let total_paid_receivable = subject + let scanner_after = subject + .receivable + .as_any() + .downcast_ref::() + .unwrap(); + let total_paid_receivable = scanner_after .financial_statistics .borrow() .total_paid_receivable_wei; - assert_eq!(message_opt, None); - assert_eq!(subject.scan_started_at(), None); + assert_eq!(ui_msg_opt, None); + assert_eq!(scanner_after.scan_started_at(), None); assert_eq!(total_paid_receivable, 2_222_123_123 + 45_780 + 3_333_345); let more_money_received_params = more_money_received_params_arc.lock().unwrap(); assert_eq!(*more_money_received_params, vec![(now, receivables)]); @@ -3277,7 +4045,7 @@ mod tests { let test_name = "signal_scanner_completion_and_log_if_timestamp_is_correct"; let logger = Logger::new(test_name); let mut subject = ScannerCommon::new(Rc::new(make_custom_payment_thresholds())); - let start = from_time_t(1_000_000_000); + let start = from_unix_timestamp(1_000_000_000); let end = start.checked_add(Duration::from_millis(145)).unwrap(); subject.initiated_at_opt = Some(start); @@ -3303,8 +4071,8 @@ mod tests { )); } - fn assert_elapsed_time_in_mark_as_ended( - subject: &mut dyn Scanner, + fn assert_elapsed_time_in_mark_as_ended( + subject: &mut dyn Scanner, scanner_name: &str, test_name: &str, logger: &Logger, @@ -3343,21 +4111,21 @@ mod tests { let logger = Logger::new(test_name); let log_handler = TestLogHandler::new(); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PayableScannerBuilder::new().build(), "Payables", test_name, &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PendingPayableScannerBuilder::new().build(), "PendingPayables", test_name, &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::>( &mut ReceivableScannerBuilder::new().build(), "Receivables", test_name, @@ -3367,38 +4135,227 @@ mod tests { } #[test] - fn scan_schedulers_can_be_properly_initialized() { - let scan_intervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(240), - pending_payable_scan_interval: Duration::from_secs(300), - receivable_scan_interval: Duration::from_secs(360), - }; + fn scan_already_running_msg_displays_correctly_if_blocked_by_requested_scan() { + test_scan_already_running_msg( + ScanType::PendingPayables, + None, + "PendingPayables scan was already initiated at", + ". Hence, this scan request will be ignored.", + ) + } + + #[test] + fn scan_already_running_msg_displays_correctly_if_blocked_by_other_scan_than_directly_requested( + ) { + test_scan_already_running_msg( + ScanType::PendingPayables, + Some(ScanType::Payables), + "Payables scan was already initiated at", + ". Hence, the PendingPayables scan request will be ignored.", + ) + } - let result = ScanSchedulers::new(scan_intervals); + fn test_scan_already_running_msg( + requested_scan: ScanType, + cross_scan_blocking_cause_opt: Option, + expected_leading_msg_fragment: &str, + expected_trailing_msg_fragment: &str, + ) { + let some_time = SystemTime::now(); - assert_eq!( - result - .schedulers - .get(&ScanType::Payables) - .unwrap() - .interval(), - scan_intervals.payable_scan_interval + let result = StartScanError::scan_already_running_msg( + requested_scan, + cross_scan_blocking_cause_opt, + some_time, ); - assert_eq!( + + assert!( + result.contains(expected_leading_msg_fragment), + "We expected {} but the msg is: {}", + expected_leading_msg_fragment, result - .schedulers - .get(&ScanType::PendingPayables) - .unwrap() - .interval(), - scan_intervals.pending_payable_scan_interval ); - assert_eq!( + assert!( + result.contains(expected_trailing_msg_fragment), + "We expected {} but the msg is: {}", + expected_trailing_msg_fragment, result - .schedulers - .get(&ScanType::Receivables) - .unwrap() - .interval(), - scan_intervals.receivable_scan_interval ); + assert_timestamps_from_str(&result, vec![some_time]); + } + + #[test] + fn acknowledge_scan_error_works() { + fn scan_error(scan_type: ScanType) -> ScanError { + ScanError { + scan_type, + response_skeleton_opt: None, + msg: "bluh".to_string(), + } + } + + init_test_logging(); + let test_name = "acknowledge_scan_error_works"; + let inputs: Vec<( + ScanType, + Box, + Box Option>, + )> = vec![ + ( + ScanType::Payables, + Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.payable.scan_started_at()), + ), + ( + ScanType::PendingPayables, + Box::new(|subject| subject.pending_payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.pending_payable.scan_started_at()), + ), + ( + ScanType::Receivables, + Box::new(|subject| subject.receivable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.receivable.scan_started_at()), + ), + ]; + let mut subject = make_dull_subject(); + subject.payable = Box::new(PayableScannerBuilder::new().build()); + subject.pending_payable = Box::new(PendingPayableScannerBuilder::new().build()); + subject.receivable = Box::new(ReceivableScannerBuilder::new().build()); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + + inputs + .into_iter() + .for_each(|(scan_type, set_started, get_started_at)| { + set_started(&mut subject); + let started_at_before = get_started_at(&subject); + + subject.acknowledge_scan_error(&scan_error(scan_type), &logger); + + let started_at_after = get_started_at(&subject); + assert!( + started_at_before.is_some(), + "Should've been started for {:?}", + scan_type + ); + assert_eq!( + started_at_after, None, + "Should've been unset for {:?}", + scan_type + ); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: The {:?} scan ended in", + scan_type + )); + }) + } + + #[test] + fn log_error_works_fine() { + init_test_logging(); + let test_name = "log_error_works_fine"; + let now = SystemTime::now(); + let input: Vec<(StartScanError, Box String>, &str, &str)> = vec![ + ( + StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: now, + }, + Box::new(|sev| { + format!( + "{sev}: {test_name}: Payables scan was already initiated at {}", + StartScanError::timestamp_as_string(now) + ) + }), + "INFO", + "DEBUG", + ), + ( + StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") + }), + "WARN", + "WARN", + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + hint_opt: Some("Wise words".to_string()), + }), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings. Wise words") + }), + "INFO", + "DEBUG", + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: None }), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings.") + }), + "INFO", + "DEBUG", + ), + ( + StartScanError::CalledFromNullScanner, + Box::new(|sev| { + format!( + "{sev}: {test_name}: Called from NullScanner, not the Payables scanner." + ) + }), + "WARN", + "WARN", + ), + ( + StartScanError::NoConsumingWalletFound, + Box::new(|sev| { + format!("{sev}: {test_name}: Cannot initiate Payables scan because no consuming wallet was found.") + }), + "WARN", + "WARN", + ), + ( + StartScanError::NothingToProcess, + Box::new(|sev| { + format!( + "{sev}: {test_name}: There was nothing to process during Payables scan." + ) + }), + "INFO", + "DEBUG", + ), + ]; + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + + input.into_iter().for_each( + |( + err, + form_expected_log_msg, + log_severity_for_externally_triggered_scans, + log_severity_for_automatic_scans, + )| { + let test_log_error_by_mode = + |is_externally_triggered: bool, expected_severity: &str| { + err.log_error(&logger, ScanType::Payables, is_externally_triggered); + let expected_log_msg = form_expected_log_msg(expected_severity); + test_log_handler.exists_log_containing(&expected_log_msg); + }; + + test_log_error_by_mode(true, log_severity_for_externally_triggered_scans); + + test_log_error_by_mode(false, log_severity_for_automatic_scans); + }, + ); + } + + fn make_dull_subject() -> Scanners { + Scanners { + payable: Box::new(NullScanner::new()), + aware_of_unresolved_pending_payable: false, + initial_pending_payable_scan: false, + pending_payable: Box::new(NullScanner::new()), + receivable: Box::new(NullScanner::new()), + } } } diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs similarity index 94% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs rename to node/src/accountant/scanners/payable_scanner_extension/agent_null.rs index e95673002..5f9811204 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; @@ -77,8 +77,8 @@ impl Default for BlockchainAgentNull { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_null::BlockchainAgentNull; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner_extension::agent_null::BlockchainAgentNull; + use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs similarity index 93% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs rename to node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs index 725e14f00..8acf40ef8 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use masq_lib::blockchains::chains::Chain; @@ -64,10 +64,10 @@ impl BlockchainAgentWeb3 { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::{ + use crate::accountant::scanners::payable_scanner_extension::agent_web3::{ BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, }; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::make_wallet; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs b/node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs similarity index 100% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs rename to node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs similarity index 62% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs rename to node/src/accountant/scanners/payable_scanner_extension/mod.rs index 257c88fde..649bc820f 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -7,22 +7,25 @@ pub mod msgs; pub mod test_utils; use crate::accountant::payment_adjuster::Adjustment; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::Scanner; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; +use crate::accountant::scanners::{Scanner, StartableScanner}; +use crate::accountant::{ScanForNewPayables, ScanForRetryPayables, SentPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use actix::Message; use itertools::Either; use masq_lib::logger::Logger; -pub trait MultistagePayableScanner: - Scanner + SolvencySensitivePaymentInstructor -where - BeginMessage: Message, - EndMessage: Message, +pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: + StartableScanner + + StartableScanner + + SolvencySensitivePaymentInstructor + + Scanner { } -pub trait SolvencySensitivePaymentInstructor { +pub(in crate::accountant::scanners) trait SolvencySensitivePaymentInstructor { fn try_skipping_payment_adjustment( &self, msg: BlockchainAgentWithContextMessage, @@ -55,7 +58,7 @@ impl PreparedAdjustment { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; + use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; impl Clone for PreparedAdjustment { fn clone(&self) -> Self { diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs similarity index 66% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs rename to node/src/accountant/scanners/payable_scanner_extension/msgs.rs index 41a1b3940..599f17390 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs @@ -1,27 +1,27 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; use crate::sub_lib::wallet::Wallet; use actix::Message; -use masq_lib::type_obfuscation::Obfuscated; use std::fmt::Debug; #[derive(Debug, Message, PartialEq, Eq, Clone)] pub struct QualifiedPayablesMessage { - pub protected_qualified_payables: Obfuscated, + pub qualified_payables: Vec, pub consuming_wallet: Wallet, pub response_skeleton_opt: Option, } impl QualifiedPayablesMessage { pub(in crate::accountant) fn new( - protected_qualified_payables: Obfuscated, + qualified_payables: Vec, consuming_wallet: Wallet, response_skeleton_opt: Option, ) -> Self { Self { - protected_qualified_payables, + qualified_payables, consuming_wallet, response_skeleton_opt, } @@ -36,20 +36,20 @@ impl SkeletonOptHolder for QualifiedPayablesMessage { #[derive(Message)] pub struct BlockchainAgentWithContextMessage { - pub protected_qualified_payables: Obfuscated, + pub qualified_payables: Vec, pub agent: Box, pub response_skeleton_opt: Option, } impl BlockchainAgentWithContextMessage { pub fn new( - qualified_payables: Obfuscated, - blockchain_agent: Box, + qualified_payables: Vec, + agent: Box, response_skeleton_opt: Option, ) -> Self { Self { - protected_qualified_payables: qualified_payables, - agent: blockchain_agent, + qualified_payables, + agent, response_skeleton_opt, } } @@ -58,8 +58,8 @@ impl BlockchainAgentWithContextMessage { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; impl Clone for BlockchainAgentWithContextMessage { fn clone(&self) -> Self { @@ -67,7 +67,7 @@ mod tests { let cloned_agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(original_agent_id); Self { - protected_qualified_payables: self.protected_qualified_payables.clone(), + qualified_payables: self.qualified_payables.clone(), agent: Box::new(cloned_agent), response_skeleton_opt: self.response_skeleton_opt, } diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs similarity index 96% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs rename to node/src/accountant/scanners/payable_scanner_extension/test_utils.rs index d3ab97284..def16f20e 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs @@ -2,7 +2,7 @@ #![cfg(test)] -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs new file mode 100644 index 000000000..74e102aff --- /dev/null +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -0,0 +1,941 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::scanners::StartScanError; +use crate::accountant::{ + Accountant, ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, + ScanForRetryPayables, +}; +use crate::sub_lib::accountant::ScanIntervals; +use crate::sub_lib::utils::{ + NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal, +}; +use actix::{Actor, Context, Handler}; +use masq_lib::logger::Logger; +use masq_lib::messages::ScanType; +use std::fmt::{Debug, Display, Formatter}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub struct ScanSchedulers { + pub payable: PayableScanScheduler, + pub pending_payable: SimplePeriodicalScanScheduler, + pub receivable: SimplePeriodicalScanScheduler, + pub reschedule_on_error_resolver: Box, + pub automatic_scans_enabled: bool, +} + +impl ScanSchedulers { + pub fn new(scan_intervals: ScanIntervals, automatic_scans_enabled: bool) -> Self { + Self { + payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), + pending_payable: SimplePeriodicalScanScheduler::new( + scan_intervals.pending_payable_scan_interval, + ), + receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), + reschedule_on_error_resolver: Box::new(RescheduleScanOnErrorResolverReal::default()), + automatic_scans_enabled, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PayableScanSchedulerError { + ScanForNewPayableAlreadyScheduled, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ScanRescheduleAfterEarlyStop { + Schedule(ScanType), + DoNotSchedule, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum PayableSequenceScanner { + NewPayables, + RetryPayables, + PendingPayables { initial_pending_payable_scan: bool }, +} + +impl Display for PayableSequenceScanner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PayableSequenceScanner::NewPayables => write!(f, "NewPayables"), + PayableSequenceScanner::RetryPayables => write!(f, "RetryPayables"), + PayableSequenceScanner::PendingPayables { .. } => write!(f, "PendingPayables"), + } + } +} + +impl From for ScanType { + fn from(scanner: PayableSequenceScanner) -> Self { + match scanner { + PayableSequenceScanner::NewPayables => ScanType::Payables, + PayableSequenceScanner::RetryPayables => ScanType::Payables, + PayableSequenceScanner::PendingPayables { .. } => ScanType::PendingPayables, + } + } +} + +pub struct PayableScanScheduler { + pub new_payable_notify_later: Box>, + pub dyn_interval_computer: Box, + pub inner: Arc>, + pub new_payable_interval: Duration, + pub new_payable_notify: Box>, + pub retry_payable_notify: Box>, +} + +impl PayableScanScheduler { + fn new(new_payable_interval: Duration) -> Self { + Self { + new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), + dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::default()), + inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), + new_payable_interval, + new_payable_notify: Box::new(NotifyHandleReal::default()), + retry_payable_notify: Box::new(NotifyHandleReal::default()), + } + } + + pub fn schedule_new_payable_scan(&self, ctx: &mut Context, logger: &Logger) { + let inner = self.inner.lock().expect("couldn't acquire inner"); + let last_new_payable_scan_timestamp = inner.last_new_payable_scan_timestamp; + let new_payable_interval = self.new_payable_interval; + let now = SystemTime::now(); + if let Some(interval) = self.dyn_interval_computer.compute_interval( + now, + last_new_payable_scan_timestamp, + new_payable_interval, + ) { + debug!( + logger, + "Scheduling a new-payable scan in {}ms", + interval.as_millis() + ); + + let _ = self.new_payable_notify_later.notify_later( + ScanForNewPayables { + response_skeleton_opt: None, + }, + interval, + ctx, + ); + } else { + debug!(logger, "Scheduling a new-payable scan asap"); + + self.new_payable_notify.notify( + ScanForNewPayables { + response_skeleton_opt: None, + }, + ctx, + ); + } + } + + // This message ships into the Accountant's mailbox with no delay. + // Can also be triggered by command, following up after the PendingPayableScanner + // that requests it. That's why the response skeleton is possible to be used. + pub fn schedule_retry_payable_scan( + &self, + ctx: &mut Context, + response_skeleton_opt: Option, + logger: &Logger, + ) { + debug!(logger, "Scheduling a retry-payable scan asap"); + + self.retry_payable_notify.notify( + ScanForRetryPayables { + response_skeleton_opt, + }, + ctx, + ) + } +} + +pub struct PayableScanSchedulerInner { + pub last_new_payable_scan_timestamp: SystemTime, +} + +impl Default for PayableScanSchedulerInner { + fn default() -> Self { + Self { + last_new_payable_scan_timestamp: UNIX_EPOCH, + } + } +} + +pub trait NewPayableScanDynIntervalComputer { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option; +} + +#[derive(Default)] +pub struct NewPayableScanDynIntervalComputerReal {} + +impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option { + let elapsed = now + .duration_since(last_new_payable_scan_timestamp) + .unwrap_or_else(|_| { + panic!( + "Unexpected now ({:?}) earlier than past timestamp ({:?})", + now, last_new_payable_scan_timestamp + ) + }); + if elapsed >= interval { + None + } else { + Some(interval - elapsed) + } + } +} + +pub struct SimplePeriodicalScanScheduler { + pub handle: Box>, + pub interval: Duration, +} + +impl SimplePeriodicalScanScheduler +where + Message: actix::Message + Default + Debug + Send + 'static, + Accountant: Actor + Handler, +{ + fn new(interval: Duration) -> Self { + Self { + handle: Box::new(NotifyLaterHandleReal::default()), + interval, + } + } + pub fn schedule(&self, ctx: &mut Context, logger: &Logger) { + // The default of the message implies response_skeleton_opt to be None because scheduled + // scans don't respond + let msg = Message::default(); + + debug!( + logger, + "Scheduling a scan via {:?} in {}ms", + msg, + self.interval.as_millis() + ); + + let _ = self.handle.notify_later(msg, self.interval, ctx); + } +} + +// Scanners that take part in a scan sequence composed of different scanners must handle +// StartScanErrors delicately to maintain the continuity and periodicity of this process. Where +// possible, either the same, some other, but traditional, or even a totally unrelated scan chosen +// just in the event of emergency, may be scheduled. The intention is to prevent a full panic while +// ensuring no harmful, toxic issues are left behind for the future scans. Following that philosophy, +// panic is justified only if the error was thought to be impossible by design and contextual +// things but still happened. +pub trait RescheduleScanOnErrorResolver { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop; +} + +#[derive(Default)] +pub struct RescheduleScanOnErrorResolverReal {} + +impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop { + let reschedule_hint = match scanner { + PayableSequenceScanner::NewPayables => { + Self::resolve_new_payables(error, is_externally_triggered) + } + PayableSequenceScanner::RetryPayables => { + Self::resolve_retry_payables(error, is_externally_triggered) + } + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan, + } => Self::resolve_pending_payables( + error, + initial_pending_payable_scan, + is_externally_triggered, + ), + }; + + Self::log_rescheduling(scanner, is_externally_triggered, logger, &reschedule_hint); + + reschedule_hint + } +} + +impl RescheduleScanOnErrorResolverReal { + fn resolve_new_payables( + err: &StartScanError, + is_externally_triggered: bool, + ) -> ScanRescheduleAfterEarlyStop { + if is_externally_triggered { + ScanRescheduleAfterEarlyStop::DoNotSchedule + } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { + unreachable!( + "an automatic scan of NewPayableScanner should never interfere with itself {:?}", + err + ) + } else { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + } + } + + // Paradoxical at first, but this scanner is meant to be shielded by the scanner right before + // it. That should ensure this scanner will not be requested if there was already something + // fishy. We can impose strictness. + fn resolve_retry_payables( + err: &StartScanError, + is_externally_triggered: bool, + ) -> ScanRescheduleAfterEarlyStop { + if is_externally_triggered { + ScanRescheduleAfterEarlyStop::DoNotSchedule + } else { + unreachable!( + "{:?} should be impossible with RetryPayableScanner in automatic mode", + err + ) + } + } + + fn resolve_pending_payables( + err: &StartScanError, + initial_pending_payable_scan: bool, + is_externally_triggered: bool, + ) -> ScanRescheduleAfterEarlyStop { + if is_externally_triggered { + ScanRescheduleAfterEarlyStop::DoNotSchedule + } else if err == &StartScanError::NothingToProcess { + if initial_pending_payable_scan { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + } else { + unreachable!( + "the automatic pending payable scan should always be requested only in need, \ + which contradicts the current StartScanError::NothingToProcess" + ) + } + } else if err == &StartScanError::NoConsumingWalletFound { + if initial_pending_payable_scan { + // Cannot deduce there are strayed pending payables from the previous Node's run + // (StartScanError::NoConsumingWalletFound is thrown before + // StartScanError::NothingToProcess can be evaluated); but may be cautious and + // prevent starting the NewPayableScanner. Repeating this scan endlessly may alarm + // the user. + // TODO Correctly, a check-point during the bootstrap that wouldn't allow to come + // this far should be the solution. Part of the issue mentioned in GH-799 + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) + } else { + unreachable!( + "PendingPayableScanner called later than the initial attempt, but \ + the consuming wallet is still missing; this should not be possible" + ) + } + } else { + unreachable!( + "{:?} should be impossible with PendingPayableScanner in automatic mode", + err + ) + } + } + + fn log_rescheduling( + scanner: PayableSequenceScanner, + is_externally_triggered: bool, + logger: &Logger, + reschedule_hint: &ScanRescheduleAfterEarlyStop, + ) { + let scan_mode = if is_externally_triggered { + "Manual" + } else { + "Automatic" + }; + + debug!( + logger, + "{} {} scan failed - rescheduling strategy: \"{:?}\"", + scan_mode, + scanner, + reschedule_hint + ); + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::scan_schedulers::{ + NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, + PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, + }; + use crate::accountant::scanners::{MTError, StartScanError}; + use crate::sub_lib::accountant::ScanIntervals; + use itertools::Itertools; + use lazy_static::lazy_static; + use masq_lib::logger::Logger; + use masq_lib::messages::ScanType; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + #[test] + fn scan_schedulers_are_initialized_correctly() { + let scan_intervals = ScanIntervals { + payable_scan_interval: Duration::from_secs(14), + pending_payable_scan_interval: Duration::from_secs(2), + receivable_scan_interval: Duration::from_secs(7), + }; + let automatic_scans_enabled = true; + + let schedulers = ScanSchedulers::new(scan_intervals, automatic_scans_enabled); + + assert_eq!( + schedulers.payable.new_payable_interval, + scan_intervals.payable_scan_interval + ); + let payable_scheduler_inner = schedulers.payable.inner.lock().unwrap(); + assert_eq!( + payable_scheduler_inner.last_new_payable_scan_timestamp, + UNIX_EPOCH + ); + assert_eq!( + schedulers.pending_payable.interval, + scan_intervals.pending_payable_scan_interval + ); + assert_eq!( + schedulers.receivable.interval, + scan_intervals.receivable_scan_interval + ); + assert_eq!(schedulers.automatic_scans_enabled, automatic_scans_enabled) + } + + #[test] + fn scan_dyn_interval_computer_computes_remaining_time_to_standard_interval_correctly() { + let now = SystemTime::now(); + let inputs = vec![ + ( + now.checked_sub(Duration::from_secs(32)).unwrap(), + Duration::from_secs(100), + Duration::from_secs(68), + ), + ( + now.checked_sub(Duration::from_millis(1111)).unwrap(), + Duration::from_millis(3333), + Duration::from_millis(2222), + ), + ( + now.checked_sub(Duration::from_secs(200)).unwrap(), + Duration::from_secs(204), + Duration::from_secs(4), + ), + ]; + let subject = NewPayableScanDynIntervalComputerReal::default(); + + inputs + .into_iter() + .for_each(|(past_instant, standard_interval, expected_result)| { + let result = subject.compute_interval(now, past_instant, standard_interval); + assert_eq!( + result, + Some(expected_result), + "We expected Some({}) ms, but got {:?} ms", + expected_result.as_millis(), + result.map(|duration| duration.as_millis()) + ) + }) + } + + #[test] + fn scan_dyn_interval_computer_realizes_the_standard_interval_has_been_exceeded() { + let now = SystemTime::now(); + let inputs = vec![ + ( + now.checked_sub(Duration::from_millis(32001)).unwrap(), + Duration::from_secs(32), + ), + ( + now.checked_sub(Duration::from_millis(1112)).unwrap(), + Duration::from_millis(1111), + ), + ( + now.checked_sub(Duration::from_secs(200)).unwrap(), + Duration::from_secs(123), + ), + ]; + let subject = NewPayableScanDynIntervalComputerReal::default(); + + inputs + .into_iter() + .enumerate() + .for_each(|(idx, (past_instant, standard_interval))| { + let result = subject.compute_interval(now, past_instant, standard_interval); + assert_eq!( + result, + None, + "We expected None ms, but got {:?} ms at idx {}", + result.map(|duration| duration.as_millis()), + idx + ) + }) + } + + #[test] + fn scan_dyn_interval_computer_realizes_standard_interval_just_met() { + let now = SystemTime::now(); + let subject = NewPayableScanDynIntervalComputerReal::default(); + + let result = subject.compute_interval( + now, + now.checked_sub(Duration::from_secs(32)).unwrap(), + Duration::from_secs(32), + ); + + assert_eq!( + result, + None, + "We expected None ms, but got {:?} ms", + result.map(|duration| duration.as_millis()) + ) + } + + #[test] + #[should_panic( + expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier than past \ + timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + )] + fn scan_dyn_interval_computer_panics() { + let now = UNIX_EPOCH + .checked_add(Duration::from_secs(1_000_000)) + .unwrap(); + let subject = NewPayableScanDynIntervalComputerReal::default(); + + let _ = subject.compute_interval( + now.checked_sub(Duration::from_secs(1)).unwrap(), + now, + Duration::from_secs(32), + ); + } + + lazy_static! { + static ref ALL_START_SCAN_ERRORS: Vec = { + + let candidates = vec![ + StartScanError::NothingToProcess, + StartScanError::NoConsumingWalletFound, + StartScanError::ScanAlreadyRunning { cross_scan_cause_opt: None, started_at: SystemTime::now()}, + StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + StartScanError::CalledFromNullScanner + ]; + + + let mut check_vec = candidates + .iter() + .fold(vec![],|mut acc, current|{ + acc.push(ListOfStartScanErrors::number_variant(current)); + acc + }); + // Making sure we didn't count in one variant multiple times + check_vec.dedup(); + assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "The check on variant \ + exhaustiveness failed."); + candidates + }; + } + + struct ListOfStartScanErrors<'a> { + errors: Vec<&'a StartScanError>, + } + + impl<'a> Default for ListOfStartScanErrors<'a> { + fn default() -> Self { + Self { + errors: ALL_START_SCAN_ERRORS.iter().collect_vec(), + } + } + } + + impl<'a> ListOfStartScanErrors<'a> { + fn eliminate_already_tested_variants( + mut self, + errors_to_eliminate: Vec, + ) -> Self { + let error_variants_to_remove: Vec<_> = errors_to_eliminate + .iter() + .map(Self::number_variant) + .collect(); + self.errors + .retain(|err| !error_variants_to_remove.contains(&Self::number_variant(*err))); + self + } + + fn number_variant(error: &StartScanError) -> usize { + match error { + StartScanError::NothingToProcess => 1, + StartScanError::NoConsumingWalletFound => 2, + StartScanError::ScanAlreadyRunning { .. } => 3, + StartScanError::CalledFromNullScanner => 4, + StartScanError::ManualTriggerError(..) => 5, + } + } + } + + #[test] + fn resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let test_name = + "resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered"; + + test_what_if_externally_triggered( + &format!("{}(initial_pending_payable_scan = false)", test_name), + &subject, + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }, + ); + test_what_if_externally_triggered( + &format!("{}(initial_pending_payable_scan = true)", test_name), + &subject, + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }, + ); + } + + fn test_what_if_externally_triggered( + test_name: &str, + subject: &ScanSchedulers, + scanner: PayableSequenceScanner, + ) { + init_test_logging(); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + ALL_START_SCAN_ERRORS + .iter() + .enumerate() + .for_each(|(idx, error)| { + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error(scanner, error, true, &logger); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::DoNotSchedule, + "We expected DoNotSchedule but got {:?} at idx {} for {:?}", + result, + idx, + scanner + ); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Manual {} scan failed - rescheduling strategy: \ + \"DoNotSchedule\"", + scanner + )); + }) + } + + #[test] + fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( + ) { + init_test_logging(); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let test_name = "resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true"; + let logger = Logger::new(test_name); + + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }, + &StartScanError::NothingToProcess, + false, + &logger, + ); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got {:?}", + result, + ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Automatic PendingPayables scan failed - rescheduling strategy: \ + \"Schedule(Payables)\"" + )); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: the automatic pending payable scan \ + should always be requested only in need, which contradicts the current \ + StartScanError::NothingToProcess" + )] + fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( + ) { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }, + &StartScanError::NothingToProcess, + false, + &Logger::new("test"), + ); + } + + #[test] + fn resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan() { + init_test_logging(); + let test_name = "resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan"; + let logger = Logger::new(test_name); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }; + + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, + &logger, + ); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables), + "We expected Schedule(PendingPayables) but got {:?} for {:?}", + result, + scanner + ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Automatic PendingPayables scan failed - rescheduling strategy: \ + \"Schedule(PendingPayables)\"" + )); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: PendingPayableScanner called later \ + than the initial attempt, but the consuming wallet is still missing; this should not be \ + possible" + )] + fn pending_p_scan_attempt_if_no_consuming_wallet_found_mustnt_happen_if_not_initial_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }; + + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, + &Logger::new("test"), + ); + } + + #[test] + fn resolve_error_for_pending_payables_forbidden_states() { + fn test_forbidden_states( + subject: &ScanSchedulers, + inputs: &ListOfStartScanErrors, + initial_pending_payable_scan: bool, + ) { + inputs.errors.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan, + }, + *error, + false, + &Logger::new("test"), + ) + })) + .unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} should be impossible with \ + PendingPayableScanner in automatic mode", + error + ); + assert_eq!( + panic_msg, &expected_msg, + "We expected '{}' but got '{}' for initial_pending_payable_scan = {}", + expected_msg, panic_msg, initial_pending_payable_scan + ) + }) + } + + let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ + StartScanError::NothingToProcess, + StartScanError::NoConsumingWalletFound, + ]); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_forbidden_states(&subject, &inputs, false); + test_forbidden_states(&subject, &inputs, true); + } + + #[test] + fn resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered() { + let test_name = + "resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered"; + let subject = ScanSchedulers::new(ScanIntervals::default(), false); + + test_what_if_externally_triggered( + test_name, + &subject, + PayableSequenceScanner::RetryPayables {}, + ); + } + + #[test] + fn any_automatic_scan_with_start_scan_error_is_fatal_for_retry_payables() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + ALL_START_SCAN_ERRORS.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::RetryPayables, + error, + false, + &Logger::new("test"), + ) + })) + .unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} should be impossible \ + with RetryPayableScanner in automatic mode", + error + ); + assert_eq!( + panic_msg, &expected_msg, + "We expected '{}' but got '{}'", + expected_msg, panic_msg, + ) + }) + } + + #[test] + fn resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered() { + let test_name = + "resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered"; + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_what_if_externally_triggered( + test_name, + &subject, + PayableSequenceScanner::NewPayables {}, + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: an automatic scan of NewPayableScanner \ + should never interfere with itself ScanAlreadyRunning { cross_scan_cause_opt: None, started_at:" + )] + fn resolve_hint_for_new_payables_if_scan_is_already_running_error_and_is_automatic_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::NewPayables, + &StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: SystemTime::now(), + }, + false, + &Logger::new("test"), + ); + } + + #[test] + fn resolve_new_payables_with_error_cases_resulting_in_future_rescheduling() { + let test_name = "resolve_new_payables_with_error_cases_resulting_in_future_rescheduling"; + let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ + StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: SystemTime::now(), + }, + ]); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + inputs.errors.iter().for_each(|error| { + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::NewPayables, + *error, + false, + &logger, + ); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got '{:?}'", + result, + ); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Automatic NewPayables scan failed - rescheduling strategy: \ + \"Schedule(Payables)\"", + )); + }) + } + + #[test] + fn conversion_between_hintable_scanner_and_scan_type_works() { + assert_eq!( + ScanType::from(PayableSequenceScanner::NewPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::RetryPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false + }), + ScanType::PendingPayables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true + }), + ScanType::PendingPayables + ); + } +} diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 30b3a3d2d..b50a1388b 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -16,6 +16,7 @@ pub mod payable_scanner_utils { use std::time::SystemTime; use thousands::Separable; use web3::types::H256; + use masq_lib::ui_gateway::NodeToUiMessage; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -26,6 +27,18 @@ pub mod payable_scanner_utils { RemotelyCausedErrors(Vec), } + #[derive(Debug, PartialEq)] + pub struct PayableScanResult { + pub ui_response_opt: Option, + pub result: OperationOutcome, + } + + #[derive(Debug, PartialEq, Eq)] + pub enum OperationOutcome { + NewPendingPayable, + Failure, + } + //debugging purposes only pub fn investigate_debt_extremes( timestamp: SystemTime, @@ -312,6 +325,7 @@ pub mod pending_payable_scanner_utils { use crate::accountant::PendingPayableId; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use masq_lib::logger::Logger; + use masq_lib::ui_gateway::NodeToUiMessage; use std::time::SystemTime; #[derive(Debug, Default, PartialEq, Eq, Clone)] @@ -321,6 +335,18 @@ pub mod pending_payable_scanner_utils { pub confirmed: Vec, } + impl PendingPayableScanReport { + pub fn requires_payments_retry(&self) -> bool { + todo!("complete my within GH-642") + } + } + + #[derive(Debug, PartialEq, Eq)] + pub enum PendingPayableScanResult { + NoPendingPayablesLeft(Option), + PaymentRetryRequired, + } + pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { timestamp .elapsed() @@ -440,7 +466,7 @@ pub mod receivable_scanner_utils { #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ @@ -467,21 +493,21 @@ mod tests { #[test] fn investigate_debt_extremes_picks_the_most_relevant_records() { let now = SystemTime::now(); - let now_t = to_time_t(now); + let now_t = to_unix_timestamp(now); let same_amount_significance = 2_000_000; - let same_age_significance = from_time_t(now_t - 30000); + let same_age_significance = from_unix_timestamp(now_t - 30000); let payables = &[ PayableAccount { wallet: make_wallet("wallet0"), balance_wei: same_amount_significance, - last_paid_timestamp: from_time_t(now_t - 5000), + last_paid_timestamp: from_unix_timestamp(now_t - 5000), pending_payable_opt: None, }, //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked PayableAccount { wallet: make_wallet("wallet1"), balance_wei: same_amount_significance, - last_paid_timestamp: from_time_t(now_t - 10000), + last_paid_timestamp: from_unix_timestamp(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 @@ -511,7 +537,7 @@ mod tests { let receivable_account = ReceivableAccount { wallet: make_wallet("wallet0"), balance_wei: 10_000_000_000, - last_received_timestamp: from_time_t(to_time_t(now) - offset), + last_received_timestamp: from_unix_timestamp(to_unix_timestamp(now) - offset), }; let (balance, age) = balance_and_age(now, &receivable_account); @@ -614,7 +640,7 @@ mod tests { #[test] fn payables_debug_summary_prints_pretty_summary() { init_test_logging(); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let payment_thresholds = PaymentThresholds { threshold_interval_sec: 2_592_000, debt_threshold_gwei: 1_000_000_000, @@ -628,7 +654,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2000), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec, @@ -642,7 +668,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( payment_thresholds.maturity_threshold_sec + 55, ), @@ -843,4 +869,64 @@ mod tests { "Got 0 properly sent payables of an unknown number of attempts" ) } + + #[test] + fn requires_payments_retry_says_yes() { + todo!("complete this test with GH-604") + // let cases = vec![ + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // ]; + // + // cases.into_iter().enumerate().for_each(|(idx, case)| { + // let result = case.requires_payments_retry(); + // assert_eq!( + // result, true, + // "We expected true, but got false for case of idx {}", + // idx + // ) + // }) + } + + #[test] + fn requires_payments_retry_says_no() { + todo!("complete this test with GH-604") + // let report = PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }; + // + // let result = report.requires_payments_retry(); + // + // assert_eq!(result, false) + } } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index c43d6f71b..26aa15dc3 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,9 +2,489 @@ #![cfg(test)] -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use masq_lib::type_obfuscation::Obfuscated; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +}; +use crate::accountant::scanners::payable_scanner_extension::{ + MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, +}; +use crate::accountant::scanners::scan_schedulers::{ + NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, + ScanRescheduleAfterEarlyStop, +}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; +use crate::accountant::scanners::{ + PayableScanner, PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, + Scanner, StartScanError, StartableScanner, +}; +use crate::accountant::{ + ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, + SentPayables, +}; +use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use crate::sub_lib::wallet::Wallet; +use actix::{Message, System}; +use itertools::Either; +use masq_lib::logger::{Logger, TIME_FORMATTING_STRING}; +use masq_lib::ui_gateway::NodeToUiMessage; +use regex::Regex; +use std::any::type_name; +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use time::{format_description, PrimitiveDateTime}; -pub fn protect_payables_in_test(payables: Vec) -> Obfuscated { - Obfuscated::obfuscate_vector(payables) +pub struct NullScanner {} + +impl + PrivateScanner for NullScanner +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ +} + +impl StartableScanner for NullScanner +where + TriggerMessage: Message, + StartMessage: Message, +{ + fn start_scan( + &mut self, + _wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + Err(StartScanError::CalledFromNullScanner) + } +} + +impl Scanner for NullScanner +where + EndMessage: Message, +{ + fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> ScanResult { + panic!("Called finish_scan() from NullScanner"); + } + + fn scan_started_at(&self) -> Option { + None + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + panic!("Called mark_as_started() from NullScanner"); + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + panic!("Called mark_as_ended() from NullScanner"); + } + + as_any_ref_in_trait_impl!(); +} + +impl MultistageDualPayableScanner for NullScanner {} + +impl SolvencySensitivePaymentInstructor for NullScanner { + fn try_skipping_payment_adjustment( + &self, + _msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + intentionally_blank!() + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } +} + +impl Default for NullScanner { + fn default() -> Self { + Self::new() + } +} + +impl NullScanner { + pub fn new() -> Self { + Self {} + } +} + +pub struct ScannerMock { + start_scan_params: + Arc, Logger, String)>>>, + start_scan_results: RefCell>>, + finish_scan_params: Arc>>, + finish_scan_results: RefCell>, + scan_started_at_results: RefCell>>, + stop_system_after_last_message: RefCell, +} + +impl + PrivateScanner + for ScannerMock +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ +} + +impl + StartableScanner + for ScannerMock +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ + fn start_scan( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.start_scan_params.lock().unwrap().push(( + wallet.clone(), + timestamp, + response_skeleton_opt, + logger.clone(), + // This serves for identification in scanners allowing different modes to start + // them up through. + type_name::().to_string(), + )); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.start_scan_results.borrow_mut().remove(0) + } +} + +impl Scanner + for ScannerMock +where + StartMessage: Message, + EndMessage: Message, +{ + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult { + self.finish_scan_params + .lock() + .unwrap() + .push((message, logger.clone())); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.finish_scan_results.borrow_mut().remove(0) + } + + fn scan_started_at(&self) -> Option { + self.scan_started_at_results.borrow_mut().remove(0) + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + intentionally_blank!() + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + +impl Default + for ScannerMock +{ + fn default() -> Self { + Self::new() + } +} + +impl ScannerMock { + pub fn new() -> Self { + Self { + start_scan_params: Arc::new(Mutex::new(vec![])), + start_scan_results: RefCell::new(vec![]), + finish_scan_params: Arc::new(Mutex::new(vec![])), + finish_scan_results: RefCell::new(vec![]), + scan_started_at_results: RefCell::new(vec![]), + stop_system_after_last_message: RefCell::new(false), + } + } + + pub fn start_scan_params( + mut self, + params: &Arc, Logger, String)>>>, + ) -> Self { + self.start_scan_params = params.clone(); + self + } + + pub fn start_scan_result(self, result: Result) -> Self { + self.start_scan_results.borrow_mut().push(result); + self + } + + pub fn scan_started_at_result(self, result: Option) -> Self { + self.scan_started_at_results.borrow_mut().push(result); + self + } + + pub fn finish_scan_params(mut self, params: &Arc>>) -> Self { + self.finish_scan_params = params.clone(); + self + } + + pub fn finish_scan_result(self, result: ScanResult) -> Self { + self.finish_scan_results.borrow_mut().push(result); + self + } + + pub fn stop_the_system_after_last_msg(self) -> Self { + self.stop_system_after_last_message.replace(true); + self + } + + pub fn is_allowed_to_stop_the_system(&self) -> bool { + *self.stop_system_after_last_message.borrow() + } + + pub fn is_last_message(&self) -> bool { + self.is_last_message_from_start_scan() || self.is_last_message_from_end_scan() + } + + pub fn is_last_message_from_start_scan(&self) -> bool { + self.start_scan_results.borrow().len() == 1 && self.finish_scan_results.borrow().is_empty() + } + + pub fn is_last_message_from_end_scan(&self) -> bool { + self.finish_scan_results.borrow().len() == 1 && self.start_scan_results.borrow().is_empty() + } +} + +impl MultistageDualPayableScanner + for ScannerMock +{ +} + +impl SolvencySensitivePaymentInstructor + for ScannerMock +{ + fn try_skipping_payment_adjustment( + &self, + msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + // Always passes... + // It would be quite inconvenient if we had to add specialized features to the generic + // mock, plus this functionality can be tested better with the other components mocked, + // not the scanner itself. + Ok(Either::Left(OutboundPaymentsInstructions { + affordable_accounts: msg.qualified_payables, + agent: msg.agent, + response_skeleton_opt: msg.response_skeleton_opt, + })) + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } +} + +pub trait ScannerMockMarker {} + +impl ScannerMockMarker for ScannerMock {} + +#[derive(Default)] +pub struct NewPayableScanDynIntervalComputerMock { + compute_interval_params: Arc>>, + compute_interval_results: RefCell>>, +} + +impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerMock { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option { + self.compute_interval_params.lock().unwrap().push(( + now, + last_new_payable_scan_timestamp, + interval, + )); + self.compute_interval_results.borrow_mut().remove(0) + } +} + +impl NewPayableScanDynIntervalComputerMock { + pub fn compute_interval_params( + mut self, + params: &Arc>>, + ) -> Self { + self.compute_interval_params = params.clone(); + self + } + + pub fn compute_interval_result(self, result: Option) -> Self { + self.compute_interval_results.borrow_mut().push(result); + self + } +} + +pub enum ReplacementType +where + ScannerReal: RealScannerMarker, + ScannerMock: ScannerMockMarker, +{ + Real(ScannerReal), + Mock(ScannerMock), + Null, +} + +// The scanners are categorized by types because we want them to become an abstract object +// represented by a private trait. Of course, such an object cannot be constructed directly in +// the outer world; therefore, we have to provide specific objects that will cast accordingly +// under the hood. +pub enum ScannerReplacement { + Payable( + ReplacementType< + PayableScanner, + ScannerMock, + >, + ), + PendingPayable( + ReplacementType< + PendingPayableScanner, + ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + >, + ), + Receivable( + ReplacementType< + ReceivableScanner, + ScannerMock>, + >, + ), +} + +pub enum MarkScanner<'a> { + Ended(&'a Logger), + Started(SystemTime), +} + +// Cautious: Don't compare to another timestamp on a full match; this timestamp is trimmed in +// nanoseconds down to three digits +pub fn parse_system_time_from_str(examined_str: &str) -> Vec { + let regex = Regex::new(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})").unwrap(); + let captures = regex.captures_iter(examined_str); + captures + .map(|captures| { + let captured_str_timestamp = captures.get(0).unwrap().as_str(); + let format = format_description::parse(TIME_FORMATTING_STRING).unwrap(); + let dt = PrimitiveDateTime::parse(captured_str_timestamp, &format).unwrap(); + let duration = Duration::from_secs(dt.assume_utc().unix_timestamp() as u64) + + Duration::from_nanos(dt.nanosecond() as u64); + UNIX_EPOCH + duration + }) + .collect() +} + +fn trim_expected_timestamp_to_three_digits_nanos(value: SystemTime) -> SystemTime { + let duration = value.duration_since(UNIX_EPOCH).unwrap(); + let full_nanos = duration.subsec_nanos(); + let diffuser = 10_u32.pow(6); + let trimmed_nanos = (full_nanos / diffuser) * diffuser; + let duration = duration + .checked_sub(Duration::from_nanos(full_nanos as u64)) + .unwrap() + .checked_add(Duration::from_nanos(trimmed_nanos as u64)) + .unwrap(); + UNIX_EPOCH + duration +} + +pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec) { + let parsed_timestamps = parse_system_time_from_str(examined_str); + if parsed_timestamps.len() != expected_timestamps.len() { + panic!( + "You supplied {} expected timestamps, but the examined text contains only {}", + expected_timestamps.len(), + parsed_timestamps.len() + ) + } + let zipped = parsed_timestamps + .into_iter() + .zip(expected_timestamps.into_iter()); + zipped.for_each(|(parsed_timestamp, expected_timestamp)| { + let expected_timestamp_trimmed = + trim_expected_timestamp_to_three_digits_nanos(expected_timestamp); + assert_eq!( + parsed_timestamp, expected_timestamp_trimmed, + "We expected this timestamp {:?} in this fragment '{}' but found {:?}", + expected_timestamp_trimmed, examined_str, parsed_timestamp + ) + }) +} + +#[derive(Default)] +pub struct RescheduleScanOnErrorResolverMock { + resolve_rescheduling_on_error_params: + Arc>>, + resolve_rescheduling_on_error_results: RefCell>, +} + +impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop { + self.resolve_rescheduling_on_error_params + .lock() + .unwrap() + .push(( + scanner, + error.clone(), + is_externally_triggered, + logger.clone(), + )); + self.resolve_rescheduling_on_error_results + .borrow_mut() + .remove(0) + } +} + +impl RescheduleScanOnErrorResolverMock { + pub fn resolve_rescheduling_on_error_params( + mut self, + params: &Arc>>, + ) -> Self { + self.resolve_rescheduling_on_error_params = params.clone(); + self + } + pub fn resolve_rescheduling_on_error_result( + self, + result: ScanRescheduleAfterEarlyStop, + ) -> Self { + self.resolve_rescheduling_on_error_results + .borrow_mut() + .push(result); + self + } } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 5c9d6f14a..e0e5a6cdd 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -12,22 +12,15 @@ use crate::accountant::db_access_objects::pending_payable_dao::{ use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; -use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; -use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, -}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{ - MultistagePayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, +use crate::accountant::db_access_objects::utils::{ + from_unix_timestamp, to_unix_timestamp, CustomQuery, }; +use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; -use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, PeriodicalScanScheduler, - ReceivableScanner, ScanSchedulers, Scanner, -}; -use crate::accountant::{ - gwei_to_wei, Accountant, ResponseSkeleton, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC, -}; +use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; @@ -39,17 +32,13 @@ use crate::db_config::mocks::ConfigDaoMock; use crate::sub_lib::accountant::{DaoFactories, FinancialStatistics}; use crate::sub_lib::accountant::{MessageIdGenerator, PaymentThresholds}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use crate::sub_lib::utils::NotifyLaterHandle; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::make_bc_with_defaults; -use actix::{Message, System}; +use actix::System; use ethereum_types::H256; -use itertools::Either; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; -use masq_lib::ui_gateway::NodeToUiMessage; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; @@ -57,10 +46,10 @@ use std::fmt::Debug; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime}; +use std::time::SystemTime; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); ReceivableAccount { wallet: make_wallet(&format!( "wallet{}{}", @@ -68,13 +57,13 @@ pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableA if expected_delinquent { "d" } else { "n" } )), balance_wei: gwei_to_wei(n), - last_received_timestamp: from_time_t(now - (n as i64)), + last_received_timestamp: from_unix_timestamp(now - (n as i64)), } } pub fn make_payable_account(n: u64) -> PayableAccount { - let now = to_time_t(SystemTime::now()); - let timestamp = from_time_t(now - (n as i64)); + let now = to_unix_timestamp(SystemTime::now()); + let timestamp = from_unix_timestamp(now - (n as i64)); make_payable_account_with_wallet_and_balance_and_timestamp_opt( make_wallet(&format!("wallet{}", n)), gwei_to_wei(n), @@ -1250,7 +1239,7 @@ pub fn make_custom_payment_thresholds() -> PaymentThresholds { pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { PendingPayableFingerprint { rowid: 33, - timestamp: from_time_t(222_222_222), + timestamp: from_unix_timestamp(222_222_222), hash: make_tx_hash(456), attempt: 1, amount: 12345, @@ -1258,7 +1247,7 @@ pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { } } -pub fn make_payables( +pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, ) -> ( @@ -1269,8 +1258,8 @@ pub fn make_payables( let unqualified_payable_accounts = vec![PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, ), pending_payable_opt: None, }]; @@ -1280,8 +1269,8 @@ pub fn make_payables( balance_wei: gwei_to_wei( payment_thresholds.permanent_debt_allowed_gwei + 1_000_000_000, ), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 1, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1, ), pending_payable_opt: None, }, @@ -1290,8 +1279,8 @@ pub fn make_payables( balance_wei: gwei_to_wei( payment_thresholds.permanent_debt_allowed_gwei + 1_200_000_000, ), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 100, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 100, ), pending_payable_opt: None, }, @@ -1508,208 +1497,3 @@ impl PaymentAdjusterMock { self } } - -macro_rules! formal_traits_for_payable_mid_scan_msg_handling { - ($scanner:ty) => { - impl MultistagePayableScanner for $scanner {} - - impl SolvencySensitivePaymentInstructor for $scanner { - fn try_skipping_payment_adjustment( - &self, - _msg: BlockchainAgentWithContextMessage, - _logger: &Logger, - ) -> Result, String> { - intentionally_blank!() - } - - fn perform_payment_adjustment( - &self, - _setup: PreparedAdjustment, - _logger: &Logger, - ) -> OutboundPaymentsInstructions { - intentionally_blank!() - } - } - }; -} - -pub struct NullScanner {} - -impl Scanner for NullScanner -where - BeginMessage: Message, - EndMessage: Message, -{ - fn begin_scan( - &mut self, - _wallet_opt: Wallet, - _timestamp: SystemTime, - _response_skeleton_opt: Option, - _logger: &Logger, - ) -> Result { - Err(BeginScanError::CalledFromNullScanner) - } - - fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> Option { - panic!("Called finish_scan() from NullScanner"); - } - - fn scan_started_at(&self) -> Option { - panic!("Called scan_started_at() from NullScanner"); - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - panic!("Called mark_as_started() from NullScanner"); - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - panic!("Called mark_as_ended() from NullScanner"); - } - - as_any_ref_in_trait_impl!(); -} - -formal_traits_for_payable_mid_scan_msg_handling!(NullScanner); - -impl Default for NullScanner { - fn default() -> Self { - Self::new() - } -} - -impl NullScanner { - pub fn new() -> Self { - Self {} - } -} - -pub struct ScannerMock { - begin_scan_params: Arc, Logger)>>>, - 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, - wallet: Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.begin_scan_params.lock().unwrap().push(( - wallet, - timestamp, - response_skeleton_opt, - logger.clone(), - )); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.begin_scan_results.borrow_mut().remove(0) - } - - fn finish_scan(&mut self, message: EndMessage, _logger: &Logger) -> Option { - self.end_scan_params.lock().unwrap().push(message); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.end_scan_results.borrow_mut().remove(0) - } - - fn scan_started_at(&self) -> Option { - intentionally_blank!() - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - intentionally_blank!() - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - intentionally_blank!() - } -} - -impl Default for ScannerMock { - fn default() -> Self { - Self::new() - } -} - -impl ScannerMock { - pub fn new() -> Self { - Self { - begin_scan_params: Arc::new(Mutex::new(vec![])), - begin_scan_results: RefCell::new(vec![]), - end_scan_params: Arc::new(Mutex::new(vec![])), - end_scan_results: RefCell::new(vec![]), - stop_system_after_last_message: RefCell::new(false), - } - } - - pub fn begin_scan_params( - mut self, - params: &Arc, Logger)>>>, - ) -> 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_after_last_msg(self) -> Self { - self.stop_system_after_last_message.replace(true); - self - } - - pub fn is_allowed_to_stop_the_system(&self) -> bool { - *self.stop_system_after_last_message.borrow() - } - - pub fn is_last_message(&self) -> bool { - self.is_last_message_from_begin_scan() || self.is_last_message_from_end_scan() - } - - pub fn is_last_message_from_begin_scan(&self) -> bool { - self.begin_scan_results.borrow().len() == 1 && self.end_scan_results.borrow().is_empty() - } - - pub fn is_last_message_from_end_scan(&self) -> bool { - self.end_scan_results.borrow().len() == 1 && self.begin_scan_results.borrow().is_empty() - } -} - -formal_traits_for_payable_mid_scan_msg_handling!(ScannerMock); - -impl ScanSchedulers { - pub fn update_scheduler( - &mut self, - scan_type: ScanType, - handle_opt: Option>>, - interval_opt: Option, - ) { - let scheduler = self - .schedulers - .get_mut(&scan_type) - .unwrap() - .as_any_mut() - .downcast_mut::>() - .unwrap(); - if let Some(new_handle) = handle_opt { - scheduler.handle = new_handle - } - if let Some(new_interval) = interval_opt { - scheduler.interval = new_interval - } - } -} diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 8ab3426cf..9e15f9c5d 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -1166,7 +1166,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: Some(ScanIntervals::default()), - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1241,7 +1241,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1544,7 +1544,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1730,7 +1730,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index e4275b036..b93d8770c 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::{ @@ -36,7 +36,6 @@ use futures::Future; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; @@ -45,8 +44,9 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use ethabi::Hash; use web3::types::H256; +use masq_lib::messages::ScanType; use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; @@ -267,7 +267,7 @@ impl BlockchainBridge { .map_err(|e| format!("Blockchain agent build error: {:?}", e)) .and_then(move |agent| { let outgoing_message = BlockchainAgentWithContextMessage::new( - incoming_message.protected_qualified_payables, + incoming_message.qualified_payables, agent, incoming_message.response_skeleton_opt, ); @@ -430,7 +430,7 @@ impl BlockchainBridge { .expect("Accountant is unbound"); let transaction_hashes = msg - .pending_payable + .pending_payable_fingerprints .iter() .map(|finger_print| finger_print.hash) .collect::>(); @@ -443,7 +443,7 @@ impl BlockchainBridge { let pairs = transaction_receipts_results .into_iter() - .zip(msg.pending_payable.into_iter()) + .zip(msg.pending_payable_fingerprints.into_iter()) .collect_vec(); accountant_recipient @@ -548,10 +548,9 @@ mod tests { use super::*; use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; - use crate::accountant::db_access_objects::utils::from_time_t; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; @@ -566,14 +565,13 @@ mod tests { make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; use crate::db_config::persistent_configuration::PersistentConfigError; - use crate::match_every_type_id; + use crate::match_lazily_every_type_id; use crate::node_test_utils::check_timestamp; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{ make_accountant_subs_from_recorder, make_recorder, peer_actors_builder, }; - use crate::test_utils::recorder_stop_conditions::StopCondition; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::unshared_test_utils::{ @@ -583,7 +581,6 @@ mod tests { use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; - use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; @@ -724,9 +721,8 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(qualified_payables.clone()); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables.clone(), + qualified_payables: qualified_payables.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -746,7 +742,7 @@ mod tests { let blockchain_agent_with_context_msg_actual: &BlockchainAgentWithContextMessage = accountant_received_payment.get_record(0); assert_eq!( - blockchain_agent_with_context_msg_actual.protected_qualified_payables, + blockchain_agent_with_context_msg_actual.qualified_payables, qualified_payables ); assert_eq!( @@ -808,9 +804,8 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(vec![]); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables, + qualified_payables: vec![make_payable_account(123)], consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -858,7 +853,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(SentPayables)) + .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); let wallet_account = make_wallet("blah"); let consuming_wallet = make_paying_wallet(b"consuming_wallet"); @@ -876,7 +871,7 @@ mod tests { let accounts = vec![PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, - last_paid_timestamp: from_time_t(150_000_000), + last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, }]; let agent_id_stamp = ArbitraryIdStamp::new(); @@ -949,7 +944,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(SentPayables)) + .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); let wallet_account = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); @@ -966,7 +961,7 @@ mod tests { let accounts = vec![PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, - last_paid_timestamp: from_time_t(150_000_000), + last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, }]; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); @@ -1149,7 +1144,7 @@ mod tests { #[test] fn blockchain_bridge_processes_requests_for_a_complete_and_null_transaction_receipt() { let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); let hash_1 = pending_payable_fingerprint_1.hash; let hash_2 = make_tx_hash(78989); @@ -1184,7 +1179,7 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payable_fingerprints: vec![ pending_payable_fingerprint_1.clone(), pending_payable_fingerprint_2.clone(), ], @@ -1239,7 +1234,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); let received_payments_subs: Recipient = accountant_addr.recipient(); @@ -1308,7 +1303,10 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ReportTransactionReceipts, ScanError)) + .system_stop_conditions(match_lazily_every_type_id!( + ReportTransactionReceipts, + ScanError + )) .start(); let report_transaction_receipt_recipient: Recipient = accountant_addr.clone().recipient(); @@ -1337,7 +1335,7 @@ mod tests { }; let fingerprint_4 = PendingPayableFingerprint { rowid: 450, - timestamp: from_time_t(230_000_000), + timestamp: from_unix_timestamp(230_000_000), hash: hash_4, attempt: 1, amount: 7879, @@ -1362,7 +1360,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payable_fingerprints: vec![ fingerprint_1.clone(), fingerprint_2.clone(), fingerprint_3.clone(), @@ -1406,7 +1404,7 @@ mod tests { init_test_logging(); let (accountant, _, accountant_recording) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); let report_transaction_recipient: Recipient = @@ -1441,7 +1439,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![fingerprint_1, fingerprint_2], + pending_payable_fingerprints: vec![fingerprint_1, fingerprint_2], response_skeleton_opt: None, }; let system = System::new("test"); @@ -1615,7 +1613,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = - accountant.system_stop_conditions(match_every_type_id!(ReceivedPayments)); + accountant.system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)); let some_wallet = make_wallet("somewallet"); let recipient_wallet = make_wallet("recipient_wallet"); let amount = 996000000; @@ -1708,7 +1706,7 @@ mod tests { let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = - accountant.system_stop_conditions(match_every_type_id!(ReceivedPayments)); + accountant.system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)); let earning_wallet = make_wallet("earning_wallet"); let amount = 996000000; let blockchain_interface = make_blockchain_interface_web3(port); @@ -1792,7 +1790,8 @@ mod tests { .ok_response(expected_response_logs, 1) .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant_addr = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant_addr = + accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let earning_wallet = make_wallet("earning_wallet"); let mut blockchain_interface = make_blockchain_interface_web3(port); blockchain_interface.logger = logger; @@ -1849,7 +1848,7 @@ mod tests { .err_response(-32005, "Blockheight too far in the past. Check params passed to eth_getLogs or eth_call requests.Range of blocks allowed for your plan: 1000", 0) .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let earning_wallet = make_wallet("earning_wallet"); let blockchain_interface = make_blockchain_interface_web3(port); let set_max_block_count_params_arc = Arc::new(Mutex::new(vec![])); @@ -2024,7 +2023,7 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); @@ -2075,7 +2074,7 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index 5879a47a3..b7353b7c2 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -7,6 +7,8 @@ use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchai use ethereum_types::{H256, U256, U64}; use futures::Future; use serde_json::Value; +use std::fmt::Display; +use std::str::FromStr; use web3::contract::{Contract, Options}; use web3::transports::{Batch, Http}; use web3::types::{Address, BlockNumber, Filter, Log, TransactionReceipt}; @@ -25,13 +27,57 @@ pub enum TxStatus { Succeeded(TransactionBlock), } +impl FromStr for TxStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "Pending" => Ok(TxStatus::Pending), + "Failed" => Ok(TxStatus::Failed), // TODO: GH-631: This should be removed + s if s.starts_with("Succeeded") => { + // The format is "Succeeded(block_number, block_hash)" + let parts: Vec<&str> = s[10..s.len() - 1].split(',').collect(); + if parts.len() != 2 { + return Err("Invalid Succeeded format".to_string()); + } + let block_number: u64 = parts[0] + .parse() + .map_err(|_| "Invalid block number".to_string())?; + let block_hash = + H256::from_str(&parts[1][2..]).map_err(|_| "Invalid block hash".to_string())?; + Ok(TxStatus::Succeeded(TransactionBlock { + block_hash, + block_number: U64::from(block_number), + })) + } + _ => Err(format!("Unknown status: {}", s)), + } + } +} + +impl Display for TxStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TxStatus::Failed => write!(f, "Failed"), + TxStatus::Pending => write!(f, "Pending"), + TxStatus::Succeeded(block) => { + write!( + f, + "Succeeded({},{:?})", + block.block_number, block.block_hash + ) + } + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct TxReceipt { pub transaction_hash: H256, pub status: TxStatus, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct TransactionBlock { pub block_hash: H256, pub block_number: U64, @@ -184,7 +230,7 @@ mod tests { use masq_lib::utils::find_free_port; use std::str::FromStr; use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, TransactionReceipt, U256}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TxReceipt, TxStatus}; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; #[test] fn get_transaction_fee_balance_works() { @@ -612,6 +658,74 @@ mod tests { assert_eq!(tx_receipt.status, TxStatus::Pending); } + #[test] + fn tx_status_display_works() { + // Test Failed + assert_eq!(TxStatus::Failed.to_string(), "Failed"); + + // Test Pending + assert_eq!(TxStatus::Pending.to_string(), "Pending"); + + // Test Succeeded + let block_number = U64::from(12345); + let block_hash = H256::from_low_u64_be(0xabcdef); + let succeeded = TxStatus::Succeeded(TransactionBlock { + block_hash, + block_number, + }); + assert_eq!( + succeeded.to_string(), + format!("Succeeded({},0x{:x})", block_number, block_hash) + ); + } + + #[test] + fn tx_status_from_str_works() { + // Test Pending + assert_eq!(TxStatus::from_str("Pending"), Ok(TxStatus::Pending)); + + // Test Failed + assert_eq!(TxStatus::from_str("Failed"), Ok(TxStatus::Failed)); + + // Test Succeeded with valid input + let block_number = 123456789; + let block_hash = H256::from_low_u64_be(0xabcdef); + let input = format!("Succeeded({},0x{:x})", block_number, block_hash); + assert_eq!( + TxStatus::from_str(&input), + Ok(TxStatus::Succeeded(TransactionBlock { + block_hash, + block_number: U64::from(block_number), + })) + ); + + // Test Succeeded with invalid format + assert_eq!( + TxStatus::from_str("Succeeded(123)"), + Err("Invalid Succeeded format".to_string()) + ); + + // Test Succeeded with invalid block number + assert_eq!( + TxStatus::from_str( + "Succeeded(abc,0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef)" + ), + Err("Invalid block number".to_string()) + ); + + // Test Succeeded with invalid block hash + assert_eq!( + TxStatus::from_str("Succeeded(123,0xinvalidhash)"), + Err("Invalid block hash".to_string()) + ); + + // Test unknown status + assert_eq!( + TxStatus::from_str("InProgress"), + Err("Unknown status: InProgress".to_string()) + ); + } + fn create_tx_receipt( status: Option, block_hash: Option, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 92f8e9145..852b02a4e 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,7 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -430,7 +430,7 @@ impl BlockchainInterfaceWeb3 { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index 03ed4150b..7172987f4 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -2,8 +2,8 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::BlockchainAgentWeb3; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::agent_web3::BlockchainAgentWeb3; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, @@ -330,9 +330,9 @@ pub fn create_blockchain_agent_web3( #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::from_time_t; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::gwei_to_wei; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::test_utils::{ make_payable_account, make_payable_account_with_wallet_and_balance_and_timestamp_opt, }; @@ -522,13 +522,13 @@ mod tests { PayableAccount { wallet: make_wallet("4567"), balance_wei: 2_345_678, - last_paid_timestamp: from_time_t(4500000), + last_paid_timestamp: from_unix_timestamp(4500000), pending_payable_opt: None, }, PayableAccount { wallet: make_wallet("5656"), balance_wei: 6_543_210, - last_paid_timestamp: from_time_t(333000), + last_paid_timestamp: from_unix_timestamp(333000), pending_payable_opt: None, }, ]; diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index ef1e8d373..bdcbf6a91 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,7 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 4124e283a..6259e8739 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -185,10 +185,18 @@ pub fn make_default_signed_transaction() -> SignedTransaction { } } -pub fn make_tx_hash(base: u32) -> H256 { +pub fn make_hash(base: u32) -> Hash { H256::from_uint(&U256::from(base)) } +pub fn make_tx_hash(base: u32) -> H256 { + make_hash(base) +} + +pub fn make_block_hash(base: u32) -> H256 { + make_hash(base + 1000000000) +} + pub fn all_chains() -> [Chain; 4] { [ Chain::EthMainnet, diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index cf82499b6..71a0751b0 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -336,7 +336,7 @@ pub struct BootstrapperConfig { pub log_level: LevelFilter, pub dns_servers: Vec, pub scan_intervals_opt: Option, - pub suppress_initial_scans: bool, + pub automatic_scans_enabled: bool, pub when_pending_too_long_sec: u64, pub crash_point: CrashPoint, pub clandestine_discriminator_factories: Vec>, @@ -372,7 +372,7 @@ impl BootstrapperConfig { log_level: LevelFilter::Off, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, crash_point: CrashPoint::None, clandestine_discriminator_factories: vec![], ui_gateway_config: UiGatewayConfig { @@ -416,7 +416,7 @@ impl BootstrapperConfig { self.consuming_wallet_opt = unprivileged.consuming_wallet_opt; self.db_password_opt = unprivileged.db_password_opt; self.scan_intervals_opt = unprivileged.scan_intervals_opt; - self.suppress_initial_scans = unprivileged.suppress_initial_scans; + self.automatic_scans_enabled = unprivileged.automatic_scans_enabled; self.payment_thresholds_opt = unprivileged.payment_thresholds_opt; self.when_pending_too_long_sec = unprivileged.when_pending_too_long_sec; } @@ -1253,7 +1253,7 @@ mod tests { unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); unprivileged_config.scan_intervals_opt = Some(ScanIntervals::default()); - unprivileged_config.suppress_initial_scans = false; + unprivileged_config.automatic_scans_enabled = true; unprivileged_config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; privileged_config.merge_unprivileged(unprivileged_config); @@ -1278,7 +1278,7 @@ mod tests { privileged_config.scan_intervals_opt, Some(ScanIntervals::default()) ); - assert_eq!(privileged_config.suppress_initial_scans, false); + assert_eq!(privileged_config.automatic_scans_enabled, true); assert_eq!( privileged_config.when_pending_too_long_sec, DEFAULT_PENDING_TOO_LONG_SEC diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 4d31683a0..3d0a79b6b 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -3446,10 +3446,13 @@ mod tests { #[test] fn scan_intervals_computed_default_persistent_config_unequal_to_default() { let mut scan_intervals = *DEFAULT_SCAN_INTERVALS; - scan_intervals.pending_payable_scan_interval = scan_intervals - .pending_payable_scan_interval + scan_intervals.payable_scan_interval = scan_intervals + .payable_scan_interval .add(Duration::from_secs(15)); scan_intervals.pending_payable_scan_interval = scan_intervals + .pending_payable_scan_interval + .add(Duration::from_secs(20)); + scan_intervals.receivable_scan_interval = scan_intervals .receivable_scan_interval .sub(Duration::from_secs(33)); diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index be5547576..86e82aed1 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -135,6 +135,8 @@ impl DbInitializerReal { Self::create_config_table(conn); Self::initialize_config(conn, external_params); Self::create_payable_table(conn); + Self::create_sent_payable_table(conn); + Self::create_failed_payable_table(conn); Self::create_pending_payable_table(conn); Self::create_receivable_table(conn); Self::create_banned_table(conn); @@ -258,6 +260,58 @@ impl DbInitializerReal { Self::set_config_value(conn, "max_block_count", None, false, "maximum block count"); } + pub fn create_sent_payable_table(conn: &Connection) { + conn.execute( + "create table if not exists sent_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + block_hash text null, + block_number integer null + )", + [], + ) + .expect("Can't create sent_payable table"); + + conn.execute( + "CREATE UNIQUE INDEX sent_payable_tx_hash_idx ON sent_payable (tx_hash)", + [], + ) + .expect("Can't create transaction hash index in sent payments"); + } + + pub fn create_failed_payable_table(conn: &Connection) { + conn.execute( + "create table if not exists failed_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + reason text not null, + rechecked integer not null + )", + [], + ) + .expect("Can't create failed_payable table"); + + conn.execute( + "CREATE UNIQUE INDEX failed_payable_tx_hash_idx ON sent_payable (tx_hash)", + [], + ) + .expect("Can't create transaction hash index in failed payments"); + } + pub fn create_pending_payable_table(conn: &Connection) { conn.execute( "create table if not exists pending_payable ( @@ -621,6 +675,9 @@ impl Debug for DbInitializationConfig { mod tests { use super::*; use crate::database::db_initializer::InitializationError::SqliteError; + use crate::database::test_utils::{ + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + }; use crate::db_config::config_dao::{ConfigDao, ConfigDaoReal}; use crate::test_utils::database_utils::{ assert_create_table_stm_contains_all_parts, @@ -652,7 +709,7 @@ mod tests { #[test] fn constants_have_correct_values() { assert_eq!(DATABASE_FILE, "node-data.db"); - assert_eq!(CURRENT_SCHEMA_VERSION, 10); + assert_eq!(CURRENT_SCHEMA_VERSION, 11); } #[test] @@ -670,7 +727,7 @@ mod tests { let mut stmt = conn .prepare("select name, value, encrypted from config") .unwrap(); - let _ = stmt.query_map([], |_| Ok(42)).unwrap(); + let _ = stmt.execute([]); let expected_key_words: &[&[&str]] = &[ &["name", "text", "primary", "key"], &["value", "text"], @@ -692,9 +749,20 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let mut stmt = conn.prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable").unwrap(); - let mut payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(payable_contents.next().is_none()); + let mut stmt = conn + .prepare( + "SELECT rowid, + transaction_hash, + amount_high_b, + amount_low_b, + payable_timestamp, + attempt, + process_error + FROM pending_payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); let expected_key_words: &[&[&str]] = &[ &["rowid", "integer", "primary", "key"], &["transaction_hash", "text", "not", "null"], @@ -713,6 +781,90 @@ mod tests { ) } + #[test] + fn db_initialize_creates_sent_payable_table() { + let home_dir = ensure_node_home_directory_does_not_exist( + "db_initializer", + "db_initialize_creates_sent_payable_table", + ); + let subject = DbInitializerReal::default(); + + let conn = subject + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let mut stmt = conn + .prepare( + "SELECT rowid, + tx_hash, + receiver_address, + amount_high_b, + amount_low_b, + timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + nonce, + block_hash, + block_number + FROM sent_payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); + assert_create_table_stm_contains_all_parts( + &*conn, + "sent_payable", + SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + ); + let expected_key_words: &[&[&str]] = &[&["tx_hash"]]; + assert_index_stm_is_coupled_with_right_parameter( + conn.as_ref(), + "sent_payable_tx_hash_idx", + expected_key_words, + ) + } + + #[test] + fn db_initialize_creates_failed_payable_table() { + let home_dir = ensure_node_home_directory_does_not_exist( + "db_initializer", + "db_initialize_creates_failed_payable_table", + ); + let subject = DbInitializerReal::default(); + + let conn = subject + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let mut stmt = conn + .prepare( + "SELECT rowid, + tx_hash, + receiver_address, + amount_high_b, + amount_low_b, + timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + nonce, + reason, + rechecked + FROM failed_payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); + assert_create_table_stm_contains_all_parts( + &*conn, + "failed_payable", + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, + ); + let expected_key_words: &[&[&str]] = &[&["tx_hash"]]; + assert_index_stm_is_coupled_with_right_parameter( + conn.as_ref(), + "failed_payable_tx_hash_idx", + expected_key_words, + ) + } + #[test] fn db_initialize_creates_payable_table() { let home_dir = ensure_node_home_directory_does_not_exist( @@ -725,9 +877,18 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let mut stmt = conn.prepare ("select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid from payable").unwrap (); - let mut payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(payable_contents.next().is_none()); + let mut stmt = conn + .prepare( + "SELECT wallet_address, + balance_high_b, + balance_low_b, + last_paid_timestamp, + pending_payable_rowid + FROM payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); assert_table_created_as_strict(&*conn, "payable"); let expected_key_words: &[&[&str]] = &[ &["wallet_address", "text", "primary", "key"], @@ -753,10 +914,16 @@ mod tests { .unwrap(); let mut stmt = conn - .prepare("select wallet_address, balance_high_b, balance_low_b, last_received_timestamp from receivable") + .prepare( + "SELECT wallet_address, + balance_high_b, + balance_low_b, + last_received_timestamp + FROM receivable", + ) .unwrap(); - let mut receivable_contents = stmt.query_map([], |_| Ok(())).unwrap(); - assert!(receivable_contents.next().is_none()); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); assert_table_created_as_strict(&*conn, "receivable"); let expected_key_words: &[&[&str]] = &[ &["wallet_address", "text", "primary", "key"], @@ -782,8 +949,8 @@ mod tests { .unwrap(); let mut stmt = conn.prepare("select wallet_address from banned").unwrap(); - let mut banned_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(banned_contents.next().is_none()); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); let expected_key_words: &[&[&str]] = &[&["wallet_address", "text", "primary", "key"]]; assert_create_table_stm_contains_all_parts(conn.as_ref(), "banned", expected_key_words); assert_no_index_exists_for_table(conn.as_ref(), "banned") diff --git a/node/src/database/db_migrations/db_migrator.rs b/node/src/database/db_migrations/db_migrator.rs index 7d1ec4f8c..6cae9599a 100644 --- a/node/src/database/db_migrations/db_migrator.rs +++ b/node/src/database/db_migrations/db_migrator.rs @@ -2,6 +2,7 @@ use crate::database::db_initializer::ExternalData; use crate::database::db_migrations::migrations::migration_0_to_1::Migrate_0_to_1; +use crate::database::db_migrations::migrations::migration_10_to_11::Migrate_10_to_11; use crate::database::db_migrations::migrations::migration_1_to_2::Migrate_1_to_2; use crate::database::db_migrations::migrations::migration_2_to_3::Migrate_2_to_3; use crate::database::db_migrations::migrations::migration_3_to_4::Migrate_3_to_4; @@ -80,6 +81,7 @@ impl DbMigratorReal { &Migrate_7_to_8, &Migrate_8_to_9, &Migrate_9_to_10, + &Migrate_10_to_11, // TODO: GH-598: Make this one as null migration and yours as 12 ] } diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs new file mode 100644 index 000000000..4dbfd5b5e --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -0,0 +1,111 @@ +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; + +#[allow(non_camel_case_types)] +pub struct Migrate_10_to_11; + +impl DatabaseMigration for Migrate_10_to_11 { + fn migrate<'a>( + &self, + declaration_utils: Box, + ) -> rusqlite::Result<()> { + let sql_statement_for_sent_payable = "create table if not exists sent_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + block_hash text null, + block_number integer null + )"; + + let sql_statement_for_failed_payable = "create table if not exists failed_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + reason text not null, + rechecked integer not null + )"; + + declaration_utils.execute_upon_transaction(&[ + &sql_statement_for_sent_payable, + &sql_statement_for_failed_payable, + ]) + } + + fn old_version(&self) -> usize { + 10 + } +} + +#[cfg(test)] +mod tests { + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::database::test_utils::{ + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + }; + use crate::test_utils::database_utils::{ + assert_create_table_stm_contains_all_parts, assert_table_exists, + bring_db_0_back_to_life_and_return_connection, make_external_data, + }; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use std::fs::create_dir_all; + + #[test] + fn migration_from_10_to_11_is_applied_correctly() { + init_test_logging(); + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_10_to_11_is_properly_set", + ); + create_dir_all(&dir_path).unwrap(); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + + let _prev_connection = subject + .initialize_to_version( + &dir_path, + 10, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + + let connection = subject + .initialize_to_version( + &dir_path, + 11, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + + assert_table_exists(connection.as_ref(), "sent_payable"); + assert_table_exists(connection.as_ref(), "failed_payable"); + assert_create_table_stm_contains_all_parts( + &*connection, + "sent_payable", + SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + ); + assert_create_table_stm_contains_all_parts( + &*connection, + "failed_payable", + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, + ); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + "DbMigrator: Database successfully migrated from version 10 to 11", + ]); + } +} diff --git a/node/src/database/db_migrations/migrations/migration_4_to_5.rs b/node/src/database/db_migrations/migrations/migration_4_to_5.rs index 06deb809f..4b5bbb50a 100644 --- a/node/src/database/db_migrations/migrations/migration_4_to_5.rs +++ b/node/src/database/db_migrations/migrations/migration_4_to_5.rs @@ -76,7 +76,7 @@ impl DatabaseMigration for Migrate_4_to_5 { #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, DATABASE_FILE, }; @@ -124,7 +124,7 @@ mod tests { None, &wallet_1, 113344, - from_time_t(250_000_000), + from_unix_timestamp(250_000_000), ); let config_table_before = fetch_all_from_config_table(conn.as_ref()); @@ -160,7 +160,7 @@ mod tests { let params: &[&dyn ToSql] = &[ &wallet, &amount, - &to_time_t(timestamp), + &to_unix_timestamp(timestamp), if !hash_str.is_empty() { &hash_str } else { @@ -208,7 +208,7 @@ mod tests { Some(transaction_hash_2), &wallet_2, 1111111, - from_time_t(200_000_000), + from_unix_timestamp(200_000_000), ); let config_table_before = fetch_all_from_config_table(&conn); diff --git a/node/src/database/db_migrations/migrations/migration_8_to_9.rs b/node/src/database/db_migrations/migrations/migration_8_to_9.rs index 4bf95e955..eb89ac002 100644 --- a/node/src/database/db_migrations/migrations/migration_8_to_9.rs +++ b/node/src/database/db_migrations/migrations/migration_8_to_9.rs @@ -43,21 +43,22 @@ mod tests { let _ = bring_db_0_back_to_life_and_return_connection(&db_path); let subject = DbInitializerReal::default(); - let result = subject.initialize_to_version( - &dir_path, - 8, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); - - assert!(result.is_ok()); + let _prev_connection = subject + .initialize_to_version( + &dir_path, + 8, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let result = subject.initialize_to_version( - &dir_path, - 9, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); + let connection = subject + .initialize_to_version( + &dir_path, + 9, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let connection = result.unwrap(); let (mp_value, mp_encrypted) = retrieve_config_row(connection.as_ref(), "max_block_count"); let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); assert_eq!(mp_value, None); diff --git a/node/src/database/db_migrations/migrations/migration_9_to_10.rs b/node/src/database/db_migrations/migrations/migration_9_to_10.rs index 7622ef01f..be240429a 100644 --- a/node/src/database/db_migrations/migrations/migration_9_to_10.rs +++ b/node/src/database/db_migrations/migrations/migration_9_to_10.rs @@ -43,21 +43,22 @@ mod tests { let _ = bring_db_0_back_to_life_and_return_connection(&db_path); let subject = DbInitializerReal::default(); - let result = subject.initialize_to_version( - &dir_path, - 9, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); - - assert!(result.is_ok()); + let _prev_connection = subject + .initialize_to_version( + &dir_path, + 9, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let result = subject.initialize_to_version( - &dir_path, - 10, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); + let connection = subject + .initialize_to_version( + &dir_path, + 10, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let connection = result.unwrap(); let (mp_value, mp_encrypted) = retrieve_config_row(connection.as_ref(), "max_block_count"); let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); assert_eq!(mp_value, Some(100_000u64.to_string())); diff --git a/node/src/database/db_migrations/migrations/mod.rs b/node/src/database/db_migrations/migrations/mod.rs index bcdb14176..e093df006 100644 --- a/node/src/database/db_migrations/migrations/mod.rs +++ b/node/src/database/db_migrations/migrations/mod.rs @@ -10,3 +10,5 @@ pub mod migration_6_to_7; pub mod migration_7_to_8; pub mod migration_8_to_9; pub mod migration_9_to_10; +#[rustfmt::skip] +pub mod migration_10_to_11; diff --git a/node/src/database/test_utils/mod.rs b/node/src/database/test_utils/mod.rs index e8b9060f1..6e88e1292 100644 --- a/node/src/database/test_utils/mod.rs +++ b/node/src/database/test_utils/mod.rs @@ -12,6 +12,34 @@ use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +pub const SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE: &[&[&str]] = &[ + &["rowid", "integer", "primary", "key"], + &["tx_hash", "text", "not", "null"], + &["receiver_address", "text", "not", "null"], + &["amount_high_b", "integer", "not", "null"], + &["amount_low_b", "integer", "not", "null"], + &["timestamp", "integer", "not", "null"], + &["gas_price_wei_high_b", "integer", "not", "null"], + &["gas_price_wei_low_b", "integer", "not", "null"], + &["nonce", "integer", "not", "null"], + &["block_hash", "text", "null"], + &["block_number", "integer", "null"], +]; + +pub const SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE: &[&[&str]] = &[ + &["rowid", "integer", "primary", "key"], + &["tx_hash", "text", "not", "null"], + &["receiver_address", "text", "not", "null"], + &["amount_high_b", "integer", "not", "null"], + &["amount_low_b", "integer", "not", "null"], + &["timestamp", "integer", "not", "null"], + &["gas_price_wei_high_b", "integer", "not", "null"], + &["gas_price_wei_low_b", "integer", "not", "null"], + &["nonce", "integer", "not", "null"], + &["reason", "text", "not", "null"], + &["rechecked", "integer", "not", "null"], +]; + #[derive(Debug, Default)] pub struct ConnectionWrapperMock<'conn> { prepare_params: Arc>>, diff --git a/node/src/db_config/persistent_configuration.rs b/node/src/db_config/persistent_configuration.rs index 532048a34..ebc4efba6 100644 --- a/node/src/db_config/persistent_configuration.rs +++ b/node/src/db_config/persistent_configuration.rs @@ -1949,10 +1949,10 @@ mod tests { fn scan_intervals_get_method_works() { persistent_config_plain_data_assertions_for_simple_get_method!( "scan_intervals", - "40|60|50", + "60|5|50", ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(40), payable_scan_interval: Duration::from_secs(60), + pending_payable_scan_interval: Duration::from_secs(5), receivable_scan_interval: Duration::from_secs(50), } ); diff --git a/node/src/node_configurator/configurator.rs b/node/src/node_configurator/configurator.rs index 19b0b958a..b025abef9 100644 --- a/node/src/node_configurator/configurator.rs +++ b/node/src/node_configurator/configurator.rs @@ -652,8 +652,8 @@ impl Configurator { }, start_block_opt, scan_intervals: UiScanIntervals { - pending_payable_sec, payable_sec, + pending_payable_sec, receivable_sec, }, }; @@ -2591,8 +2591,8 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 122, payable_sec: 125, + pending_payable_sec: 122, receivable_sec: 128 } } @@ -2610,8 +2610,8 @@ mod tests { exit_service_rate: 13, })) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(122), payable_scan_interval: Duration::from_secs(125), + pending_payable_scan_interval: Duration::from_secs(122), receivable_scan_interval: Duration::from_secs(128), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -2722,8 +2722,8 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 122, payable_sec: 125, + pending_payable_sec: 122, receivable_sec: 128 } } @@ -2760,8 +2760,8 @@ mod tests { exit_service_rate: 0, })) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Default::default(), payable_scan_interval: Default::default(), + pending_payable_scan_interval: Default::default(), receivable_scan_interval: Default::default(), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -2815,8 +2815,8 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 0, payable_sec: 0, + pending_payable_sec: 0, receivable_sec: 0 } } diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 4238bd8d5..801aa4456 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -504,12 +504,13 @@ fn configure_accountant_config( |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 automatic_scans_enabled = + value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == "on"; config.payment_thresholds_opt = Some(payment_thresholds); config.scan_intervals_opt = Some(scan_intervals); - config.suppress_initial_scans = suppress_initial_scans; + config.automatic_scans_enabled = automatic_scans_enabled; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; Ok(()) } @@ -1818,7 +1819,7 @@ mod tests { "--ip", "1.2.3.4", "--scan-intervals", - "180|150|130", + "180|50|130", "--payment-thresholds", "100000|10000|1000|20000|1000|20000", ]; @@ -1827,8 +1828,8 @@ mod tests { let mut persistent_configuration = configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(100), payable_scan_interval: Duration::from_secs(101), + pending_payable_scan_interval: Duration::from_secs(33), receivable_scan_interval: Duration::from_secs(102), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -1855,8 +1856,8 @@ mod tests { .unwrap(); let expected_scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(50), receivable_scan_interval: Duration::from_secs(130), }; let expected_payment_thresholds = PaymentThresholds { @@ -1872,13 +1873,13 @@ mod tests { Some(expected_payment_thresholds) ); assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); - assert_eq!(config.suppress_initial_scans, false); + assert_eq!(config.automatic_scans_enabled, true); assert_eq!( config.when_pending_too_long_sec, DEFAULT_PENDING_TOO_LONG_SEC ); let set_scan_intervals_params = set_scan_intervals_params_arc.lock().unwrap(); - assert_eq!(*set_scan_intervals_params, vec!["180|150|130".to_string()]); + assert_eq!(*set_scan_intervals_params, vec!["180|50|130".to_string()]); let set_payment_thresholds_params = set_payment_thresholds_params_arc.lock().unwrap(); assert_eq!( *set_payment_thresholds_params, @@ -1894,7 +1895,7 @@ mod tests { "--ip", "1.2.3.4", "--scan-intervals", - "180|150|130", + "180|15|130", "--payment-thresholds", "100000|1000|1000|20000|1000|20000", ]; @@ -1903,8 +1904,8 @@ mod tests { let mut persistent_configuration = configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -1935,11 +1936,11 @@ mod tests { unban_below_gwei: 20000, }; let expected_scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), }; - let expected_suppress_initial_scans = false; + let expected_automatic_scans_enabled = true; let expected_when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; assert_eq!( config.payment_thresholds_opt, @@ -1947,8 +1948,8 @@ mod tests { ); assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); assert_eq!( - config.suppress_initial_scans, - expected_suppress_initial_scans + config.automatic_scans_enabled, + expected_automatic_scans_enabled ); assert_eq!( config.when_pending_too_long_sec, @@ -2578,7 +2579,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, true); + assert_eq!(bootstrapper_config.automatic_scans_enabled, false); } #[test] @@ -2599,7 +2600,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } #[test] @@ -2620,7 +2621,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } fn make_persistent_config( diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index fc0334834..d18a7ceba 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -1353,7 +1353,7 @@ impl Hostname { #[cfg(test)] mod tests { use super::*; - use crate::match_every_type_id; + use crate::match_lazily_every_type_id; use crate::proxy_server::protocol_pack::ServerImpersonator; use crate::proxy_server::server_impersonator_http::ServerImpersonatorHttp; use crate::proxy_server::server_impersonator_tls::ServerImpersonatorTls; @@ -1380,7 +1380,7 @@ mod tests { 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::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::{ make_request_payload, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; @@ -2627,8 +2627,8 @@ mod tests { let cryptde = main_cryptde(); let http_request = b"GET /index.html HTTP/1.1\r\nHost: nowhere.com\r\n\r\n"; let (proxy_server_mock, _, proxy_server_recording_arc) = make_recorder(); - let proxy_server_mock = - proxy_server_mock.system_stop_conditions(match_every_type_id!(AddRouteResultMessage)); + let proxy_server_mock = proxy_server_mock + .system_stop_conditions(match_lazily_every_type_id!(AddRouteResultMessage)); let route_query_response = None; let (neighborhood_mock, _, _) = make_recorder(); let neighborhood_mock = @@ -5230,7 +5230,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!(RouteQueryMessage)) + .system_stop_conditions(match_lazily_every_type_id!(RouteQueryMessage)) .route_query_response(Some(route_query_response_expected.clone())); let cryptde = main_cryptde(); let mut subject = ProxyServer::new( @@ -5400,7 +5400,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( RouteQueryMessage, RouteQueryMessage, RouteQueryMessage diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 4b005f713..f1f174e6e 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -3,7 +3,7 @@ use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoFactory; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, SentPayables, @@ -38,8 +38,8 @@ lazy_static! { unban_below_gwei: 500_000_000, }; pub static ref DEFAULT_SCAN_INTERVALS: ScanIntervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(600), payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(60), receivable_scan_interval: Duration::from_secs(600) }; } @@ -231,8 +231,8 @@ mod tests { unban_below_gwei: 500_000_000, }; let scan_intervals_expected = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(600), payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(60), receivable_scan_interval: Duration::from_secs(600), }; assert_eq!(*DEFAULT_SCAN_INTERVALS, scan_intervals_expected); diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 2ec840cc9..84aaabe48 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::peer_actors::BindMessage; diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index f70f60f0f..53a3e8488 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -177,8 +177,8 @@ impl CombinedParams { ScanIntervals, &parsed_values, Duration::from_secs, - "pending_payable_scan_interval", "payable_scan_interval", + "pending_payable_scan_interval", "receivable_scan_interval" ))) } @@ -208,8 +208,8 @@ impl From<&CombinedParams> for &[(&str, CombinedParamsDataTypes)] { ("unban_below_gwei", U64), ], CombinedParams::ScanIntervals(Uninitialized) => &[ - ("pending_payable_scan_interval", U64), ("payable_scan_interval", U64), + ("pending_payable_scan_interval", U64), ("receivable_scan_interval", U64), ], _ => panic!( @@ -225,8 +225,8 @@ impl Display for ScanIntervals { write!( f, "{}|{}|{}", - self.pending_payable_scan_interval.as_secs(), self.payable_scan_interval.as_secs(), + self.pending_payable_scan_interval.as_secs(), self.receivable_scan_interval.as_secs() ) } @@ -400,8 +400,8 @@ mod tests { assert_eq!( scan_interval, &[ - ("pending_payable_scan_interval", U64), ("payable_scan_interval", U64), + ("pending_payable_scan_interval", U64), ("receivable_scan_interval", U64), ] ); @@ -550,15 +550,15 @@ mod tests { #[test] fn scan_intervals_from_combined_params() { - let scan_intervals_str = "110|115|113"; + let scan_intervals_str = "115|55|113"; let result = ScanIntervals::try_from(scan_intervals_str).unwrap(); assert_eq!( result, ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(110), payable_scan_interval: Duration::from_secs(115), + pending_payable_scan_interval: Duration::from_secs(55), receivable_scan_interval: Duration::from_secs(113) } ) @@ -567,14 +567,14 @@ mod tests { #[test] fn scan_intervals_to_combined_params() { let scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(60), - payable_scan_interval: Duration::from_secs(70), + payable_scan_interval: Duration::from_secs(90), + pending_payable_scan_interval: Duration::from_secs(40), receivable_scan_interval: Duration::from_secs(100), }; let result = scan_intervals.to_string(); - assert_eq!(result, "60|70|100".to_string()); + assert_eq!(result, "90|40|100".to_string()); } #[test] diff --git a/node/src/sub_lib/peer_actors.rs b/node/src/sub_lib/peer_actors.rs index 3a51be868..571eca3fe 100644 --- a/node/src/sub_lib/peer_actors.rs +++ b/node/src/sub_lib/peer_actors.rs @@ -14,6 +14,8 @@ use std::fmt::Debug; use std::fmt::Formatter; use std::net::IpAddr; +// TODO This file should be test only + #[derive(Clone, PartialEq, Eq)] pub struct PeerActors { pub proxy_server: ProxyServerSubs, diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index d68d721bb..5bd7a655a 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -222,6 +222,7 @@ impl NLSpawnHandleHolder for NLSpawnHandleHolderReal { } } +#[derive(Default)] pub struct NotifyHandleReal { phantom: PhantomData, } diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index 02ba441a4..fb8ba3a83 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -103,6 +103,11 @@ pub fn retrieve_config_row(conn: &dyn ConnectionWrapper, name: &str) -> (Option< }) } +pub fn assert_table_exists(conn: &dyn ConnectionWrapper, table_name: &str) { + let result = conn.prepare(&format!("select * from {}", table_name)); + assert!(result.is_ok(), "Table {} should exist", table_name); +} + pub fn assert_table_does_not_exist(conn: &dyn ConnectionWrapper, table_name: &str) { let error_stm = conn .prepare(&format!("select * from {}", table_name)) diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 1bf32b4b5..b36199b75 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -12,6 +12,7 @@ pub mod logfile_name_guard; pub mod neighborhood_test_utils; pub mod persistent_configuration_mock; pub mod recorder; +pub mod recorder_counter_msgs; pub mod recorder_stop_conditions; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; @@ -539,7 +540,7 @@ pub mod unshared_test_utils { use crate::test_utils::neighborhood_test_utils::MIN_HOPS_FOR_TEST; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{make_recorder, Recorder, Recording}; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; use actix::{Actor, Addr, AsyncContext, Context, Handler, Recipient, System}; use actix::{Message, SpawnHandle}; @@ -682,7 +683,7 @@ pub mod unshared_test_utils { pub fn make_bc_with_defaults() -> BootstrapperConfig { let mut config = BootstrapperConfig::new(); config.scan_intervals_opt = Some(ScanIntervals::default()); - config.suppress_initial_scans = false; + config.automatic_scans_enabled = true; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; config.payment_thresholds_opt = Some(PaymentThresholds::default()); config @@ -698,9 +699,9 @@ pub mod unshared_test_utils { { let (recorder, _, recording_arc) = make_recorder(); let recorder = match stopping_message { - Some(type_id) => recorder.system_stop_conditions(StopConditions::All(vec![ - StopCondition::StopOnType(type_id), - ])), // No need to write stop message after this + Some(type_id) => recorder.system_stop_conditions(StopConditions::AllLazily(vec![ + MsgIdentification::ByType(type_id), + ])), // This will take care of stopping the system None => recorder, }; let addr = recorder.start(); @@ -871,17 +872,23 @@ pub mod unshared_test_utils { pub mod notify_handlers { use super::*; + use std::fmt::Debug; pub struct NotifyLaterHandleMock { notify_later_params: Arc>>, + stop_system_on_count_received_opt: RefCell>, send_message_out: bool, + // To prove that no msg was tried to be scheduled + panic_on_schedule_attempt: bool, } impl Default for NotifyLaterHandleMock { fn default() -> Self { Self { notify_later_params: Arc::new(Mutex::new(vec![])), + stop_system_on_count_received_opt: RefCell::new(None), send_message_out: false, + panic_on_schedule_attempt: false, } } } @@ -892,15 +899,30 @@ pub mod unshared_test_utils { self } + pub fn stop_system_on_count_received(self, count: usize) -> Self { + if count == 0 { + panic!("Should be a none-zero value") + } + let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + system_killer.start(); + self.stop_system_on_count_received_opt.replace(Some(count)); + self + } + pub fn capture_msg_and_let_it_fly_on(mut self) -> Self { self.send_message_out = true; self } + + pub fn panic_on_schedule_attempt(mut self) -> Self { + self.panic_on_schedule_attempt = true; + self + } } impl NotifyLaterHandle for NotifyLaterHandleMock where - M: Message + 'static + Clone, + M: Message + Clone + Debug + Send + 'static, A: Actor> + Handler, { fn notify_later<'a>( @@ -909,10 +931,26 @@ pub mod unshared_test_utils { interval: Duration, ctx: &'a mut Context, ) -> Box { + if self.panic_on_schedule_attempt { + panic!( + "Message scheduling request for {:?} and interval {}ms, thought not \ + expected", + msg, + interval.as_millis() + ); + } self.notify_later_params .lock() .unwrap() .push((msg.clone(), interval)); + if let Some(remaining) = + self.stop_system_on_count_received_opt.borrow_mut().as_mut() + { + *remaining -= 1; + if remaining == &0 { + System::current().stop(); + } + } if self.send_message_out { let handle = ctx.notify_later(msg, interval); Box::new(NLSpawnHandleHolderReal::new(handle)) @@ -933,6 +971,8 @@ pub mod unshared_test_utils { pub struct NotifyHandleMock { notify_params: Arc>>, send_message_out: bool, + stop_system_on_count_received_opt: RefCell>, + panic_on_schedule_attempt: bool, } impl Default for NotifyHandleMock { @@ -940,6 +980,8 @@ pub mod unshared_test_utils { Self { notify_params: Arc::new(Mutex::new(vec![])), send_message_out: false, + stop_system_on_count_received_opt: RefCell::new(None), + panic_on_schedule_attempt: false, } } } @@ -950,19 +992,50 @@ pub mod unshared_test_utils { self } - pub fn permit_to_send_out(mut self) -> Self { + pub fn capture_msg_and_let_it_fly_on(mut self) -> Self { self.send_message_out = true; self } + + pub fn stop_system_on_count_received(self, msg_count: usize) -> Self { + if msg_count == 0 { + panic!("Should be a non-zero value") + } + let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + system_killer.start(); + self.stop_system_on_count_received_opt + .replace(Some(msg_count)); + self + } + + pub fn panic_on_schedule_attempt(mut self) -> Self { + self.panic_on_schedule_attempt = true; + self + } } impl NotifyHandle for NotifyHandleMock where - M: Message + 'static + Clone, + M: Message + Debug + Clone + 'static, A: Actor> + Handler, { fn notify<'a>(&'a self, msg: M, ctx: &'a mut Context) { + if self.panic_on_schedule_attempt { + panic!( + "Message scheduling request for {:?}, thought not expected", + msg + ) + } self.notify_params.lock().unwrap().push(msg.clone()); + if let Some(remaining) = + self.stop_system_on_count_received_opt.borrow_mut().as_mut() + { + *remaining -= 1; + if remaining == &0 { + System::current().stop(); + return; + } + } if self.send_message_out { ctx.notify(msg) } @@ -984,7 +1057,7 @@ pub mod unshared_test_utils { // you've pasted in before at the other end. // 3) Using raw pointers to link the real memory address to your objects does not lead to good // results in all cases (It was found confusing and hard to be done correctly or even impossible - // to implement especially for references pointing to a dereferenced Box that was originally + // to implement, especially for references pointing to a dereferenced Box that was originally // supplied as an owned argument into the testing environment at the beginning, or we can // suspect the memory link already broken because of moves of the owned boxed instance // around the subjected code) diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index f66125182..6633ee948 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -1,13 +1,13 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; -use crate::accountant::ReportTransactionReceipts; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::{ - ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForPayables, - ScanForPendingPayables, ScanForReceivables, SentPayables, + ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, + ScanForReceivables, SentPayables, }; +use crate::accountant::{ReportTransactionReceipts, ScanForPendingPayables, ScanForRetryPayables}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::daemon::crash_notification::CrashNotification; @@ -47,8 +47,11 @@ use crate::sub_lib::stream_handler_pool::DispatcherNodeQueryResponse; use crate::sub_lib::stream_handler_pool::TransmitDataMsg; use crate::sub_lib::ui_gateway::UiGatewaySubs; use crate::sub_lib::utils::MessageScheduler; +use crate::test_utils::recorder_counter_msgs::{ + CounterMessages, CounterMsgGear, SingleTypeCounterMsgSetup, +}; use crate::test_utils::recorder_stop_conditions::{ - ForcedMatchable, PretendedMatchableWrapper, StopCondition, StopConditions, + ForcedMatchable, MsgIdentification, PretendedMatchableWrapper, StopConditions, }; use crate::test_utils::to_millis; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; @@ -59,7 +62,7 @@ use actix::MessageResult; use actix::System; use actix::{Actor, Message}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; -use std::any::{Any, TypeId}; +use std::any::{type_name, Any, TypeId}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; @@ -70,6 +73,7 @@ pub struct Recorder { recording: Arc>, node_query_responses: Vec>, route_query_responses: Vec>, + counter_msgs_opt: Option, stop_conditions_opt: Option, } @@ -101,7 +105,7 @@ macro_rules! message_handler_common { macro_rules! matchable { ($message_type: ty) => { impl ForcedMatchable<$message_type> for $message_type { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::<$message_type>() } } @@ -162,7 +166,8 @@ recorder_message_handler_t_m_p!(ReportTransactionReceipts); recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); -recorder_message_handler_t_m_p!(ScanForPayables); +recorder_message_handler_t_m_p!(ScanForNewPayables); +recorder_message_handler_t_m_p!(ScanForRetryPayables); recorder_message_handler_t_m_p!(ScanForPendingPayables); recorder_message_handler_t_m_p!(ScanForReceivables); recorder_message_handler_t_m_p!(SentPayables); @@ -187,7 +192,7 @@ where OuterM: PartialEq + 'static, InnerM: PartialEq + Send + Message, { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::() } } @@ -210,6 +215,16 @@ impl Handler for Recorder { matchable!(RouteQueryMessage); +impl Handler for Recorder { + type Result = (); + + fn handle(&mut self, msg: SetUpCounterMsgs, _ctx: &mut Self::Context) -> Self::Result { + msg.setups + .into_iter() + .for_each(|msg_setup| self.add_counter_msg(msg_setup)) + } +} + fn extract_response(responses: &mut Vec, err_msg: &str) -> T where T: Clone, @@ -261,11 +276,21 @@ impl Recorder { self.start_system_killer(); self.stop_conditions_opt = Some(stop_conditions) } else { - panic!("Stop conditions must be set by a single method call. Consider to use StopConditions::All") + panic!("Stop conditions must be set by a single method call. Consider using StopConditions::All") }; self } + fn add_counter_msg(&mut self, counter_msg_setup: SingleTypeCounterMsgSetup) { + if let Some(counter_msgs) = self.counter_msgs_opt.as_mut() { + counter_msgs.add_msg(counter_msg_setup) + } else { + let mut counter_msgs = CounterMessages::default(); + counter_msgs.add_msg(counter_msg_setup); + self.counter_msgs_opt = Some(counter_msgs) + } + } + fn start_system_killer(&mut self) { let system_killer = SystemKillerActor::new(Duration::from_secs(15)); system_killer.start(); @@ -275,7 +300,9 @@ impl Recorder { where M: 'static + ForcedMatchable + Send, { - let kill_system = if let Some(stop_conditions) = &mut self.stop_conditions_opt { + let counter_msg_opt = self.check_on_counter_msg(&msg); + + let stop_system = if let Some(stop_conditions) = &mut self.stop_conditions_opt { stop_conditions.resolve_stop_conditions::(&msg) } else { false @@ -283,7 +310,11 @@ impl Recorder { self.record(msg); - if kill_system { + if let Some(sendable_msgs) = counter_msg_opt { + sendable_msgs.into_iter().for_each(|msg| msg.try_send()) + } + + if stop_system { System::current().stop() } } @@ -295,6 +326,17 @@ impl Recorder { { self.handle_msg_t_m_p(PretendedMatchableWrapper(msg)) } + + fn check_on_counter_msg(&mut self, msg: &M) -> Option>> + where + M: ForcedMatchable + 'static, + { + if let Some(counter_msgs) = self.counter_msgs_opt.as_mut() { + counter_msgs.search_for_msg_gear(msg) + } else { + None + } + } } impl Recording { @@ -344,14 +386,15 @@ impl Recording { match item_box.downcast_ref::() { Some(item) => Ok(item), None => { - // double-checking for an uncommon, yet possible other type of an actor message, which doesn't implement PartialEq + // double-checking for an uncommon, yet possible other type of actor message, which doesn't implement PartialEq let item_opt = item_box.downcast_ref::>(); match item_opt { Some(item) => Ok(&item.0), None => Err(format!( - "Message {:?} could not be downcast to the expected type", - item_box + "Message {:?} could not be downcast to the expected type {}.", + item_box, + type_name::() )), } } @@ -385,6 +428,27 @@ impl RecordAwaiter { } } +#[derive(Message)] +pub struct SetUpCounterMsgs { + // Trigger msg - it arrives at the Recorder from the Actor being tested and matches one of the + // msg ID methods. + // Counter msg - it is sent back from the Recorder when a trigger msg is recognized + // + // In general, the triggering is data driven. Shuffling with the setups of differently typed + // trigger messages can't have any adverse effect. + // + // However, setups of the same trigger message types compose clusters. + // Keep in mind these are tested over their ID method sequentially, according to the order + // in which they are fed into this vector, with the other messages ignored. + setups: Vec, +} + +impl SetUpCounterMsgs { + pub fn new(setups: Vec) -> Self { + Self { setups } + } +} + pub fn make_recorder() -> (Recorder, RecordAwaiter, Arc>) { let recorder = Recorder::new(); let awaiter = recorder.get_awaiter(); @@ -576,8 +640,9 @@ impl PeerActorsBuilder { self } - // This must be called after System.new and before System.run - pub fn build(self) -> PeerActors { + // This must be called after System.new and before System.run. + // These addresses may be helpful for setting up the Counter Messages. + pub fn build_and_provide_addresses(self) -> (PeerActors, PeerActorAddrs) { let proxy_server_addr = self.proxy_server.start(); let dispatcher_addr = self.dispatcher.start(); let hopper_addr = self.hopper.start(); @@ -588,27 +653,73 @@ impl PeerActorsBuilder { let blockchain_bridge_addr = self.blockchain_bridge.start(); let configurator_addr = self.configurator.start(); - PeerActors { - proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), - dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), - hopper: make_hopper_subs_from_recorder(&hopper_addr), - proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), - neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), - accountant: make_accountant_subs_from_recorder(&accountant_addr), - ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), - blockchain_bridge: make_blockchain_bridge_subs_from_recorder(&blockchain_bridge_addr), - configurator: make_configurator_subs_from_recorder(&configurator_addr), - } + ( + PeerActors { + proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), + dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), + hopper: make_hopper_subs_from_recorder(&hopper_addr), + proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), + neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), + accountant: make_accountant_subs_from_recorder(&accountant_addr), + ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), + blockchain_bridge: make_blockchain_bridge_subs_from_recorder( + &blockchain_bridge_addr, + ), + configurator: make_configurator_subs_from_recorder(&configurator_addr), + }, + PeerActorAddrs { + proxy_server_addr, + dispatcher_addr, + hopper_addr, + proxy_client_addr, + neighborhood_addr, + accountant_addr, + ui_gateway_addr, + blockchain_bridge_addr, + configurator_addr, + }, + ) + } + + // This must be called after System.new and before System.run + pub fn build(self) -> PeerActors { + let (peer_actors, _) = self.build_and_provide_addresses(); + peer_actors } } +pub struct PeerActorAddrs { + pub proxy_server_addr: Addr, + pub dispatcher_addr: Addr, + pub hopper_addr: Addr, + pub proxy_client_addr: Addr, + pub neighborhood_addr: Addr, + pub accountant_addr: Addr, + pub ui_gateway_addr: Addr, + pub blockchain_bridge_addr: Addr, + pub configurator_addr: Addr, +} + #[cfg(test)] mod tests { use super::*; - use crate::match_every_type_id; + use crate::blockchain::blockchain_bridge::BlockchainBridge; + use crate::sub_lib::neighborhood::{ConfigChange, Hops, WalletPair}; + use crate::test_utils::make_wallet; + use crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient; + use crate::{ + match_lazily_every_type_id, setup_for_counter_msg_triggered_via_specific_msg_id_method, + setup_for_counter_msg_triggered_via_type_id, + }; use actix::Message; use actix::System; + use masq_lib::messages::{ + SerializableLogLevel, ToMessageBody, UiLogBroadcast, UiUnmarshalError, + }; + use masq_lib::ui_gateway::MessageTarget; use std::any::TypeId; + use std::net::{IpAddr, Ipv4Addr}; + use std::vec; #[derive(Debug, PartialEq, Eq, Message)] struct FirstMessageType { @@ -669,7 +780,7 @@ mod tests { fn recorder_can_be_stopped_on_a_particular_message() { let system = System::new("recorder_can_be_stopped_on_a_particular_message"); let recorder = - Recorder::new().system_stop_conditions(match_every_type_id!(FirstMessageType)); + Recorder::new().system_stop_conditions(match_lazily_every_type_id!(FirstMessageType)); let recording_arc = recorder.get_recording(); let rec_addr: Addr = recorder.start(); @@ -705,4 +816,324 @@ mod tests { TypeId::of::>() ) } + + #[test] + fn counter_msgs_with_diff_id_methods_are_used_together_and_one_was_not_triggered() { + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( + ScanForReceivables, + NodeToUiMessage + )); + let respondent_addr = respondent.start(); + // Case 1 + // This msg will trigger as the recorder will detect the arrival of StartMessage (no more + // requirement). + let (trigger_message_1, cm_setup_1) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + // Taking an opportunity to test a setup via the macro for the simplest identification, + // by the TypeId. + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) + }; + // Case two + // This msg will not trigger as it is declared with a wrong TypeId of the supposed trigger + // msg. The supplied ID does not even belong to an Actor msg type. + let cm_setup_2 = { + let counter_msg_strayed = StartMessage {}; + let screwed_id = TypeId::of::(); + let id_method = MsgIdentification::ByType(screwed_id); + SingleTypeCounterMsgSetup::new( + screwed_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg_strayed, + respondent_addr.clone().recipient(), + ))], + ) + }; + // Case three + // This msg will not trigger as it is declared to have to be matched entirely (The message + // type, plus the data of the message). The expected msg and the actual sent msg bear + // different IP addresses. + let (trigger_msg_3_unmatching, cm_setup_3) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), + }; + let type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(4), + body: UiUnmarshalError { + message: "abc".to_string(), + bad_data: "456".to_string(), + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 6, 5, 4)), + }), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg, + respondent_addr.clone().recipient(), + ))], + ), + ) + }; + // Case four + // This msg will trigger as the performed msg is an exact match of the expected msg. + let (trigger_msg_4_matching, cm_setup_4, counter_msg_4) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + }; + let msg_type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(234), + body: UiLogBroadcast { + msg: "Good one".to_string(), + log_level: SerializableLogLevel::Error, + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(trigger_msg.clone()), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + msg_type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg.clone(), + respondent_addr.clone().recipient(), + ))], + ), + counter_msg, + ) + }; + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + // Supplying messages deliberately in a tangled manner to express that the mechanism is + // robust enough to compensate for it. + // This works because we don't supply overlapping setups, such as that could apply to + // a single trigger msg. + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![cm_setup_3, cm_setup_1, cm_setup_2, cm_setup_4], + }) + .unwrap(); + + subject_addr.try_send(trigger_message_1).unwrap(); + subject_addr + .try_send(trigger_msg_3_unmatching.clone()) + .unwrap(); + subject_addr + .try_send(trigger_msg_4_matching.clone()) + .unwrap(); + + system.run(); + // Actual counter-messages that flew in this test + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let _first_counter_msg_recorded = respondent_recording.get_record::(0); + let second_counter_msg_recorded = respondent_recording.get_record::(1); + assert_eq!(second_counter_msg_recorded, &counter_msg_4); + assert_eq!(respondent_recording.len(), 2); + // Recorded trigger messages + let subject_recording = subject_recording_arc.lock().unwrap(); + let _first_recorded_trigger_msg = subject_recording.get_record::(0); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_3_unmatching); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_4_matching); + assert_eq!(subject_recording.len(), 3) + } + + #[test] + fn counter_msgs_evaluate_lazily_so_the_msgs_with_the_same_triggers_are_eliminated_sequentially() + { + // This test demonstrates the need for caution in setups where multiple messages are sent + // at different times and should be responded to by different counter-messages. However, + // the trigger methods of these setups also apply to each other. Which setup gets + // triggered depends purely on the order used to supply them to the recorder + // in SetUpCounterMsgs. + + // Notice that three of the messages share the same data type, with one additional message + // serving a special purpose in assertions. Two of the three use only TypeId for + // identification. This already requires greater caution since you probably need the three + // messages to be dispatched in a specific sequence. However, this wasn't considered + // properly and, as you can see in the test, the trigger messages aren't sent in the same + // order as the counter-message setups were supplied. + + // This results in an inevitable mismatch. The first counter-message that was sent should + // have belonged to the second trigger message, but was triggered by the third trigger + // message (which actually introduces the test). Similarly, the second trigger message + // activates a message rightfully meant for the first trigger message. To complete + // the picture, even the first trigger message is matched with the third counter-message. + + // This shows how important it is to avoid ambiguous setups. When operating with multiple + // calls of the same typed message as triggers, it is highly recommended not to use + // MsgIdentification::ByTypeId but to use more specific, unmistakable settings instead: + // MsgIdentification::ByMatch or MsgIdentification::ByPredicate. + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( + ConfigChangeMsg, + ConfigChangeMsg, + ConfigChangeMsg + )); + let respondent_addr = respondent.start(); + // Case 1 + let (trigger_msg_1, cm_setup_1) = { + let trigger_msg = CrashNotification { + process_id: 7777777, + exit_code: None, + stderr: Some("blah".to_string()), + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateMinHops(Hops::SixHops), + }; + let id_method = MsgIdentification::ByPredicate { + predicate: Box::new(|msg_boxed| { + let msg = msg_boxed.downcast_ref::().unwrap(); + msg.process_id == 1010 + }), + }; + ( + trigger_msg, + // Taking an opportunity to test a setup via the macro allowing more specific + // identification methods. + setup_for_counter_msg_triggered_via_specific_msg_id_method!( + CrashNotification, + id_method, + counter_msg, + &respondent_addr + ), + ) + }; + // Case two + let (trigger_msg_2, cm_setup_2) = { + let trigger_msg = CrashNotification { + process_id: 1010, + exit_code: Some(11), + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdatePassword("betterPassword".to_string()), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) + }; + // Case three + let (trigger_msg_3, cm_setup_3) = { + let trigger_msg = CrashNotification { + process_id: 9999999, + exit_code: None, + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def"), + }), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) + }; + // Case four + let (trigger_msg_4, cm_setup_4) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) + }; + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + // Adding messages in standard order + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![cm_setup_1, cm_setup_2, cm_setup_3, cm_setup_4], + }) + .unwrap(); + + // Now the fun begins, the trigger messages are shuffled + subject_addr.try_send(trigger_msg_3.clone()).unwrap(); + // The fourth message demonstrates that the previous trigger didn't activate two messages + // at once, even though this trigger actually matches two different setups. This shows + // that each trigger can only be matched with one setup at a time, consuming it. If you + // want to trigger multiple messages in response, you must configure that setup with + // multiple counter-messages (a one-to-many scenario). + subject_addr.try_send(trigger_msg_4.clone()).unwrap(); + subject_addr.try_send(trigger_msg_2.clone()).unwrap(); + subject_addr.try_send(trigger_msg_1.clone()).unwrap(); + + system.run(); + // Actual counter-messages that flew in this test + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let first_counter_msg_recorded = respondent_recording.get_record::(0); + assert_eq!( + first_counter_msg_recorded.change, + ConfigChange::UpdatePassword("betterPassword".to_string()) + ); + let _ = respondent_recording.get_record::(1); + let third_counter_msg_recorded = respondent_recording.get_record::(2); + assert_eq!( + third_counter_msg_recorded.change, + ConfigChange::UpdateMinHops(Hops::SixHops) + ); + let fourth_counter_msg_recorded = respondent_recording.get_record::(3); + assert_eq!( + fourth_counter_msg_recorded.change, + ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def") + }) + ); + assert_eq!(respondent_recording.len(), 4); + // Recorded trigger messages + let subject_recording = subject_recording_arc.lock().unwrap(); + let first_recorded_trigger_msg = subject_recording.get_record::(0); + assert_eq!(first_recorded_trigger_msg, &trigger_msg_3); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_4); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_2); + let fourth_recorded_trigger_msg = subject_recording.get_record::(3); + assert_eq!(fourth_recorded_trigger_msg, &trigger_msg_1); + assert_eq!(subject_recording.len(), 4) + } } diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs new file mode 100644 index 000000000..ee56936f6 --- /dev/null +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -0,0 +1,172 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use crate::test_utils::recorder_stop_conditions::{ForcedMatchable, MsgIdentification}; +use actix::{Message, Recipient}; +use std::any::TypeId; +use std::cell::RefCell; +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +// Counter-messages are a powerful tool that allows you to actively simulate communication within +// a system. They enable sending either a single message or multiple messages in response to +// a specific trigger, which is just another Actor message arriving at the Recorder. +// By trigger, we mean the moment when an incoming message is tested sequentially against collected +// identification methods and matches. Each counter-message must have its ID method attached when +// it is being prepared for storage in the Recorder. This bundle is called a setup. Each setup has +// one ID method but can contain multiple counter-messages that are all sent when triggered. + +// Counter-messages can be independently customized and targeted at different actors by +// providing their addresses, supporting complex interaction patterns. This design facilitates +// sophisticated testing scenarios by mimicking real communication flows between multiple Actors. +// The actual preparation of the Recorder needs to be carried out somewhat specifically during the +// late stage of configuring the test, when all participating Actors are already started and their +// addresses are known. The setup for counter-messages must be registered with the appropriate +// Recorder using a specially designated Actor message SetUpCounterMsgs. + +// If a trigger message matches multiple counter-message setups, the triggered setup depends +// on the order in which setups are provided. Consider using MsgIdentification::ByMatch +// or MsgIdentification::ByPredicate instead of MsgIdentification::ByTypeId to avoid confusion +// about setup ordering. + +pub trait CounterMsgGear: Send { + fn try_send(&self); +} + +pub struct SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + msg_opt: RefCell>, + recipient: Recipient, +} + +impl CounterMsgGear for SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + fn try_send(&self) { + let msg = self.msg_opt.take().unwrap(); + self.recipient.try_send(msg).unwrap() + } +} + +impl SendableCounterMsgWithRecipient +where + Msg: Message + Send + 'static, + Msg::Result: Send, +{ + pub fn new(msg: Msg, recipient: Recipient) -> SendableCounterMsgWithRecipient { + Self { + msg_opt: RefCell::new(Some(msg)), + recipient, + } + } +} + +pub struct SingleTypeCounterMsgSetup { + // Leave them private + trigger_msg_type_id: TriggerMsgTypeId, + trigger_msg_id_method: MsgIdentification, + // Responding by multiple outbound messages to a single incoming (trigger) message is supported. + // (Imitates a message handler whose execution implies a couple of message dispatches) + msg_gears: Vec>, +} + +impl SingleTypeCounterMsgSetup { + pub fn new( + trigger_msg_type_id: TriggerMsgTypeId, + trigger_msg_id_method: MsgIdentification, + msg_gears: Vec>, + ) -> Self { + Self { + trigger_msg_type_id, + trigger_msg_id_method, + msg_gears, + } + } +} + +pub type TriggerMsgTypeId = TypeId; + +#[derive(Default)] +pub struct CounterMessages { + msgs: HashMap>, +} + +impl CounterMessages { + pub fn search_for_msg_gear( + &mut self, + trigger_msg: &Msg, + ) -> Option>> + where + Msg: ForcedMatchable + 'static, + { + let type_id = trigger_msg.trigger_msg_type_id(); + if let Some(msgs_vec) = self.msgs.get_mut(&type_id) { + msgs_vec + .iter_mut() + .position(|cm_setup| { + cm_setup + .trigger_msg_id_method + .resolve_condition(trigger_msg) + }) + .map(|idx| msgs_vec.remove(idx).msg_gears) + } else { + None + } + } + + pub fn add_msg(&mut self, counter_msg_setup: SingleTypeCounterMsgSetup) { + let type_id = counter_msg_setup.trigger_msg_type_id; + match self.msgs.entry(type_id) { + Entry::Occupied(mut existing_vec) => existing_vec.get_mut().push(counter_msg_setup), + Entry::Vacant(vacancy) => { + vacancy.insert(vec![counter_msg_setup]); + } + } + } +} + +// Note that you're not limited to triggering only one message at a time, but you can supply more +// messages to this macro, all triggered by the same type id. +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_type_id{ + ($trigger_msg_type: ty, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + + crate::setup_for_counter_msg_triggered_via_specific_msg_id_method!( + $trigger_msg_type, + MsgIdentification::ByType(TypeId::of::<$trigger_msg_type>()), + $($owned_counter_msg, $respondent_actor_addr_ref),+ + ) + }; +} + +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_specific_msg_id_method{ + ($trigger_msg_type: ty, $msg_id_method: expr, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + // This macro returns a block of operations. That's why it begins with these curly brackets + { + let msg_gears: Vec< + Box + > = vec![ + // This part can be repeated as long as there are more expression pairs suplied + $(Box::new( + crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient::new( + $owned_counter_msg, + $respondent_actor_addr_ref.clone().recipient() + ) + )),+ + ]; + + SingleTypeCounterMsgSetup::new( + TypeId::of::<$trigger_msg_type>(), + $msg_id_method, + msg_gears + ) + } + }; +} diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index b3dca287d..9a3214eea 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] @@ -6,16 +6,21 @@ use itertools::Itertools; use std::any::{Any, TypeId}; pub enum StopConditions { - Any(Vec), - All(Vec), + Any(Vec), + // Single message can eliminate _multiple_ ID Methods (previously stop conditions) by matching + // on them. + AllGreedily(Vec), + // Single message can eliminate _only one_ ID Method (previously stop conditions) by matching + // on them. To remove others, a new message must be received. + AllLazily(Vec), } -pub enum StopCondition { - StopOnType(TypeId), - StopOnMatch { +pub enum MsgIdentification { + ByType(TypeId), + ByMatch { exemplar: BoxedMsgExpected, }, - StopOnPredicate { + ByPredicate { predicate: Box bool + Send>, }, } @@ -24,43 +29,48 @@ pub type BoxedMsgExpected = Box; pub type RefMsgExpected<'a> = &'a (dyn Any + Send); impl StopConditions { - pub fn resolve_stop_conditions + Send + 'static>( + pub fn resolve_stop_conditions + Send + 'static>( &mut self, - msg: &T, + msg: &Msg, ) -> bool { match self { - StopConditions::Any(conditions) => Self::resolve_any::(conditions, msg), - StopConditions::All(conditions) => Self::resolve_all::(conditions, msg), + StopConditions::Any(conditions) => Self::resolve_any::(conditions, msg), + StopConditions::AllGreedily(conditions) => { + Self::resolve_all_greedily::(conditions, msg) + } + StopConditions::AllLazily(conditions) => { + Self::resolve_all_lazily::(conditions, msg) + } } } - fn resolve_any + Send + 'static>( - conditions: &Vec, - msg: &T, + fn resolve_any + Send + 'static>( + conditions: &Vec, + msg: &Msg, ) -> bool { conditions .iter() - .any(|condition| condition.resolve_condition::(msg)) + .any(|condition| condition.resolve_condition::(msg)) } - fn resolve_all + Send + 'static>( - conditions: &mut Vec, - msg: &T, + fn resolve_all_greedily + Send + 'static>( + conditions: &mut Vec, + msg: &Msg, ) -> bool { let indexes_to_remove = Self::indexes_of_matched_conditions(conditions, msg); Self::remove_matched_conditions(conditions, indexes_to_remove); conditions.is_empty() } - fn indexes_of_matched_conditions + Send + 'static>( - conditions: &[StopCondition], - msg: &T, + fn indexes_of_matched_conditions + Send + 'static>( + conditions: &[MsgIdentification], + msg: &Msg, ) -> Vec { conditions .iter() .enumerate() .fold(vec![], |mut acc, (idx, condition)| { - let matches = condition.resolve_condition::(msg); + let matches = condition.resolve_condition::(msg); if matches { acc.push(idx) } @@ -68,8 +78,21 @@ impl StopConditions { }) } + fn resolve_all_lazily + Send + 'static>( + conditions: &mut Vec, + msg: &Msg, + ) -> bool { + if let Some(idx) = conditions + .iter() + .position(|condition| condition.resolve_condition::(msg)) + { + conditions.remove(idx); + } + conditions.is_empty() + } + fn remove_matched_conditions( - conditions: &mut Vec, + conditions: &mut Vec, indexes_to_remove: Vec, ) { if !indexes_to_remove.is_empty() { @@ -84,44 +107,42 @@ impl StopConditions { } } -impl StopCondition { - fn resolve_condition + Send + 'static>(&self, msg: &T) -> bool { +impl MsgIdentification { + pub fn resolve_condition + Send + 'static>(&self, msg: &Msg) -> bool { match self { - StopCondition::StopOnType(type_id) => Self::matches_stop_on_type::(msg, *type_id), - StopCondition::StopOnMatch { exemplar } => { - Self::matches_stop_on_match::(exemplar, msg) - } - StopCondition::StopOnPredicate { predicate } => { - Self::matches_stop_on_predicate(predicate.as_ref(), msg) + MsgIdentification::ByType(type_id) => Self::matches_by_type::(msg, *type_id), + MsgIdentification::ByMatch { exemplar } => Self::is_identical::(exemplar, msg), + MsgIdentification::ByPredicate { predicate } => { + Self::matches_by_predicate(predicate.as_ref(), msg) } } } - fn matches_stop_on_type>(msg: &T, expected_type_id: TypeId) -> bool { - let correct_msg_type_id = msg.correct_msg_type_id(); - correct_msg_type_id == expected_type_id + fn matches_by_type>(msg: &Msg, expected_type_id: TypeId) -> bool { + let trigger_msg_type_id = msg.trigger_msg_type_id(); + trigger_msg_type_id == expected_type_id } - fn matches_stop_on_match + 'static + Send>( + fn is_identical + 'static + Send>( exemplar: &BoxedMsgExpected, - msg: &T, + msg: &Msg, ) -> bool { - if let Some(downcast_exemplar) = exemplar.downcast_ref::() { + if let Some(downcast_exemplar) = exemplar.downcast_ref::() { return downcast_exemplar == msg; } false } - fn matches_stop_on_predicate( + fn matches_by_predicate( predicate: &dyn Fn(RefMsgExpected) -> bool, - msg: &T, + msg: &Msg, ) -> bool { predicate(msg as RefMsgExpected) } } -pub trait ForcedMatchable: PartialEq + Send { - fn correct_msg_type_id(&self) -> TypeId; +pub trait ForcedMatchable: PartialEq + Send { + fn trigger_msg_type_id(&self) -> TypeId; } pub struct PretendedMatchableWrapper(pub M); @@ -131,7 +152,7 @@ where OuterM: PartialEq, InnerM: Send, { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::() } } @@ -139,7 +160,7 @@ where impl PartialEq for PretendedMatchableWrapper { fn eq(&self, _other: &Self) -> bool { panic!( - r#"You requested StopCondition::StopOnMatch for message + r#"You requested MsgIdentification::ByMatch for message that does not implement PartialEq. Consider two other options: matching the type simply by its TypeId or using a predicate."# @@ -148,53 +169,59 @@ impl PartialEq for PretendedMatchableWrapper { } #[macro_export] -macro_rules! match_every_type_id{ +macro_rules! match_lazily_every_type_id{ ($($single_message: ident),+) => { - StopConditions::All(vec![$(StopCondition::StopOnType(TypeId::of::<$single_message>())),+]) + StopConditions::AllLazily(vec![ + $( + crate::test_utils::recorder_stop_conditions::MsgIdentification::ByType( + TypeId::of::<$single_message>() + ) + ),+ + ]) } } mod tests { - use crate::accountant::{ResponseSkeleton, ScanError, ScanForPayables}; + use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use masq_lib::messages::ScanType; use std::any::TypeId; - use std::net::{IpAddr, Ipv4Addr}; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::vec; #[test] fn remove_matched_conditions_works_with_unsorted_indexes() { let mut conditions = vec![ - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), ]; let indexes = vec![2, 0]; StopConditions::remove_matched_conditions(&mut conditions, indexes); assert_eq!(conditions.len(), 1); - let type_id = if let StopCondition::StopOnType(type_id) = conditions[0] { + let type_id = if let MsgIdentification::ByType(type_id) = conditions[0] { type_id } else { - panic!("expected StopOnType but got a different variant") + panic!("expected ByType but got a different variant") }; - assert_eq!(type_id, TypeId::of::()) + assert_eq!(type_id, TypeId::of::()) } #[test] fn stop_on_match_works() { - let mut cond1 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond1 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }]); - let mut cond2 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond2 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 8, 6, 4)), }), }]); - let mut cond3 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond3 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(44, 2, 3, 1)), }), @@ -219,7 +246,7 @@ mod tests { #[test] fn stop_on_predicate_works() { - let mut cond_set = StopConditions::All(vec![StopCondition::StopOnPredicate { + let mut cond_set = StopConditions::AllGreedily(vec![MsgIdentification::ByPredicate { predicate: Box::new(|msg| { let scan_err_msg: &ScanError = msg.downcast_ref().unwrap(); scan_err_msg.scan_type == ScanType::PendingPayables @@ -249,12 +276,12 @@ mod tests { #[test] fn match_any_works_with_every_matching_condition_and_no_need_to_take_elements_out() { let mut cond_set = StopConditions::Any(vec![ - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnMatch { + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }, ]); - let first_msg = ScanForPayables { + let first_msg = ScanForNewPayables { response_skeleton_opt: None, }; let second_msg = StartMessage {}; @@ -265,11 +292,16 @@ mod tests { }; let inspect_len_of_any = |cond_set: &StopConditions, msg_number: usize| match cond_set { StopConditions::Any(conditions) => conditions.len(), - StopConditions::All(_) => panic!("stage {}: expected Any but got All", msg_number), + StopConditions::AllGreedily(_) => { + panic!("stage {}: expected Any but got AllGreedily", msg_number) + } + StopConditions::AllLazily(_) => { + panic!("stage {}: expected Any but got AllLazily", msg_number) + } }; assert_eq!( - cond_set.resolve_stop_conditions::(&first_msg), + cond_set.resolve_stop_conditions::(&first_msg), false ); let len_after_stage_1 = inspect_len_of_any(&cond_set, 1); @@ -289,9 +321,9 @@ mod tests { } #[test] - fn match_all_with_conditions_gradually_eliminated_until_vector_is_emptied_and_it_is_match() { - let mut cond_set = StopConditions::All(vec![ - StopCondition::StopOnPredicate { + fn match_all_with_conditions_gradually_eliminated_greedily_until_empty() { + let mut cond_set = StopConditions::AllGreedily(vec![ + MsgIdentification::ByPredicate { predicate: Box::new(|msg| { if let Some(ip_msg) = msg.downcast_ref::() { ip_msg.new_ip.is_ipv4() @@ -300,34 +332,31 @@ mod tests { } }), }, - StopCondition::StopOnMatch { - exemplar: Box::new(ScanForPayables { + MsgIdentification::ByMatch { + exemplar: Box::new(ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, }), }), }, - StopCondition::StopOnType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), ]); - let tested_msg_1 = ScanForPayables { + let tested_msg_1 = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, }), }; - let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); assert_eq!(kill_system, false); - match &cond_set { - StopConditions::All(conds) => { - assert_eq!(conds.len(), 2); - assert!(matches!(conds[0], StopCondition::StopOnPredicate { .. })); - assert!(matches!(conds[1], StopCondition::StopOnType(_))); - } - StopConditions::Any(_) => panic!("Stage 1: expected StopConditions::All, not ...Any"), - } + assert_state_after_greedily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); let tested_msg_2 = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 4, 1)), }; @@ -335,11 +364,109 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); assert_eq!(kill_system, true); + assert_state_after_greedily_matched(2, &cond_set, |conds| assert!(conds.is_empty())) + } + + fn assert_state_after_greedily_matched( + stage: usize, + cond_set: &StopConditions, + apply_assertions: fn(&[MsgIdentification]), + ) { match cond_set { - StopConditions::All(conds) => { - assert!(conds.is_empty()) + StopConditions::AllGreedily(conds) => apply_assertions(conds), + StopConditions::Any(_) => { + panic!("Stage {stage}: expected StopConditions::AllGreedily, not Any") + } + StopConditions::AllLazily(_) => { + panic!("Stage {stage}: expected StopConditions::AllGreedily, not AllLazily") + } + } + } + + #[test] + fn match_all_with_conditions_gradually_eliminated_lazily_until_empty() { + let mut cond_set = StopConditions::AllLazily(vec![ + MsgIdentification::ByPredicate { + predicate: Box::new(|msg| { + if let Some(ip_msg) = msg.downcast_ref::() { + ip_msg.new_ip.is_ipv6() + } else { + false + } + }), + }, + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + ]); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage one + let tested_msg_1 = ScanForNewPayables { + response_skeleton_opt: None, + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 3); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + assert!(matches!(conds[2], MsgIdentification::ByType(_))); + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage two + let tested_msg_2 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(6, 7, 8, 9)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(2, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage three + let tested_msg_3 = NewPublicIp { + new_ip: IpAddr::V6(Ipv6Addr::new(1, 2, 4, 1, 4, 3, 2, 1)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_3); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(3, &cond_set, |conds| { + assert_eq!(conds.len(), 1); + assert!(matches!(conds[0], MsgIdentification::ByType(_))) + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage four + let tested_msg_4 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(45, 45, 45, 45)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_4); + + assert_eq!(kill_system, true); + assert_state_after_lazily_matched(4, &cond_set, |conds| { + assert!(conds.is_empty()); + }); + } + + fn assert_state_after_lazily_matched( + stage: usize, + cond_set: &StopConditions, + apply_assertions: fn(&[MsgIdentification]), + ) { + match &cond_set { + StopConditions::AllLazily(conds) => apply_assertions(conds), + StopConditions::Any(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not Any") + } + StopConditions::AllGreedily(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not AllGreedily") } - StopConditions::Any(_) => panic!("Stage 2: expected StopConditions::All, not ...Any"), } } } diff --git a/node/tests/financials_test.rs b/node/tests/financials_test.rs index 9847efa38..7aff319d2 100644 --- a/node/tests/financials_test.rs +++ b/node/tests/financials_test.rs @@ -13,7 +13,7 @@ use masq_lib::test_utils::utils::{ensure_node_home_directory_exists, open_all_fi use masq_lib::utils::find_free_port; use node_lib::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoReal}; use node_lib::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoReal}; -use node_lib::accountant::db_access_objects::utils::{from_time_t, to_time_t}; +use node_lib::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use node_lib::accountant::gwei_to_wei; use node_lib::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, @@ -30,9 +30,9 @@ fn financials_command_retrieves_payable_and_receivable_records_integration() { let port = find_free_port(); let home_dir = ensure_node_home_directory_exists("integration", test_name); let now = SystemTime::now(); - let timestamp_payable = from_time_t(to_time_t(now) - 678); - let timestamp_receivable_1 = from_time_t(to_time_t(now) - 10000); - let timestamp_receivable_2 = from_time_t(to_time_t(now) - 1111); + let timestamp_payable = from_unix_timestamp(to_unix_timestamp(now) - 678); + let timestamp_receivable_1 = from_unix_timestamp(to_unix_timestamp(now) - 10000); + let timestamp_receivable_2 = from_unix_timestamp(to_unix_timestamp(now) - 1111); let wallet_payable = make_wallet("efef"); let wallet_receivable_1 = make_wallet("abcde"); let wallet_receivable_2 = make_wallet("ccccc");