diff --git a/masq_lib/src/utils.rs b/masq_lib/src/utils.rs index 8d563ef374..9355d0624c 100644 --- a/masq_lib/src/utils.rs +++ b/masq_lib/src/utils.rs @@ -463,6 +463,25 @@ macro_rules! test_only_use { } } +#[macro_export(local_inner_macros)] +macro_rules! btreemap { + () => { + ::std::collections::BTreeMap::new() + }; + ($($key:expr => $val:expr,)+) => { + btreemap!($($key => $val),+) + }; + ($($key:expr => $value:expr),+) => { + { + let mut _btm = ::std::collections::BTreeMap::new(); + $( + let _ = _btm.insert($key, $value); + )* + _btm + } + }; +} + #[macro_export(local_inner_macros)] macro_rules! hashmap { () => { @@ -482,10 +501,30 @@ macro_rules! hashmap { }; } +#[macro_export(local_inner_macros)] +macro_rules! hashset { + () => { + ::std::collections::HashSet::new() + }; + ($($val:expr,)+) => { + hashset!($($val),+) + }; + ($($value:expr),+) => { + { + let mut _hs = ::std::collections::HashSet::new(); + $( + let _ = _hs.insert($value); + )* + _hs + } + }; +} + #[cfg(test)] mod tests { use super::*; - use std::collections::HashMap; + use itertools::Itertools; + use std::collections::{BTreeMap, HashMap, HashSet}; use std::env::current_dir; use std::fmt::Write; use std::fs::{create_dir_all, File, OpenOptions}; @@ -814,7 +853,8 @@ mod tests { let hashmap_with_one_element = hashmap!(1 => 2); let hashmap_with_multiple_elements = hashmap!(1 => 2, 10 => 20, 12 => 42); let hashmap_with_trailing_comma = hashmap!(1 => 2, 10 => 20,); - let hashmap_of_string = hashmap!("key" => "val"); + let hashmap_of_string = hashmap!("key_1" => "val_a", "key_2" => "val_b"); + let hashmap_with_duplicate = hashmap!(1 => 2, 1 => 2); let expected_empty_hashmap: HashMap = HashMap::new(); let mut expected_hashmap_with_one_element = HashMap::new(); @@ -827,7 +867,10 @@ mod tests { expected_hashmap_with_trailing_comma.insert(1, 2); expected_hashmap_with_trailing_comma.insert(10, 20); let mut expected_hashmap_of_string = HashMap::new(); - expected_hashmap_of_string.insert("key", "val"); + expected_hashmap_of_string.insert("key_1", "val_a"); + expected_hashmap_of_string.insert("key_2", "val_b"); + let mut expected_hashmap_with_duplicate = HashMap::new(); + expected_hashmap_with_duplicate.insert(1, 2); assert_eq!(empty_hashmap, expected_empty_hashmap); assert_eq!(hashmap_with_one_element, expected_hashmap_with_one_element); assert_eq!( @@ -839,5 +882,78 @@ mod tests { expected_hashmap_with_trailing_comma ); assert_eq!(hashmap_of_string, expected_hashmap_of_string); + assert_eq!(hashmap_with_duplicate, expected_hashmap_with_duplicate); + } + + #[test] + fn btreemap_macro_works() { + let empty_btm: BTreeMap = btreemap!(); + let btm_with_one_element = btreemap!("ABC" => "234"); + let btm_with_multiple_elements = btreemap!("Bobble" => 2, "Hurrah" => 20, "Boom" => 42); + let btm_with_trailing_comma = btreemap!(12 => 1, 22 =>2,); + let btm_with_duplicate = btreemap!("A"=>123, "A"=>222); + + let expected_empty_btm: BTreeMap = BTreeMap::new(); + let mut expected_btm_with_one_element = BTreeMap::new(); + expected_btm_with_one_element.insert("ABC", "234"); + let mut expected_btm_with_multiple_elements = BTreeMap::new(); + expected_btm_with_multiple_elements.insert("Bobble", 2); + expected_btm_with_multiple_elements.insert("Hurrah", 20); + expected_btm_with_multiple_elements.insert("Boom", 42); + let mut expected_btm_with_trailing_comma = BTreeMap::new(); + expected_btm_with_trailing_comma.insert(12, 1); + expected_btm_with_trailing_comma.insert(22, 2); + let mut expected_btm_with_duplicate = BTreeMap::new(); + expected_btm_with_duplicate.insert("A", 222); + assert_eq!(empty_btm, expected_empty_btm); + assert_eq!(btm_with_one_element, expected_btm_with_one_element); + assert_eq!( + btm_with_multiple_elements, + expected_btm_with_multiple_elements + ); + assert_eq!( + btm_with_multiple_elements.into_iter().collect_vec(), + vec![("Bobble", 2), ("Boom", 42), ("Hurrah", 20)] + ); + assert_eq!(btm_with_trailing_comma, expected_btm_with_trailing_comma); + assert_eq!(btm_with_duplicate, expected_btm_with_duplicate); + } + + #[test] + fn hashset_macro_works() { + let empty_hashset: HashSet = hashset!(); + let hashset_with_one_element = hashset!(2); + let hashset_with_multiple_elements = hashset!(2, 20, 42); + let hashset_with_trailing_comma = hashset!(2, 20,); + let hashset_of_string = hashset!("val_a", "val_b"); + let hashset_with_duplicate = hashset!(2, 2); + + let expected_empty_hashset: HashSet = HashSet::new(); + let mut expected_hashset_with_one_element = HashSet::new(); + expected_hashset_with_one_element.insert(2); + let mut expected_hashset_with_multiple_elements = HashSet::new(); + expected_hashset_with_multiple_elements.insert(2); + expected_hashset_with_multiple_elements.insert(20); + expected_hashset_with_multiple_elements.insert(42); + let mut expected_hashset_with_trailing_comma = HashSet::new(); + expected_hashset_with_trailing_comma.insert(2); + expected_hashset_with_trailing_comma.insert(20); + let mut expected_hashset_of_string = HashSet::new(); + expected_hashset_of_string.insert("val_a"); + expected_hashset_of_string.insert("val_b"); + let mut expected_hashset_with_duplicate = HashSet::new(); + expected_hashset_with_duplicate.insert(2); + assert_eq!(empty_hashset, expected_empty_hashset); + assert_eq!(hashset_with_one_element, expected_hashset_with_one_element); + assert_eq!( + hashset_with_multiple_elements, + expected_hashset_with_multiple_elements + ); + assert_eq!( + hashset_with_trailing_comma, + expected_hashset_with_trailing_comma + ); + assert_eq!(hashset_of_string, expected_hashset_of_string); + assert_eq!(hashset_with_duplicate, expected_hashset_with_duplicate); } } diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 3202807b3d..7a6e509bcb 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -1,11 +1,11 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::utils::{ - DaoFactoryReal, TxHash, TxIdentifiers, VigilantRusqliteFlatten, + DaoFactoryReal, TxHash, TxIdentifiers, TxRecordWithHash, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; -use crate::blockchain::errors::validation_status::PreviousAttempts; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; use itertools::Itertools; use masq_lib::utils::ExpectValue; @@ -73,46 +73,61 @@ impl FromStr for FailureStatus { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum ValidationStatus { - Waiting, - Reattempting(PreviousAttempts), -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct FailedTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub reason: FailureReason, pub status: FailureStatus, } +impl TxRecordWithHash for FailedTx { + fn hash(&self) -> TxHash { + self.hash + } +} + +#[derive(Debug, PartialEq, Eq)] pub enum FailureRetrieveCondition { + ByTxHash(Vec), ByStatus(FailureStatus), + EveryRecheckRequiredRecord, } impl Display for FailureRetrieveCondition { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { + FailureRetrieveCondition::ByTxHash(hashes) => { + write!( + f, + "WHERE tx_hash IN ({})", + comma_joined_stringifiable(hashes, |hash| format!("'{:?}'", hash)) + ) + } FailureRetrieveCondition::ByStatus(status) => { write!(f, "WHERE status = '{}'", status) } + FailureRetrieveCondition::EveryRecheckRequiredRecord => { + write!(f, "WHERE status LIKE 'RecheckRequired%'") + } } } } pub trait FailedPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + //TODO potentially atomically fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError>; fn retrieve_txs(&self, condition: Option) -> Vec; fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError>; } @@ -187,11 +202,11 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { status ) 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_checked = checked_conversion::(tx.amount_minor); + let gas_price_minor_checked = checked_conversion::(tx.gas_price_minor); 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); + BigIntDivider::deconstruct(gas_price_minor_checked); format!( "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}', '{}')", tx.hash, @@ -255,11 +270,11 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { 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 amount_minor = 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 = + let gas_price_minor = 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"); @@ -272,9 +287,9 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { Ok(FailedTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, reason, status, @@ -287,7 +302,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { if status_updates.is_empty() { return Err(FailedPayableDaoError::EmptyInput); @@ -376,15 +391,16 @@ mod tests { }; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, - FailureRetrieveCondition, FailureStatus, ValidationStatus, + FailureRetrieveCondition, FailureStatus, }; 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::accountant::db_access_objects::utils::{current_unix_timestamp, TxRecordWithHash}; + use crate::accountant::test_utils::make_failed_tx; use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClockReal, + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, }; use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::{make_tx_hash, ValidationFailureClockMock}; @@ -470,12 +486,12 @@ mod tests { [FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 0, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ nonce: 0, reason: PendingTooLong, status: RetryRequired }, \ FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 0, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ nonce: 0, reason: PendingTooLong, status: RecheckRequired(Waiting) }]" .to_string() )) @@ -587,6 +603,29 @@ mod tests { assert_eq!(result.get(&another_present_hash), Some(&2u64)); } + #[test] + fn display_for_failure_retrieve_condition_works() { + let tx_hash_1 = make_tx_hash(123); + let tx_hash_2 = make_tx_hash(456); + assert_eq!(FailureRetrieveCondition::ByTxHash(vec![tx_hash_1, tx_hash_2]).to_string(), + "WHERE tx_hash IN ('0x000000000000000000000000000000000000000000000000000000000000007b', \ + '0x00000000000000000000000000000000000000000000000000000000000001c8')" + ); + assert_eq!( + FailureRetrieveCondition::ByStatus(RetryRequired).to_string(), + "WHERE status = '\"RetryRequired\"'" + ); + assert_eq!( + FailureRetrieveCondition::ByStatus(RecheckRequired(ValidationStatus::Waiting)) + .to_string(), + "WHERE status = '{\"RecheckRequired\":\"Waiting\"}'" + ); + assert_eq!( + FailureRetrieveCondition::EveryRecheckRequiredRecord.to_string(), + "WHERE status LIKE 'RecheckRequired%'" + ); + } + #[test] fn failure_reason_from_str_works() { // Submission error @@ -794,7 +833,7 @@ mod tests { (tx3.hash, Concluded), ]); - let result = subject.update_statuses(hashmap); + let result = subject.update_statuses(&hashmap); let updated_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); @@ -815,6 +854,7 @@ mod tests { updated_txs[3].status, RecheckRequired(ValidationStatus::Waiting) ); + assert_eq!(updated_txs.len(), 4); } #[test] @@ -828,7 +868,7 @@ mod tests { .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let result = subject.update_statuses(HashMap::new()); + let result = subject.update_statuses(&HashMap::new()); assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); } @@ -842,7 +882,7 @@ mod tests { let wrapped_conn = make_read_only_db_connection(home_dir); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.update_statuses(HashMap::from([(make_tx_hash(1), Concluded)])); + let result = subject.update_statuses(&HashMap::from([(make_tx_hash(1), Concluded)])); assert_eq!( result, @@ -955,4 +995,14 @@ mod tests { )) ) } + + #[test] + fn tx_record_with_hash_is_implemented_for_failed_tx() { + let failed_tx = make_failed_tx(1234); + let hash = failed_tx.hash; + + let hash_from_trait = failed_tx.hash(); + + assert_eq!(hash_from_trait, hash); + } } diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index ae165909a9..0141e87968 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -3,8 +3,8 @@ 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_and_failed_payable_data_conversion; 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 c7d438a418..0226c0c68a 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -1,24 +1,24 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::{ - PendingPayableRowid, WalletAddress, -}; -use crate::accountant::db_big_integer::big_int_db_processor::{BigIntDbProcessor, BigIntDbProcessorReal, BigIntSqlConfig, DisplayableRusqliteParamPair, ParamByUse, SQLParamsBuilder, TableNameDAO, WeiChange, WeiChangeDirection}; -use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_access_objects::utils; use crate::accountant::db_access_objects::utils::{ sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, - RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten, + RangeStmConfig, RowId, TopStmConfig, TxHash, VigilantRusqliteFlatten, }; -use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::{ - compose_case_expression, execute_command, serialize_wallets, +use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; +use crate::accountant::db_big_integer::big_int_db_processor::{ + BigIntDbProcessor, BigIntDbProcessorReal, BigIntSqlConfig, DisplayableRusqliteParamPair, + ParamByUse, SQLParamsBuilder, TableNameDAO, WeiChange, WeiChangeDirection, }; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, sign_conversion, PendingPayableId}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; +use ethabi::Address; #[cfg(test)] use ethereum_types::{BigEndianHash, U256}; +use itertools::Either; use masq_lib::utils::ExpectValue; #[cfg(test)] use rusqlite::OptionalExtension; @@ -26,7 +26,6 @@ use rusqlite::{Error, Row}; use std::fmt::Debug; use std::str::FromStr; use std::time::SystemTime; -use itertools::Either; use web3::types::H256; #[derive(Debug, PartialEq, Eq)] @@ -48,18 +47,15 @@ pub trait PayableDao: Debug + Send { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError>; fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], + mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError>; - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], - ) -> Result<(), PayableDaoError>; + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError>; fn non_pending_payables(&self) -> Vec; @@ -81,6 +77,11 @@ impl PayableDaoFactory for DaoFactoryReal { } } +pub struct MarkPendingPayableID { + pub receiver_wallet: Address, + pub rowid: RowId, +} + #[derive(Debug)] pub struct PayableDaoReal { conn: Box, @@ -92,7 +93,7 @@ impl PayableDao for PayableDaoReal { &self, timestamp: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError> { let main_sql = "insert into payable (wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid) \ values (:wallet, :balance_high_b, :balance_low_b, :last_paid_timestamp, null) on conflict (wallet_address) do update set \ @@ -105,7 +106,7 @@ impl PayableDao for PayableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( @@ -123,46 +124,42 @@ impl PayableDao for PayableDaoReal { fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], + _mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError> { - if wallets_and_rowids.is_empty() { - panic!("broken code: empty input is not permit to enter this method") - } - - let case_expr = compose_case_expression(wallets_and_rowids); - let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); - //the Wallet type is secure against SQL injections - let sql = format!( - "update payable set \ - pending_payable_rowid = {} \ - where - pending_payable_rowid is null and wallet_address in ({}) - returning - pending_payable_rowid", - case_expr, wallets, - ); - execute_command(&*self.conn, wallets_and_rowids, &sql) + todo!("Will be an object of removal in GH-662") + // if wallets_and_rowids.is_empty() { + // panic!("broken code: empty input is not permit to enter this method") + // } + // + // let case_expr = compose_case_expression(wallets_and_rowids); + // let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); + // //the Wallet type is secure against SQL injections + // let sql = format!( + // "update payable set \ + // pending_payable_rowid = {} \ + // where + // pending_payable_rowid is null and wallet_address in ({}) + // returning + // pending_payable_rowid", + // case_expr, wallets, + // ); + // execute_command(&*self.conn, wallets_and_rowids, &sql) } - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], - ) -> Result<(), PayableDaoError> { - confirmed_payables.iter().try_for_each(|pending_payable_fingerprint| { - + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError> { + confirmed_payables.iter().try_for_each(|confirmed_payable| { let main_sql = "update payable set \ balance_high_b = balance_high_b + :balance_high_b, balance_low_b = balance_low_b + :balance_low_b, \ - last_paid_timestamp = :last_paid, pending_payable_rowid = null where pending_payable_rowid = :rowid"; + last_paid_timestamp = :last_paid, pending_payable_rowid = null where wallet_address = :wallet"; let update_clause_with_compensated_overflow = "update payable set \ balance_high_b = :balance_high_b, balance_low_b = :balance_low_b, last_paid_timestamp = :last_paid, \ - pending_payable_rowid = null where pending_payable_rowid = :rowid"; + pending_payable_rowid = null where wallet_address = :wallet"; - let i64_rowid = checked_conversion::(pending_payable_fingerprint.rowid); - let last_paid = to_unix_timestamp(pending_payable_fingerprint.timestamp); + let wallet = format!("{:?}", confirmed_payable.receiver_address); let params = SQLParamsBuilder::default() - .key( PendingPayableRowid(&i64_rowid)) - .wei_change(WeiChange::new( "balance", pending_payable_fingerprint.amount, WeiChangeDirection::Subtraction)) - .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &last_paid))]) + .key( WalletAddress(&wallet)) + .wei_change(WeiChange::new("balance", confirmed_payable.amount_minor, WeiChangeDirection::Subtraction)) + .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &confirmed_payable.timestamp))]) .build(); self.big_int_db_processor.execute(Either::Left(self.conn.as_ref()), BigIntSqlConfig::new( @@ -389,175 +386,182 @@ impl TableNameDAO for PayableDaoReal { } } -mod mark_pending_payable_associated_functions { - use crate::accountant::comma_joined_stringifiable; - use crate::accountant::db_access_objects::payable_dao::PayableDaoError; - use crate::accountant::db_access_objects::utils::{ - update_rows_and_return_valid_count, VigilantRusqliteFlatten, - }; - use crate::database::rusqlite_wrappers::ConnectionWrapper; - use crate::sub_lib::wallet::Wallet; - use itertools::Itertools; - use rusqlite::Row; - use std::fmt::Display; - - pub fn execute_command( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - sql: &str, - ) -> Result<(), PayableDaoError> { - let mut stm = conn.prepare(sql).expect("Internal Error"); - let validator = validate_row_updated; - let rows_affected_res = update_rows_and_return_valid_count(&mut stm, validator); - - match rows_affected_res { - Ok(rows_affected) => match rows_affected { - num if num == wallets_and_rowids.len() => Ok(()), - num => mismatched_row_count_panic(conn, wallets_and_rowids, num), - }, - Err(errs) => { - let err_msg = format!( - "Multi-row update to mark pending payable hit these errors: {:?}", - errs - ); - Err(PayableDaoError::RusqliteError(err_msg)) - } - } - } - - pub fn compose_case_expression(wallets_and_rowids: &[(&Wallet, u64)]) -> String { - //the Wallet type is secure against SQL injections - fn when_clause((wallet, rowid): &(&Wallet, u64)) -> String { - format!("when wallet_address = '{wallet}' then {rowid}") - } - - format!( - "case {} end", - wallets_and_rowids.iter().map(when_clause).join("\n") - ) - } - - pub fn serialize_wallets( - wallets_and_rowids: &[(&Wallet, u64)], - quotes_opt: Option, - ) -> String { - wallets_and_rowids - .iter() - .map(|(wallet, _)| match quotes_opt { - Some(char) => format!("{}{}{}", char, wallet, char), - None => wallet.to_string(), - }) - .join(", ") - } - - fn validate_row_updated(row: &Row) -> Result { - row.get::>(0).map(|opt| opt.is_some()) - } - - fn mismatched_row_count_panic( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - actual_count: usize, - ) -> ! { - let serialized_wallets = serialize_wallets(wallets_and_rowids, None); - let expected_count = wallets_and_rowids.len(); - let extension = explanatory_extension(conn, wallets_and_rowids); - panic!( - "Marking pending payable rowid for wallets {serialized_wallets} affected \ - {actual_count} rows but expected {expected_count}. {extension}" - ) - } - - pub(super) fn explanatory_extension( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> String { - let resulting_pairs_collection = - query_resulting_pairs_of_wallets_and_rowids(conn, wallets_and_rowids); - let resulting_pairs_summary = if resulting_pairs_collection.is_empty() { - "".to_string() - } else { - pairs_in_pretty_string(&resulting_pairs_collection, |rowid_opt: &Option| { - match rowid_opt { - Some(rowid) => Box::new(*rowid), - None => Box::new("N/A"), - } - }) - }; - let wallets_and_non_optional_rowids = - pairs_in_pretty_string(wallets_and_rowids, |rowid: &u64| Box::new(*rowid)); - format!( - "\ - The demanded data according to {} looks different from the resulting state {}!. Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ - points to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ - probably had not managed to complete successfully before another payment was requested: preventive measures failed.\n", - wallets_and_non_optional_rowids, resulting_pairs_summary) - } - - fn query_resulting_pairs_of_wallets_and_rowids( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> Vec<(Wallet, Option)> { - let select_dealt_accounts = - format!( - "select wallet_address, pending_payable_rowid from payable where wallet_address in ({})", - serialize_wallets(wallets_and_rowids, Some('\'')) - ); - let row_processor = |row: &Row| { - Ok(( - row.get::(0) - .expect("database corrupt: wallet addresses found in bad format"), - row.get::>(1) - .expect("database_corrupt: rowid found in bad format"), - )) - }; - conn.prepare(&select_dealt_accounts) - .expect("select failed") - .query_map([], row_processor) - .expect("no args yet binding failed") - .vigilant_flatten() - .collect() - } - - fn pairs_in_pretty_string( - pairs: &[(W, R)], - rowid_pretty_writer: fn(&R) -> Box, - ) -> String { - comma_joined_stringifiable(pairs, |(wallet, rowid)| { - format!( - "( Wallet: {}, Rowid: {} )", - wallet, - rowid_pretty_writer(rowid) - ) - }) - } -} +// TODO Will be an object of removal in GH-662 +// mod mark_pending_payable_associated_functions { +// use crate::accountant::comma_joined_stringifiable; +// use crate::accountant::db_access_objects::payable_dao::{MarkPendingPayableID, PayableDaoError}; +// use crate::accountant::db_access_objects::utils::{ +// update_rows_and_return_valid_count, VigilantRusqliteFlatten, +// }; +// use crate::database::rusqlite_wrappers::ConnectionWrapper; +// use crate::sub_lib::wallet::Wallet; +// use itertools::Itertools; +// use rusqlite::Row; +// use std::fmt::Display; +// +// pub fn execute_command( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// sql: &str, +// ) -> Result<(), PayableDaoError> { +// let mut stm = conn.prepare(sql).expect("Internal Error"); +// let validator = validate_row_updated; +// let rows_affected_res = update_rows_and_return_valid_count(&mut stm, validator); +// +// match rows_affected_res { +// Ok(rows_affected) => match rows_affected { +// num if num == wallets_and_rowids.len() => Ok(()), +// num => mismatched_row_count_panic(conn, wallets_and_rowids, num), +// }, +// Err(errs) => { +// let err_msg = format!( +// "Multi-row update to mark pending payable hit these errors: {:?}", +// errs +// ); +// Err(PayableDaoError::RusqliteError(err_msg)) +// } +// } +// } +// +// pub fn compose_case_expression(wallets_and_rowids: &[(&Wallet, u64)]) -> String { +// //the Wallet type is secure against SQL injections +// fn when_clause((wallet, rowid): &(&Wallet, u64)) -> String { +// format!("when wallet_address = '{wallet}' then {rowid}") +// } +// +// format!( +// "case {} end", +// wallets_and_rowids.iter().map(when_clause).join("\n") +// ) +// } +// +// pub fn serialize_wallets( +// wallets_and_rowids: &[MarkPendingPayableID], +// quotes_opt: Option, +// ) -> String { +// wallets_and_rowids +// .iter() +// .map(|(wallet, _)| match quotes_opt { +// Some(char) => format!("{}{}{}", char, wallet, char), +// None => wallet.to_string(), +// }) +// .join(", ") +// } +// +// fn validate_row_updated(row: &Row) -> Result { +// row.get::>(0).map(|opt| opt.is_some()) +// } +// +// fn mismatched_row_count_panic( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// actual_count: usize, +// ) -> ! { +// let serialized_wallets = serialize_wallets(wallets_and_rowids, None); +// let expected_count = wallets_and_rowids.len(); +// let extension = explanatory_extension(conn, wallets_and_rowids); +// panic!( +// "Marking pending payable rowid for wallets {serialized_wallets} affected \ +// {actual_count} rows but expected {expected_count}. {extension}" +// ) +// } +// +// pub(super) fn explanatory_extension( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// ) -> String { +// let resulting_pairs_collection = +// query_resulting_pairs_of_wallets_and_rowids(conn, wallets_and_rowids); +// let resulting_pairs_summary = if resulting_pairs_collection.is_empty() { +// "".to_string() +// } else { +// pairs_in_pretty_string(&resulting_pairs_collection, |rowid_opt: &Option| { +// match rowid_opt { +// Some(rowid) => Box::new(*rowid), +// None => Box::new("N/A"), +// } +// }) +// }; +// let wallets_and_non_optional_rowids = +// pairs_in_pretty_string(wallets_and_rowids, |rowid: &u64| Box::new(*rowid)); +// format!( +// "\ +// The demanded data according to {} looks different from the resulting state {}!. Operation failed.\n\ +// Notes:\n\ +// a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ +// b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ +// points to figure out if you were put in danger of double payment,\n\ +// c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ +// The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ +// probably had not managed to complete successfully before another payment was requested: preventive measures failed.\n", +// wallets_and_non_optional_rowids, resulting_pairs_summary) +// } +// +// fn query_resulting_pairs_of_wallets_and_rowids( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// ) -> Vec<(Wallet, Option)> { +// let select_dealt_accounts = +// format!( +// "select wallet_address, pending_payable_rowid from payable where wallet_address in ({})", +// serialize_wallets(wallets_and_rowids, Some('\'')) +// ); +// let row_processor = |row: &Row| { +// Ok(( +// row.get::(0) +// .expect("database corrupt: wallet addresses found in bad format"), +// row.get::>(1) +// .expect("database_corrupt: rowid found in bad format"), +// )) +// }; +// conn.prepare(&select_dealt_accounts) +// .expect("select failed") +// .query_map([], row_processor) +// .expect("no args yet binding failed") +// .vigilant_flatten() +// .collect() +// } +// +// fn pairs_in_pretty_string( +// pairs: &[(W, R)], +// rowid_pretty_writer: fn(&R) -> Box, +// ) -> String { +// comma_joined_stringifiable(pairs, |(wallet, rowid)| { +// format!( +// "( Wallet: {}, Rowid: {} )", +// wallet, +// rowid_pretty_writer(rowid) +// ) +// }) +// } +// } #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::{from_unix_timestamp, current_unix_timestamp, to_unix_timestamp}; + use crate::accountant::db_access_objects::sent_payable_dao::SentTx; + use crate::accountant::db_access_objects::utils::{ + current_unix_timestamp, from_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}; + use crate::accountant::test_utils::{ + assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_sent_tx, + trick_rusqlite_with_read_only_conn, + }; use crate::blockchain::test_utils::make_tx_hash; - use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; + use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::test_utils::make_wallet; + use itertools::Itertools; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::ToSql; use rusqlite::{Connection, OpenFlags}; - use rusqlite::{ToSql}; use std::path::Path; use std::str::FromStr; - use crate::database::test_utils::ConnectionWrapperMock; + use std::time::Duration; #[test] fn more_money_payable_works_for_new_address() { @@ -704,260 +708,271 @@ mod tests { fn mark_pending_payables_marks_pending_transactions_for_new_addresses() { //the extra unchanged record checks the safety of right count of changed rows; //experienced serious troubles in the past - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_marks_pending_transactions_for_new_addresses", - ); - let wallet_0 = make_wallet("wallet"); - let wallet_1 = make_wallet("booga"); - let pending_payable_rowid_1 = 656; - let wallet_2 = make_wallet("bagaboo"); - let pending_payable_rowid_2 = 657; - let boxed_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ - last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; - let mut stm = boxed_conn.prepare(insert).unwrap(); - let params = [ - [&wallet_0 as &dyn ToSql, &12345, &1, &45678], - [&wallet_1, &0, &i64::MAX, &150_000_000], - [&wallet_2, &3, &0, &151_000_000], - ] - .into_iter() - .flatten() - .collect::>(); - stm.execute(params.as_slice()).unwrap(); - } - let subject = PayableDaoReal::new(boxed_conn); - - subject - .mark_pending_payables_rowids(&[ - (&wallet_1, pending_payable_rowid_1), - (&wallet_2, pending_payable_rowid_2), - ]) - .unwrap(); - - let account_statuses = [&wallet_0, &wallet_1, &wallet_2] - .iter() - .map(|wallet| subject.account_status(wallet).unwrap()) - .collect::>(); - assert_eq!( - account_statuses, - vec![ - PayableAccount { - wallet: wallet_0, - balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), - 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_unix_timestamp(150_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_1, - make_tx_hash(0) - )), - }, - //notice the hashes are garbage generated by a test method not knowing doing better - PayableAccount { - wallet: wallet_2, - balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), - last_paid_timestamp: from_unix_timestamp(151_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_2, - make_tx_hash(0) - )) - } - ] - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_marks_pending_transactions_for_new_addresses", + // ); + // let wallet_0 = make_wallet("wallet"); + // let wallet_1 = make_wallet("booga"); + // let pending_payable_rowid_1 = 656; + // let wallet_2 = make_wallet("bagaboo"); + // let pending_payable_rowid_2 = 657; + // let boxed_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // { + // let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ + // last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; + // let mut stm = boxed_conn.prepare(insert).unwrap(); + // let params = [ + // [&wallet_0 as &dyn ToSql, &12345, &1, &45678], + // [&wallet_1, &0, &i64::MAX, &150_000_000], + // [&wallet_2, &3, &0, &151_000_000], + // ] + // .into_iter() + // .flatten() + // .collect::>(); + // stm.execute(params.as_slice()).unwrap(); + // } + // let subject = PayableDaoReal::new(boxed_conn); + // + // subject + // .mark_pending_payables_rowids(&[ + // (&wallet_1, pending_payable_rowid_1), + // (&wallet_2, pending_payable_rowid_2), + // ]) + // .unwrap(); + // + // let account_statuses = [&wallet_0, &wallet_1, &wallet_2] + // .iter() + // .map(|wallet| subject.account_status(wallet).unwrap()) + // .collect::>(); + // assert_eq!( + // account_statuses, + // vec![ + // PayableAccount { + // wallet: wallet_0, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), + // 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_unix_timestamp(150_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_1, + // make_tx_hash(0) + // )), + // }, + // //notice the hashes are garbage generated by a test method not knowing doing better + // PayableAccount { + // wallet: wallet_2, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(151_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_2, + // make_tx_hash(0) + // )) + // } + // ] + // ) } #[test] - #[should_panic(expected = "\ - Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ - 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ - The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ - ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ - points to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ - probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] + // #[should_panic(expected = "\ + // Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ + // 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ + // ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ + // points to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ + // probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] fn mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified( ) { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let first_wallet = make_wallet("booga"); - let first_rowid = 456; - insert_payable_record_fn( - &*conn, - &first_wallet.to_string(), - 123456, - 789789, - Some(first_rowid), - ); - let subject = PayableDaoReal::new(conn); - - let _ = subject.mark_pending_payables_rowids(&[ - (&first_wallet, first_rowid as u64), - (&make_wallet("yahoo"), 789), - ]); + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let first_wallet = make_wallet("booga"); + // let first_rowid = 456; + // insert_payable_record_fn( + // &*conn, + // &first_wallet.to_string(), + // 123456, + // 789789, + // Some(first_rowid), + // ); + // let subject = PayableDaoReal::new(conn); + // + // let _ = subject.mark_pending_payables_rowids(&[ + // (&first_wallet, first_rowid as u64), + // (&make_wallet("yahoo"), 789), + // ]); } #[test] fn explanatory_extension_shows_resulting_account_with_unpopulated_rowid() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", - ); - let wallet_1 = make_wallet("hooga"); - let rowid_1 = 550; - let wallet_2 = make_wallet("booga"); - let rowid_2 = 555; - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let record_seeds = [ - (&wallet_1.to_string(), 12345, 1_000_000_000, None), - (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), - ]; - record_seeds - .into_iter() - .for_each(|(wallet, balance, timestamp, rowid_opt)| { - insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) - }); - - let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); - - assert_eq!(result, "\ - The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ - ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ - ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ - Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double \ - payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be \ - suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for \ - this account\nprobably had not managed to complete successfully before another payment was \ - requested: preventive measures failed.\n".to_string()) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", + // ); + // let wallet_1 = make_wallet("hooga"); + // let rowid_1 = 550; + // let wallet_2 = make_wallet("booga"); + // let rowid_2 = 555; + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let record_seeds = [ + // (&wallet_1.to_string(), 12345, 1_000_000_000, None), + // (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), + // ]; + // record_seeds + // .into_iter() + // .for_each(|(wallet, balance, timestamp, rowid_opt)| { + // insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) + // }); + // + // let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); + // + // assert_eq!(result, "\ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ + // ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ + // ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ + // Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double \ + // payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be \ + // suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for \ + // this account\nprobably had not managed to complete successfully before another payment was \ + // requested: preventive measures failed.\n".to_string()) } #[test] fn mark_pending_payables_rowids_handles_general_sql_error() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_handles_general_sql_error", - ); - let wallet = make_wallet("booga"); - let rowid = 656; - let conn = payable_read_only_conn(&home_dir); - let conn_wrapped = ConnectionWrapperReal::new(conn); - let subject = PayableDaoReal::new(Box::new(conn_wrapped)); - - let result = subject.mark_pending_payables_rowids(&[(&wallet, rowid)]); - - assert_eq!( - result, - Err(PayableDaoError::RusqliteError( - "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ - Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ - database\"))]" - .to_string() - )) - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_handles_general_sql_error", + // ); + // let wallet = make_wallet("booga"); + // let rowid = 656; + // let single_mark_instruction = MarkPendingPayableID::new(wallet.address(), rowid); + // let conn = payable_read_only_conn(&home_dir); + // let conn_wrapped = ConnectionWrapperReal::new(conn); + // let subject = PayableDaoReal::new(Box::new(conn_wrapped)); + // + // let result = subject.mark_pending_payables_rowids(&[single_mark_instruction]); + // + // assert_eq!( + // result, + // Err(PayableDaoError::RusqliteError( + // "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ + // Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ + // database\"))]" + // .to_string() + // )) + // ) } #[test] - #[should_panic(expected = "broken code: empty input is not permit to enter this method")] + //#[should_panic(expected = "broken code: empty input is not permit to enter this method")] fn mark_pending_payables_rowids_is_strict_about_empty_input() { - let wrapped_conn = ConnectionWrapperMock::default(); - let subject = PayableDaoReal::new(Box::new(wrapped_conn)); - - let _ = subject.mark_pending_payables_rowids(&[]); + // TODO Will be an object of removal in GH-662 + // let wrapped_conn = ConnectionWrapperMock::default(); + // let subject = PayableDaoReal::new(Box::new(wrapped_conn)); + // + // let _ = subject.mark_pending_payables_rowids(&[]); } struct TestSetupValuesHolder { - fingerprint_1: PendingPayableFingerprint, - fingerprint_2: PendingPayableFingerprint, - wallet_1: Wallet, - wallet_2: Wallet, - previous_timestamp_1: SystemTime, - previous_timestamp_2: SystemTime, + account_1: TxWalletAndTimestamp, + account_2: TxWalletAndTimestamp, + } + + struct TxWalletAndTimestamp { + pending_payable: SentTx, + previous_timestamp: SystemTime, } - fn make_fingerprint_pair_and_insert_initial_payable_records( + struct TestInputs { + hash: TxHash, + previous_timestamp: SystemTime, + new_payable_timestamp: SystemTime, + receiver_wallet: Address, + initial_amount_wei: u128, + balance_change: u128, + } + + fn insert_initial_payable_records_and_return_sent_txs( conn: &dyn ConnectionWrapper, - initial_amount_1: u128, - initial_amount_2: u128, - balance_change_1: u128, - balance_change_2: u128, + (initial_amount_1, balance_change_1): (u128, u128), + (initial_amount_2, balance_change_2): (u128, u128), ) -> TestSetupValuesHolder { - 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_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_unix_timestamp(191_333_000); - let wallet_2 = make_wallet("booble bobble"); - { + let now = SystemTime::now(); + let (account_1, account_2) = [ + TestInputs { + hash: make_tx_hash(12345), + previous_timestamp: now.checked_sub(Duration::from_secs(45_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("bobbles").address(), + initial_amount_wei: initial_amount_1, + balance_change: balance_change_1, + }, + TestInputs { + hash: make_tx_hash(54321), + previous_timestamp: now.checked_sub(Duration::from_secs(22_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("yet more bobbles").address(), + initial_amount_wei: initial_amount_2, + balance_change: balance_change_2, + }, + ] + .into_iter() + .enumerate() + .map(|(idx, test_inputs)| { insert_payable_record_fn( conn, - &wallet_1.to_string(), - i128::try_from(initial_amount_1).unwrap(), - previous_timestamp_1_s, - Some(rowid_1 as i64), + &format!("{:?}", test_inputs.receiver_wallet), + i128::try_from(test_inputs.initial_amount_wei).unwrap(), + to_unix_timestamp(test_inputs.previous_timestamp), + // TODO argument will be eliminated in GH-662 + None, ); - insert_payable_record_fn( - conn, - &wallet_2.to_string(), - i128::try_from(initial_amount_2).unwrap(), - previous_timestamp_2_s, - Some(rowid_2 as i64), - ) - } - let fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: new_payable_timestamp_1, - hash: hash_1, - attempt: 1, - amount: balance_change_1, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: new_payable_timestamp_2, - hash: hash_2, - attempt: 1, - amount: balance_change_2, - process_error: None, - }; - let previous_timestamp_1 = from_unix_timestamp(previous_timestamp_1_s); - let previous_timestamp_2 = from_unix_timestamp(previous_timestamp_2_s); + let mut sent_tx = make_sent_tx((idx as u64 + 1) * 1234); + sent_tx.hash = test_inputs.hash; + sent_tx.amount_minor = test_inputs.balance_change; + sent_tx.receiver_address = test_inputs.receiver_wallet; + sent_tx.timestamp = to_unix_timestamp(test_inputs.new_payable_timestamp); + sent_tx.amount_minor = test_inputs.balance_change; + + TxWalletAndTimestamp { + pending_payable: sent_tx, + previous_timestamp: test_inputs.previous_timestamp, + } + }) + .collect_tuple() + .unwrap(); + TestSetupValuesHolder { - fingerprint_1, - fingerprint_2, - wallet_1, - wallet_2, - previous_timestamp_1, - previous_timestamp_2, + account_1, + account_2, } } @@ -968,7 +983,7 @@ mod tests { //initial (1, 9999) let initial_changing_end_resulting_values = (initial, 11111, initial as u128 - 11111); //change (-1, abs(i64::MIN) - 11111) - transaction_confirmed_works( + test_transaction_confirmed_works( "transaction_confirmed_works_without_overflow", initial_changing_end_resulting_values, ) @@ -981,77 +996,80 @@ mod tests { //initial (0, 10000) //change (-1, abs(i64::MIN) - 111) //10000 + (abs(i64::MIN) - 111) > i64::MAX -> overflow - transaction_confirmed_works( + test_transaction_confirmed_works( "transaction_confirmed_works_hitting_overflow", initial_changing_end_resulting_values, ) } - fn transaction_confirmed_works( + fn test_transaction_confirmed_works( test_name: &str, (initial_amount_1, balance_change_1, expected_balance_after_1): (u128, u128, u128), ) { let home_dir = ensure_node_home_directory_exists("payable_dao", test_name); - //a hardcoded set that just makes a complement to the crucial, supplied one; this points to the ability of - //handling multiple transactions together + // A hardcoded set that just makes a complement to the crucial, supplied first one; this + // shows the ability to handle multiple transactions together let initial_amount_2 = 5_678_901; let balance_change_2 = 678_902; let expected_balance_after_2 = 4_999_999; let boxed_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let setup_holder = make_fingerprint_pair_and_insert_initial_payable_records( + let setup_holder = insert_initial_payable_records_and_return_sent_txs( boxed_conn.as_ref(), - initial_amount_1, - initial_amount_2, - balance_change_1, - balance_change_2, + (initial_amount_1, balance_change_1), + (initial_amount_2, balance_change_2), ); let subject = PayableDaoReal::new(boxed_conn); - let status_1_before_opt = subject.account_status(&setup_holder.wallet_1); - let status_2_before_opt = subject.account_status(&setup_holder.wallet_2); + let wallet_1 = Wallet::from(setup_holder.account_1.pending_payable.receiver_address); + let wallet_2 = Wallet::from(setup_holder.account_2.pending_payable.receiver_address); + let status_1_before_opt = subject.account_status(&wallet_1); + let status_2_before_opt = subject.account_status(&wallet_2); let result = subject.transactions_confirmed(&[ - setup_holder.fingerprint_1.clone(), - setup_holder.fingerprint_2.clone(), + setup_holder.account_1.pending_payable.clone(), + setup_holder.account_2.pending_payable.clone(), ]); assert_eq!(result, Ok(())); + let expected_last_paid_timestamp_1 = + from_unix_timestamp(to_unix_timestamp(setup_holder.account_1.previous_timestamp)); + let expected_last_paid_timestamp_2 = + from_unix_timestamp(to_unix_timestamp(setup_holder.account_2.previous_timestamp)); + // TODO yes these pending_payable_opt values are unsensible now but it will eventually be all cleaned up with GH-662 let expected_status_before_1 = PayableAccount { - wallet: setup_holder.wallet_1.clone(), + wallet: wallet_1.clone(), balance_wei: initial_amount_1, - last_paid_timestamp: setup_holder.previous_timestamp_1, - pending_payable_opt: Some(PendingPayableId::new( - setup_holder.fingerprint_1.rowid, - H256::from_uint(&U256::from(0)), - )), //hash is just garbage + last_paid_timestamp: expected_last_paid_timestamp_1, + pending_payable_opt: None, }; let expected_status_before_2 = PayableAccount { - wallet: setup_holder.wallet_2.clone(), + wallet: wallet_2.clone(), balance_wei: initial_amount_2, - last_paid_timestamp: setup_holder.previous_timestamp_2, - pending_payable_opt: Some(PendingPayableId::new( - setup_holder.fingerprint_2.rowid, - H256::from_uint(&U256::from(0)), - )), //hash is just garbage + last_paid_timestamp: expected_last_paid_timestamp_2, + pending_payable_opt: None, }; let expected_resulting_status_1 = PayableAccount { - wallet: setup_holder.wallet_1.clone(), + wallet: wallet_1.clone(), balance_wei: expected_balance_after_1, - last_paid_timestamp: setup_holder.fingerprint_1.timestamp, + last_paid_timestamp: from_unix_timestamp( + setup_holder.account_1.pending_payable.timestamp, + ), pending_payable_opt: None, }; let expected_resulting_status_2 = PayableAccount { - wallet: setup_holder.wallet_2.clone(), + wallet: wallet_2.clone(), balance_wei: expected_balance_after_2, - last_paid_timestamp: setup_holder.fingerprint_2.timestamp, + last_paid_timestamp: from_unix_timestamp( + setup_holder.account_2.pending_payable.timestamp, + ), pending_payable_opt: None, }; assert_eq!(status_1_before_opt, Some(expected_status_before_1)); assert_eq!(status_2_before_opt, Some(expected_status_before_2)); - let resulting_account_1_opt = subject.account_status(&setup_holder.wallet_1); + let resulting_account_1_opt = subject.account_status(&wallet_1); assert_eq!(resulting_account_1_opt, Some(expected_resulting_status_1)); - let resulting_account_2_opt = subject.account_status(&setup_holder.wallet_2); + let resulting_account_2_opt = subject.account_status(&wallet_2); assert_eq!(resulting_account_2_opt, Some(expected_resulting_status_2)) } @@ -1063,22 +1081,20 @@ mod tests { ); let conn = payable_read_only_conn(&home_dir); let conn_wrapped = Box::new(ConnectionWrapperReal::new(conn)); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - let hash = make_tx_hash(12345); - let rowid = 789; - pending_payable_fingerprint.hash = hash; - pending_payable_fingerprint.rowid = rowid; + let mut confirmed_transaction = make_sent_tx(5); + confirmed_transaction.amount_minor = 12345; + let wallet_address = confirmed_transaction.receiver_address; let subject = PayableDaoReal::new(conn_wrapped); - let result = subject.transactions_confirmed(&[pending_payable_fingerprint]); + let result = subject.transactions_confirmed(&[confirmed_transaction]); assert_eq!( result, - Err(PayableDaoError::RusqliteError( + Err(PayableDaoError::RusqliteError(format!( "Error from invalid update command for payable table and change of -12345 wei to \ - 'pending_payable_rowid = 789' with error 'attempt to write a readonly database'" - .to_string() - )) + 'wallet_address = {:?}' with error 'attempt to write a readonly database'", + wallet_address + ))) ) } @@ -1086,26 +1102,21 @@ mod tests { #[should_panic( expected = "Overflow detected with 340282366920938463463374607431768211455: cannot be converted from u128 to i128" )] - fn transaction_confirmed_works_for_overflow_from_amount_stored_in_pending_payable_fingerprint() - { + fn transaction_confirmed_works_for_overflow_from_sent_tx_record() { let home_dir = ensure_node_home_directory_exists( "payable_dao", - "transaction_confirmed_works_for_overflow_from_amount_stored_in_pending_payable_fingerprint", + "transaction_confirmed_works_for_overflow_from_sent_tx_record", ); let subject = PayableDaoReal::new( DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(), ); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - let hash = make_tx_hash(12345); - let rowid = 789; - pending_payable_fingerprint.hash = hash; - pending_payable_fingerprint.rowid = rowid; - pending_payable_fingerprint.amount = u128::MAX; + let mut sent_tx = make_sent_tx(456); + sent_tx.amount_minor = u128::MAX; //The overflow occurs before we start modifying the payable account so we can have the database empty - let _ = subject.transactions_confirmed(&[pending_payable_fingerprint]); + let _ = subject.transactions_confirmed(&[sent_tx]); } #[test] @@ -1117,38 +1128,37 @@ mod tests { let conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let setup_holder = make_fingerprint_pair_and_insert_initial_payable_records( + let setup_holder = insert_initial_payable_records_and_return_sent_txs( conn.as_ref(), - 1_111_111, - 2_222_222, - 111_111, - 222_222, + (1_111_111, 111_111), + (2_222_222, 222_222), ); + let wallet_1 = Wallet::from(setup_holder.account_1.pending_payable.receiver_address); + let wallet_2 = Wallet::from(setup_holder.account_2.pending_payable.receiver_address); conn.prepare("delete from payable where wallet_address = ?") .unwrap() - .execute(&[&setup_holder.wallet_2]) + .execute(&[&wallet_2.to_string()]) .unwrap(); let subject = PayableDaoReal::new(conn); - let expected_account = PayableAccount { - wallet: setup_holder.wallet_1.clone(), - balance_wei: 1_111_111 - setup_holder.fingerprint_1.amount, - last_paid_timestamp: setup_holder.fingerprint_1.timestamp, - pending_payable_opt: None, - }; - let result = subject - .transactions_confirmed(&[setup_holder.fingerprint_1, setup_holder.fingerprint_2]); + let result = subject.transactions_confirmed(&[ + setup_holder.account_1.pending_payable, + setup_holder.account_2.pending_payable, + ]); + let expected_err_msg = format!( + "Expected 1 row to be changed for the unique key \ + {} but got this count: 0", + wallet_2 + ); assert_eq!( result, - Err(PayableDaoError::RusqliteError( - "Expected 1 row to be changed for the unique key 792 but got this count: 0" - .to_string() - )) + Err(PayableDaoError::RusqliteError(expected_err_msg)) ); - let account_1_opt = subject.account_status(&setup_holder.wallet_1); - assert_eq!(account_1_opt, Some(expected_account)); - let account_2_opt = subject.account_status(&setup_holder.wallet_2); + let expected_resulting_balance_1 = 1_111_111 - 111_111; + let account_1 = subject.account_status(&wallet_1).unwrap(); + assert_eq!(account_1.balance_wei, expected_resulting_balance_1); + let account_2_opt = subject.account_status(&wallet_2); assert_eq!(account_2_opt, None); } 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 e555fcc9ae..414c364d84 100644 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ b/node/src/accountant/db_access_objects/pending_payable_dao.rs @@ -5,7 +5,6 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; @@ -36,7 +35,7 @@ pub struct TransactionHashes { pub trait PendingPayableDao { // Note that the order of the returned results is not guaranteed fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes; - fn return_all_errorless_fingerprints(&self) -> Vec; + // fn return_all_errorless_fingerprints(&self) -> Vec; fn insert_new_fingerprints( &self, hashes_and_amounts: &[HashAndAmount], @@ -87,42 +86,42 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { } } - fn return_all_errorless_fingerprints(&self) -> Vec { - let mut stm = self - .conn - .prepare( - "select rowid, transaction_hash, amount_high_b, amount_low_b, \ - payable_timestamp, attempt from pending_payable where process_error is null", - ) - .expect("Internal error"); - stm.query_map([], |row| { - let rowid: u64 = Self::get_with_expect(row, 0); - let transaction_hash: String = Self::get_with_expect(row, 1); - let amount_high_bytes: i64 = Self::get_with_expect(row, 2); - let amount_low_bytes: i64 = Self::get_with_expect(row, 3); - let timestamp: i64 = Self::get_with_expect(row, 4); - let attempt: u16 = Self::get_with_expect(row, 5); - Ok(PendingPayableFingerprint { - rowid, - timestamp: from_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { - panic!( - "Invalid hash format (\"{}\": {:?}) - database corrupt", - transaction_hash, e - ) - }), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_bytes, - amount_low_bytes, - )), - process_error: None, - }) - }) - .expect("rusqlite failure") - .vigilant_flatten() - .collect() - } + // fn return_all_errorless_fingerprints(&self) -> Vec { + // let mut stm = self + // .conn + // .prepare( + // "select rowid, transaction_hash, amount_high_b, amount_low_b, \ + // payable_timestamp, attempt from pending_payable where process_error is null", + // ) + // .expect("Internal error"); + // stm.query_map([], |row| { + // let rowid: u64 = Self::get_with_expect(row, 0); + // let transaction_hash: String = Self::get_with_expect(row, 1); + // let amount_high_bytes: i64 = Self::get_with_expect(row, 2); + // let amount_low_bytes: i64 = Self::get_with_expect(row, 3); + // let timestamp: i64 = Self::get_with_expect(row, 4); + // let attempt: u16 = Self::get_with_expect(row, 5); + // Ok(SentTx { + // rowid, + // timestamp: from_unix_timestamp(timestamp), + // hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { + // panic!( + // "Invalid hash format (\"{}\": {:?}) - database corrupt", + // transaction_hash, e + // ) + // }), + // attempt, + // amount_minor: checked_conversion::(BigIntDivider::reconstitute( + // amount_high_bytes, + // amount_low_bytes, + // )), + // process_error: None, + // }) + // }) + // .expect("rusqlite failure") + // .vigilant_flatten() + // .collect() + // } fn insert_new_fingerprints( &self, @@ -179,7 +178,7 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { { Ok(x) if x == ids.len() => Ok(()), Ok(num) => panic!( - "deleting fingerprint, expected {} rows to be changed, but the actual number is {}", + "deleting sent tx record, expected {} rows to be changed, but the actual number is {}", ids.len(), num ), @@ -225,21 +224,6 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PendingPayable { - pub recipient_wallet: Wallet, - pub hash: H256, -} - -impl PendingPayable { - pub fn new(recipient_wallet: Wallet, hash: H256) -> Self { - Self { - recipient_wallet, - hash, - } - } -} - #[derive(Debug)] pub struct PendingPayableDaoReal<'a> { conn: Box, @@ -272,12 +256,11 @@ impl PendingPayableDaoFactory for DaoFactoryReal { #[cfg(test)] mod tests { use crate::accountant::checked_conversion; - use crate::accountant::db_access_objects::pending_payable_dao::{ + use crate::accountant::db_access_objects::sent_payable_dao::{ PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, }; 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; use crate::blockchain::test_utils::make_tx_hash; use crate::database::db_initializer::{ @@ -291,660 +274,660 @@ mod tests { use std::time::SystemTime; use web3::types::H256; - #[test] - fn insert_new_fingerprints_happy_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "insert_new_fingerprints_happy_path", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(4546); - let amount_1 = 55556; - let hash_2 = make_tx_hash(6789); - let amount_2 = 44445; - 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, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - let _ = subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - - let records = subject.return_all_errorless_fingerprints(); - assert_eq!( - records, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_1.hash, - attempt: 1, - amount: hash_and_amount_1.amount, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_2.hash, - attempt: 1, - amount: hash_and_amount_2.amount, - process_error: None - } - ] - ) - } - - #[test] - fn insert_new_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "insert_new_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let hash = make_tx_hash(45466); - let amount = 55556; - let timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { hash, amount }; - - let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); - - assert_eq!( - result, - Err(PendingPayableDaoError::InsertionFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic(expected = "expected 1 changed rows but got 0")] - fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { - let setup_conn = Connection::open_in_memory().unwrap(); - // injecting a by-plan failing statement into the mocked connection in order to provoke - // a reaction that would've been untestable directly on the table the act is closely coupled with - let statement = { - setup_conn - .execute("create table example (id integer)", []) - .unwrap(); - setup_conn.prepare("select id from example").unwrap() - }; - 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_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - - let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); - } - - #[test] - fn fingerprints_rowids_when_records_reachable() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_records_reachable", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - 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 = - H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") - .unwrap(); - let hash_2 = - H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") - .unwrap(); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 4567, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 6789, - }; - let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; - { - subject - .insert_new_fingerprints(&fingerprints_init_input, timestamp) - .unwrap(); - } - - let result = subject.fingerprints_rowids(&[hash_1, hash_2]); - - let first_expected_pair = &(1, hash_1); - assert!( - result.rowid_results.contains(first_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - first_expected_pair, - result.rowid_results - ); - let second_expected_pair = &(2, hash_2); - assert!( - result.rowid_results.contains(second_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - second_expected_pair, - result.rowid_results - ); - assert_eq!(result.rowid_results.len(), 2); - } - - #[test] - fn fingerprints_rowids_when_nonexistent_records() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_nonexistent_records", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let hash_1 = make_tx_hash(11119); - let hash_2 = make_tx_hash(22229); - let hash_3 = make_tx_hash(33339); - let hash_4 = make_tx_hash(44449); - // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for - // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of - // just an ambiguous 1 - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_2, - amount: 8901234, - }], - SystemTime::now(), - ) - .unwrap(); - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_3, - amount: 1234567, - }], - SystemTime::now(), - ) - .unwrap(); - subject.delete_fingerprints(&[1]).unwrap(); - - let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); - - assert_eq!(result.rowid_results, vec![(2, hash_3),]); - assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); - } - - #[test] - fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - 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); - let amount_2 = 333; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: None - } - ] - ) - } - - #[test] - fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_unix_timestamp(198_000_000); - let hash = make_tx_hash(10000); - let amount = 333; - let hash_and_amount_1 = HashAndAmount { - hash: make_tx_hash(11119), - amount: 2000, - }; - let hash_and_amount_2 = HashAndAmount { hash, amount }; - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - subject.mark_failures(&[1]).unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![PendingPayableFingerprint { - rowid: 2, - timestamp, - hash, - attempt: 1, - amount, - process_error: None - }] - ) - } - - #[test] - #[should_panic( - expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" - )] - fn return_all_errorless_fingerprints_panics_on_malformed_hash() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_panics_on_malformed_hash", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - wrapped_conn - .prepare("insert into pending_payable \ - (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ - values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") - .unwrap() - .execute([]) - .unwrap(); - } - let subject = PendingPayableDaoReal::new(wrapped_conn); - - let _ = subject.return_all_errorless_fingerprints(); - } - - #[test] - fn delete_fingerprints_happy_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_happy_path", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[ - HashAndAmount { - hash: make_tx_hash(1234), - amount: 1111, - }, - HashAndAmount { - hash: make_tx_hash(2345), - amount: 5555, - }, - HashAndAmount { - hash: make_tx_hash(3456), - amount: 2222, - }, - ], - SystemTime::now(), - ) - .unwrap(); - } - - let result = subject.delete_fingerprints(&[2, 3]); - - assert_eq!(result, Ok(())); - let records_in_the_db = subject.return_all_errorless_fingerprints(); - let record_left_in = &records_in_the_db[0]; - assert_eq!(record_left_in.hash, make_tx_hash(1234)); - assert_eq!(record_left_in.rowid, 1); - assert_eq!(records_in_the_db.len(), 1); - } - - #[test] - fn delete_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let rowid = 45; - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.delete_fingerprints(&[rowid]); - - assert_eq!( - result, - Err(PendingPayableDaoError::RecordDeletion( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "deleting fingerprint, expected 2 rows to be changed, but the actual number is 1" - )] - fn delete_fingerprints_changed_different_number_of_rows_than_expected() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_changed_different_number_of_rows_than_expected", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let rowid_1 = 1; - let rowid_2 = 2; - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: make_tx_hash(666666), - amount: 5555, - }], - SystemTime::now(), - ) - .unwrap(); - } - - let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); - } - - #[test] - fn increment_scan_attempts_works() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(345); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(567); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 1122, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 2233, - }; - let hash_and_amount_3 = HashAndAmount { - hash: hash_3, - amount: 3344, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], - timestamp, - ) - .unwrap(); - } - - let result = subject.increment_scan_attempts(&[2, 3]); - - assert_eq!(result, Ok(())); - let mut all_records = subject.return_all_errorless_fingerprints(); - assert_eq!(all_records.len(), 3); - let record_1 = all_records.remove(0); - assert_eq!(record_1.hash, hash_1); - assert_eq!(record_1.attempt, 1); - let record_2 = all_records.remove(0); - assert_eq!(record_2.hash, hash_2); - assert_eq!(record_2.attempt, 2); - let record_3 = all_records.remove(0); - assert_eq!(record_3.hash, hash_3); - assert_eq!(record_3.attempt, 2); - } - - #[test] - fn increment_scan_attempts_works_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.increment_scan_attempts(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::UpdateFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" - )] - fn increment_scan_attempts_panics_on_unexpected_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_panics_on_unexpected_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.increment_scan_attempts(&[1, 2]); - } - - #[test] - fn mark_failures_works() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_works"); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(555); - let amount_1 = 1234; - let hash_2 = make_tx_hash(666); - let amount_2 = 2345; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - } - - let result = subject.mark_failures(&[2]); - - assert_eq!(result, Ok(())); - let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); - let mut assert_stm = assert_conn - .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") - .unwrap(); - let found_fingerprints = assert_stm - .query_map([], |row| { - let rowid: u64 = row.get(0).unwrap(); - let transaction_hash: String = row.get(1).unwrap(); - let amount_high_b: i64 = row.get(2).unwrap(); - let amount_low_b: i64 = row.get(3).unwrap(); - let timestamp: i64 = row.get(4).unwrap(); - let attempt: u16 = row.get(5).unwrap(); - let process_error: Option = row.get(6).unwrap(); - Ok(PendingPayableFingerprint { - rowid, - timestamp: from_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap(), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_b, - amount_low_b, - )), - process_error, - }) - }) - .unwrap() - .flatten() - .collect::>(); - assert_eq!( - *found_fingerprints, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: Some("ERROR".to_string()) - } - ] - ) - } - - #[test] - fn mark_failures_sad_path() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_sad_path"); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.mark_failures(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::ErrorMarkFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" - )] - fn mark_failures_panics_on_wrong_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "mark_failures_panics_on_wrong_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.mark_failures(&[10, 20]); - } + // #[test] + // fn insert_new_fingerprints_happy_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "insert_new_fingerprints_happy_path", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(4546); + // let amount_1 = 55556; + // let hash_2 = make_tx_hash(6789); + // let amount_2 = 44445; + // 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, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // + // let _ = subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2], + // batch_wide_timestamp, + // ) + // .unwrap(); + // + // let records = subject.return_all_errorless_fingerprints(); + // assert_eq!( + // records, + // vec![ + // SentTx { + // rowid: 1, + // timestamp: batch_wide_timestamp, + // hash: hash_and_amount_1.hash, + // attempt: 1, + // amount_minor: hash_and_amount_1.amount, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp: batch_wide_timestamp, + // hash: hash_and_amount_2.hash, + // attempt: 1, + // amount_minor: hash_and_amount_2.amount, + // process_error: None + // } + // ] + // ) + // } + // + // #[test] + // fn insert_new_fingerprints_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "insert_new_fingerprints_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let hash = make_tx_hash(45466); + // let amount = 55556; + // let timestamp = from_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // let hash_and_amount = HashAndAmount { hash, amount }; + // + // let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::InsertionFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic(expected = "expected 1 changed rows but got 0")] + // fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { + // let setup_conn = Connection::open_in_memory().unwrap(); + // // injecting a by-plan failing statement into the mocked connection in order to provoke + // // a reaction that would've been untestable directly on the table the act is closely coupled with + // let statement = { + // setup_conn + // .execute("create table example (id integer)", []) + // .unwrap(); + // setup_conn.prepare("select id from example").unwrap() + // }; + // 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_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // let hash_and_amount = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // + // let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); + // } + // + // #[test] + // fn fingerprints_rowids_when_records_reachable() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "fingerprints_rowids_when_records_reachable", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // 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 = + // H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") + // .unwrap(); + // let hash_2 = + // H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") + // .unwrap(); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: 4567, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: 6789, + // }; + // let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; + // { + // subject + // .insert_new_fingerprints(&fingerprints_init_input, timestamp) + // .unwrap(); + // } + // + // let result = subject.fingerprints_rowids(&[hash_1, hash_2]); + // + // let first_expected_pair = &(1, hash_1); + // assert!( + // result.rowid_results.contains(first_expected_pair), + // "Returned rowid pairs should have contained {:?} but all it did is {:?}", + // first_expected_pair, + // result.rowid_results + // ); + // let second_expected_pair = &(2, hash_2); + // assert!( + // result.rowid_results.contains(second_expected_pair), + // "Returned rowid pairs should have contained {:?} but all it did is {:?}", + // second_expected_pair, + // result.rowid_results + // ); + // assert_eq!(result.rowid_results.len(), 2); + // } + // + // #[test] + // fn fingerprints_rowids_when_nonexistent_records() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "fingerprints_rowids_when_nonexistent_records", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let hash_1 = make_tx_hash(11119); + // let hash_2 = make_tx_hash(22229); + // let hash_3 = make_tx_hash(33339); + // let hash_4 = make_tx_hash(44449); + // // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for + // // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of + // // just an ambiguous 1 + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: hash_2, + // amount_minor: 8901234, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: hash_3, + // amount_minor: 1234567, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // subject.delete_fingerprints(&[1]).unwrap(); + // + // let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); + // + // assert_eq!(result.rowid_results, vec![(2, hash_3),]); + // assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); + // } + // + // #[test] + // fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // 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); + // let amount_2 = 333; + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // + // { + // subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2], + // batch_wide_timestamp, + // ) + // .unwrap(); + // } + // + // let result = subject.return_all_errorless_fingerprints(); + // + // assert_eq!( + // result, + // vec![ + // SentTx { + // rowid: 1, + // timestamp: batch_wide_timestamp, + // hash: hash_1, + // attempt: 1, + // amount_minor: amount_1, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp: batch_wide_timestamp, + // hash: hash_2, + // attempt: 1, + // amount_minor: amount_2, + // process_error: None + // } + // ] + // ) + // } + // + // #[test] + // fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let timestamp = from_unix_timestamp(198_000_000); + // let hash = make_tx_hash(10000); + // let amount = 333; + // let hash_and_amount_1 = HashAndAmount { + // hash: make_tx_hash(11119), + // amount_minor: 2000, + // }; + // let hash_and_amount_2 = HashAndAmount { hash, amount }; + // { + // subject + // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) + // .unwrap(); + // subject.mark_failures(&[1]).unwrap(); + // } + // + // let result = subject.return_all_errorless_fingerprints(); + // + // assert_eq!( + // result, + // vec![SentTx { + // rowid: 2, + // timestamp, + // hash, + // attempt: 1, + // amount, + // process_error: None + // }] + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" + // )] + // fn return_all_errorless_fingerprints_panics_on_malformed_hash() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_panics_on_malformed_hash", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // { + // wrapped_conn + // .prepare("insert into pending_payable \ + // (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ + // values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") + // .unwrap() + // .execute([]) + // .unwrap(); + // } + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // + // let _ = subject.return_all_errorless_fingerprints(); + // } + // + // #[test] + // fn delete_fingerprints_happy_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_happy_path", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[ + // HashAndAmount { + // hash: make_tx_hash(1234), + // amount_minor: 1111, + // }, + // HashAndAmount { + // hash: make_tx_hash(2345), + // amount_minor: 5555, + // }, + // HashAndAmount { + // hash: make_tx_hash(3456), + // amount_minor: 2222, + // }, + // ], + // SystemTime::now(), + // ) + // .unwrap(); + // } + // + // let result = subject.delete_fingerprints(&[2, 3]); + // + // assert_eq!(result, Ok(())); + // let records_in_the_db = subject.return_all_errorless_fingerprints(); + // let record_left_in = &records_in_the_db[0]; + // assert_eq!(record_left_in.hash, make_tx_hash(1234)); + // assert_eq!(record_left_in.rowid, 1); + // assert_eq!(records_in_the_db.len(), 1); + // } + // + // #[test] + // fn delete_fingerprints_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let rowid = 45; + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.delete_fingerprints(&[rowid]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::RecordDeletion( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "deleting sent tx record, expected 2 rows to be changed, but the actual number is 1" + // )] + // fn delete_fingerprints_changed_different_number_of_rows_than_expected() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_changed_different_number_of_rows_than_expected", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let rowid_1 = 1; + // let rowid_2 = 2; + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: make_tx_hash(666666), + // amount_minor: 5555, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // } + // + // let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); + // } + // + // #[test] + // fn increment_scan_attempts_works() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_works", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(345); + // let hash_2 = make_tx_hash(456); + // let hash_3 = make_tx_hash(567); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: 1122, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: 2233, + // }; + // let hash_and_amount_3 = HashAndAmount { + // hash: hash_3, + // amount_minor: 3344, + // }; + // let timestamp = from_unix_timestamp(190_000_000); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], + // timestamp, + // ) + // .unwrap(); + // } + // + // let result = subject.increment_scan_attempts(&[2, 3]); + // + // assert_eq!(result, Ok(())); + // let mut all_records = subject.return_all_errorless_fingerprints(); + // assert_eq!(all_records.len(), 3); + // let record_1 = all_records.remove(0); + // assert_eq!(record_1.hash, hash_1); + // assert_eq!(record_1.attempt, 1); + // let record_2 = all_records.remove(0); + // assert_eq!(record_2.hash, hash_2); + // assert_eq!(record_2.attempt, 2); + // let record_3 = all_records.remove(0); + // assert_eq!(record_3.hash, hash_3); + // assert_eq!(record_3.attempt, 2); + // } + // + // #[test] + // fn increment_scan_attempts_works_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_works_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.increment_scan_attempts(&[1]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::UpdateFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" + // )] + // fn increment_scan_attempts_panics_on_unexpected_row_change_count() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_panics_on_unexpected_row_change_count", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // + // let _ = subject.increment_scan_attempts(&[1, 2]); + // } + // + // #[test] + // fn mark_failures_works() { + // let home_dir = + // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_works"); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(555); + // let amount_1 = 1234; + // let hash_2 = make_tx_hash(666); + // let amount_2 = 2345; + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // let timestamp = from_unix_timestamp(190_000_000); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) + // .unwrap(); + // } + // + // let result = subject.mark_failures(&[2]); + // + // assert_eq!(result, Ok(())); + // let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); + // let mut assert_stm = assert_conn + // .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") + // .unwrap(); + // let found_fingerprints = assert_stm + // .query_map([], |row| { + // let rowid: u64 = row.get(0).unwrap(); + // let transaction_hash: String = row.get(1).unwrap(); + // let amount_high_b: i64 = row.get(2).unwrap(); + // let amount_low_b: i64 = row.get(3).unwrap(); + // let timestamp: i64 = row.get(4).unwrap(); + // let attempt: u16 = row.get(5).unwrap(); + // let process_error: Option = row.get(6).unwrap(); + // Ok(SentTx { + // rowid, + // timestamp: from_unix_timestamp(timestamp), + // hash: H256::from_str(&transaction_hash[2..]).unwrap(), + // attempt, + // amount_minor: checked_conversion::(BigIntDivider::reconstitute( + // amount_high_b, + // amount_low_b, + // )), + // process_error, + // }) + // }) + // .unwrap() + // .flatten() + // .collect::>(); + // assert_eq!( + // *found_fingerprints, + // vec![ + // SentTx { + // rowid: 1, + // timestamp, + // hash: hash_1, + // attempt: 1, + // amount_minor: amount_1, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp, + // hash: hash_2, + // attempt: 1, + // amount_minor: amount_2, + // process_error: Some("ERROR".to_string()) + // } + // ] + // ) + // } + // + // #[test] + // fn mark_failures_sad_path() { + // let home_dir = + // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_sad_path"); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.mark_failures(&[1]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::ErrorMarkFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" + // )] + // fn mark_failures_panics_on_wrong_row_change_count() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "mark_failures_panics_on_wrong_row_change_count", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // + // let _ = subject.mark_failures(&[10, 20]); + // } } diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index ad8f524622..9d100c6337 100644 --- a/node/src/accountant/db_access_objects/receivable_dao.rs +++ b/node/src/accountant/db_access_objects/receivable_dao.rs @@ -55,7 +55,7 @@ pub trait ReceivableDao { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError>; fn more_money_received( @@ -112,7 +112,7 @@ impl ReceivableDao for ReceivableDaoReal { &self, timestamp: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError> { let main_sql = "insert into receivable (wallet_address, balance_high_b, balance_low_b, last_received_timestamp) values \ (:wallet, :balance_high_b, :balance_low_b, :last_received_timestamp) on conflict (wallet_address) do update set \ @@ -125,7 +125,7 @@ impl ReceivableDao for ReceivableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( diff --git a/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs new file mode 100644 index 0000000000..26c7dd5fed --- /dev/null +++ b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; + +impl From<(FailedTx, TxBlock)> for SentTx { + fn from((failed_tx, confirmation_block): (FailedTx, TxBlock)) -> Self { + SentTx { + hash: failed_tx.hash, + receiver_address: failed_tx.receiver_address, + amount_minor: failed_tx.amount_minor, + timestamp: failed_tx.timestamp, + gas_price_minor: failed_tx.gas_price_minor, + nonce: failed_tx.nonce, + status: TxStatus::Confirmed { + block_hash: format!("{:?}", confirmation_block.block_hash), + block_number: confirmation_block.block_number.as_u64(), + detection: Detection::Reclaim, + }, + } + } +} + +impl From<(SentTx, FailureReason)> for FailedTx { + fn from((sent_tx, failure_reason): (SentTx, FailureReason)) -> Self { + FailedTx { + hash: sent_tx.hash, + receiver_address: sent_tx.receiver_address, + amount_minor: sent_tx.amount_minor, + timestamp: sent_tx.timestamp, + gas_price_minor: sent_tx.gas_price_minor, + nonce: sent_tx.nonce, + reason: failure_reason, + status: FailureStatus::RetryRequired, + } + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; + use crate::accountant::db_access_objects::utils::to_unix_timestamp; + use crate::accountant::gwei_to_wei; + use crate::accountant::test_utils::make_transaction_block; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; + use crate::blockchain::errors::validation_status::ValidationStatus; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::make_wallet; + use std::time::{Duration, SystemTime}; + + #[test] + fn sent_tx_record_can_be_converted_from_failed_tx_record() { + let failed_tx = FailedTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + }; + let tx_block = make_transaction_block(789); + + let result = SentTx::from((failed_tx.clone(), tx_block)); + + assert_eq!( + result, + SentTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + status: TxStatus::Confirmed { + block_hash: + "0x000000000000000000000000000000000000000000000000000000003b9acd15" + .to_string(), + block_number: 491169069, + detection: Detection::Reclaim, + }, + } + ); + } + + #[test] + fn conversion_from_sent_tx_and_failure_reason_to_failed_tx_works() { + let sent_tx = SentTx { + hash: make_tx_hash(789), + receiver_address: make_wallet("receiver").address(), + amount_minor: 123_456_789, + timestamp: to_unix_timestamp( + SystemTime::now() + .checked_sub(Duration::from_secs(10_000)) + .unwrap(), + ), + gas_price_minor: gwei_to_wei(424_u64), + nonce: 456_u64.into(), + status: TxStatus::Pending(ValidationStatus::Waiting), + }; + + let result_1 = FailedTx::from((sent_tx.clone(), FailureReason::Reverted)); + let result_2 = FailedTx::from(( + sent_tx.clone(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + )); + + assert_conversion_into_failed_tx(result_1, sent_tx.clone(), FailureReason::Reverted); + assert_conversion_into_failed_tx( + result_2, + sent_tx, + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + ); + } + + fn assert_conversion_into_failed_tx( + result: FailedTx, + original_sent_tx: SentTx, + expected_failure_reason: FailureReason, + ) { + assert_eq!(result.hash, original_sent_tx.hash); + assert_eq!(result.receiver_address, original_sent_tx.receiver_address); + assert_eq!(result.amount_minor, original_sent_tx.amount_minor); + assert_eq!(result.timestamp, original_sent_tx.timestamp); + assert_eq!(result.gas_price_minor, original_sent_tx.gas_price_minor); + assert_eq!(result.nonce, original_sent_tx.nonce); + assert_eq!(result.status, FailureStatus::RetryRequired); + assert_eq!(result.reason, expected_failure_reason); + } +} diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs index 09e293edf7..a82bafdce1 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -1,19 +1,21 @@ // 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}; -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_access_objects::utils::{ + DaoFactoryReal, TxHash, TxIdentifiers, TxRecordWithHash, +}; 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::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; +use ethereum_types::H256; use itertools::Itertools; +use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; -use crate::accountant::db_access_objects::failed_payable_dao::ValidationStatus; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use web3::types::Address; #[derive(Debug, PartialEq, Eq)] pub enum SentPayableDaoError { @@ -25,16 +27,22 @@ pub enum SentPayableDaoError { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Tx { +pub struct SentTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub status: TxStatus, } +impl TxRecordWithHash for SentTx { + fn hash(&self) -> TxHash { + self.hash + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TxStatus { Pending(ValidationStatus), @@ -69,26 +77,21 @@ pub enum Detection { Reclaim, } -impl From<&TxConfirmation> for TxStatus { - fn from(tx_confirmation: &TxConfirmation) -> Self { +impl From for TxStatus { + fn from(tx_block: TxBlock) -> Self { TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number) - .expect("block number too big"), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).expect("block number too big"), + detection: Detection::Normal, } } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TxConfirmation { - block_info: TransactionBlock, - detection: Detection, -} - +#[derive(Debug, PartialEq, Eq)] pub enum RetrieveCondition { IsPending, ByHash(Vec), + ByNonce(Vec), } impl Display for RetrieveCondition { @@ -104,19 +107,29 @@ impl Display for RetrieveCondition { comma_joined_stringifiable(tx_hashes, |hash| format!("'{:?}'", hash)) ) } + RetrieveCondition::ByNonce(nonces) => { + write!( + f, + "WHERE nonce IN ({})", + comma_joined_stringifiable(nonces, |nonce| nonce.to_string()) + ) + } } } } 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 confirm_tx( + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> Vec; + //TODO potentially atomically + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError>; + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError>; + fn update_statuses( &self, - hash_map: &HashMap, + hash_map: &HashMap, ) -> Result<(), SentPayableDaoError>; - fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; } @@ -156,7 +169,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError> { + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { if txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } @@ -190,8 +203,8 @@ impl SentPayableDao for SentPayableDaoReal<'_> { status \ ) 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_checked = checked_conversion::(tx.amount_minor); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); 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); @@ -226,7 +239,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } - fn retrieve_txs(&self, condition_opt: Option) -> Vec { + 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, status FROM sent_payable" .to_string(); @@ -248,22 +261,22 @@ impl SentPayableDao for SentPayableDaoReal<'_> { 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 amount_minor = 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 = + let gas_price_minor = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); let status_str: String = row.get(8).expectv("status"); let status = TxStatus::from_str(&status_str).expect("Failed to parse TxStatus"); - Ok(Tx { + Ok(SentTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, status, }) @@ -273,18 +286,15 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn confirm_tx( - &self, - hash_map: &HashMap, - ) -> Result<(), SentPayableDaoError> { + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { if hash_map.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - for (hash, tx_confirmation) in hash_map { + for (hash, tx_block) in hash_map { let sql = format!( "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", - TxStatus::from(tx_confirmation), + TxStatus::from(*tx_block), hash ); @@ -308,12 +318,12 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Ok(()) } - fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError> { + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { if new_txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - let build_case = |value_fn: fn(&Tx) -> String| { + let build_case = |value_fn: fn(&SentTx) -> String| { new_txs .iter() .map(|tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx))) @@ -323,23 +333,23 @@ impl SentPayableDao for SentPayableDaoReal<'_> { 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 amount_checked = checked_conversion::(tx.amount_minor); 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 amount_checked = checked_conversion::(tx.amount_minor); 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 gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); 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 gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); low.to_string() }); @@ -391,6 +401,47 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } + fn update_statuses( + &self, + status_updates: &HashMap, + ) -> Result<(), SentPayableDaoError> { + if status_updates.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let case_statements = status_updates + .iter() + .map(|(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status)) + .join(" "); + let tx_hashes = comma_joined_stringifiable(&status_updates.keys().collect_vec(), |hash| { + format!("'{:?}'", hash) + }); + + let sql = format!( + "UPDATE sent_payable \ + SET \ + status = CASE \ + {case_statements} \ + END \ + WHERE tx_hash IN ({tx_hashes})" + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(rows_changed) => { + if rows_changed == status_updates.len() { + Ok(()) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} of {} records had their status updated.", + rows_changed, + status_updates.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); @@ -421,30 +472,54 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } +pub trait SentPayableDaoFactory { + fn make(&self) -> Box; +} + +impl SentPayableDaoFactory for DaoFactoryReal { + fn make(&self) -> Box { + Box::new(SentPayableDaoReal::new(self.make_connection())) + } +} + #[cfg(test)] mod tests { - use std::collections::{HashMap, HashSet}; - use std::ops::Add; - use std::str::FromStr; - use std::sync::{Arc, Mutex}; - use std::time::{Duration, UNIX_EPOCH}; - use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, TxConfirmation, TxStatus}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ + ByHash, ByNonce, IsPending, + }; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{ + EmptyInput, PartialExecution, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, + TxStatus, + }; + use crate::accountant::db_access_objects::test_utils::{ + make_read_only_db_connection, TxBuilder, + }; + use crate::accountant::db_access_objects::utils::TxRecordWithHash; + use crate::accountant::test_utils::make_sent_tx; + use crate::blockchain::blockchain_interface::data_structures::TxBlock; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{ + make_block_hash, make_tx_hash, ValidationFailureClockMock, + }; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; use crate::database::test_utils::ConnectionWrapperMock; - use ethereum_types::{ H256, U64}; + 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::failed_payable_dao::{ValidationStatus}; - 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::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, RemoteErrorKind}; - use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; - use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; + use rusqlite::Connection; + use std::collections::{HashMap, HashSet}; + use std::ops::{Add, Sub}; + use std::str::FromStr; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] fn insert_new_records_works() { @@ -530,15 +605,15 @@ mod tests { result, Err(SentPayableDaoError::InvalidInput( "Duplicate hashes found in the input. Input Transactions: \ - [Tx { \ + [SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ nonce: 0, status: Pending(Waiting) }, \ - Tx { \ + SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204020, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204020, gas_price_minor: 0, \ nonce: 0, status: Confirmed { block_hash: \ \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ block_number: 7890123, detection: Reclaim } }]" @@ -657,8 +732,8 @@ mod tests { '0x0000000000000000000000000000000000000000000000000000000123456789', \ '0x0000000000000000000000000000000000000000000000000000000987654321'\ )" - .to_string() ); + assert_eq!(ByNonce(vec![45, 47]).to_string(), "WHERE nonce IN (45, 47)") } #[test] @@ -742,6 +817,35 @@ mod tests { assert_eq!(result, vec![tx1, tx3]); } + #[test] + fn tx_can_be_retrieved_by_nonce() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_nonce"); + 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(123)) + .nonce(33) + .build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(456)) + .nonce(34) + .build(); + let tx3 = TxBuilder::default() + .hash(make_tx_hash(789)) + .nonce(35) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(ByNonce(vec![33, 35]))); + + assert_eq!(result, vec![tx1, tx3]); + } + #[test] fn confirm_tx_works() { let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "confirm_tx_works"); @@ -757,26 +861,20 @@ mod tests { let updated_pre_assert_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); let pre_assert_status_tx1 = updated_pre_assert_txs[0].status.clone(); let pre_assert_status_tx2 = updated_pre_assert_txs[1].status.clone(); - let tx_confirmation_1 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(3), - block_number: U64::from(1), - }, - detection: Detection::Normal, + let confirmed_tx_block_1 = TxBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), }; - let tx_confirmation_2 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(4), - block_number: U64::from(2), - }, - detection: Detection::Reclaim, + let confirmed_tx_block_2 = TxBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), }; let hash_map = HashMap::from([ - (tx1.hash, tx_confirmation_1.clone()), - (tx2.hash, tx_confirmation_2.clone()), + (tx1.hash, confirmed_tx_block_1.clone()), + (tx2.hash, confirmed_tx_block_2.clone()), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); assert_eq!(result, Ok(())); @@ -787,9 +885,9 @@ mod tests { assert_eq!( updated_txs[0].status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_1.block_info.block_hash), - block_number: tx_confirmation_1.block_info.block_number.as_u64(), - detection: tx_confirmation_1.detection + block_hash: format!("{:?}", confirmed_tx_block_1.block_hash), + block_number: confirmed_tx_block_1.block_number.as_u64(), + detection: Detection::Normal } ); assert_eq!( @@ -799,9 +897,9 @@ mod tests { assert_eq!( updated_txs[1].status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_2.block_info.block_hash), - block_number: tx_confirmation_2.block_info.block_number.as_u64(), - detection: tx_confirmation_2.detection + block_hash: format!("{:?}", confirmed_tx_block_2.block_hash), + block_number: confirmed_tx_block_2.block_number.as_u64(), + detection: Detection::Normal } ); } @@ -821,7 +919,7 @@ mod tests { subject.insert_new_records(&vec![tx]).unwrap(); let hash_map = HashMap::new(); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); } @@ -843,27 +941,21 @@ mod tests { let hash_map = HashMap::from([ ( existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), }, ), ( non_existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(2), - block_number: U64::from(2), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), }, ), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -885,16 +977,13 @@ mod tests { let hash = make_tx_hash(1); let hash_map = HashMap::from([( hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::default(), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), }, )]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -1007,6 +1096,146 @@ mod tests { ) } + #[test] + fn update_statuses_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "update_statuses_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let timestamp_a = SystemTime::now().sub(Duration::from_millis(11)); + let timestamp_b = SystemTime::now().sub(Duration::from_millis(1234)); + let subject = SentPayableDaoReal::new(wrapped_conn); + let mut tx1 = make_sent_tx(456); + tx1.status = TxStatus::Pending(ValidationStatus::Waiting); + let mut tx2 = make_sent_tx(789); + tx2.status = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ))); + let mut tx3 = make_sent_tx(123); + tx3.status = TxStatus::Pending(ValidationStatus::Waiting); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone()]) + .unwrap(); + let hashmap = HashMap::from([ + ( + tx1.hash, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ))), + ), + ( + tx2.hash, + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + ( + tx3.hash, + TxStatus::Confirmed { + block_hash: + "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + }, + ), + ]); + + let result = subject.update_statuses(&hashmap); + + let updated_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!( + updated_txs[0].status, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a) + ))) + ); + assert_eq!( + updated_txs[1].status, + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable + )), + &ValidationFailureClockMock::default().now_result(timestamp_b) + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable + )), + &ValidationFailureClockReal::default() + ) + )) + ); + assert_eq!( + updated_txs[2].status, + TxStatus::Confirmed { + block_hash: "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + } + ); + assert_eq!(updated_txs.len(), 3) + } + + #[test] + fn update_statuses_handles_empty_input_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_statuses_handles_empty_input_error", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.update_statuses(&HashMap::new()); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn update_statuses_handles_sql_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_statuses_handles_sql_error", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.update_statuses(&HashMap::from([( + make_tx_hash(1), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockReal::default(), + ))), + )])); + + 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( @@ -1189,6 +1418,7 @@ mod tests { fn tx_status_from_str_works() { let validation_failure_clock = ValidationFailureClockMock::default() .now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); + assert_eq!( TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), TxStatus::Pending(ValidationStatus::Waiting) @@ -1232,22 +1462,29 @@ mod tests { } #[test] - fn tx_status_can_be_converted_from_tx_confirmation() { - let tx_confirmation = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(6), - block_number: 456789_u64.into(), - }, - detection: Detection::Normal, + fn tx_status_can_be_made_from_transaction_block() { + let tx_block = TxBlock { + block_hash: make_block_hash(6), + block_number: 456789_u64.into(), }; assert_eq!( - TxStatus::from(&tx_confirmation), + TxStatus::from(tx_block), TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number).unwrap(), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).unwrap(), + detection: Detection::Normal, } ) } + + #[test] + fn tx_record_with_hash_is_implemented_for_sent_tx() { + let sent_tx = make_sent_tx(1234); + let hash = sent_tx.hash; + + let hash_from_trait = sent_tx.hash(); + + assert_eq!(hash_from_trait, hash); + } } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index a1a2eeb310..e395aa2deb 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -2,10 +2,11 @@ #![cfg(test)] use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedTx, FailureReason, FailureStatus, ValidationStatus, + FailedTx, FailureReason, FailureStatus, }; -use crate::accountant::db_access_objects::sent_payable_dao::{Tx, TxStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; @@ -50,13 +51,13 @@ impl TxBuilder { self } - pub fn build(self) -> Tx { - Tx { + pub fn build(self) -> SentTx { + SentTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: 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(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), status: self .status_opt @@ -111,9 +112,9 @@ impl FailedTxBuilder { FailedTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: self.amount_opt.unwrap_or_default(), timestamp: self.timestamp_opt.unwrap_or_default(), - gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), reason: self .reason_opt diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 8fbc875c26..21c9cdc831 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -46,6 +46,10 @@ pub fn from_unix_timestamp(unix_timestamp: i64) -> SystemTime { SystemTime::UNIX_EPOCH + interval } +pub trait TxRecordWithHash { + fn hash(&self) -> TxHash; +} + pub struct DaoFactoryReal { pub data_directory: PathBuf, pub init_config: DbInitializationConfig, diff --git a/node/src/accountant/db_big_integer/big_int_db_processor.rs b/node/src/accountant/db_big_integer/big_int_db_processor.rs index 3ef15278d4..c362e3740f 100644 --- a/node/src/accountant/db_big_integer/big_int_db_processor.rs +++ b/node/src/accountant/db_big_integer/big_int_db_processor.rs @@ -322,6 +322,7 @@ pub trait DisplayableParamValue: ToSql + Display {} impl DisplayableParamValue for i64 {} impl DisplayableParamValue for &str {} +impl DisplayableParamValue for String {} impl DisplayableParamValue for Wallet {} #[derive(Default)] diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 2ca5025575..f54a7dbd6a 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -14,10 +14,10 @@ use masq_lib::constants::{SCAN_ERROR, WEIS_IN_GWEI}; use std::cell::{Ref, RefCell}; use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; use crate::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoError}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, SentTx}; use crate::accountant::db_access_objects::utils::{ - remap_payable_accounts, remap_receivable_accounts, CustomQuery, DaoFactoryReal, + remap_payable_accounts, remap_receivable_accounts, CustomQuery, DaoFactoryReal, TxHash, }; use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, @@ -25,13 +25,22 @@ use crate::accountant::financials::visibility_restricted_module::{ use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -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::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableScanResult, Retry, TxHashByTable, +}; +use crate::accountant::scanners::scan_schedulers::{ + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, +}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; +use crate::accountant::scanners::{Scanners, StartScanError}; +use crate::blockchain::blockchain_bridge::{ + BlockMarker, RegisterNewPendingPayables, RetrieveTransactions, +}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, ProcessedPayableFallible, + BlockchainTransaction, ProcessedPayableFallible, StatusReadFromReceiptCheck, }; +use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::bootstrapper::BootstrapperConfig; use crate::database::db_initializer::DbInitializationConfig; use crate::sub_lib::accountant::AccountantSubs; @@ -57,17 +66,17 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, - UiScanRequest, + QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; +use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::ui_gateway::MessageTarget::ClientId; 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; +use std::collections::HashMap; #[cfg(test)] use std::default::Default; use std::fmt::Display; @@ -76,10 +85,6 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; -use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; -use crate::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 @@ -89,7 +94,7 @@ pub struct Accountant { earning_wallet: Wallet, payable_dao: Box, receivable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, crashable: bool, scanners: Scanners, scan_schedulers: ScanSchedulers, @@ -136,9 +141,11 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } +pub type TxReceiptResult = Result; + #[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, +pub struct TxReceiptsMessage { + pub results: HashMap, pub response_skeleton_opt: Option, } @@ -230,20 +237,20 @@ impl Handler for Accountant { self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) => self .scan_schedulers .pending_payable .schedule(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::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 => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the pending payable scan did find results" @@ -267,16 +274,16 @@ impl Handler for Accountant { let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(other_scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::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 => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the new payable scan did find results" @@ -308,10 +315,10 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: TxReceiptsMessage, 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) => { @@ -321,18 +328,30 @@ impl Handler for Accountant { .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. + // Non-automatic scan for pending payables is not permitted to spark a payable + // scan bringing over new payables with fresh nonces. The job's done here. } 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), + PendingPayableScanResult::PaymentRetryRequired(retry_either) => match retry_either { + Either::Left(Retry::RetryPayments) => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + Either::Left(Retry::RetryTxStatusCheckOnly) => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + Either::Right(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"), + }, }; } } @@ -469,7 +488,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable_fingerprints: Vec, + pub tx_hashes: Vec, pub response_skeleton_opt: Option, } @@ -479,14 +498,14 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); fn handle( &mut self, - msg: PendingPayableFingerprintSeeds, + msg: RegisterNewPendingPayables, _ctx: &mut Self::Context, ) -> Self::Result { - self.handle_new_pending_payable_fingerprints(msg) + self.register_new_pending_sent_tx(msg) } } @@ -519,13 +538,12 @@ impl Accountant { let earning_wallet = config.earning_wallet.clone(); let financial_statistics = Rc::new(RefCell::new(FinancialStatistics::default())); let payable_dao = dao_factories.payable_dao_factory.make(); - let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); + let sent_payable_dao = dao_factories.sent_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), - config.when_pending_too_long_sec, Rc::clone(&financial_statistics), ); @@ -534,7 +552,7 @@ impl Accountant { earning_wallet, payable_dao, receivable_dao, - pending_payable_dao, + sent_payable_dao, scanners, crashable: config.crash_point == CrashPoint::Message, scan_schedulers, @@ -561,8 +579,8 @@ impl Accountant { report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), - init_pending_payable_fingerprints: recipient!(addr, PendingPayableFingerprintSeeds), - report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), + register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), + report_transaction_status: recipient!(addr, TxReceiptsMessage), report_sent_payments: recipient!(addr, SentPayables), scan_errors: recipient!(addr, ScanError), ui_message_sub: recipient!(addr, NodeFromUiMessage), @@ -596,12 +614,12 @@ impl Accountant { byte_rate, payload_size ), - Err(e) => panic!("Recording services provided for {} but has hit fatal database error: {:?}", wallet, e) + Err(e) => panic!("Was recording services provided for {} but hit a fatal database error: {:?}", wallet, e) }; } else { warning!( self.logger, - "Declining to record a receivable against our wallet {} for service we provided", + "Declining to record a receivable against our wallet {} for services we provided", wallet ); } @@ -924,7 +942,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -944,7 +962,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::NewPayables, @@ -978,6 +996,8 @@ impl Accountant { .expect("BlockchainBridge is dead"); } Err(e) => { + // It is thrown away and there is no rescheduling downstream because every error + // happening here on the start resolves into a panic by the current design let _ = self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::RetryPayables, e, @@ -990,7 +1010,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -1003,14 +1023,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: ScanRescheduleAfterEarlyStop = match result { + let hint: ScanReschedulingAfterEarlyStop = 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 + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1036,7 +1056,7 @@ impl Accountant { scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error(&self.logger, scanner.into(), is_externally_triggered); @@ -1119,27 +1139,23 @@ impl Accountant { } } - fn handle_new_pending_payable_fingerprints(&self, msg: PendingPayableFingerprintSeeds) { - fn serialize_hashes(fingerprints_data: &[HashAndAmount]) -> String { - comma_joined_stringifiable(fingerprints_data, |hash_and_amount| { - format!("{:?}", hash_and_amount.hash) - }) + fn register_new_pending_sent_tx(&self, msg: RegisterNewPendingPayables) { + fn serialize_hashes(tx_hashes: &[SentTx]) -> String { + comma_joined_stringifiable(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } - match self - .pending_payable_dao - .insert_new_fingerprints(&msg.hashes_and_balances, msg.batch_wide_timestamp) - { + + match self.sent_payable_dao.insert_new_records(&msg.new_sent_txs) { Ok(_) => debug!( self.logger, - "Saved new pending payable fingerprints for: {}", - serialize_hashes(&msg.hashes_and_balances) + "Registered new pending payables for: {}", + serialize_hashes(&msg.new_sent_txs) ), Err(e) => error!( self.logger, - "Failed to process new pending payable fingerprints due to '{:?}', \ - disabling the automated confirmation for all these transactions: {}", - e, - serialize_hashes(&msg.hashes_and_balances) + "Failed to save new pending payable records for {} due to '{:?}' which is integral \ + to the function of the automated tx confirmation", + serialize_hashes(&msg.new_sent_txs), + e ), } } @@ -1149,33 +1165,31 @@ impl Accountant { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingPayable { + pub recipient_wallet: Wallet, + pub hash: TxHash, +} + +impl PendingPayable { + pub fn new(recipient_wallet: Wallet, hash: TxHash) -> Self { + Self { + recipient_wallet, + hash, + } + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct PendingPayableId { pub rowid: u64, - pub hash: H256, + pub hash: TxHash, } impl PendingPayableId { - pub fn new(rowid: u64, hash: H256) -> Self { + pub fn new(rowid: u64, hash: TxHash) -> Self { Self { rowid, hash } } - - fn rowids(ids: &[Self]) -> Vec { - ids.iter().map(|id| id.rowid).collect() - } - - fn serialize_hashes_to_string(ids: &[Self]) -> String { - comma_joined_stringifiable(ids, |id| format!("{:?}", id.hash)) - } -} - -impl From for PendingPayableId { - fn from(pending_payable_fingerprint: PendingPayableFingerprint) -> Self { - Self { - hash: pending_payable_fingerprint.hash, - rowid: pending_payable_fingerprint.rowid, - } - } } pub fn comma_joined_stringifiable(collection: &[T], stringify: F) -> String @@ -1216,33 +1230,58 @@ pub fn wei_to_gwei, S: Display + Copy + Div + From> for Accountant { type Result = (); @@ -1314,7 +1352,8 @@ mod tests { fn new_calls_factories_properly() { let config = make_bc_with_defaults(); let payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let receivable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let banned_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); @@ -1323,11 +1362,14 @@ mod tests { .make_result(PayableDaoMock::new()) // For Accountant .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()); // For PendingPayable Scanner - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() - .make_params(&pending_payable_dao_factory_params_arc) - .make_result(PendingPayableDaoMock::new()) // For Accountant - .make_result(PendingPayableDaoMock::new()) // For Payable Scanner - .make_result(PendingPayableDaoMock::new()); // For PendingPayable Scanner + let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() + .make_params(&sent_payable_dao_factory_params_arc) + .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner + .make_result(SentPayableDaoMock::new()); // For PendingPayable Scanner + let failed_payable_dao_factory = FailedPayableDaoFactoryMock::new() + .make_params(&failed_payable_dao_factory_params_arc) + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(vec![])); // For PendingPayableScanner; let receivable_dao_factory = ReceivableDaoFactoryMock::new() .make_params(&receivable_dao_factory_params_arc) .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1343,7 +1385,8 @@ mod tests { config, DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), @@ -1355,9 +1398,13 @@ mod tests { vec![(), (), ()] ); assert_eq!( - *pending_payable_dao_factory_params_arc.lock().unwrap(), + *sent_payable_dao_factory_params_arc.lock().unwrap(), vec![(), (), ()] ); + assert_eq!( + *failed_payable_dao_factory_params_arc.lock().unwrap(), + vec![()] + ); assert_eq!( *receivable_dao_factory_params_arc.lock().unwrap(), vec![(), ()] @@ -1375,12 +1422,16 @@ mod tests { .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()), // For PendingPayable Scanner ); - let pending_payable_dao_factory = Box::new( - PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) // For Accountant - .make_result(PendingPayableDaoMock::new()) // For Payable Scanner - .make_result(PendingPayableDaoMock::new()), // For PendingPayable Scanner + let sent_payable_dao_factory = Box::new( + SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner + .make_result(SentPayableDaoMock::new()), // For PendingPayable Scanner ); + let failed_payable_dao_factory = Box::new( + FailedPayableDaoFactoryMock::new() + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(vec![])), + ); // For PendingPayableScanner; let receivable_dao_factory = Box::new( ReceivableDaoFactoryMock::new() .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1395,7 +1446,8 @@ mod tests { bootstrapper_config, DaoFactories { payable_dao_factory, - pending_payable_dao_factory, + sent_payable_dao_factory, + failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, config_dao_factory, @@ -1555,14 +1607,12 @@ mod tests { #[test] fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { let config = bc_from_earning_wallet(make_wallet("earning_wallet")); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(1, make_tx_hash(123))], - no_rowid_results: vec![], - }); + let tx_hash = make_tx_hash(123); + let sent_payable_dao = + SentPayableDaoMock::default().get_tx_identifiers_result(hashmap! (tx_hash => 1)); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); let mut subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); @@ -1576,7 +1626,7 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: make_wallet("blah"), - hash: make_tx_hash(123), + hash: tx_hash, })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1836,20 +1886,15 @@ mod tests { receivable_scan_interval: Duration::from_millis(10_000), pending_payable_scan_interval: Duration::from_secs(100), }); - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let sent_tx = make_sent_tx(555); + let tx_hash = sent_tx.hash; + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![sent_tx]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge @@ -1879,7 +1924,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable_fingerprints: vec![fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1898,12 +1943,22 @@ mod tests { 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)]) + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Ok(())); + let mut subject = AccountantBuilder::default().build(); + let mut sent_tx = make_sent_tx(123); + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let sent_payable_cache = + PendingPayableCacheMock::default().get_record_by_hash_result(Some(sent_tx.clone())); + let pending_payable_scanner = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Real( + pending_payable_scanner, + ))); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let ui_gateway = ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); @@ -1919,26 +1974,27 @@ mod tests { 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(), + let tx_block = TxBlock { + block_hash: make_tx_hash(456), + block_number: 78901234.into(), + }; + let tx_receipts_msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Succeeded(tx_block), )], response_skeleton_opt, }; - subject_addr.try_send(report_tx_receipts).unwrap(); + subject_addr.try_send(tx_receipts_msg).unwrap(); system.run(); let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transaction_confirmed_params, vec![vec![tx_fingerprint]]); + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, + }; + assert_eq!(*transaction_confirmed_params, vec![vec![sent_tx]]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2111,20 +2167,28 @@ mod tests { #[test] fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( ) { + // TODO now only GH-605 logic is missing let response_skeleton_opt = Some(ResponseSkeleton { client_id: 4555, context_id: 5566, }); - // TODO when we have more logic in place with the other cards taken in, we'll need to configure these - // accordingly + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); 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 sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let sent_payable_dao = SentPayableDaoMock::default() + .retrieve_txs_result(vec![sent_tx.clone()]) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_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)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); subject.scan_schedulers.automatic_scans_enabled = false; let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -2139,14 +2203,10 @@ mod tests { let system = System::new("test"); 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() - )], + TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Reverted + ),], response_skeleton_opt }, &subject_addr @@ -2179,6 +2239,11 @@ mod tests { subject_addr.try_send(pending_payable_request).unwrap(); system.run(); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); + assert_eq!(*insert_new_records_params, vec![vec![expected_failed_tx]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash]]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2717,7 +2782,7 @@ mod tests { 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 tx_hash = make_tx_hash(456); let payable_scanner = ScannerMock::new() .scan_started_at_result(None) .scan_started_at_result(None) @@ -2741,11 +2806,13 @@ mod tests { .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], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, })) .finish_scan_params(&scan_params.pending_payable_finish_scan) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryPayments), + )); let receivable_scanner = ScannerMock::new() .scan_started_at_result(None) .start_scan_params(&scan_params.receivable_start_scan) @@ -2762,13 +2829,9 @@ mod tests { 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(), + let expected_tx_receipts_msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok( + StatusReadFromReceiptCheck::Reverted, )], response_skeleton_opt: None, }; @@ -2781,7 +2844,7 @@ mod tests { }; let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, - expected_report_transaction_receipts.clone(), + expected_tx_receipts_msg.clone(), &subject_addr ); let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( @@ -2810,7 +2873,7 @@ mod tests { &scan_params, ¬ify_and_notify_later_params.pending_payables_notify_later, pending_payable_expected_notify_later_interval, - expected_report_transaction_receipts, + expected_tx_receipts_msg, before, after, ); @@ -2828,9 +2891,9 @@ mod tests { ¬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. + // Since the assertions proved that the pending payable scanner had run multiple times + // before the new payable scanner started or was scheduled, the front position definitely + // belonged to the one first mentioned. } #[derive(Default)] @@ -2840,10 +2903,9 @@ mod tests { payable_finish_scan: Arc>>, pending_payable_start_scan: Arc, Logger, String)>>>, - pending_payable_finish_scan: Arc>>, + pending_payable_finish_scan: Arc>>, receivable_start_scan: Arc, Logger, String)>>>, - // receivable_finish_scan ... not needed } #[derive(Default)] @@ -2862,7 +2924,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -2921,7 +2983,7 @@ mod tests { payable_scanner: ScannerMock, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -2975,7 +3037,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -3048,7 +3110,7 @@ mod tests { Mutex>, >, pending_payable_expected_notify_later_interval: Duration, - expected_report_tx_receipts_msg: ReportTransactionReceipts, + expected_tx_receipts_msg: TxReceiptsMessage, act_started_at: SystemTime, act_finished_at: SystemTime, ) { @@ -3061,12 +3123,9 @@ mod tests { 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) = + let (actual_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_eq!(actual_tx_receipts_msg, expected_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 @@ -3287,11 +3346,13 @@ mod tests { #[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 sent_payable_dao = + SentPayableDaoMock::default().retrieve_txs_result(vec![make_sent_tx(789)]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let system = System::new("test"); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -3304,7 +3365,7 @@ mod tests { System::current().stop(); system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, ScanRescheduleAfterEarlyStop::DoNotSchedule); + assert_eq!(hint, ScanReschedulingAfterEarlyStop::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); @@ -3313,11 +3374,12 @@ mod tests { #[test] fn initial_pending_payable_scan_if_no_payables_found() { - let pending_payable_dao = - PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let flag_before = subject.scanners.initial_pending_payable_scan(); @@ -3326,7 +3388,7 @@ mod tests { let flag_after = subject.scanners.initial_pending_payable_scan(); assert_eq!( hint, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); assert_eq!(flag_before, true); assert_eq!(flag_after, false); @@ -3493,6 +3555,7 @@ mod tests { response_skeleton_opt: None, }; let transaction_hash = make_tx_hash(789); + let tx_hash = make_tx_hash(456); let creditor_wallet = make_wallet("blah"); let counter_msg_2 = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( @@ -3500,23 +3563,16 @@ mod tests { )]), 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(), - )], + let tx_status = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), + }); + let counter_msg_3 = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], response_skeleton_opt: None, }; let request_transaction_receipts_msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![pending_payable_fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, }; let qualified_payables_msg = QualifiedPayablesMessage { @@ -3853,7 +3909,7 @@ mod tests { system.run(); assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); @@ -3942,7 +3998,7 @@ mod tests { 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( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4051,41 +4107,40 @@ mod tests { } #[test] - fn scan_for_pending_payables_finds_still_pending_payables() { + fn scan_for_pending_payables_finds_various_payables() { init_test_logging(); + let test_name = "scan_for_pending_payables_finds_various_payables"; + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 2, - amount: 7999, - process_error: None, + let tx_hash_1 = make_tx_hash(456); + let tx_hash_2 = make_tx_hash(789); + let tx_hash_3 = make_tx_hash(123); + let expected_composed_msg_for_blockchain_bridge = RequestTransactionReceipts { + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + TxHashByTable::FailedPayable(tx_hash_3), + ], + response_skeleton_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![ - payable_fingerprint_1.clone(), - payable_fingerprint_2.clone(), - ]); - let config = bc_from_earning_wallet(make_wallet("mine")); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Ok(expected_composed_msg_for_blockchain_bridge.clone())); + let consuming_wallet = make_wallet("consuming"); let system = System::new("pending payable scan"); let mut subject = AccountantBuilder::default() - .consuming_wallet(make_paying_wallet(b"consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) + .logger(Logger::new(test_name)) .build(); - + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); @@ -4095,19 +4150,24 @@ mod tests { }) .unwrap(); + let before = SystemTime::now(); system.run(); + let after = SystemTime::now(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(before <= timestamp && timestamp <= after); + assert_eq!(response_skeleton_opt, None); + assert!( + start_scan_params.is_empty(), + "Should be empty but {:?}", + start_scan_params + ); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let received_msg = blockchain_bridge_recording.get_record::(0); - assert_eq!( - received_msg, - &RequestTransactionReceipts { - pending_payable_fingerprints: vec![payable_fingerprint_1, payable_fingerprint_2], - response_skeleton_opt: None, - } - ); + assert_eq!(received_msg, &expected_composed_msg_for_blockchain_bridge); 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] @@ -4163,7 +4223,7 @@ mod tests { { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() - .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4267,7 +4327,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", consuming_wallet, )); } @@ -4312,7 +4372,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", earning_wallet, )); } @@ -4404,7 +4464,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", consuming_wallet )); } @@ -4449,7 +4509,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", earning_wallet, )); } @@ -4718,8 +4778,8 @@ mod tests { #[test] #[should_panic( - expected = "Recording services provided for 0x000000000000000000000000000000626f6f6761 \ - but has hit fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" + expected = "Was recording services provided for 0x000000000000000000000000000000626f6f6761 \ + but hit a fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" )] fn record_service_provided_panics_on_fatal_errors() { init_test_logging(); @@ -4810,27 +4870,19 @@ mod tests { #[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 get_tx_identifiers_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 sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result(hashmap! (expected_hash => expected_rowid)); 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)]) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .build(); let pending_payable_interval = Duration::from_millis(55); subject.scan_schedulers.pending_payable.interval = pending_payable_interval; @@ -4851,14 +4903,8 @@ mod tests { 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 get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); + assert_eq!(*get_tx_identifiers_params, vec![hashset!(expected_hash)]); let pending_payable_notify_later_params = pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -4908,7 +4954,7 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "booga".to_string(), - hashes: vec![make_tx_hash(456)], + hashes: hashset![make_tx_hash(456)], }), response_skeleton_opt: None, }; @@ -4945,17 +4991,33 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryPayments), + )); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); + subject.scan_schedulers.payable.new_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.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); 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 (mut msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Pending, + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Reverted, + }, + ]); let response_skeleton_opt = Some(ResponseSkeleton { client_id: 45, context_id: 7, @@ -4981,21 +5043,146 @@ mod tests { } #[test] - fn accountant_confirms_payable_txs_and_schedules_the_new_payable_scanner_timely() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + fn accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed() { + init_test_logging(); + let test_name = + "accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_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( + Either::Left(Retry::RetryTxStatusCheckOnly), + )); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_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()); + let interval = Duration::from_secs(20); + subject.scan_schedulers.pending_payable.interval = interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), + ); + let system = System::new(test_name); + let msg = TxReceiptsMessage { + results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: None, + }; + let subject_addr = subject.start(); + + 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 pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payable_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + interval + )] + ); + assert_using_the_same_logger(&logger, test_name, None) + } + + #[test] + fn accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected() + { + init_test_logging(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let test_name = + "accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + subject.ui_message_sub_opt = Some(ui_gateway.start().recipient()); + let response_skeleton = ResponseSkeleton { + client_id: 123, + context_id: 333, + }; + let node_to_ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(123), + body: UiScanResponse {}.tmb(333), + }; + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Right(node_to_ui_msg.clone()), + )); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_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.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let system = System::new(test_name); + + let msg = TxReceiptsMessage { + results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: Some(response_skeleton), + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + 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 ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let captured_msg = ui_gateway_recording.get_record::(0); + assert_eq!(captured_msg, &node_to_ui_msg); + assert_using_the_same_logger(&logger, test_name, None) + } + + #[test] + fn accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely() { + init_test_logging(); + let test_name = + "accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely"; + let finish_scan_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 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)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(3)) .unwrap(); @@ -5019,23 +5206,36 @@ mod tests { 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), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(555)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).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 finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); 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); @@ -5060,19 +5260,24 @@ mod tests { #[test] fn accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + init_test_logging(); + let test_name = + "accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap"; + let finish_scan_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 mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(8)) .unwrap(); @@ -5094,25 +5299,34 @@ mod tests { ); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let tx_block_1 = make_transaction_block(4567); + let tx_block_2 = make_transaction_block(1234); 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), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_1), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_2), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); - let system = System::new("new_payable_scanner_asap"); + let system = System::new(test_name); 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 finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); 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); @@ -5129,20 +5343,23 @@ mod tests { new_payable_notify_later ); let new_payable_notify = new_payable_notify_arc.lock().unwrap(); - assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]) + 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 test_name = "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)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_millis(3500)) .unwrap(); @@ -5158,17 +5375,15 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); + let system = System::new(test_name); let subject_addr = subject.start(); - let (msg, _) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { + let (msg, _) = make_tx_receipts_msg(vec![SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { 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(); @@ -5198,126 +5413,147 @@ mod tests { ); } - 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(); + pub struct SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + } + + fn make_tx_receipts_msg( + seeds: Vec, + ) -> (TxReceiptsMessage, Vec) { + let (tx_receipt_results, tx_record_vec) = seeds.into_iter().enumerate().fold( + (hashmap![], vec![]), + |(mut tx_receipt_results, mut record_by_table_vec), (idx, seed_params)| { + let tx_hash = seed_params.tx_hash; + let status = seed_params.status; + let (key, value, record) = + make_receipt_check_result_and_record(tx_hash, status, idx as u64); + tx_receipt_results.insert(key, value); + record_by_table_vec.push(record); + (tx_receipt_results, record_by_table_vec) + }, + ); - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: receipt_result_fingerprint_pairs, + let msg = TxReceiptsMessage { + results: tx_receipt_results, response_skeleton_opt: None, }; - (msg, fingerprints) + (msg, tx_record_vec) + } + + fn make_receipt_check_result_and_record( + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + idx: u64, + ) -> (TxHashByTable, TxReceiptResult, TxByTable) { + match tx_hash { + TxHashByTable::SentPayable(hash) => { + let mut sent_tx = make_sent_tx(1 + idx); + sent_tx.hash = hash; + + if let StatusReadFromReceiptCheck::Succeeded(block) = &status { + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + } + } + + let result = Ok(status); + let record_by_table = TxByTable::SentPayable(sent_tx); + (tx_hash, result, record_by_table) + } + TxHashByTable::FailedPayable(hash) => { + let mut failed_tx = make_failed_tx(1 + idx); + failed_tx.hash = hash; + + let result = Ok(status); + let record_by_table = TxByTable::FailedPayable(failed_tx); + (tx_hash, result, record_by_table) + } + } } #[test] - fn accountant_handles_inserting_new_fingerprints() { + fn accountant_handles_registering_new_pending_payables() { init_test_logging(); - let insert_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .insert_fingerprints_params(&insert_fingerprint_params_arc) - .insert_fingerprints_result(Ok(())); + let test_name = "accountant_handles_registering_new_pending_payables"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); let subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForAccountantBody(pending_payable_dao)]) + .sent_payable_daos(vec![ForAccountantBody(sent_payable_dao)]) + .logger(Logger::new(test_name)) .build(); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); - let timestamp = SystemTime::now(); + let mut sent_tx_1 = make_sent_tx(456); let hash_1 = make_tx_hash(0x6c81c); - let amount_1 = 12345; + sent_tx_1.hash = hash_1; + let mut sent_tx_2 = make_sent_tx(789); let hash_2 = make_tx_hash(0x1b207); - let amount_2 = 87654; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - let init_params = vec![hash_and_amount_1, hash_and_amount_2]; - let init_fingerprints_msg = PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: init_params.clone(), - }; + sent_tx_2.hash = hash_2; + let new_sent_txs = vec![sent_tx_1.clone(), sent_tx_2.clone()]; + let msg = RegisterNewPendingPayables { new_sent_txs }; let _ = accountant_subs - .init_pending_payable_fingerprints - .try_send(init_fingerprints_msg) + .register_new_pending_payables + .try_send(msg) .unwrap(); - let system = System::new("ordering payment fingerprint test"); + let system = System::new("ordering payment sent tx record test"); System::current().stop(); assert_eq!(system.run(), 0); - let insert_fingerprint_params = insert_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *insert_fingerprint_params, - vec![(vec![hash_and_amount_1, hash_and_amount_2], timestamp)] - ); - TestLogHandler::new().exists_log_containing( - "DEBUG: Accountant: Saved new pending payable fingerprints for: \ - 0x000000000000000000000000000000000000000000000000000000000006c81c, 0x000000000000000000000000000000000000000000000000000000000001b207", - ); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Registered new pending payables for: \ + 0x000000000000000000000000000000000000000000000000000000000006c81c, \ + 0x000000000000000000000000000000000000000000000000000000000001b207", + )); } #[test] - fn payable_fingerprint_insertion_clearly_failed_and_we_log_it_at_least() { - //despite it doesn't end so here this event would be a cause of a later panic + fn sent_payable_insertion_clearly_failed_and_we_log_at_least() { + // Even though it's factually a filed db operation, which is treated by an instant panic + // due to the broken db reliance, this is an exception. We give out some time to complete + // the actual paying and panic soon after when we figure out, from a different place + // that some sent tx records are missing. This should eventually be eliminated by GH-655 init_test_logging(); - let insert_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .insert_fingerprints_params(&insert_fingerprint_params_arc) - .insert_fingerprints_result(Err(PendingPayableDaoError::InsertionFailed( + let test_name = "sent_payable_insertion_clearly_failed_and_we_log_at_least"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Err(SentPayableDaoError::SqlExecutionFailed( "Crashed".to_string(), ))); - let amount = 2345; - let transaction_hash = make_tx_hash(0x1c8); - let hash_and_amount = HashAndAmount { - hash: transaction_hash, - amount, - }; + let tx_hash_1 = make_tx_hash(0x1c8); + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.hash = tx_hash_1; + let tx_hash_2 = make_tx_hash(0x1b2); + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.hash = tx_hash_2; let subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForAccountantBody(pending_payable_dao)]) + .sent_payable_daos(vec![ForAccountantBody(sent_payable_dao)]) + .logger(Logger::new(test_name)) .build(); - let timestamp = SystemTime::now(); - let report_new_fingerprints = PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: vec![hash_and_amount], + let msg = RegisterNewPendingPayables { + new_sent_txs: vec![sent_tx_1.clone(), sent_tx_2.clone()], }; - let _ = subject.handle_new_pending_payable_fingerprints(report_new_fingerprints); + let _ = subject.register_new_pending_sent_tx(msg); - let insert_fingerprint_params = insert_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *insert_fingerprint_params, - vec![(vec![hash_and_amount], timestamp)] - ); - TestLogHandler::new().exists_log_containing("ERROR: Accountant: Failed to process \ - new pending payable fingerprints due to 'InsertionFailed(\"Crashed\")', disabling the automated \ - confirmation for all these transactions: 0x00000000000000000000000000000000000000000000000000000000000001c8"); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Failed to save new pending payable records for \ + 0x00000000000000000000000000000000000000000000000000000000000001c8, \ + 0x00000000000000000000000000000000000000000000000000000000000001b2 \ + due to 'SqlExecutionFailed(\"Crashed\")' which is integral to the function \ + of the automated tx confirmation" + )); } const EXAMPLE_RESPONSE_SKELETON: ResponseSkeleton = ResponseSkeleton { @@ -5329,7 +5565,7 @@ mod tests { #[test] fn handling_scan_error_for_externally_triggered_payables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_payables", ScanError { scan_type: ScanType::Payables, @@ -5341,19 +5577,21 @@ mod tests { #[test] fn handling_scan_error_for_externally_triggered_pending_payables() { - assert_scan_error_is_handled_properly( + let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); + test_scan_error_is_handled_properly_more_specifically( "handling_scan_error_for_externally_triggered_pending_payables", ScanError { scan_type: ScanType::PendingPayables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, + Some(additional_test_setup_and_assertions), ); } #[test] fn handling_scan_error_for_externally_triggered_receivables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_receivables", ScanError { scan_type: ScanType::Receivables, @@ -5365,7 +5603,7 @@ mod tests { #[test] fn handling_scan_error_for_internally_triggered_payables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_payables", ScanError { scan_type: ScanType::Payables, @@ -5377,19 +5615,21 @@ mod tests { #[test] fn handling_scan_error_for_internally_triggered_pending_payables() { - assert_scan_error_is_handled_properly( + let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); + test_scan_error_is_handled_properly_more_specifically( "handling_scan_error_for_internally_triggered_pending_payables", ScanError { scan_type: ScanType::PendingPayables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + Some(additional_test_setup_and_assertions), ); } #[test] fn handling_scan_error_for_internally_triggered_receivables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_receivables", ScanError { scan_type: ScanType::Receivables, @@ -5399,6 +5639,34 @@ mod tests { ); } + fn prepare_setup_and_assertion_fns() -> (Box, Box) { + let ensure_empty_cache_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_sent_tx_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_failed_tx_params_arc); + let scanner = PendingPayableScannerBuilder::new() + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + ( + Box::new(|scanners: &mut Scanners| { + scanners.replace_scanner(ScannerReplacement::PendingPayable( + ReplacementType::Real(scanner), + )); + }) as Box, + Box::new(move || { + let ensure_empty_cache_sent_tx_params = + ensure_empty_cache_sent_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_sent_tx_params, vec![()]); + let ensure_empty_cache_failed_tx_params = + ensure_empty_cache_failed_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_failed_tx_params, vec![()]); + }) as Box, + ) + } + #[test] fn financials_request_with_nothing_to_respond_to_is_refused() { let system = System::new("test"); @@ -6144,15 +6412,32 @@ mod tests { let _: u64 = wei_to_gwei(u128::MAX); } - fn assert_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + fn test_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + test_scan_error_is_handled_properly_more_specifically(test_name, message, None) + } + fn test_scan_error_is_handled_properly_more_specifically( + test_name: &str, + message: ScanError, + additional_assertion_opt: Option<(Box, Box)>, + ) { init_test_logging(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); - subject - .scanners - .reset_scan_started(message.scan_type, MarkScanner::Started(SystemTime::now())); + let (adjust_scanner, run_additional_assertion) = match additional_assertion_opt { + Some(two_functions) => two_functions, + None => ( + Box::new(|scanners: &mut Scanners| { + scanners.reset_scan_started( + message.scan_type, + MarkScanner::Started(SystemTime::now()), + ) + }) as Box, + Box::new(|| ()) as Box, + ), + }; + adjust_scanner(&mut subject.scanners); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -6209,6 +6494,7 @@ mod tests { )); } } + run_additional_assertion(); } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index d49cf3efcd..71ff0f62c2 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -8,16 +8,15 @@ pub mod scanners_utils; pub mod test_utils; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; -use crate::accountant::db_access_objects::pending_payable_dao::{PendingPayable, PendingPayableDao}; 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, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::{ScanError, ScanForPendingPayables, ScanForRetryPayables}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_sent_tx_record, investigate_debt_extremes, payables_debug_summary, separate_errors, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMissingInDb}; +use crate::accountant::{PendingPayable, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, + TxReceiptsMessage, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; @@ -41,7 +40,8 @@ use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use variant_count::VariantCount; -use web3::types::H256; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao}; +use crate::accountant::db_access_objects::utils::{TxHash}; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; @@ -59,7 +59,7 @@ pub struct Scanners { dyn PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, >, @@ -77,21 +77,20 @@ impl Scanners { pub fn new( dao_factories: DaoFactories, payment_thresholds: Rc, - when_pending_too_long_sec: u64, financial_statistics: Rc>, ) -> Self { let payable = Box::new(PayableScanner::new( dao_factories.payable_dao_factory.make(), - dao_factories.pending_payable_dao_factory.make(), + dao_factories.sent_payable_dao_factory.make(), Rc::clone(&payment_thresholds), Box::new(PaymentAdjusterReal::new()), )); let pending_payable = Box::new(PendingPayableScanner::new( dao_factories.payable_dao_factory.make(), - dao_factories.pending_payable_dao_factory.make(), + dao_factories.sent_payable_dao_factory.make(), + dao_factories.failed_payable_dao_factory.make(), Rc::clone(&payment_thresholds), - when_pending_too_long_sec, Rc::clone(&financial_statistics), )); @@ -215,6 +214,7 @@ impl Scanners { } (None, None) => (), } + self.pending_payable .start_scan(wallet, timestamp, response_skeleton_opt, logger) } @@ -255,7 +255,7 @@ impl Scanners { pub fn finish_pending_payable_scan( &mut self, - msg: ReportTransactionReceipts, + msg: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { self.pending_payable.finish_scan(msg, logger) @@ -275,6 +275,7 @@ impl Scanners { self.payable.mark_as_ended(logger); } ScanType::PendingPayables => { + self.empty_caches(logger); self.pending_payable.mark_as_ended(logger); } ScanType::Receivables => { @@ -283,6 +284,20 @@ impl Scanners { }; } + fn empty_caches(&mut self, logger: &Logger) { + let pending_payable_scanner = self + .pending_payable + .as_any_mut() + .downcast_mut::() + .expect("mismatched types"); + pending_payable_scanner + .current_sent_payables + .ensure_empty_cache(logger); + pending_payable_scanner + .yet_unproven_failed_payables + .ensure_empty_cache(logger); + } + pub fn try_skipping_payable_adjustment( &self, msg: BlockchainAgentWithContextMessage, @@ -308,7 +323,7 @@ impl Scanners { } // 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 compiler requires to specify which trigger message distinguishes 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>( @@ -428,7 +443,7 @@ impl ScannerCommon { None => { error!( logger, - "Called scan_finished() for {:?} scanner but timestamp was not found", + "Called scan_finished() for {:?} scanner but could not find any timestamp", scan_type ); } @@ -461,7 +476,7 @@ pub struct PayableScanner { pub payable_threshold_gauge: Box, pub common: ScannerCommon, pub payable_dao: Box, - pub pending_payable_dao: Box, + pub sent_payable_dao: Box, pub payment_adjuster: Box, } @@ -536,8 +551,9 @@ impl Scanner for PayableScanner { ); if !sent_payables.is_empty() { - self.mark_pending_payable(&sent_payables, logger); + self.check_on_missing_sent_tx_records(&sent_payables); } + self.handle_sent_payable_errors(err_opt, logger); self.mark_as_ended(logger); @@ -600,14 +616,14 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { impl PayableScanner { pub fn new( payable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, payment_thresholds: Rc, payment_adjuster: Box, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, - pending_payable_dao, + sent_payable_dao, payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), payment_adjuster, } @@ -675,169 +691,200 @@ impl PayableScanner { } } - fn separate_existent_and_nonexistent_fingerprints<'a>( - &'a self, - sent_payables: &[&'a PendingPayable], - ) -> (Vec, Vec) { - let hashes = sent_payables + fn check_for_missing_records( + &self, + just_baked_sent_payables: &[&PendingPayable], + ) -> Vec { + let actual_sent_payables_len = just_baked_sent_payables.len(); + let hashset_with_hashes_to_eliminate_duplicates = just_baked_sent_payables .iter() .map(|pending_payable| pending_payable.hash) - .collect::>(); - let mut sent_payables_hashmap = sent_payables - .iter() - .map(|payable| (payable.hash, &payable.recipient_wallet)) - .collect::>(); - - let transaction_hashes = self.pending_payable_dao.fingerprints_rowids(&hashes); - let mut hashes_from_db = transaction_hashes - .rowid_results - .iter() - .map(|(_rowid, hash)| *hash) - .collect::>(); - for hash in &transaction_hashes.no_rowid_results { - hashes_from_db.insert(*hash); - } - let sent_payables_hashes = hashes.iter().copied().collect::>(); + .collect::>(); - if !Self::is_symmetrical(sent_payables_hashes, hashes_from_db) { + if hashset_with_hashes_to_eliminate_duplicates.len() != actual_sent_payables_len { panic!( - "Inconsistency in two maps, they cannot be matched by hashes. Data set directly \ - sent from BlockchainBridge: {:?}, set derived from the DB: {:?}", - sent_payables, transaction_hashes - ) + "Found duplicates in the recent sent txs: {:?}", + just_baked_sent_payables + ); } - let pending_payables_with_rowid = transaction_hashes - .rowid_results - .into_iter() - .map(|(rowid, hash)| { - let wallet = sent_payables_hashmap - .remove(&hash) - .expect("expect transaction hash, but it disappear"); - PendingPayableMetadata::new(wallet, hash, Some(rowid)) - }) - .collect_vec(); - let pending_payables_without_rowid = transaction_hashes - .no_rowid_results + let transaction_hashes_and_rowids_from_db = self + .sent_payable_dao + .get_tx_identifiers(&hashset_with_hashes_to_eliminate_duplicates); + let hashes_from_db = transaction_hashes_and_rowids_from_db + .keys() + .copied() + .collect::>(); + + let missing_sent_payables_hashes: Vec = hashset_with_hashes_to_eliminate_duplicates + .difference(&hashes_from_db) + .copied() + .collect(); + + let mut sent_payables_hashmap = just_baked_sent_payables + .iter() + .map(|payable| (payable.hash, &payable.recipient_wallet)) + .collect::>(); + missing_sent_payables_hashes .into_iter() .map(|hash| { - let wallet = sent_payables_hashmap + let wallet_address = sent_payables_hashmap .remove(&hash) - .expect("expect transaction hash, but it disappear"); - PendingPayableMetadata::new(wallet, hash, None) + .expectv("wallet") + .address(); + PendingPayableMissingInDb::new(wallet_address, hash) }) - .collect_vec(); - - (pending_payables_with_rowid, pending_payables_without_rowid) + .collect() } - fn is_symmetrical( - sent_payables_hashes: HashSet, - fingerptint_hashes: HashSet, - ) -> bool { - sent_payables_hashes == fingerptint_hashes - } - - fn mark_pending_payable(&self, sent_payments: &[&PendingPayable], logger: &Logger) { - fn missing_fingerprints_msg(nonexistent: &[PendingPayableMetadata]) -> String { + fn check_on_missing_sent_tx_records(&self, sent_payments: &[&PendingPayable]) { + fn missing_record_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { format!( - "Expected pending payable fingerprints for {} were not found; system unreliable", - comma_joined_stringifiable(nonexistent, |pp_triple| format!( - "(tx: {:?}, to wallet: {})", - pp_triple.hash, pp_triple.recipient + "Expected sent-payable records for {} were not found. The system has become unreliable", + comma_joined_stringifiable(nonexistent, |missing_sent_tx_ids| format!( + "(tx: {:?}, to wallet: {:?})", + missing_sent_tx_ids.hash, missing_sent_tx_ids.recipient )) ) } - fn ready_data_for_supply<'a>( - existent: &'a [PendingPayableMetadata], - ) -> Vec<(&'a Wallet, u64)> { - existent - .iter() - .map(|pp_triple| (pp_triple.recipient, pp_triple.rowid_opt.expectv("rowid"))) - .collect() - } - let (existent, nonexistent) = - self.separate_existent_and_nonexistent_fingerprints(sent_payments); - let mark_pp_input_data = ready_data_for_supply(&existent); - if !mark_pp_input_data.is_empty() { - if let Err(e) = self - .payable_dao - .as_ref() - .mark_pending_payables_rowids(&mark_pp_input_data) - { - mark_pending_payable_fatal_error( - sent_payments, - &nonexistent, - e, - missing_fingerprints_msg, - logger, - ) - } - debug!( - logger, - "Payables {} marked as pending in the payable table", - comma_joined_stringifiable(sent_payments, |pending_p| format!( - "{:?}", - pending_p.hash - )) - ) - } - if !nonexistent.is_empty() { - panic!("{}", missing_fingerprints_msg(&nonexistent)) + let missing_sent_tx_records = self.check_for_missing_records(sent_payments); + if !missing_sent_tx_records.is_empty() { + panic!("{}", missing_record_msg(&missing_sent_tx_records)) } } + // TODO this has become dead (GH-662) + #[allow(dead_code)] + fn mark_pending_payable(&self, _sent_payments: &[&PendingPayable], _logger: &Logger) { + todo!("remove me when the time comes") + // fn missing_fingerprints_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { + // format!( + // "Expected pending payable fingerprints for {} were not found; system unreliable", + // comma_joined_stringifiable(nonexistent, |pp_triple| format!( + // "(tx: {:?}, to wallet: {})", + // pp_triple.hash, pp_triple.recipient + // )) + // ) + // } + // fn ready_data_for_supply<'a>( + // existent: &'a [PendingPayableMissingInDb], + // ) -> Vec<(&'a Wallet, u64)> { + // existent + // .iter() + // .map(|pp_triple| (pp_triple.recipient, pp_triple.rowid_opt.expectv("rowid"))) + // .collect() + // } + // + // // TODO eventually should be taken over by GH-655 + // let missing_sent_tx_records = + // self.check_for_missing_records(sent_payments); + // + // if !existent.is_empty() { + // if let Err(e) = self + // .payable_dao + // .as_ref() + // .mark_pending_payables_rowids(&existent) + // { + // mark_pending_payable_fatal_error( + // sent_payments, + // &nonexistent, + // e, + // missing_fingerprints_msg, + // logger, + // ) + // } + // debug!( + // logger, + // "Payables {} marked as pending in the payable table", + // comma_joined_stringifiable(sent_payments, |pending_p| format!( + // "{:?}", + // pending_p.hash + // )) + // ) + // } + // if !missing_sent_tx_records.is_empty() { + // panic!("{}", missing_fingerprints_msg(&missing_sent_tx_records)) + // } + } + fn handle_sent_payable_errors( &self, err_opt: Option, logger: &Logger, ) { - if let Some(err) = err_opt { + fn decide_on_tx_error_handling( + err: &PayableTransactingErrorEnum, + ) -> Option<&HashSet> { match err { LocallyCausedError(PayableTransactionError::Sending { hashes, .. }) - | RemotelyCausedErrors(hashes) => { - self.discard_failed_transactions_with_possible_fingerprints(hashes, logger) - } - non_fatal => - debug!( - logger, - "Ignoring a non-fatal error on our end from before the transactions are hashed: {:?}", - non_fatal - ) + | RemotelyCausedErrors(hashes) => Some(hashes), + _ => None, + } + } + + if let Some(err) = err_opt { + if let Some(hashes) = decide_on_tx_error_handling(&err) { + self.discard_failed_transactions_with_possible_sent_tx_records(hashes, logger) + } else { + debug!( + logger, + "A non-fatal error {:?} will be ignored as it is from before any tx could \ + even be hashed", + err + ) } } } - fn discard_failed_transactions_with_possible_fingerprints( + fn discard_failed_transactions_with_possible_sent_tx_records( &self, - hashes_of_failed: Vec, + hashes_of_failed: &HashSet, logger: &Logger, ) { - fn serialize_hashes(hashes: &[H256]) -> String { + fn serialize_hashes(hashes: &[TxHash]) -> String { comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) } - let existent_and_nonexistent = self - .pending_payable_dao - .fingerprints_rowids(&hashes_of_failed); - let missing_fgp_err_msg_opt = err_msg_for_failure_with_expected_but_missing_fingerprints( - existent_and_nonexistent.no_rowid_results, + + let existent_sent_tx_in_db = self.sent_payable_dao.get_tx_identifiers(&hashes_of_failed); + + let hashes_of_missing_sent_tx = hashes_of_failed + .difference( + &existent_sent_tx_in_db + .keys() + .copied() + .collect::>(), + ) + .copied() + .sorted() + .collect(); + + let missing_fgp_err_msg_opt = err_msg_for_failure_with_expected_but_missing_sent_tx_record( + hashes_of_missing_sent_tx, serialize_hashes, ); - if !existent_and_nonexistent.rowid_results.is_empty() { - let (ids, hashes) = separate_rowids_and_hashes(existent_and_nonexistent.rowid_results); + + if !existent_sent_tx_in_db.is_empty() { + let hashes = existent_sent_tx_in_db + .keys() + .copied() + .sorted() + .collect_vec(); warning!( logger, - "Deleting fingerprints for failed transactions {}", + "Deleting sent payable records for {}", serialize_hashes(&hashes) ); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&ids) { + if let Err(e) = self + .sent_payable_dao + .delete_records(&existent_sent_tx_in_db.keys().copied().collect()) + { if let Some(msg) = missing_fgp_err_msg_opt { error!(logger, "{}", msg) }; panic!( - "Database corrupt: payable fingerprint deletion for transactions {} \ - failed due to {:?}", + "Database corrupt: sent payable record deletion for txs {} failed \ + due to {:?}", serialize_hashes(&hashes), e ) @@ -968,51 +1015,84 @@ impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScann #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; 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::sent_payable_dao::{ + Detection, SentPayableDaoError, SentTx, TxStatus, }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; - use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables}; - use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, + UnpricedQualifiedPayables, + }; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, Retry, + TxHashByTable, + }; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + OperationOutcome, PayableScanResult, + }; + use crate::accountant::scanners::test_utils::{ + assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, + PendingPayableCacheMock, ReplacementType, ScannerReplacement, + }; + use crate::accountant::scanners::{ + ManulTriggerError, PayableScanner, PendingPayableScanner, ReceivableScanner, Scanner, + ScannerCommon, Scanners, StartScanError, StartableScanner, + }; + use crate::accountant::test_utils::{ + make_custom_payment_thresholds, make_failed_tx, make_payable_account, + make_qualified_and_unqualified_payables, make_receivable_account, make_sent_tx, + BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, + FailedPayableDaoMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, + PayableThresholdsGaugeMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, + ReceivableDaoMock, ReceivableScannerBuilder, SentPayableDaoFactoryMock, SentPayableDaoMock, + }; + use crate::accountant::{ + gwei_to_wei, PendingPayable, ReceivedPayments, RequestTransactionReceipts, ScanError, + ScanForRetryPayables, SentPayables, TxReceiptsMessage, + }; + use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ BlockchainTransaction, ProcessedPayableFallible, RpcPayableFailure, + StatusReadFromReceiptCheck, TxBlock, }; - use crate::blockchain::test_utils::make_tx_hash; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, RemoteError, RemoteErrorKind, + }; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; 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::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, - DEFAULT_PAYMENT_THRESHOLDS, + DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::{Message, System}; use ethereum_types::U64; + use itertools::Either; use masq_lib::logger::Logger; + use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::{Regex}; + use masq_lib::ui_gateway::NodeToUiMessage; + use regex::Regex; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; + use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use web3::types::{H256}; use web3::Error; - use masq_lib::messages::ScanType; - use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; - use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; impl Scanners { pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { @@ -1096,18 +1176,19 @@ mod tests { let payable_dao_factory = PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()); - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()); - let receivable_dao = ReceivableDaoMock::new(); - let receivable_dao_factory = ReceivableDaoFactoryMock::new().make_result(receivable_dao); + let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()); + let failed_payable_dao_factory = + FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new()); + let receivable_dao_factory = + ReceivableDaoFactoryMock::new().make_result(ReceivableDaoMock::new()); let banned_dao_factory = BannedDaoFactoryMock::new().make_result(BannedDaoMock::new()); let set_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_mock = ConfigDaoMock::new() .set_params(&set_params_arc) .set_result(Ok(())); let config_dao_factory = ConfigDaoFactoryMock::new().make_result(config_dao_mock); - let when_pending_too_long_sec = 1234; let financial_statistics = FinancialStatistics { total_paid_payable_wei: 1, total_paid_receivable_wei: 2, @@ -1119,13 +1200,13 @@ mod tests { let mut scanners = Scanners::new( DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), }, Rc::clone(&payment_thresholds_rc), - when_pending_too_long_sec, Rc::new(RefCell::new(financial_statistics.clone())), ); @@ -1136,8 +1217,8 @@ mod tests { .unwrap(); let pending_payable_scanner = scanners .pending_payable - .as_any() - .downcast_ref::() + .as_any_mut() + .downcast_mut::() .unwrap(); let receivable_scanner = scanners .receivable @@ -1151,10 +1232,6 @@ mod tests { 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 - ); assert_eq!( *pending_payable_scanner.financial_statistics.borrow(), financial_statistics @@ -1167,6 +1244,19 @@ mod tests { pending_payable_scanner.common.initiated_at_opt.is_some(), false ); + let dumped_records = pending_payable_scanner + .yet_unproven_failed_payables + .dump_cache(); + assert!( + dumped_records.is_empty(), + "There should be no yet unproven failures but found {:?}.", + dumped_records + ); + assert_eq!( + receivable_scanner.common.payment_thresholds.as_ref(), + &payment_thresholds + ); + assert_eq!(receivable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!( *receivable_scanner.financial_statistics.borrow(), financial_statistics @@ -1472,9 +1562,9 @@ mod tests { fn payable_scanner_handles_sent_payable_message() { init_test_logging(); let test_name = "payable_scanner_handles_sent_payable_message"; - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let mark_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let correct_payable_hash_1 = make_tx_hash(0x6f); let correct_payable_rowid_1 = 125; let correct_payable_wallet_1 = make_wallet("tralala"); @@ -1485,7 +1575,7 @@ mod tests { let failure_payable_wallet_2 = make_wallet("hihihi"); let failure_payable_2 = RpcPayableFailure { rpc_error: Error::InvalidResponse( - "Learn how to write before you send your garbage!".to_string(), + "Ged rid of your illiteracy before you send your garbage!".to_string(), ), recipient_wallet: failure_payable_wallet_2, hash: failure_payable_hash_2, @@ -1495,28 +1585,21 @@ mod tests { let correct_payable_wallet_3 = make_wallet("booga"); let correct_pending_payable_3 = PendingPayable::new(correct_payable_wallet_3.clone(), correct_payable_hash_3); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (correct_payable_rowid_3, correct_payable_hash_3), - (correct_payable_rowid_1, correct_payable_hash_1), - ], - no_rowid_results: vec![], - }) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(failure_payable_rowid_2, failure_payable_hash_2)], - no_rowid_results: vec![], - }) - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result(hashmap!(correct_payable_hash_3 => correct_payable_rowid_3, + correct_payable_hash_1 => correct_payable_rowid_1, + )) + .get_tx_identifiers_result(hashmap!(failure_payable_hash_2 => failure_payable_rowid_2)) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); let payable_dao = PayableDaoMock::new() .mark_pending_payables_rowids_params(&mark_pending_payables_params_arc) .mark_pending_payables_rowids_result(Ok(())) .mark_pending_payables_rowids_result(Ok(())); let mut payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let logger = Logger::new(test_name); let sent_payable = SentPayables { @@ -1547,41 +1630,30 @@ mod tests { 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(); + let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); assert_eq!( - *fingerprints_rowids_params, + *get_tx_identifiers_params, vec![ - vec![correct_payable_hash_1, correct_payable_hash_3], - vec![failure_payable_hash_2] + hashset![correct_payable_hash_1, correct_payable_hash_3], + hashset![failure_payable_hash_2] ] ); - let mark_pending_payables_params = mark_pending_payables_params_arc.lock().unwrap(); - assert_eq!( - *mark_pending_payables_params, - vec![vec![ - (correct_payable_wallet_3, correct_payable_rowid_3), - (correct_payable_wallet_1, correct_payable_rowid_1), - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); + let delete_records_params = delete_records_params_arc.lock().unwrap(); assert_eq!( - *delete_fingerprints_params, - vec![vec![failure_payable_rowid_2]] + *delete_records_params, + vec![hashset![failure_payable_hash_2]] ); let log_handler = TestLogHandler::new(); log_handler.assert_logs_contain_in_order(vec![ &format!( - "WARN: {test_name}: Remote transaction failure: 'Got invalid response: Learn how to write before you send your garbage!' \ - for payment to 0x0000000000000000000000000000686968696869 and transaction hash \ - 0x00000000000000000000000000000000000000000000000000000000000000de. Please check your blockchain service URL configuration" + "WARN: {test_name}: Remote sent payable failure 'Got invalid response: Ged rid of \ + your illiteracy before you send your garbage!' \ + for wallet 0x0000000000000000000000000000686968696869 and tx hash \ + 0x00000000000000000000000000000000000000000000000000000000000000de" ), &format!("DEBUG: {test_name}: Got 2 properly sent payables of 3 attempts"), &format!( - "DEBUG: {test_name}: Payables 0x000000000000000000000000000000000000000000000000000000000000006f, \ - 0x000000000000000000000000000000000000000000000000000000000000014d marked as pending in the payable table" - ), - &format!( - "WARN: {test_name}: Deleting fingerprints for failed transactions \ + "WARN: {test_name}: Deleting sent payable records for \ 0x00000000000000000000000000000000000000000000000000000000000000de" ), ]); @@ -1590,44 +1662,86 @@ mod tests { )); } + #[test] + fn no_missing_records() { + let wallet_1 = make_wallet("abc"); + let hash_1 = make_tx_hash(123); + let wallet_2 = make_wallet("def"); + let hash_2 = make_tx_hash(345); + let wallet_3 = make_wallet("ghi"); + let hash_3 = make_tx_hash(546); + let wallet_4 = make_wallet("jkl"); + let hash_4 = make_tx_hash(678); + let pending_payables_owned = vec![ + PendingPayable::new(wallet_1.clone(), hash_1), + PendingPayable::new(wallet_2.clone(), hash_2), + PendingPayable::new(wallet_3.clone(), hash_3), + PendingPayable::new(wallet_4.clone(), hash_4), + ]; + let pending_payables_ref = pending_payables_owned + .iter() + .collect::>(); + let sent_payable_dao = SentPayableDaoMock::new().get_tx_identifiers_result( + hashmap!(hash_4 => 4, hash_1 => 1, hash_3 => 3, hash_2 => 2), + ); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .build(); + + let missing_records = subject.check_for_missing_records(&pending_payables_ref); + + assert!( + missing_records.is_empty(), + "We thought the vec would be empty but contained: {:?}", + missing_records + ); + } + #[test] #[should_panic( - expected = "Expected pending payable fingerprints for (tx: 0x0000000000000000000000000000000000000000000000000000000000000315, \ - to wallet: 0x000000000000000000000000000000626f6f6761), (tx: 0x000000000000000000000000000000000000000000000000000000000000007b, \ - to wallet: 0x00000000000000000000000000000061676f6f62) were not found; system unreliable" + expected = "Found duplicates in the recent sent txs: [PendingPayable { recipient_wallet: \ + Wallet { kind: Address(0x0000000000000000000000000000000000616263) }, hash: \ + 0x000000000000000000000000000000000000000000000000000000000000007b }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000646566) }, \ + hash: 0x00000000000000000000000000000000000000000000000000000000000001c8 }, \ + PendingPayable { recipient_wallet: Wallet { kind: \ + Address(0x0000000000000000000000000000000000676869) }, hash: \ + 0x00000000000000000000000000000000000000000000000000000000000001c8 }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x00000000000000000000000000000000006a6b6c) }, \ + hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }]" )] - fn payable_scanner_panics_when_fingerprints_for_correct_payments_not_found() { - let hash_1 = make_tx_hash(0x315); - let payment_1 = PendingPayable::new(make_wallet("booga"), hash_1); - let hash_2 = make_tx_hash(0x7b); - let payment_2 = PendingPayable::new(make_wallet("agoob"), hash_2); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![], - no_rowid_results: vec![hash_1, hash_2], - }); - let payable_dao = PayableDaoMock::new(); - let mut subject = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + fn just_baked_pending_payables_contain_duplicates() { + let hash_1 = make_tx_hash(123); + let hash_2 = make_tx_hash(456); + let hash_3 = make_tx_hash(789); + let pending_payables = vec![ + PendingPayable::new(make_wallet("abc"), hash_1), + PendingPayable::new(make_wallet("def"), hash_2), + PendingPayable::new(make_wallet("ghi"), hash_2), + PendingPayable::new(make_wallet("jkl"), hash_3), + ]; + let pending_payables_ref = pending_payables.iter().collect::>(); + let sent_payable_dao = SentPayableDaoMock::new() + .get_tx_identifiers_result(hashmap!(hash_1 => 1, hash_2 => 3, hash_3 => 5)); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) .build(); - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(payment_1), - ProcessedPayableFallible::Correct(payment_2), - ]), - response_skeleton_opt: None, - }; - let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + subject.check_for_missing_records(&pending_payables_ref); } - fn assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name: &str, - pending_payable_dao: PendingPayableDaoMock, - hash_1: H256, - hash_2: H256, - ) { + #[test] + #[should_panic(expected = "Expected sent-payable records for \ + (tx: 0x00000000000000000000000000000000000000000000000000000000000000f8, \ + to wallet: 0x00000000000000000000000000626c6168323232) \ + were not found. The system has become unreliable")] + fn payable_scanner_found_out_nonexistent_sent_tx_records() { + init_test_logging(); + let test_name = "payable_scanner_found_out_nonexistent_sent_tx_records"; + let hash_1 = make_tx_hash(0xff); + let hash_2 = make_tx_hash(0xf8); + let sent_payable_dao = + SentPayableDaoMock::default().get_tx_identifiers_result(hashmap!(hash_1 => 7881)); let payable_1 = PendingPayable::new(make_wallet("blah111"), hash_1); let payable_2 = PendingPayable::new(make_wallet("blah222"), hash_2); let payable_dao = PayableDaoMock::new().mark_pending_payables_rowids_result(Err( @@ -1635,7 +1749,7 @@ mod tests { )); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let sent_payables = SentPayables { payment_procedure_result: Ok(vec![ @@ -1645,99 +1759,36 @@ mod tests { response_skeleton_opt: None, }; - let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { - subject.finish_scan(sent_payables, &Logger::new(test_name)) - })); - - let caught_panic = caught_panic_in_err.unwrap_err(); - let panic_msg = caught_panic.downcast_ref::().unwrap(); - assert_eq!( - panic_msg, - "Unable to create a mark in the payable table for wallets 0x00000000000\ - 000000000000000626c6168313131, 0x00000000000000000000000000626c6168323232 due to \ - SignConversion(9999999999999)" - ); - } - - #[test] - fn payable_scanner_mark_pending_payable_only_panics_all_fingerprints_found() { - init_test_logging(); - let test_name = "payable_scanner_mark_pending_payable_only_panics_all_fingerprints_found"; - let hash_1 = make_tx_hash(248); - let hash_2 = make_tx_hash(139); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(7879, hash_1), (7881, hash_2)], - no_rowid_results: vec![], - }); - - assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name, - pending_payable_dao, - hash_1, - hash_2, - ); - - // Missing fingerprints, being an additional issue, would provoke an error log, but not here. - TestLogHandler::new().exists_no_log_containing(&format!("ERROR: {test_name}:")); + subject.finish_scan(sent_payables, &Logger::new(test_name)); } #[test] - fn payable_scanner_mark_pending_payable_panics_nonexistent_fingerprints_also_found() { + fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist() { init_test_logging(); let test_name = - "payable_scanner_mark_pending_payable_panics_nonexistent_fingerprints_also_found"; - let hash_1 = make_tx_hash(0xff); - let hash_2 = make_tx_hash(0xf8); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(7881, hash_1)], - no_rowid_results: vec![hash_2], - }); - - assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name, - pending_payable_dao, - hash_1, - hash_2, - ); - - TestLogHandler::new().exists_log_containing(&format!("ERROR: {test_name}: Expected pending payable \ - fingerprints for (tx: 0x00000000000000000000000000000000000000000000000000000000000000f8, to wallet: \ - 0x00000000000000000000000000626c6168323232) were not found; system unreliable")); - } - - #[test] - fn payable_scanner_is_facing_failed_transactions_and_their_fingerprints_exist() { - init_test_logging(); - let test_name = - "payable_scanner_is_facing_failed_transactions_and_their_fingerprints_exist"; - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + "payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist"; + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let hash_tx_1 = make_tx_hash(0x15b3); let hash_tx_2 = make_tx_hash(0x3039); - let first_fingerprint_rowid = 3; - let second_fingerprint_rowid = 5; + let first_sent_tx_rowid = 3; + let second_sent_tx_rowid = 5; let system = System::new(test_name); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (first_fingerprint_rowid, hash_tx_1), - (second_fingerprint_rowid, hash_tx_2), - ], - no_rowid_results: vec![], - }) - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result( + hashmap!(hash_tx_1 => first_sent_tx_rowid, hash_tx_2 => second_sent_tx_rowid), + ) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); let payable_scanner = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let logger = Logger::new(test_name); let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "Attempt failed".to_string(), - hashes: vec![hash_tx_1, hash_tx_2], + hashes: hashset![hash_tx_1, hash_tx_2], }), response_skeleton_opt: None, }; @@ -1760,26 +1811,25 @@ mod tests { ); 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, - vec![vec![hash_tx_1, hash_tx_2]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *delete_fingerprints_params, - vec![vec![first_fingerprint_rowid, second_fingerprint_rowid]] - ); + let sent_tx_rowids_params = get_tx_identifiers_params_arc.lock().unwrap(); + assert_eq!(*sent_tx_rowids_params, vec![hashset!(hash_tx_1, hash_tx_2)]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset!(hash_tx_1, hash_tx_2)]); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!("WARN: {test_name}: \ - Any persisted data from failed process will be deleted. Caused by: Sending phase: \"Attempt failed\". \ - Signed and hashed transactions: 0x000000000000000000000000000000000000000000000000000\ - 00000000015b3, 0x0000000000000000000000000000000000000000000000000000000000003039")); - log_handler.exists_log_containing( - &format!("WARN: {test_name}: \ - Deleting fingerprints for failed transactions 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Any persisted data from the failed process will be deleted. Caused by: Sending phase: \ + \"Attempt failed\". \ + Signed and hashed txs: \ + 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + 0x0000000000000000000000000000000000000000000000000000000000003039" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Deleting sent payable records for \ + 0x00000000000000000000000000000000000000000000000000000000000015b3, \ 0x0000000000000000000000000000000000000000000000000000000000003039", - )); + )); // we haven't supplied any result for mark_pending_payable() and so it's proved uncalled } @@ -1809,15 +1859,15 @@ mod tests { "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" )); log_handler.exists_log_containing(&format!( - "DEBUG: {test_name}: Ignoring a non-fatal error on our end from before \ - the transactions are hashed: LocallyCausedError(Signing(\"Some error\"))" + "DEBUG: {test_name}: A non-fatal error LocallyCausedError(Signing(\"Some error\")) \ + will be ignored as it is from before any tx could even be hashed" )); } #[test] - fn payable_scanner_finds_fingerprints_for_failed_payments_but_panics_at_their_deletion() { + fn payable_scanner_finds_sent_tx_record_for_failed_payments_but_panics_at_their_deletion() { let test_name = - "payable_scanner_finds_fingerprints_for_failed_payments_but_panics_at_their_deletion"; + "payable_scanner_finds_sent_tx_record_for_failed_payments_but_panics_at_their_deletion"; let rowid_1 = 4; let hash_1 = make_tx_hash(0x7b); let rowid_2 = 6; @@ -1825,20 +1875,17 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "blah".to_string(), - hashes: vec![hash_1, hash_2], + hashes: hashset![hash_1, hash_2], }), response_skeleton_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(rowid_1, hash_1), (rowid_2, hash_2)], - no_rowid_results: vec![], - }) - .delete_fingerprints_result(Err(PendingPayableDaoError::RecordDeletion( - "Gosh, I overslept without an alarm set".to_string(), + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(hash_1 => rowid_1, hash_2 => rowid_2)) + .delete_records_result(Err(SentPayableDaoError::SqlExecutionFailed( + "I overslept since my brain thinks the alarm is just a lullaby".to_string(), ))); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { @@ -1849,37 +1896,34 @@ mod tests { let panic_msg = caught_panic.downcast_ref::().unwrap(); assert_eq!( panic_msg, - "Database corrupt: payable fingerprint deletion for transactions \ + "Database corrupt: sent payable record deletion for txs \ 0x000000000000000000000000000000000000000000000000000000000000007b, 0x00000000000000000000\ - 00000000000000000000000000000000000000000315 failed due to RecordDeletion(\"Gosh, I overslept \ - without an alarm set\")"); + 00000000000000000000000000000000000000000315 failed due to SqlExecutionFailed(\"I overslept \ + since my brain thinks the alarm is just a lullaby\")"); let log_handler = TestLogHandler::new(); - // There is a possible situation when we stumble over missing fingerprints, so we log it. + // There's a possibility that we stumble over missing sent tx records, so we log it. // Here we don't and so any ERROR log shouldn't turn up log_handler.exists_no_log_containing(&format!("ERROR: {}", test_name)) } #[test] - fn payable_scanner_panics_for_missing_fingerprints_but_deletion_of_some_works() { + fn payable_scanner_panics_for_missing_sent_tx_records_but_deletion_of_some_works() { init_test_logging(); let test_name = - "payable_scanner_panics_for_missing_fingerprints_but_deletion_of_some_works"; + "payable_scanner_panics_for_missing_sent_tx_records_but_deletion_of_some_works"; let hash_1 = make_tx_hash(0x1b669); let hash_2 = make_tx_hash(0x3039); let hash_3 = make_tx_hash(0x223d); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(333, hash_1)], - no_rowid_results: vec![hash_2, hash_3], - }) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(hash_1 => 333)) + .delete_records_result(Ok(())); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "SQLite migraine".to_string(), - hashes: vec![hash_1, hash_2, hash_3], + hashes: hashset![hash_1, hash_2, hash_3], }), response_skeleton_opt: None, }; @@ -1890,41 +1934,42 @@ mod tests { let caught_panic = caught_panic_in_err.unwrap_err(); let panic_msg = caught_panic.downcast_ref::().unwrap(); - assert_eq!(panic_msg, "Ran into failed transactions 0x0000000000000000000000000000000000\ - 000000000000000000000000003039, 0x000000000000000000000000000000000000000000000000000000000000223d \ - with missing fingerprints. System no longer reliable"); + assert_eq!( + panic_msg, + "Ran into failed payables \ + 0x000000000000000000000000000000000000000000000000000000000000223d, \ + 0x0000000000000000000000000000000000000000000000000000000000003039 \ + with missing records. The system has become unreliable" + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - &format!("WARN: {test_name}: Any persisted data from failed process will be deleted. Caused by: \ - Sending phase: \"SQLite migraine\". Signed and hashed transactions: \ - 0x000000000000000000000000000000000000000000000000000000000001b669, \ - 0x0000000000000000000000000000000000000000000000000000000000003039, \ - 0x000000000000000000000000000000000000000000000000000000000000223d")); log_handler.exists_log_containing(&format!( - "WARN: {test_name}: Deleting fingerprints for failed transactions {:?}", + "WARN: {test_name}: Any persisted data from the failed process will \ + be deleted. Caused by: Sending phase: \"SQLite migraine\". Signed and hashed txs: \ + 0x000000000000000000000000000000000000000000000000000000000000223d, \ + 0x0000000000000000000000000000000000000000000000000000000000003039, \ + 0x000000000000000000000000000000000000000000000000000000000001b669" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Deleting sent payable records for {:?}", hash_1 )); } #[test] - fn payable_scanner_for_failed_rpcs_one_fingerprint_missing_and_deletion_of_the_other_one_fails() - { - // Two fatal failures at once, missing fingerprints and fingerprint deletion error are both - // legitimate reasons for panic + fn payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails() { + // Two fatal failures at once, missing sent tx records and another record deletion error + // are both legitimate reasons for panic init_test_logging(); - let test_name = "payable_scanner_for_failed_rpcs_one_fingerprint_missing_and_deletion_of_the_other_one_fails"; + let test_name = "payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails"; let existent_record_hash = make_tx_hash(0xb26e); let nonexistent_record_hash = make_tx_hash(0x4d2); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(45, existent_record_hash)], - no_rowid_results: vec![nonexistent_record_hash], - }) - .delete_fingerprints_result(Err(PendingPayableDaoError::RecordDeletion( + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(existent_record_hash => 45)) + .delete_records_result(Err(SentPayableDaoError::SqlExecutionFailed( "Another failure. Really???".to_string(), ))); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let failed_payment_1 = RpcPayableFailure { rpc_error: Error::Unreachable, @@ -1952,20 +1997,33 @@ mod tests { let panic_msg = caught_panic.downcast_ref::().unwrap(); assert_eq!( panic_msg, - "Database corrupt: payable fingerprint deletion for transactions 0x00000000000000000000000\ - 0000000000000000000000000000000000000b26e failed due to RecordDeletion(\"Another failure. Really???\")"); + "Database corrupt: sent payable record deletion for txs \ + 0x000000000000000000000000000000000000000000000000000000000000b26e failed due to \ + SqlExecutionFailed(\"Another failure. Really???\")" + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!("WARN: {test_name}: Remote transaction failure: 'Server is unreachable' \ - for payment to 0x0000000000000000000000000000000000616263 and transaction hash 0x00000000000000000000000\ - 0000000000000000000000000000000000000b26e. Please check your blockchain service URL configuration.")); - log_handler.exists_log_containing(&format!("WARN: {test_name}: Remote transaction failure: 'Internal Web3 error' \ - for payment to 0x0000000000000000000000000000000000646566 and transaction hash 0x000000000000000000000000\ - 00000000000000000000000000000000000004d2. Please check your blockchain service URL configuration.")); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Remote sent payable \ + failure 'Server is unreachable' for wallet 0x0000000000000000000000000000000000616263 \ + and tx hash 0x000000000000000000000000000000000000000000000000000000000000b26e" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Remote sent payable \ + failure 'Internal Web3 error' for wallet 0x0000000000000000000000000000000000646566 \ + and tx hash 0x00000000000000000000000000000000000000000000000000000000000004d2" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Please check your blockchain service URL configuration due to detected remote failures" + )); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of 2 attempts" )); - log_handler.exists_log_containing(&format!("ERROR: {test_name}: Ran into failed transactions 0x0000000000000000\ - 0000000000000000000000000000000000000000000004d2 with missing fingerprints. System no longer reliable")); + log_handler.exists_log_containing(&format!( + "ERROR: {test_name}: Ran into failed \ + payables 0x00000000000000000000000000000000000000000000000000000000000004d2 with missing \ + records. The system has become unreliable" + )); } #[test] @@ -2029,7 +2087,7 @@ mod tests { gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) )] ) - //no other method was called (absence of panic) and that means we returned early + //no other method was called (absence of panic), and that means we returned early } #[test] @@ -2154,9 +2212,9 @@ mod tests { assert_eq!(result, vec![qualified_payable]); TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {}: Paying qualified debts:\n999,999,999,000,000,\ - 000 wei owed for \\d+ sec exceeds threshold: 500,000,000,000,000,000 wei; creditor: \ - 0x0000000000000000000000000077616c6c657430", + "DEBUG: {}: Paying qualified debts:\n\ + 999,999,999,000,000,000 wei owed for \\d+ sec exceeds the threshold \ + 500,000,000,000,000,000 wei for creditor 0x0000000000000000000000000077616c6c657430", test_name )); } @@ -2194,28 +2252,18 @@ mod tests { let test_name = "pending_payable_scanner_can_initiate_a_scan"; let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 1, - amount: 7999, - process_error: None, - }; - let fingerprints = vec![payable_fingerprint_1, payable_fingerprint_2]; - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(fingerprints.clone()); + let sent_tx = make_sent_tx(456); + let sent_tx_hash = sent_tx.hash; + let failed_tx = make_failed_tx(789); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![sent_tx.clone()]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(vec![failed_tx.clone()]); let mut subject = make_dull_subject(); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -2231,21 +2279,21 @@ mod tests { true, ); - let no_of_pending_payables = fingerprints.len(); 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: fingerprints, + tx_hashes: vec![ + TxHashByTable::SentPayable(sent_tx_hash), + TxHashByTable::FailedPayable(failed_tx.hash) + ], response_skeleton_opt: None }) ); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!("INFO: {test_name}: Scanning for pending payable"), - &format!( - "DEBUG: {test_name}: Found {no_of_pending_payables} pending payables to process" - ), + &format!("DEBUG: {test_name}: Found 1 pending payables and 1 unfinalized failures to process"), ]) } @@ -2254,10 +2302,15 @@ mod tests { 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![make_pending_payable_fingerprint()]); + let sent_payable_dao = + SentPayableDaoMock::new().retrieve_txs_result(vec![make_sent_tx(123)]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(vec![make_failed_tx(456)]); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -2400,24 +2453,6 @@ mod tests { ); } - #[test] - fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { - let now = SystemTime::now(); - let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let pending_payable_dao = - PendingPayableDaoMock::new().return_all_errorless_fingerprints_result(vec![]); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - let result = - 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(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(); @@ -2430,60 +2465,110 @@ mod tests { } #[test] - fn pending_payable_scanner_handles_report_transaction_receipts_message() { + fn pending_payable_scanner_handles_tx_receipts_message() { + // Note: the choice of those hashes isn't random; I tried to make sure I will know the order, + // in which these records will be processed, because they are in an ordered map. + // It is important because otherwise preparation of results with the mocks would become + // chaotic, as long as you care about the exact receiver of the mock call among these records init_test_logging(); - let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message"; + let test_name = "pending_payable_scanner_handles_tx_receipts_message"; + // Normal confirmation let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); + // FailedTx reclaim + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + // New tx failure + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + // Validation failures + let update_statuses_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_failed_payable_params_arc = Arc::new(Mutex::new(vec![])); + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_millis(12)); + let timestamp_c = SystemTime::now().sub(Duration::from_millis(1234)); let payable_dao = PayableDaoMock::new() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::new().delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::new() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .update_statuses_params(&update_statuses_pending_payable_params_arc) + .update_statuses_result(Ok(())) + .replace_records_result(Ok(())) + .delete_records_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::new() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())) + .update_statuses_params(&update_statuses_failed_payable_params_arc) + .update_statuses_result(Ok(())) + .delete_records_result(Ok(())); + let tx_hash_1 = make_tx_hash(0x111); + let mut sent_tx_1 = make_sent_tx(123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(333), + block_number: U64::from(1234), + }; + let tx_status_1 = StatusReadFromReceiptCheck::Succeeded(tx_block_1); + let tx_hash_2 = make_tx_hash(0x222); + let mut failed_tx_2 = make_failed_tx(789); + failed_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(222), + block_number: U64::from(2345), + }; + let tx_status_2 = StatusReadFromReceiptCheck::Succeeded(tx_block_2); + let tx_hash_3 = make_tx_hash(0x333); + let mut sent_tx_3 = make_sent_tx(456); + sent_tx_3.hash = tx_hash_3; + let tx_status_3 = StatusReadFromReceiptCheck::Pending; + let tx_hash_4 = make_tx_hash(0x444); + let mut sent_tx_4 = make_sent_tx(4567); + sent_tx_4.hash = tx_hash_4; + sent_tx_4.status = TxStatus::Pending(ValidationStatus::Waiting); + let tx_receipt_rpc_error_4 = AppRpcError::Remote(RemoteError::Unreachable); + let tx_hash_5 = make_tx_hash(0x555); + let mut failed_tx_5 = make_failed_tx(888); + failed_tx_5.hash = tx_hash_5; + failed_tx_5.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ))); + let tx_receipt_rpc_error_5 = + AppRpcError::Remote(RemoteError::InvalidResponse("game over".to_string())); + let tx_hash_6 = make_tx_hash(0x666); + let mut sent_tx_6 = make_sent_tx(789); + sent_tx_6.hash = tx_hash_6; + let tx_status_6 = StatusReadFromReceiptCheck::Reverted; + let sent_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(sent_tx_1.clone())) + .get_record_by_hash_result(Some(sent_tx_3.clone())) + .get_record_by_hash_result(Some(sent_tx_4)) + .get_record_by_hash_result(Some(sent_tx_6.clone())); + let failed_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(failed_tx_2.clone())) + .get_record_by_hash_result(Some(failed_tx_5)); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b); let mut pending_payable_scanner = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .validation_failure_clock(Box::new(validation_failure_clock)) .build(); - 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(), - block_number: U64::from(1234), - }), - }; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, - }; - let transaction_hash_2 = make_tx_hash(1234); - let transaction_receipt_2 = TxReceipt { - transaction_hash: transaction_hash_2, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(2345), - }), - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 10, - timestamp: from_unix_timestamp(199_780_000), - hash: transaction_hash_2, - 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(), - ), + let msg = TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(tx_hash_1) => Ok(tx_status_1), + TxHashByTable::FailedPayable(tx_hash_2) => Ok(tx_status_2), + TxHashByTable::SentPayable(tx_hash_3) => Ok(tx_status_3), + TxHashByTable::SentPayable(tx_hash_4) => Err(tx_receipt_rpc_error_4), + TxHashByTable::FailedPayable(tx_hash_5) => Err(tx_receipt_rpc_error_5), + TxHashByTable::SentPayable(tx_hash_6) => Ok(tx_status_6), ], response_skeleton_opt: None, }; @@ -2493,54 +2578,75 @@ mod tests { let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); assert_eq!( result, - PendingPayableScanResult::NoPendingPayablesLeft(None) + PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) ); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + assert_eq!(*transactions_confirmed_params, vec![vec![sent_tx_1]]); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); + assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); + let sent_tx_2 = SentTx::from((failed_tx_2, tx_block_2)); + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failure_for_tx_3 = FailedTx::from((sent_tx_3, FailureReason::PendingTooLong)); + let expected_failure_for_tx_6 = FailedTx::from((sent_tx_6, FailureReason::Reverted)); assert_eq!( - *transactions_confirmed_params, - vec![vec![fingerprint_1, fingerprint_2]] + *insert_new_records_params, + vec![vec![expected_failure_for_tx_3, expected_failure_for_tx_6]] + ); + let update_statuses_pending_payable_params = + update_statuses_pending_payable_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_pending_payable_params, + vec![ + hashmap!(tx_hash_4 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_a))))) + ] + ); + let update_statuses_failed_payable_params = + update_statuses_failed_payable_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_failed_payable_params, + vec![ + hashmap!(tx_hash_5 => FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_c)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_b))))) + ] ); 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", - test_name, transaction_hash_1, transaction_hash_2 - ), - &format!("INFO: {test_name}: The PendingPayables scan ended in \\d+ms."), - ]); + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Processing receipts for 6 txs" + )); + test_log_handler.exists_log_containing(&format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000000000000000000000000000000000000000000000000000000444): Remote(Unreachable). Will retry receipt retrieval next cycle")); + test_log_handler.exists_log_containing(&format!("WARN: {test_name}: Failed to retrieve tx receipt for FailedPayable(0x0000000000000000000000000000000000000000000000000000000000000555): Remote(InvalidResponse(\"game over\")). Will retry receipt retrieval next cycle")); + test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000222 (block 2345) as confirmed on-chain")); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000111 (block 1234) was confirmed", + )); + test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000333, 0x0000000000000000000000000000000000000000000000000000000000000666 were processed in the db")); } #[test] + #[should_panic( + expected = "We should never receive an empty list of results. Even receipts that could not \ + be retrieved can be interpreted" + )] fn pending_payable_scanner_handles_empty_report_transaction_receipts_message() { - init_test_logging(); - let test_name = - "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], + let msg = TxReceiptsMessage { + results: hashmap![], response_skeleton_opt: None, }; pending_payable_scanner.mark_as_started(SystemTime::now()); let mut subject = make_dull_subject(); subject.pending_payable = Box::new(pending_payable_scanner); - let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - - 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!( - "WARN: {test_name}: No transaction receipts found." - )); - tlh.exists_log_matching(&format!( - "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." - )); + let _ = subject.finish_pending_payable_scan(msg, &Logger::new("test")); } #[test] @@ -2726,8 +2832,10 @@ mod tests { } #[test] - #[should_panic(expected = "Attempt to set new start block to 6709 failed due to: \ - UninterpretableValue(\"Illiterate database manager\")")] + #[should_panic( + expected = "Attempt to advance the start block to 6709 failed due to: \ + UninterpretableValue(\"Illiterate database manager\")" + )] fn no_transactions_received_but_start_block_setting_fails() { init_test_logging(); let test_name = "no_transactions_received_but_start_block_setting_fails"; @@ -2972,7 +3080,7 @@ mod tests { subject.signal_scanner_completion(ScanType::Receivables, SystemTime::now(), &logger); TestLogHandler::new().exists_log_containing(&format!( - "ERROR: {test_name}: Called scan_finished() for Receivables scanner but timestamp was not found" + "ERROR: {test_name}: Called scan_finished() for Receivables scanner but could not find any timestamp" )); } @@ -3023,7 +3131,7 @@ mod tests { &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PendingPayableScannerBuilder::new().build(), "PendingPayables", test_name, diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index cfb874f197..f501a7be25 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -1,37 +1,67 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod test_utils; +mod tx_receipt_interpreter; pub mod utils; -use std::cell::RefCell; -use std::rc::Rc; -use std::time::SystemTime; -use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; -use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; -use crate::accountant::db_access_objects::payable_dao::PayableDao; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; -use crate::accountant::{comma_joined_stringifiable, PendingPayableId, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables}; -use crate::accountant::scanners::{PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner}; -use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedTx, FailureRetrieveCondition, FailureStatus, +}; +use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDao, SentPayableDaoError, SentTx, TxStatus, +}; +use crate::accountant::db_access_objects::utils::{TxHash, TxRecordWithHash}; +use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, MismatchReport, PendingPayableCache, PendingPayableScanResult, + PresortedTxFailure, ReceiptScanReport, RecheckRequiringFailures, Retry, TxByTable, + TxCaseToBeInterpreted, TxHashByTable, UpdatableValidationStatus, +}; +use crate::accountant::scanners::{ + PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, +}; +use crate::accountant::{ + comma_joined_stringifiable, RequestTransactionReceipts, ResponseSkeleton, + ScanForPendingPayables, TxReceiptResult, TxReceiptsMessage, +}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; +use crate::blockchain::errors::validation_status::{ + ValidationFailureClock, ValidationFailureClockReal, +}; use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; use crate::sub_lib::wallet::Wallet; use crate::time_marking_methods; +use itertools::{Either, Itertools}; +use masq_lib::logger::Logger; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::rc::Rc; +use std::str::FromStr; +use std::time::SystemTime; +use thousands::Separable; +use web3::types::H256; pub struct PendingPayableScanner { pub common: ScannerCommon, pub payable_dao: Box, - pub pending_payable_dao: Box, - pub when_pending_too_long_sec: u64, + pub sent_payable_dao: Box, + pub failed_payable_dao: Box, pub financial_statistics: Rc>, + pub current_sent_payables: Box>, + pub yet_unproven_failed_payables: Box>, + pub clock: Box, } impl PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, > for PendingPayableScanner { @@ -49,756 +79,1975 @@ impl StartableScanner ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); - match filtered_pending_payable.is_empty() { - true => { - self.mark_as_ended(logger); - Err(StartScanError::NothingToProcess) - } - false => { - debug!( - logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - Ok(RequestTransactionReceipts { - pending_payable_fingerprints: filtered_pending_payable, - response_skeleton_opt, - }) - } + + let pending_tx_hashes_opt = self.handle_pending_payables(); + let failure_hashes_opt = self.handle_unproven_failures(); + + if pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() { + self.mark_as_ended(logger); + return Err(StartScanError::NothingToProcess); } + + Self::log_records_found_for_receipt_check( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + logger, + ); + + let all_hashes = pending_tx_hashes_opt + .unwrap_or_default() + .into_iter() + .chain(failure_hashes_opt.unwrap_or_default()) + .collect_vec(); + + Ok(RequestTransactionReceipts { + tx_hashes: all_hashes, + response_skeleton_opt, + }) } } -impl Scanner for PendingPayableScanner { +impl Scanner for PendingPayableScanner { fn finish_scan( &mut self, - message: ReportTransactionReceipts, + message: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { let response_skeleton_opt = message.response_skeleton_opt; - let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { - true => { - warning!(logger, "No transaction receipts found."); - todo!("This requires the payment retry. GH-631 must be completed first"); - } - false => { - debug!( - logger, - "Processing receipts for {} transactions", - message.fingerprints_with_receipts.len() - ); - let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - let requires_payment_retry = - self.process_transactions_by_reported_state(scan_report, logger); + let scan_report = self.interpret_tx_receipts(message, logger); - self.mark_as_ended(logger); + let retry_opt = scan_report.requires_payments_retry(); - requires_payment_retry - } - }; + self.process_txs_by_state(scan_report, logger); - 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) - } + self.mark_as_ended(logger); + + Self::compose_scan_result(retry_opt, response_skeleton_opt) } time_marking_methods!(PendingPayables); as_any_ref_in_trait_impl!(); + + as_any_mut_in_trait_impl!(); } impl PendingPayableScanner { pub fn new( payable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, + failed_payable_dao: Box, payment_thresholds: Rc, - when_pending_too_long_sec: u64, financial_statistics: Rc>, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, - pending_payable_dao, - when_pending_too_long_sec, + sent_payable_dao, + failed_payable_dao, financial_statistics, + current_sent_payables: Box::new(CurrentPendingPayables::default()), + yet_unproven_failed_payables: Box::new(RecheckRequiringFailures::default()), + clock: Box::new(ValidationFailureClockReal::default()), } } - fn handle_receipts_for_pending_transactions( - &self, - msg: ReportTransactionReceipts, + fn handle_pending_payables(&mut self) -> Option> { + let pending_txs = self + .sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::IsPending)); + + if pending_txs.is_empty() { + return None; + } + + let pending_tx_hashes = Self::get_wrapped_hashes(&pending_txs, TxHashByTable::SentPayable); + self.current_sent_payables.load_cache(pending_txs); + Some(pending_tx_hashes) + } + + fn handle_unproven_failures(&mut self) -> Option> { + let failures = self + .failed_payable_dao + .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)); + + if failures.is_empty() { + return None; + } + + let failure_hashes = Self::get_wrapped_hashes(&failures, TxHashByTable::FailedPayable); + self.yet_unproven_failed_payables.load_cache(failures); + Some(failure_hashes) + } + + fn get_wrapped_hashes( + records: &[Record], + wrap_the_hash: fn(TxHash) -> TxHashByTable, + ) -> Vec + where + Record: TxRecordWithHash, + { + records + .iter() + .map(|record| wrap_the_hash(record.hash())) + .collect_vec() + } + + fn emptiness_check(&self, msg: &TxReceiptsMessage) { + if msg.results.is_empty() { + panic!( + "We should never receive an empty list of results. \ + Even receipts that could not be retrieved can be interpreted" + ) + } + } + + fn compose_scan_result( + retry_opt: Option, + response_skeleton_opt: Option, + ) -> PendingPayableScanResult { + if let Some(retry) = retry_opt { + if let Some(response_skeleton) = response_skeleton_opt { + let ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }; + PendingPayableScanResult::PaymentRetryRequired(Either::Right(ui_msg)) + } else { + PendingPayableScanResult::PaymentRetryRequired(Either::Left(retry)) + } + } 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) + } + } + + fn interpret_tx_receipts( + &mut self, + msg: TxReceiptsMessage, + logger: &Logger, + ) -> ReceiptScanReport { + self.emptiness_check(&msg); + + debug!(logger, "Processing receipts for {} txs", msg.results.len()); + + let interpretable_data = self.prepare_cases_to_interpret(msg, logger); + TxReceiptInterpreter::default().compose_receipt_scan_report( + interpretable_data, + &self, + logger, + ) + } + + fn prepare_cases_to_interpret( + &mut self, + msg: TxReceiptsMessage, logger: &Logger, - ) -> PendingPayableScanReport { - let scan_report = PendingPayableScanReport::default(); - msg.fingerprints_with_receipts.into_iter().fold( - scan_report, - |scan_report_so_far, (receipt_result, fingerprint)| match receipt_result { - TransactionReceiptResult::RpcResponse(tx_receipt) => match tx_receipt.status { - TxStatus::Pending => handle_none_receipt( - scan_report_so_far, - fingerprint, - "none was given", - logger, - ), - TxStatus::Failed => { - handle_status_with_failure(scan_report_so_far, fingerprint, logger) + ) -> Vec { + let init: Either, MismatchReport> = Either::Left(vec![]); + let either = msg + .results + .into_iter() + // This must be in for predictability in tests + .sorted_by_key(|(hash_by_table, _)| hash_by_table.hash()) + .fold( + init, + |acc, (tx_hash_by_table, tx_receipt_result)| match acc { + Either::Left(cases) => { + self.resolve_real_query(cases, tx_receipt_result, tx_hash_by_table) } - TxStatus::Succeeded(_) => { - handle_status_with_success(scan_report_so_far, fingerprint, logger) + Either::Right(mut mismatch_report) => { + mismatch_report.remaining_hashes.push(tx_hash_by_table); + Either::Right(mismatch_report) } }, - TransactionReceiptResult::LocalError(e) => handle_none_receipt( - scan_report_so_far, - fingerprint, - &format!("failed due to {}", e), - logger, - ), - }, + ); + + let cases = match either { + Either::Left(cases) => cases, + Either::Right(mismatch_report) => self.panic_dump(mismatch_report), + }; + + self.current_sent_payables.ensure_empty_cache(logger); + self.yet_unproven_failed_payables.ensure_empty_cache(logger); + + cases + } + + fn resolve_real_query( + &mut self, + mut cases: Vec, + receipt_result: TxReceiptResult, + looked_up_hash: TxHashByTable, + ) -> Either, MismatchReport> { + match looked_up_hash { + TxHashByTable::SentPayable(tx_hash) => { + match self.current_sent_payables.get_record_by_hash(tx_hash) { + Some(sent_tx) => { + cases.push(TxCaseToBeInterpreted::new( + TxByTable::SentPayable(sent_tx), + receipt_result, + )); + Either::Left(cases) + } + None => Either::Right(MismatchReport { + noticed_with: looked_up_hash, + remaining_hashes: vec![], + }), + } + } + TxHashByTable::FailedPayable(tx_hash) => { + match self + .yet_unproven_failed_payables + .get_record_by_hash(tx_hash) + { + Some(failed_tx) => { + cases.push(TxCaseToBeInterpreted::new( + TxByTable::FailedPayable(failed_tx), + receipt_result, + )); + Either::Left(cases) + } + None => Either::Right(MismatchReport { + noticed_with: looked_up_hash, + remaining_hashes: vec![], + }), + } + } + } + } + + fn panic_dump(&mut self, mismatch_report: MismatchReport) -> ! { + fn rearrange(hashmap: HashMap) -> Vec { + hashmap + .into_iter() + .sorted_by_key(|(tx_hash, _)| *tx_hash) + .map(|(_, record)| record) + .collect_vec() + } + + panic!( + "Looking up '{:?}' in the cache, the record could not be found. Dumping \ + the remaining values. Pending payables: {:?}. Unproven failures: {:?}. \ + Hashes yet not looked up: {:?}.", + mismatch_report.noticed_with, + rearrange(self.current_sent_payables.dump_cache()), + rearrange(self.yet_unproven_failed_payables.dump_cache()), + mismatch_report.remaining_hashes ) } - fn process_transactions_by_reported_state( + fn process_txs_by_state(&mut self, scan_report: ReceiptScanReport, logger: &Logger) { + self.handle_confirmed_transactions(scan_report.confirmations, logger); + self.handle_failed_transactions(scan_report.failures, logger); + } + + fn handle_confirmed_transactions( &mut self, - scan_report: PendingPayableScanReport, + confirmed_txs: DetectedConfirmations, logger: &Logger, - ) -> bool { - let requires_payments_retry = scan_report.requires_payments_retry(); + ) { + self.handle_tx_failure_reclaims(confirmed_txs.reclaims, logger); + self.handle_normal_confirmations(confirmed_txs.normal_confirmations, logger); + } + + fn handle_tx_failure_reclaims(&mut self, reclaimed: Vec, logger: &Logger) { + if reclaimed.is_empty() { + return; + } + + let hashes_and_blocks = Self::collect_and_sort_hashes_and_blocks(&reclaimed); + + self.replace_sent_tx_records(&reclaimed, &hashes_and_blocks, logger); + + self.delete_failed_tx_records(&hashes_and_blocks, logger); + + self.add_to_the_total_of_paid_payable(&reclaimed, logger) + } + + fn isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> HashSet { + reclaimed.iter().map(|(tx_hash, _)| *tx_hash).collect() + } + + fn collect_and_sort_hashes_and_blocks(sent_txs: &[SentTx]) -> Vec<(TxHash, TxBlock)> { + Self::collect_hashes_and_blocks(sent_txs) + .into_iter() + .sorted() + .collect_vec() + } - self.confirm_transactions(scan_report.confirmed, logger); - self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger); + fn collect_hashes_and_blocks(reclaimed: &[SentTx]) -> HashMap { + reclaimed + .iter() + .map(|reclaim| { + let tx_block = if let TxStatus::Confirmed { block_hash, block_number, .. } = + &reclaim.status + { + TxBlock{ + block_hash: H256::from_str(&block_hash[2..]).expect("Failed to construct hash from str"), + block_number: (*block_number).into() + } + } else { + panic!( + "Processing a reclaim for tx {:?} which isn't filled with the confirmation details", + reclaim.hash + ) + }; + (reclaim.hash, tx_block) + }) + .collect() + } - requires_payments_retry + fn replace_sent_tx_records( + &self, + sent_txs_to_reclaim: &[SentTx], + hashes_and_blocks: &[(TxHash, TxBlock)], + logger: &Logger, + ) { + match self.sent_payable_dao.replace_records(sent_txs_to_reclaim) { + Ok(_) => { + debug!(logger, "Replaced records for txs being reclaimed") + } + Err(e) => { + panic!( + "Unable to proceed in a reclaim as the replacement of sent tx records \ + {} failed due to: {:?}", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + format!("{:?}", tx_hash) + }), + e + ) + } + } } - fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.increment_scan_attempts(&rowids) { - Ok(_) => trace!( + fn delete_failed_tx_records(&self, hashes_and_blocks: &[(TxHash, TxBlock)], logger: &Logger) { + let hashes = Self::isolate_hashes(hashes_and_blocks); + match self.failed_payable_dao.delete_records(&hashes) { + Ok(_) => { + info!( logger, - "Updated records for rowids: {} ", - comma_joined_stringifiable(&rowids, |id| id.to_string()) - ), - Err(e) => panic!( - "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", - PendingPayableId::serialize_hashes_to_string(&ids), + "Reclaimed txs {} as confirmed on-chain", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, tx_block)| { + format!("{:?} (block {})", tx_hash, tx_block.block_number) + }) + ) + } + Err(e) => { + panic!( + "Unable to delete failed tx records {} to finish the reclaims due to: {:?}", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + format!("{:?}", tx_hash) + }), e - ), + ) + } + } + } + + fn handle_normal_confirmations(&mut self, confirmed_txs: Vec, logger: &Logger) { + if confirmed_txs.is_empty() { + return; + } + + self.confirm_transactions(&confirmed_txs); + + self.update_tx_blocks(&confirmed_txs, logger); + + self.add_to_the_total_of_paid_payable(&confirmed_txs, logger); + } + + fn confirm_transactions(&self, confirmed_sent_txs: &[SentTx]) { + if let Err(e) = self.payable_dao.transactions_confirmed(confirmed_sent_txs) { + Self::transaction_confirmed_panic(confirmed_sent_txs, e); + } + } + + fn update_tx_blocks(&self, confirmed_sent_txs: &[SentTx], logger: &Logger) { + let tx_confirmations = Self::collect_hashes_and_blocks(confirmed_sent_txs); + + if let Err(e) = self.sent_payable_dao.confirm_txs(&tx_confirmations) { + Self::update_tx_blocks_panic(&tx_confirmations, e); + } else { + Self::log_tx_success(logger, &tx_confirmations); + } + } + + fn log_tx_success(logger: &Logger, tx_hashes_and_tx_blocks: &HashMap) { + logger.info(|| { + let pretty_pairs = tx_hashes_and_tx_blocks + .iter() + .sorted() + .map(|(hash, tx_confirmation)| { + format!("{:?} (block {})", hash, tx_confirmation.block_number) + }) + .join(", "); + match tx_hashes_and_tx_blocks.len() { + 1 => format!("Tx {} was confirmed", pretty_pairs), + _ => format!("Txs {} were confirmed", pretty_pairs), + } + }); + } + + fn transaction_confirmed_panic(confirmed_txs: &[SentTx], e: PayableDaoError) -> ! { + panic!( + "Unable to complete the tx confirmation by the adjustment of the payable accounts \ + {} due to: {:?}", + comma_joined_stringifiable( + &confirmed_txs + .iter() + .map(|tx| tx.receiver_address) + .collect_vec(), + |wallet| format!("{:?}", wallet) + ), + e + ) + } + fn update_tx_blocks_panic( + tx_hashes_and_tx_blocks: &HashMap, + e: SentPayableDaoError, + ) -> ! { + panic!( + "Unable to update sent payable records {} by their tx blocks due to: {:?}", + comma_joined_stringifiable( + &tx_hashes_and_tx_blocks.keys().sorted().collect_vec(), + |tx_hash| format!("{:?}", tx_hash) + ), + e + ) + } + + fn add_to_the_total_of_paid_payable(&mut self, confirmed_payments: &[SentTx], logger: &Logger) { + let to_be_added: u128 = confirmed_payments + .iter() + .map(|sent_tx| sent_tx.amount_minor) + .sum(); + + let total_paid_payable = &mut self + .financial_statistics + .borrow_mut() + .total_paid_payable_wei; + + *total_paid_payable += to_be_added; + + debug!( + logger, + "The total paid payables increased by {} to {} wei", + to_be_added.separate_with_commas(), + total_paid_payable.separate_with_commas() + ); + } + + fn handle_failed_transactions(&self, failures: DetectedFailures, logger: &Logger) { + self.handle_tx_failures(failures.tx_failures, logger); + self.handle_rpc_failures(failures.tx_receipt_rpc_failures, logger); + } + + fn handle_tx_failures(&self, failures: Vec, logger: &Logger) { + #[derive(Default)] + struct GroupedFailures { + new_failures: Vec, + rechecks_completed: Vec, + } + + let grouped_failures = + failures + .into_iter() + .fold(GroupedFailures::default(), |mut acc, failure| { + match failure { + PresortedTxFailure::NewEntry(failed_tx) => { + acc.new_failures.push(failed_tx); + } + PresortedTxFailure::RecheckCompleted(tx_hash) => { + acc.rechecks_completed.push(tx_hash); + } + } + acc + }); + + self.add_new_failures(grouped_failures.new_failures, logger); + self.finalize_unproven_failures(grouped_failures.rechecks_completed, logger); + } + + fn add_new_failures(&self, new_failures: Vec, logger: &Logger) { + fn prepare_hashset(failures: &[FailedTx]) -> HashSet { + failures.iter().map(|failure| failure.hash).collect() + } + fn log_procedure_finished(logger: &Logger, new_failures: &[FailedTx]) { + info!( + logger, + "Failed txs {} were processed in the db", + comma_joined_stringifiable(new_failures, |failure| format!("{:?}", failure.hash)) + ) + } + + if new_failures.is_empty() { + return; + } + + if let Err(e) = self.failed_payable_dao.insert_new_records(&new_failures) { + panic!( + "Unable to persist failed txs {} due to: {:?}", + comma_joined_stringifiable(&new_failures, |failure| format!("{:?}", failure.hash)), + e + ) + } + + match self + .sent_payable_dao + .delete_records(&prepare_hashset(&new_failures)) + { + Ok(_) => { + log_procedure_finished(logger, &new_failures); + } + Err(e) => { + panic!( + "Unable to purge sent payable records for failed txs {} due to: {:?}", + comma_joined_stringifiable(&new_failures, |failure| format!( + "{:?}", + failure.hash + )), + e + ) } } } - fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - //TODO this function is imperfect. It waits for GH-663 - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.mark_failures(&rowids) { - Ok(_) => warning!( + fn finalize_unproven_failures(&self, rechecks_completed: Vec, logger: &Logger) { + fn prepare_hashmap(rechecks_completed: &[TxHash]) -> HashMap { + rechecks_completed + .iter() + .map(|tx_hash| (tx_hash.clone(), FailureStatus::Concluded)) + .collect() + } + + if rechecks_completed.is_empty() { + return; + } + + match self + .failed_payable_dao + .update_statuses(&prepare_hashmap(&rechecks_completed)) + { + Ok(_) => { + debug!( logger, - "Broken transactions {} marked as an error. You should take over the care \ - of those to make sure your debts are going to be settled properly. At the moment, \ - there is no automated process fixing that without your assistance", - PendingPayableId::serialize_hashes_to_string(&ids) - ), - Err(e) => panic!( - "Unsuccessful attempt for transactions {} \ - to mark fatal error at payable fingerprint due to {:?}; database unreliable", - PendingPayableId::serialize_hashes_to_string(&ids), + "Concluded failures that had required rechecks: {}.", + comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( + "{:?}", + tx_hash + )) + ); + } + Err(e) => { + panic!( + "Unable to conclude rechecks for failed txs {} due to: {:?}", + comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( + "{:?}", + tx_hash + )), e - ), + ) } } } - fn confirm_transactions( - &mut self, - fingerprints: Vec, + fn handle_rpc_failures(&self, failures: Vec, logger: &Logger) { + if failures.is_empty() { + return; + } + + let (sent_payable_failures, failed_payable_failures): ( + Vec>, + Vec>, + ) = failures.into_iter().partition_map(|failure| match failure { + FailedValidationByTable::SentPayable(failed_validation) => { + Either::Left(failed_validation) + } + FailedValidationByTable::FailedPayable(failed_validation) => { + Either::Right(failed_validation) + } + }); + + self.update_validation_status_for_sent_txs(sent_payable_failures, logger); + + self.update_validation_status_for_failed_txs(failed_payable_failures, logger); + } + + fn update_validation_status_for_sent_txs( + &self, + sent_payable_failures: Vec>, logger: &Logger, ) { - fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { - comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) + if !sent_payable_failures.is_empty() { + let updatable = + Self::prepare_statuses_for_update(&sent_payable_failures, &*self.clock, logger); + if !updatable.is_empty() { + match self.sent_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Pending-tx statuses were processed in the db for validation failure \ + of txs {}", + comma_joined_stringifiable(&sent_payable_failures, |failure| { + format!("{:?}", failure.tx_hash) + }) + ) + } + Err(e) => { + panic!( + "Unable to update pending-tx statuses for validation failures '{:?}' \ + due to: {:?}", + sent_payable_failures, e + ) + } + } + } } + } - if !fingerprints.is_empty() { - if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { - panic!( - "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ - records due to {:?}", serialize_hashes(&fingerprints), e - ) - } else { - self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); - let rowids = fingerprints - .iter() - .map(|fingerprint| fingerprint.rowid) - .collect::>(); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { - panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", - serialize_hashes(&fingerprints), e) - } else { - info!( - logger, - "Transactions {} completed their confirmation process succeeding", - serialize_hashes(&fingerprints) - ) + fn update_validation_status_for_failed_txs( + &self, + failed_txs_validation_failures: Vec>, + logger: &Logger, + ) { + if !failed_txs_validation_failures.is_empty() { + let updatable = Self::prepare_statuses_for_update( + &failed_txs_validation_failures, + &*self.clock, + logger, + ); + if !updatable.is_empty() { + match self.failed_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Failed-tx statuses were processed in the db for validation failure \ + of txs {}", + comma_joined_stringifiable( + &failed_txs_validation_failures, + |failure| { format!("{:?}", failure.tx_hash) } + ) + ) + } + Err(e) => { + panic!( + "Unable to update failed-tx statuses for validation failures '{:?}' \ + due to: {:?}", + failed_txs_validation_failures, e + ) + } } } } } - fn add_to_the_total_of_paid_payable( - &mut self, - fingerprints: &[PendingPayableFingerprint], - serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, + fn prepare_statuses_for_update( + failures: &[FailedValidation], + clock: &dyn ValidationFailureClock, + logger: &Logger, + ) -> HashMap { + failures + .iter() + .flat_map(|failure| { + failure + .new_status(clock) + .map(|tx_status| (failure.tx_hash, tx_status)) + .or_else(|| { + debug!( + logger, + "{}", + PendingPayableScanner::status_not_updatable_log_msg( + &failure.current_status + ) + ); + None + }) + }) + .collect() + } + + fn status_not_updatable_log_msg(status: &dyn Display) -> String { + format!( + "Handling a validation failure, but the status {} cannot be updated.", + status + ) + } + + fn log_records_found_for_receipt_check( + pending_tx_hashes_opt: Option<&Vec>, + failure_hashes_opt: Option<&Vec>, logger: &Logger, ) { - fingerprints.iter().for_each(|fingerprint| { - self.financial_statistics - .borrow_mut() - .total_paid_payable_wei += fingerprint.amount - }); + fn resolve_optional_vec(vec_opt: Option<&Vec>) -> usize { + vec_opt.map(|hashes| hashes.len()).unwrap_or_default() + } + debug!( logger, - "Confirmation of transactions {}; record for total paid payable was modified", - serialize_hashes(fingerprints) + "Found {} pending payables and {} unfinalized failures to process", + resolve_optional_vec(pending_tx_hashes_opt), + resolve_optional_vec(failure_hashes_opt) ); } } #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDaoError, FailureStatus, + }; + use crate::accountant::db_access_objects::payable_dao::PayableDaoError; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, SentPayableDaoError, TxStatus, + }; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PendingPayableScanResult, PresortedTxFailure, + RecheckRequiringFailures, Retry, TxHashByTable, + }; + use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; + use crate::accountant::scanners::test_utils::PendingPayableCacheMock; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner}; + use crate::accountant::test_utils::{ + make_failed_tx, make_sent_tx, make_transaction_block, FailedPayableDaoMock, PayableDaoMock, + PendingPayableScannerBuilder, SentPayableDaoMock, + }; + use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; + use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, + }; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteErrorKind, + }; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; + use crate::test_utils::{make_paying_wallet, make_wallet}; + use itertools::{Either, Itertools}; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use regex::Regex; + use std::collections::HashMap; use std::ops::Sub; + use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use ethereum_types::{H256, U64}; - use regex::Regex; - use web3::types::TransactionReceipt; - use masq_lib::logger::Logger; - use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use crate::accountant::{PendingPayableId, ReportTransactionReceipts, DEFAULT_PENDING_TOO_LONG_SEC}; - use crate::accountant::db_access_objects::payable_dao::PayableDaoError; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoError; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::test_utils::{make_pending_payable_fingerprint, PayableDaoMock, PendingPayableDaoMock, PendingPayableScannerBuilder}; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxReceipt, TxStatus}; - use crate::blockchain::test_utils::make_tx_hash; - - fn assert_interpreting_none_status_for_pending_payable( - test_name: &str, - when_pending_too_long_sec: u64, - pending_payable_age_sec: u64, - rowid: u64, - hash: H256, - ) -> PendingPayableScanReport { - init_test_logging(); - let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: when_sent, - hash, - attempt: 1, - amount: 123, - process_error: None, - }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - - handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) - } - fn assert_log_msg_and_elapsed_time_in_log_makes_sense( - expected_msg: &str, - elapsed_after: u64, - capture_regex: &str, - ) { - let log_handler = TestLogHandler::default(); - let log_idx = log_handler.exists_log_matching(expected_msg); - let log = log_handler.get_log_at(log_idx); - let capture = captures_for_regex_time_in_sec(&log, capture_regex); - assert!(capture <= elapsed_after) - } + #[test] + fn start_scan_fills_in_caches_and_returns_msg() { + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(890); + let failed_tx_hash_2 = failed_tx_2.hash; + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_result(vec![sent_tx_1.clone(), sent_tx_2.clone()]); + let failed_payable_dao = FailedPayableDaoMock::new() + .retrieve_txs_result(vec![failed_tx_1.clone(), failed_tx_2.clone()]); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) + .build(); + let logger = Logger::new("start_scan_fills_in_caches_and_returns_msg"); + let pending_payable_cache_before = subject.current_sent_payables.dump_cache(); + let failed_payable_cache_before = subject.yet_unproven_failed_payables.dump_cache(); - fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { - let capture_regex = Regex::new(capture_regex).unwrap(); - let time_str = capture_regex - .captures(stack) - .unwrap() - .get(1) - .unwrap() - .as_str(); - time_str.parse().unwrap() - } + let result = subject.start_scan(&make_wallet("bluh"), SystemTime::now(), None, &logger); - fn elapsed_since_secs_back(sec: u64) -> u64 { - SystemTime::now() - .sub(Duration::from_secs(sec)) - .elapsed() - .unwrap() - .as_secs() + assert_eq!( + result, + Ok(RequestTransactionReceipts { + tx_hashes: vec![ + TxHashByTable::SentPayable(sent_tx_hash_1), + TxHashByTable::SentPayable(sent_tx_hash_2), + TxHashByTable::FailedPayable(failed_tx_hash_1), + TxHashByTable::FailedPayable(failed_tx_hash_2) + ], + response_skeleton_opt: None + }) + ); + assert!( + pending_payable_cache_before.is_empty(), + "Should have been empty but {:?}", + pending_payable_cache_before + ); + assert!( + failed_payable_cache_before.is_empty(), + "Should have been empty but {:?}", + failed_payable_cache_before + ); + let pending_payable_cache_after = subject.current_sent_payables.dump_cache(); + let failed_payable_cache_after = subject.yet_unproven_failed_payables.dump_cache(); + assert_eq!( + pending_payable_cache_after, + hashmap!(sent_tx_hash_1 => sent_tx_1, sent_tx_hash_2 => sent_tx_2) + ); + assert_eq!( + failed_payable_cache_after, + hashmap!(failed_tx_hash_1 => failed_tx_1, failed_tx_hash_2 => failed_tx_2) + ); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() - { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; - let hash = make_tx_hash(0x237); - let rowid = 466; + fn finish_scan_operates_caches_and_clears_them_after_use() { + let get_record_by_hash_failed_payable_cache_params_arc = Arc::new(Mutex::new(vec![])); + let get_record_by_hash_sent_payable_cache_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_payable_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_sent_payable_params_arc = Arc::new(Mutex::new(vec![])); + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(890); + let failed_tx_hash_2 = failed_tx_2.hash; + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::new() + .confirm_tx_result(Ok(())) + .replace_records_result(Ok(())) + .delete_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::new() + .insert_new_records_result(Ok(())) + .delete_records_result(Ok(())); + let sent_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_params(&get_record_by_hash_sent_payable_cache_params_arc) + .get_record_by_hash_result(Some(sent_tx_1.clone())) + .get_record_by_hash_result(Some(sent_tx_2)) + .ensure_empty_cache_params(&ensure_empty_cache_sent_payable_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_params(&get_record_by_hash_failed_payable_cache_params_arc) + .get_record_by_hash_result(Some(failed_tx_1)) + .get_record_by_hash_result(Some(failed_tx_2)) + .ensure_empty_cache_params(&ensure_empty_cache_failed_payable_params_arc); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + let logger = Logger::new("test"); + let confirmed_tx_block_sent_tx = make_transaction_block(901); + let confirmed_tx_block_failed_tx = make_transaction_block(902); + let msg = TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_sent_tx)), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_failed_tx)) + ], + response_skeleton_opt: None, + }; - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - DEFAULT_PENDING_TOO_LONG_SEC + 1, - rowid, - hash, - ); + let result = subject.finish_scan(msg, &logger); - let elapsed_after = elapsed_since_secs_back(DEFAULT_PENDING_TOO_LONG_SEC + 1); assert_eq!( result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(rowid, hash)], - confirmed: vec![] - } + PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) + ); + let get_record_by_hash_failed_payable_cache_params = + get_record_by_hash_failed_payable_cache_params_arc + .lock() + .unwrap(); + assert_eq!( + *get_record_by_hash_failed_payable_cache_params, + vec![failed_tx_hash_1, failed_tx_hash_2] + ); + let get_record_by_hash_sent_payable_cache_params = + get_record_by_hash_sent_payable_cache_params_arc + .lock() + .unwrap(); + assert_eq!( + *get_record_by_hash_sent_payable_cache_params, + vec![sent_tx_hash_1, sent_tx_hash_2] ); - let capture_regex = "(\\d+){2}sec"; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "ERROR: {}: Pending transaction 0x00000000000000000000000000000000000000\ - 00000000000000000000000237 has exceeded the maximum pending time \\({}sec\\) with the age \ - \\d+sec and the confirmation process is going to be aborted now at the final attempt 1; manual \ - resolution is required from the user to complete the transaction" - , test_name, DEFAULT_PENDING_TOO_LONG_SEC, ), elapsed_after, capture_regex) + let pending_payable_ensure_empty_cache_params = + ensure_empty_cache_sent_payable_params_arc.lock().unwrap(); + assert_eq!(*pending_payable_ensure_empty_cache_params, vec![()]); + let failed_payable_ensure_empty_cache_params = + ensure_empty_cache_failed_payable_params_arc.lock().unwrap(); + assert_eq!(*failed_payable_ensure_empty_cache_params, vec![()]); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; - let hash = make_tx_hash(0x7b); - let rowid = 333; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; + fn finish_scan_with_missing_records_inside_caches_noticed_on_missing_sent_tx() { + // Note: the ordering of the hashes matters in this test + let sent_tx_hash_1 = make_tx_hash(0x123); + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.hash = sent_tx_hash_1; + let sent_tx_hash_2 = make_tx_hash(0x876); + let failed_tx_hash_1 = make_tx_hash(0x987); + let mut failed_tx_1 = make_failed_tx(567); + failed_tx_1.hash = failed_tx_hash_1; + let failed_tx_hash_2 = make_tx_hash(0x789); + let mut failed_tx_2 = make_failed_tx(890); + failed_tx_2.hash = failed_tx_hash_2; + let mut pending_payable_cache = CurrentPendingPayables::default(); + pending_payable_cache.load_cache(vec![sent_tx_1]); + let mut failed_payable_cache = RecheckRequiringFailures::default(); + failed_payable_cache.load_cache(vec![failed_tx_1, failed_tx_2]); + let mut subject = PendingPayableScannerBuilder::new().build(); + subject.current_sent_payables = Box::new(pending_payable_cache); + subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + let logger = Logger::new("test"); + let msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok( + StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, + }; - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let regex_str_in_pieces = vec![ + r#"Looking up 'SentPayable\(0x0000000000000000000000000000000000000000000000000000000000000876\)'"#, + r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, + r#" Unproven failures: \[FailedTx \{ hash:"#, + r#" 0x0000000000000000000000000000000000000000000000000000000000000987, receiver_address:"#, + r#" 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \d*,"#, + r#" gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: RetryRequired \}\]."#, + r#" Hashes yet not looked up: \[FailedPayable\(0x000000000000000000000000000000000000000"#, + r#"0000000000000000000000987\)\]"#, + ]; + let regex_str = regex_str_in_pieces.join(""); + let expected_msg_regex = Regex::new(®ex_str).unwrap(); + assert!( + expected_msg_regex.is_match(panic_msg), + "Expected string that matches this regex '{}' but it couldn't with '{}'", + regex_str, + panic_msg ); + } - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } + #[test] + fn finish_scan_with_missing_records_inside_caches_noticed_on_missing_failed_tx() { + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_hash_2 = make_tx_hash(901); + let mut pending_payable_cache = CurrentPendingPayables::default(); + pending_payable_cache.load_cache(vec![sent_tx_1, sent_tx_2]); + let mut failed_payable_cache = RecheckRequiringFailures::default(); + failed_payable_cache.load_cache(vec![failed_tx_1]); + let mut subject = PendingPayableScannerBuilder::new().build(); + subject.current_sent_payables = Box::new(pending_payable_cache); + subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + let logger = Logger::new("test"); + let msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, + }; + + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let regex_str_in_pieces = vec![ + r#"Looking up 'FailedPayable\(0x0000000000000000000000000000000000000000000000000000000000000385\)'"#, + r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, + r#" Unproven failures: \[\]. Hashes yet not looked up: \[\]."#, + ]; + let regex_str = regex_str_in_pieces.join(""); + let expected_msg_regex = Regex::new(®ex_str).unwrap(); + assert!( + expected_msg_regex.is_match(panic_msg), + "Expected string that matches this regex '{}' but it couldn't with '{}'", + regex_str, + panic_msg ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 00000000000007b couldn't be confirmed at attempt 1 at \\d+ms after its sending"), elapsed_after_ms, capture_regex); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; - let hash = make_tx_hash(0x237); - let rowid = 466; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; + fn throws_an_error_when_no_records_to_process_were_found() { + let now = SystemTime::now(); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(vec![]); + let mut subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, - ); + let result = subject.start_scan(&consuming_wallet, now, None, &Logger::new("test")); - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 000000000000237 couldn't be confirmed at attempt 1 at \\d+ms after its sending", - ), elapsed_after_ms, capture_regex); + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(result, Err(StartScanError::NothingToProcess)); + assert_eq!(is_scan_running, false); + } + + #[test] + fn handle_failed_transactions_does_nothing_if_no_failure_detected() { + let subject = PendingPayableScannerBuilder::new().build(); + let detected_failures = DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")) + + // Mocked pending payable DAO without prepared results didn't panic which means none of its + // methods was used in this test } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { + fn handle_failed_transactions_can_process_standard_tx_failures() { init_test_logging(); - let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = make_tx_hash(0xd7); - let fingerprint = PendingPayableFingerprint { - rowid: 777777, - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt: 5, - amount: 2222, - process_error: None, + let test_name = "handle_failed_transactions_can_process_standard_tx_failures"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1.clone()), + PresortedTxFailure::NewEntry(failed_tx_2.clone()), + ], + tx_receipt_rpc_failures: vec![], }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - let result = handle_status_with_failure(scan_report, fingerprint, &logger); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(777777, hash,)], - confirmed: vec![] - } + *insert_new_records_params, + vec![vec![failed_tx_1, failed_tx_2]] ); - TestLogHandler::new().exists_log_matching(&format!( - "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ - 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ - 1500\\d\\dms from the sending" + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![hash_1, hash_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 were processed in the db" )); } #[test] - fn handle_pending_txs_with_receipts_handles_none_for_receipt() { + fn handle_failed_transactions_can_process_receipt_retrieval_rpc_failures() { init_test_logging(); - let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; - let subject = PendingPayableScannerBuilder::new().build(); - let rowid = 455; - let hash = make_tx_hash(0x913); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt: 3, - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }), - fingerprint.clone(), - )], - response_skeleton_opt: None, + let test_name = "handle_failed_transactions_can_process_receipt_retrieval_rpc_failures"; + let retrieve_failed_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let retrieve_sent_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let hash_3 = make_tx_hash(0x987); + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(1)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(2)); + let timestamp_d = SystemTime::now().sub(Duration::from_secs(3)); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + failed_tx_1.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + failed_tx_2.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ))); + let failed_payable_dao = FailedPayableDaoMock::default() + .retrieve_txs_params(&retrieve_failed_txs_params_arc) + .retrieve_txs_result(vec![failed_tx_1, failed_tx_2]) + .update_statuses_params(&update_statuses_failed_tx_params_arc) + .update_statuses_result(Ok(())); + let mut sent_tx = make_sent_tx(789); + sent_tx.hash = hash_3; + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let sent_payable_dao = SentPayableDaoMock::default() + .retrieve_txs_params(&retrieve_sent_txs_params_arc) + .retrieve_txs_result(vec![sent_tx.clone()]) + .update_statuses_params(&update_statuses_sent_tx_params_arc) + .update_statuses_result(Ok(())); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b) + .now_result(timestamp_c); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new(validation_failure_clock)) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + )), + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_d), + ), + )), + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_3, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Pending(ValidationStatus::Waiting), + )), + ], }; - let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let update_statuses_sent_tx_params = update_statuses_sent_tx_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } + *update_statuses_sent_tx_params, + vec![ + hashmap![hash_3 => TxStatus::Pending(ValidationStatus::Reattempting (PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_a))))] + ] + ); + let mut update_statuses_failed_tx_params = + update_statuses_failed_tx_params_arc.lock().unwrap(); + let actual_params = update_statuses_failed_tx_params + .remove(0) + .into_iter() + .sorted_by_key(|(key, _)| *key) + .collect::>(); + let expected_params = hashmap!( + hash_1 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_b))) + ), + hash_2 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp_d)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockReal::default()))) + ).into_iter().sorted_by_key(|(key,_)|*key).collect::>(); + assert_eq!(actual_params, expected_params); + assert!( + update_statuses_failed_tx_params.is_empty(), + "Should be empty but: {:?}", + update_statuses_sent_tx_params ); - TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {test_name}: Interpreting a receipt for transaction \ - 0x0000000000000000000000000000000000000000000000000000000000000913 \ - but none was given; attempt 3, 100\\d\\dms since sending" + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Pending-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000987" + )); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Failed-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654" )); + let expectedly_missing_log_msg_fragment = "Handling a validation failure, but the status"; + let otherwise_possible_log_msg = + PendingPayableScanner::status_not_updatable_log_msg(&"Something"); + assert!( + otherwise_possible_log_msg.contains(expectedly_missing_log_msg_fragment), + "We expected to select a true log fragment '{}', but it is not included in '{}'", + expectedly_missing_log_msg_fragment, + otherwise_possible_log_msg + ); + test_log_handler.exists_no_log_containing(&format!( + "DEBUG: {test_name}: {}", + expectedly_missing_log_msg_fragment + )) } #[test] - fn increment_scan_attempts_happy_path() { - let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let hash_1 = make_tx_hash(444888); - let rowid_1 = 3456; - let hash_2 = make_tx_hash(444888); - let rowid_2 = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) - .increment_scan_attempts_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); - let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); + fn handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated() { + init_test_logging(); + let test_name = "handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated"; + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let subject = PendingPayableScannerBuilder::new().build(); - let _ = subject.update_remaining_fingerprints( - vec![transaction_id_1, transaction_id_2], - &Logger::new("test"), + subject.handle_rpc_failures( + vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RetryRequired, + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Confirmed { + block_hash: "abc".to_string(), + block_number: 0, + detection: Detection::Normal, + }, + )), + ], + &Logger::new(test_name), ); - let update_remaining_fingerprints_params = - update_remaining_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *update_remaining_fingerprints_params, - vec![vec![rowid_1, rowid_2]] - ) + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_no_log_containing(&format!("INFO: {test_name}: ")); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \ + {{\"Confirmed\":{{\"block_hash\":\"abc\",\"block_number\":0,\"detection\":\"Normal\"}}}} \ + cannot be updated.", + )); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \"RetryRequired\" \ + cannot be updated." + )); + // It didn't panic, which means none of the DAO methods was called because the DAOs are + // mocked in this test } #[test] #[should_panic( - expected = "Failure on incrementing scan attempts for fingerprints of \ - 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ - due to UpdateFailed(\"yeah, bad\")" + expected = "Unable to update pending-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"bluh\")" )] - fn increment_scan_attempts_sad_path() { - let hash = make_tx_hash(0x6c9d8); - let rowid = 3456; - let pending_payable_dao = - PendingPayableDaoMock::default().increment_scan_attempts_result(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); + fn update_validation_status_for_sent_txs_panics_on_update_statuses() { + let failed_validation = FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ); + let sent_payable_dao = SentPayableDaoMock::default() + .update_statuses_result(Err(SentPayableDaoError::InvalidInput("bluh".to_string()))); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) .build(); - let logger = Logger::new("test"); - let transaction_id = PendingPayableId::new(rowid, hash); - let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); + let _ = subject + .update_validation_status_for_sent_txs(vec![failed_validation], &Logger::new("test")); } #[test] - fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { - let subject = PendingPayableScannerBuilder::new().build(); + #[should_panic( + expected = "Unable to update failed-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"bluh\")" + )] + fn update_validation_status_for_failed_txs_panics_on_update_statuses() { + let failed_validation = FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ); + let failed_payable_dao = FailedPayableDaoMock::default() + .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("bluh".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) + .build(); + + let _ = subject + .update_validation_status_for_failed_txs(vec![failed_validation], &Logger::new("test")); + } + + #[test] + fn handle_failed_transactions_can_process_mixed_failures() { + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let update_status_params_arc = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let timestamp = SystemTime::now(); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = tx_hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = tx_hash_2; + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .update_statuses_params(&update_status_params_arc) + .update_statuses_result(Ok(())) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new( + ValidationFailureClockMock::default().now_result(timestamp), + )) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(failed_tx_1.clone())], + tx_receipt_rpc_failures: vec![FailedValidationByTable::SentPayable( + FailedValidation::new( + tx_hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ), + )], + }; - subject.update_remaining_fingerprints(vec![], &Logger::new("test")) + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![failed_tx_1]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_1]]); + let update_statuses_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_params, + vec![ + hashmap!(tx_hash_2 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp))))) + ] + ); } #[test] - fn cancel_failed_transactions_works() { - init_test_logging(); - let test_name = "cancel_failed_transactions_works"; - let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failures_params(&mark_failures_params_arc) - .mark_failures_result(Ok(())); + #[should_panic(expected = "Unable to persist failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: NoChange")] + fn handle_failed_transactions_panics_when_it_fails_to_insert_failed_tx_record() { + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_result(Err(FailedPayableDaoError::NoChange)); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); - let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + } - let mark_failures_params = mark_failures_params_arc.lock().unwrap(); - assert_eq!(*mark_failures_params, vec![vec![2, 3]]); - TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ - 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ - the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ - process fixing that without your assistance", - )); + #[test] + #[should_panic(expected = "Unable to purge sent payable records for failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: \ + InvalidInput(\"Booga\")")] + fn handle_failed_transactions_panics_when_it_fails_to_delete_obsolete_sent_tx_records() { + let failed_payable_dao = FailedPayableDaoMock::default().insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_result(Err(SentPayableDaoError::InvalidInput("Booga".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - #[should_panic( - expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ - 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ - 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ - database unreliable" - )] - fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { - let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), + fn handle_failed_transactions_can_conclude_rechecked_failures() { + let update_status_params_arc = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = tx_hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = tx_hash_2; + let failed_payable_dao = FailedPayableDaoMock::default() + .update_statuses_params(&update_status_params_arc) + .update_statuses_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + + let update_status_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_status_params, + vec![ + hashmap!(tx_hash_1 => FailureStatus::Concluded, tx_hash_2 => FailureStatus::Concluded), + ] + ); + } + + #[test] + #[should_panic(expected = "Unable to conclude rechecks for failed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 due to: \ + InvalidInput(\"Booga\")")] + fn concluding_rechecks_fails_on_updating_statuses() { + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let failed_payable_dao = FailedPayableDaoMock::default().update_statuses_result(Err( + FailedPayableDaoError::InvalidInput("Booga".to_string()), )); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); - let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); - let transaction_ids = vec![transaction_id_1, transaction_id_2]; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { - let subject = PendingPayableScannerBuilder::new().build(); + fn handle_confirmed_transactions_does_nothing_if_no_confirmation_found_on_the_blockchain() { + let mut subject = PendingPayableScannerBuilder::new().build(); + + subject + .handle_confirmed_transactions(DetectedConfirmations::default(), &Logger::new("test")) + + // Mocked payable DAO without prepared results didn't panic, which means none of its methods + // was used in this test + } + + #[test] + fn handles_failure_reclaims_alone() { + init_test_logging(); + let test_name = "handles_failure_reclaims_alone"; + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.cancel_failed_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &logger, + ); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) as confirmed on-chain", + )); } #[test] #[should_panic( - expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ - 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ - 0000000000021a of verified transactions due to RecordDeletion(\"the database \ - is fooling around with us\")" + expected = "Unable to proceed in a reclaim as the replacement of sent tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + failed due to: NoChange" )] - fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { - let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Err( - PendingPayableDaoError::RecordDeletion( - "the database is fooling around with us".to_string(), - ), - )); + fn failure_reclaim_fails_on_replace_sent_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_result(Err(SentPayableDaoError::NoChange)); let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.rowid = 1; - fingerprint_1.hash = make_tx_hash(0x315); - let mut fingerprint_2 = make_pending_payable_fingerprint(); - fingerprint_2.rowid = 1; - fingerprint_2.hash = make_tx_hash(0x21a); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { - let mut subject = PendingPayableScannerBuilder::new().build(); + #[should_panic(expected = "Unable to delete failed tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + to finish the reclaims due to: EmptyInput")] + fn failure_reclaim_fails_on_delete_failed_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default().replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_result(Err(FailedPayableDaoError::EmptyInput)); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); + } - //mocked payable DAO didn't panic which means we skipped the actual process + #[test] + #[should_panic( + expected = "Processing a reclaim for tx 0x0000000000000000000000000000000000000000000000000\ + 000000000000123 which isn't filled with the confirmation details" + )] + fn handle_failure_reclaim_meets_a_record_without_confirmation_details() { + let mut subject = PendingPayableScannerBuilder::new().build(); + let tx_hash = make_tx_hash(0x123); + let mut sent_tx = make_sent_tx(123_123); + sent_tx.hash = tx_hash; + // Here, it should be confirmed already in this status + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_works() { + fn handles_normal_confirmations_alone() { init_test_logging(); + let test_name = "handles_normal_confirmations_alone"; let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())); + let logger = Logger::new(test_name); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let rowid_1 = 2; - let rowid_2 = 5; - let pending_payable_fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: from_unix_timestamp(199_000_000), - hash: make_tx_hash(0x123), - attempt: 1, - amount: 4567, - process_error: None, - }; - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: from_unix_timestamp(200_000_000), - hash: make_tx_hash(0x567), - attempt: 1, - amount: 5555, - process_error: None, - }; - - subject.confirm_transactions( - vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), - ], - &Logger::new("confirm_transactions_works"), + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_2.block_hash), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone(), sent_tx_2.clone()], + reclaims: vec![], + }, + &logger, ); - let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!( + *transactions_confirmed_params, + vec![vec![sent_tx_1, sent_tx_2]] + ); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); assert_eq!( - *confirm_transactions_params, - vec![vec![ - pending_payable_fingerprint_1, - pending_payable_fingerprint_2 - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); + *confirm_tx_params, + vec![hashmap![tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2]] + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "DEBUG: confirm_transactions_works: \ - Confirmation of transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567; \ - record for total paid payable was modified", + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) were confirmed", + )); + } + + #[test] + fn mixed_tx_confirmations_work() { + init_test_logging(); + let test_name = "mixed_tx_confirmations_work"; + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transactions_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x913); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(567_567); + sent_tx_2.hash = tx_hash_2; + let tx_block_3 = TxBlock { + block_hash: make_block_hash(78), + block_number: 7_898_989_878_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_3.block_hash), + block_number: tx_block_3.block_number.as_u64(), + detection: Detection::Reclaim, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone()], + reclaims: vec![sent_tx_2.clone()], + }, + &logger, ); - log_handler.exists_log_containing( - "INFO: confirm_transactions_works: \ - Transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567 \ - completed their confirmation process succeeding", + + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![vec![sent_tx_1]]); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); + assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000913 (block 7898989878) \ + as confirmed on-chain", + )); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878) was confirmed", + )); + } + + #[test] + #[should_panic( + expected = "Unable to update sent payable records 0x000000000000000000000000000000000000000\ + 000000000000000000000021a, 0x0000000000000000000000000000000000000000000000000000000000000315 \ + by their tx blocks due to: SqlExecutionFailed(\"The database manager is \ + a funny guy, he's fooling around with us\")" + )] + fn handle_confirmed_transactions_panics_while_updating_sent_payable_records_with_the_tx_blocks() + { + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Err( + SentPayableDaoError::SqlExecutionFailed( + "The database manager is a funny guy, he's fooling around with us".to_string(), + ), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let mut sent_tx_1 = make_sent_tx(456); + let block = make_transaction_block(678); + sent_tx_1.hash = make_tx_hash(0x315); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.hash = make_tx_hash(0x21a); + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![], + }, + &Logger::new("test"), ); } #[test] #[should_panic( - expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ - 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ - (\"record change not successful\")" + expected = "Unable to complete the tx confirmation by the adjustment of the payable accounts \ + 0x000000000000000000000077616c6c6574343536 due to: \ + RusqliteError(\"record change not successful\")" )] - fn confirm_transactions_panics_on_unchecking_payable_table() { + fn handle_confirmed_transactions_panics_on_unchecking_payable_table() { let hash = make_tx_hash(0x315); - let rowid = 3; let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( PayableDaoError::RusqliteError("record change not successful".to_string()), )); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let mut fingerprint = make_pending_payable_fingerprint(); - fingerprint.rowid = rowid; - fingerprint.hash = hash; + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = hash; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx], + reclaims: vec![], + }, + &Logger::new("test"), + ); + } + + #[test] + fn log_tx_success_is_agnostic_to_singular_or_plural_form() { + init_test_logging(); + let test_name = "log_tx_success_is_agnostic_to_singular_or_plural_form"; + let plural_case_name = format!("{}_testing_plural_case", test_name); + let singular_case_name = format!("{}_testing_singular_case", test_name); + let logger_plural = Logger::new(&plural_case_name); + let logger_singular = Logger::new(&singular_case_name); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut tx_block_1 = make_transaction_block(456); + tx_block_1.block_number = 1_234_501_u64.into(); + let mut tx_block_2 = make_transaction_block(789); + tx_block_2.block_number = 1_234_502_u64.into(); + let mut tx_hashes_and_blocks = hashmap!(tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2); + + PendingPayableScanner::log_tx_success(&logger_plural, &tx_hashes_and_blocks); + + tx_hashes_and_blocks.remove(&tx_hash_2); + + PendingPayableScanner::log_tx_success(&logger_singular, &tx_hashes_and_blocks); - subject.confirm_transactions(vec![fingerprint], &Logger::new("test")); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {plural_case_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 1234502) were confirmed", + )); + log_handler.exists_log_containing(&format!( + "INFO: {singular_case_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501) was confirmed", + )); } #[test] fn total_paid_payable_rises_with_each_bill_paid() { + init_test_logging(); let test_name = "total_paid_payable_rises_with_each_bill_paid"; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(189_999_888), - hash: make_tx_hash(56789), - attempt: 1, - amount: 5478, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 6, - timestamp: from_unix_timestamp(200_000_011), - hash: make_tx_hash(33333), - attempt: 1, - amount: 6543, - process_error: None, + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.amount_minor = 5478; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: 89898, + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.amount_minor = 3344; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(234)), + block_number: 66312, + detection: Detection::Normal, + }; + let mut sent_tx_3 = make_sent_tx(789); + sent_tx_3.amount_minor = 6543; + sent_tx_3.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(321)), + block_number: 67676, + detection: Detection::Reclaim, }; let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_result(Ok(())) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default().delete_records_result(Ok(())); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let mut financial_statistics = subject.financial_statistics.borrow().clone(); financial_statistics.total_paid_payable_wei += 1111; subject.financial_statistics.replace(financial_statistics); - subject.confirm_transactions( - vec![fingerprint_1.clone(), fingerprint_2.clone()], + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![sent_tx_3], + }, &Logger::new(test_name), ); let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; - assert_eq!(total_paid_payable, 1111 + 5478 + 6543); + assert_eq!(total_paid_payable, 1111 + 5478 + 3344 + 6543); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!("DEBUG: {test_name}: The total paid payables increased by 6,543 to 7,654 wei"), + &format!( + "DEBUG: {test_name}: The total paid payables increased by 8,822 to 16,476 wei" + ), + ]); } } diff --git a/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs new file mode 100644 index 0000000000..473fd28cb3 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs @@ -0,0 +1,23 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::validation_status::ValidationFailureClock; +use std::cell::RefCell; +use std::time::SystemTime; + +#[derive(Default)] +pub struct ValidationFailureClockMock { + now_results: RefCell>, +} + +impl ValidationFailureClock for ValidationFailureClockMock { + fn now(&self) -> SystemTime { + self.now_results.borrow_mut().remove(0) + } +} + +impl ValidationFailureClockMock { + pub fn now_result(self, result: SystemTime) -> Self { + self.now_results.borrow_mut().push(result); + self + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs new file mode 100644 index 0000000000..4bf96bf1ee --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -0,0 +1,706 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentTx, TxStatus, +}; +use crate::accountant::db_access_objects::utils::from_unix_timestamp; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + ConfirmationType, FailedValidation, FailedValidationByTable, ReceiptScanReport, TxByTable, + TxCaseToBeInterpreted, TxHashByTable, +}; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, +}; +use crate::blockchain::errors::internal_errors::InternalErrorKind; +use crate::blockchain::errors::rpc_errors::AppRpcError; +use crate::blockchain::errors::BlockchainErrorKind; +use itertools::Either; +use masq_lib::logger::Logger; +use std::time::SystemTime; +use thousands::Separable; + +#[derive(Default)] +pub struct TxReceiptInterpreter {} + +impl TxReceiptInterpreter { + pub fn compose_receipt_scan_report( + &self, + tx_cases: Vec, + pending_payable_scanner: &PendingPayableScanner, + logger: &Logger, + ) -> ReceiptScanReport { + let scan_report = ReceiptScanReport::default(); + tx_cases + .into_iter() + .fold(scan_report, |scan_report_so_far, tx_case| { + match tx_case.tx_receipt_result { + Ok(tx_status) => match tx_status { + StatusReadFromReceiptCheck::Succeeded(tx_block) => { + Self::handle_tx_confirmation( + scan_report_so_far, + tx_case.tx_by_table, + tx_block, + logger, + ) + } + StatusReadFromReceiptCheck::Reverted => Self::handle_reverted_tx( + scan_report_so_far, + tx_case.tx_by_table, + logger, + ), + StatusReadFromReceiptCheck::Pending => Self::handle_still_pending_tx( + scan_report_so_far, + tx_case.tx_by_table, + &*pending_payable_scanner.sent_payable_dao, + logger, + ), + }, + Err(e) => { + Self::handle_rpc_failure(scan_report_so_far, tx_case.tx_by_table, e, logger) + } + } + }) + } + + fn handle_still_pending_tx( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + sent_payable_dao: &dyn SentPayableDao, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + info!( + logger, + "Tx {:?} not confirmed within {} ms. Will resubmit with higher gas price", + sent_tx.hash, + Self::elapsed_in_ms(from_unix_timestamp(sent_tx.timestamp)) + .separate_with_commas() + ); + let failed_tx = FailedTx::from((sent_tx, FailureReason::PendingTooLong)); + scan_report.register_new_failure(failed_tx); + } + TxByTable::FailedPayable(failed_tx) => { + if failed_tx.reason != FailureReason::PendingTooLong { + unreachable!( + "Transaction is both pending and failed (failure reason: '{:?}'). Should be \ + possible only with the reason 'PendingTooLong'", + failed_tx.reason + ) + } + let replacement_tx = sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::ByNonce(vec![failed_tx.nonce]))); + let replacement_tx_hash = replacement_tx + .get(0) + .unwrap_or_else(|| { + panic!( + "Attempted to display a replacement tx for {:?} but couldn't find \ + one in the database", + failed_tx.hash + ) + }) + .hash; + warning!( + logger, + "Failed tx {:?} on a recheck was found pending on its receipt unexpectedly. \ + It was supposed to be replaced by {:?}", + failed_tx.hash, + replacement_tx_hash + ); + scan_report.register_rpc_failure(FailedValidationByTable::FailedPayable( + FailedValidation::new( + failed_tx.hash, + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + failed_tx.status, + ), + )) + } + } + scan_report + } + + fn elapsed_in_ms(timestamp: SystemTime) -> u128 { + timestamp + .elapsed() + .expect("time calculation for elapsed failed") + .as_millis() + } + + fn handle_tx_confirmation( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + tx_block: TxBlock, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + info!( + logger, + "Pending tx {:?} was confirmed on-chain", sent_tx.hash, + ); + + let completed_sent_tx = SentTx { + status: TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, + }, + ..sent_tx + }; + scan_report.register_confirmed_tx(completed_sent_tx, ConfirmationType::Normal); + } + TxByTable::FailedPayable(failed_tx) => { + info!( + logger, + "Failed tx {:?} was later confirmed on-chain and will be reclaimed", + failed_tx.hash + ); + + let sent_tx = SentTx::from((failed_tx, tx_block)); + scan_report.register_confirmed_tx(sent_tx, ConfirmationType::Reclaim); + } + } + scan_report + } + + //TODO: failures handling might need enhancement suggested by GH-693 + fn handle_reverted_tx( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + let failure_reason = FailureReason::Reverted; + let failed_tx = FailedTx::from((sent_tx, failure_reason)); + + warning!(logger, "Pending tx {:?} was reverted", failed_tx.hash,); + + scan_report.register_new_failure(failed_tx); + } + TxByTable::FailedPayable(failed_tx) => { + debug!( + logger, + "Reverted tx {:?} on a recheck after {}. Status will be changed to \"Concluded\"", + failed_tx.hash, + failed_tx.reason, + ); + + scan_report.register_finalization_of_unproven_failure(failed_tx.hash); + } + } + scan_report + } + + fn handle_rpc_failure( + mut scan_report: ReceiptScanReport, + tx_by_table: TxByTable, + rpc_error: AppRpcError, + logger: &Logger, + ) -> ReceiptScanReport { + warning!( + logger, + "Failed to retrieve tx receipt for {:?}: {:?}. Will retry receipt retrieval next cycle", + TxHashByTable::from(&tx_by_table), + rpc_error + ); + let hash = tx_by_table.hash(); + let validation_status_update = match tx_by_table { + TxByTable::SentPayable(sent_tx) => { + FailedValidationByTable::new(hash, rpc_error, Either::Left(sent_tx.status)) + } + TxByTable::FailedPayable(failed_tx) => { + FailedValidationByTable::new(hash, rpc_error, Either::Right(failed_tx.status)) + } + }; + scan_report.register_rpc_failure(validation_status_update); + scan_report + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentTx, TxStatus, + }; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + DetectedConfirmations, DetectedFailures, FailedValidation, FailedValidationByTable, + PresortedTxFailure, ReceiptScanReport, TxByTable, + }; + use crate::accountant::test_utils::{ + make_failed_tx, make_sent_tx, make_transaction_block, SentPayableDaoMock, + }; + use crate::blockchain::errors::internal_errors::InternalErrorKind; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, + }; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::unshared_test_utils::capture_digits_with_separators_from_str; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; + + #[test] + fn interprets_receipt_for_pending_tx_if_it_is_a_success() { + init_test_logging(); + let test_name = "interprets_tx_receipt_if_it_is_a_success"; + let hash = make_tx_hash(0xcdef); + let mut sent_tx = make_sent_tx(2244); + sent_tx.hash = hash; + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let tx_block = make_transaction_block(1234); + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_tx_confirmation( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + tx_block, + &logger, + ); + + let mut updated_tx = sent_tx; + updated_tx.status = TxStatus::Confirmed { + block_hash: "0x000000000000000000000000000000000000000000000000000000003b9aced2" + .to_string(), + block_number: 1879080904, + detection: Detection::Normal, + }; + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures::default(), + confirmations: DetectedConfirmations { + normal_confirmations: vec![updated_tx], + reclaims: vec![] + } + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ + 00000cdef was confirmed on-chain", + )); + } + + #[test] + fn interprets_receipt_for_failed_tx_being_rechecked_when_it_is_a_success() { + init_test_logging(); + let test_name = "interprets_receipt_for_failed_tx_being_rechecked_when_it_is_a_success"; + let hash = make_tx_hash(0xcdef); + let mut failed_tx = make_failed_tx(2244); + failed_tx.hash = hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let tx_block = make_transaction_block(1234); + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_tx_confirmation( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + tx_block, + &logger, + ); + + let sent_tx = SentTx::from((failed_tx, tx_block)); + assert!( + matches!( + sent_tx.status, + TxStatus::Confirmed { + detection: Detection::Reclaim, + .. + } + ), + "We expected reclaimed tx, but it says: {:?}", + sent_tx + ); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures::default(), + confirmations: DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx] + } + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ + 00000cdef was later confirmed on-chain and will be reclaimed", + )); + } + + #[test] + fn interprets_tx_receipt_for_pending_tx_when_tx_status_says_reverted() { + init_test_logging(); + let test_name = "interprets_tx_receipt_for_pending_tx_when_tx_status_says_reverted"; + let hash = make_tx_hash(0xabc); + let mut sent_tx = make_sent_tx(2244); + sent_tx.hash = hash; + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_reverted_tx( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + &logger, + ); + + let failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(failed_tx)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ + 000000abc was reverted", + )); + } + + #[test] + fn interprets_tx_receipt_for_failed_tx_when_newly_fetched_tx_status_says_reverted() { + init_test_logging(); + let test_name = "interprets_tx_receipt_for_failed_tx_when_tx_status_reveals_failure"; + let tx_hash = make_tx_hash(0xabc); + let mut failed_tx = make_failed_tx(2244); + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_reverted_tx( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + &logger, + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::RecheckCompleted(tx_hash)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Reverted tx 0x000000000000000000000000000000000000000000000000000000\ + 0000000abc on a recheck after \"PendingTooLong\". Status will be changed to \"Concluded\"", + )); + } + + #[test] + fn interprets_tx_receipt_for_pending_payable_if_the_tx_keeps_pending() { + init_test_logging(); + let retrieve_txs_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "interprets_tx_receipt_for_pending_payable_if_the_tx_keeps_pending"; + let newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_params(&retrieve_txs_params_arc) + .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + let hash = make_tx_hash(0x913); + let sent_tx_timestamp = to_unix_timestamp( + SystemTime::now() + .checked_sub(Duration::from_secs(120)) + .unwrap(), + ); + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = hash; + sent_tx.timestamp = sent_tx_timestamp; + let scan_report = ReceiptScanReport::default(); + let before = SystemTime::now(); + + let result = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + &sent_payable_dao, + &Logger::new(test_name), + ); + + let after = SystemTime::now(); + let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::PendingTooLong)); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(expected_failed_tx)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + let log_handler = TestLogHandler::new(); + let log_idx = log_handler.exists_log_matching(&format!( + "INFO: {test_name}: Tx \ + 0x0000000000000000000000000000000000000000000000000000000000000913 not confirmed within \ + \\d{{1,3}}(,\\d{{3}})* ms. Will resubmit with higher gas price" + )); + let log_msg = log_handler.get_log_at(log_idx); + let str_elapsed_ms = capture_digits_with_separators_from_str(&log_msg, 3, ','); + let elapsed_ms = str_elapsed_ms[0].replace(",", "").parse::().unwrap(); + let elapsed_ms_when_before = before + .duration_since(from_unix_timestamp(sent_tx_timestamp)) + .unwrap() + .as_millis(); + let elapsed_ms_when_after = after + .duration_since(from_unix_timestamp(sent_tx_timestamp)) + .unwrap() + .as_millis(); + assert!( + elapsed_ms_when_before <= elapsed_ms && elapsed_ms <= elapsed_ms_when_after, + "we expected the elapsed time {} ms to be between {} and {}.", + elapsed_ms, + elapsed_ms_when_before, + elapsed_ms_when_after + ); + } + + #[test] + fn interprets_tx_receipt_for_supposedly_failed_tx_if_the_tx_keeps_pending() { + init_test_logging(); + let retrieve_txs_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "interprets_tx_receipt_for_supposedly_failed_tx_if_the_tx_keeps_pending"; + let mut newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + newer_sent_tx_for_older_failed_tx.hash = make_tx_hash(0x7c6); + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_params(&retrieve_txs_params_arc) + .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + let tx_hash = make_tx_hash(0x913); + let mut failed_tx = make_failed_tx(789); + let failed_tx_nonce = failed_tx.nonce; + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + &sent_payable_dao, + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::Internal( + InternalErrorKind::PendingTooLongNotReplaced + ), + FailureStatus::RecheckRequired(ValidationStatus::Waiting) + ) + )] + }, + confirmations: DetectedConfirmations::default() + } + ); + let retrieve_txs_params = retrieve_txs_params_arc.lock().unwrap(); + assert_eq!( + *retrieve_txs_params, + vec![Some(RetrieveCondition::ByNonce(vec![failed_tx_nonce]))] + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ + 000000913 on a recheck was found pending on its receipt unexpectedly. It was supposed \ + to be replaced by 0x00000000000000000000000000000000000000000000000000000000000007c6" + )); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Transaction is both pending \ + and failed (failure reason: 'Reverted'). Should be possible only with the reason 'PendingTooLong'" + )] + fn interprets_failed_tx_recheck_as_still_pending_while_the_failure_reason_wasnt_pending_too_long( + ) { + let mut newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + newer_sent_tx_for_older_failed_tx.hash = make_tx_hash(0x7c6); + let sent_payable_dao = SentPayableDaoMock::new(); + let tx_hash = make_tx_hash(0x913); + let mut failed_tx = make_failed_tx(789); + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::Reverted; + let scan_report = ReceiptScanReport::default(); + + let _ = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(failed_tx), + &sent_payable_dao, + &Logger::new("test"), + ); + } + + #[test] + #[should_panic( + expected = "Attempted to display a replacement tx for 0x000000000000000000000000000\ + 00000000000000000000000000000000001c8 but couldn't find one in the database" + )] + fn handle_still_pending_tx_if_unexpected_behavior_due_to_already_failed_tx_and_db_retrieval_fails( + ) { + let scan_report = ReceiptScanReport::default(); + let still_pending_tx = make_failed_tx(456); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + + let _ = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(still_pending_tx), + &sent_payable_dao, + &Logger::new("test"), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_pending_payable_in_first_attempt() { + let test_name = + "interprets_failed_retrieval_of_receipt_for_pending_payable_in_first_attempt"; + + test_failed_retrieval_of_receipt_for_pending_payable( + test_name, + TxStatus::Pending(ValidationStatus::Waiting), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_pending_payable_as_reattempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_pending_payable_as_reattempt"; + + test_failed_retrieval_of_receipt_for_pending_payable( + test_name, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockReal::default(), + ))), + ); + } + + fn test_failed_retrieval_of_receipt_for_pending_payable( + test_name: &str, + current_tx_status: TxStatus, + ) { + init_test_logging(); + let tx_hash = make_tx_hash(913); + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = tx_hash; + sent_tx.status = current_tx_status.clone(); + let rpc_error = AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_rpc_failure( + scan_report, + TxByTable::SentPayable(sent_tx), + rpc_error.clone(), + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::SentPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&rpc_error).into()), + current_tx_status + ) + ),] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing( + &format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000\ + 000000000000000000000000000000000000000000000000000391): Remote(InvalidResponse(\"bluh\")). \ + Will retry receipt retrieval next cycle")); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_failed_tx_in_first_attempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_failed_tx_in_first_attempt"; + + test_failed_retrieval_of_receipt_for_failed_tx( + test_name, + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_failed_tx_as_reattempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_failed_tx_as_reattempt"; + + test_failed_retrieval_of_receipt_for_failed_tx( + test_name, + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockReal::default(), + ))), + ); + } + + fn test_failed_retrieval_of_receipt_for_failed_tx( + test_name: &str, + current_failure_status: FailureStatus, + ) { + init_test_logging(); + let tx_hash = make_tx_hash(914); + let mut failed_tx = make_failed_tx(456); + failed_tx.hash = tx_hash; + failed_tx.status = current_failure_status.clone(); + let rpc_error = AppRpcError::Local(LocalError::Internal); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_rpc_failure( + scan_report, + TxByTable::FailedPayable(failed_tx), + rpc_error.clone(), + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&rpc_error).into()), + current_failure_status + ) + )] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Failed to retrieve tx receipt for FailedPayable(0x0000000000\ + 000000000000000000000000000000000000000000000000000392): Local(Internal). \ + Will retry receipt retrieval next cycle" + )); + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index f277a1c911..d08808d75c 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -1,191 +1,1160 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::PendingPayableId; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::TxReceiptResult; +use crate::blockchain::errors::rpc_errors::AppRpcError; +use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClock, ValidationStatus, +}; +use crate::blockchain::errors::BlockchainErrorKind; +use itertools::Either; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; -use std::time::SystemTime; +use std::collections::HashMap; #[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct PendingPayableScanReport { - pub still_pending: Vec, - pub failures: Vec, - pub confirmed: Vec, +pub struct ReceiptScanReport { + pub failures: DetectedFailures, + pub confirmations: DetectedConfirmations, } -impl PendingPayableScanReport { - pub fn requires_payments_retry(&self) -> bool { - todo!("complete my within GH-642") +impl ReceiptScanReport { + pub fn requires_payments_retry(&self) -> Option { + match ( + self.failures.requires_retry(), + self.confirmations.is_empty(), + ) { + (None, true) => unreachable!("reading tx receipts gave no results, but always should"), + (None, _) => None, + (Some(retry), _) => Some(retry), + } + } + + pub(super) fn register_confirmed_tx( + &mut self, + confirmed_tx: SentTx, + confirmation_type: ConfirmationType, + ) { + match confirmation_type { + ConfirmationType::Normal => self.confirmations.normal_confirmations.push(confirmed_tx), + ConfirmationType::Reclaim => self.confirmations.reclaims.push(confirmed_tx), + } + } + + pub(super) fn register_new_failure(&mut self, failed_tx: FailedTx) { + self.failures + .tx_failures + .push(PresortedTxFailure::NewEntry(failed_tx)); + } + + pub(super) fn register_finalization_of_unproven_failure(&mut self, tx_hash: TxHash) { + self.failures + .tx_failures + .push(PresortedTxFailure::RecheckCompleted(tx_hash)); + } + + pub(super) fn register_rpc_failure(&mut self, status_update: FailedValidationByTable) { + self.failures.tx_receipt_rpc_failures.push(status_update); + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedConfirmations { + pub normal_confirmations: Vec, + pub reclaims: Vec, +} + +impl DetectedConfirmations { + pub(super) fn is_empty(&self) -> bool { + self.normal_confirmations.is_empty() && self.reclaims.is_empty() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ConfirmationType { + Normal, + Reclaim, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedFailures { + pub tx_failures: Vec, + pub tx_receipt_rpc_failures: Vec, +} + +impl DetectedFailures { + fn requires_retry(&self) -> Option { + if self.tx_failures.is_empty() && self.tx_receipt_rpc_failures.is_empty() { + None + } else if !self.tx_failures.is_empty() { + Some(Retry::RetryPayments) + } else { + Some(Retry::RetryTxStatusCheckOnly) + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PresortedTxFailure { + NewEntry(FailedTx), + RecheckCompleted(TxHash), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum FailedValidationByTable { + SentPayable(FailedValidation), + FailedPayable(FailedValidation), +} + +impl FailedValidationByTable { + pub fn new( + tx_hash: TxHash, + error: AppRpcError, + status: Either, + ) -> Self { + match status { + Either::Left(tx_status) => Self::SentPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + tx_status, + )), + Either::Right(failure_reason) => Self::FailedPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + failure_reason, + )), + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct FailedValidation { + pub tx_hash: TxHash, + pub validation_failure: BlockchainErrorKind, + pub current_status: RecordStatus, +} + +impl FailedValidation +where + RecordStatus: UpdatableValidationStatus, +{ + pub fn new( + tx_hash: TxHash, + validation_failure: BlockchainErrorKind, + current_status: RecordStatus, + ) -> Self { + Self { + tx_hash, + validation_failure, + current_status, + } + } + + pub fn new_status(&self, clock: &dyn ValidationFailureClock) -> Option { + self.current_status + .update_after_failure(self.validation_failure, clock) + } +} + +pub trait UpdatableValidationStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option + where + Self: Sized; +} + +impl UpdatableValidationStatus for TxStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + TxStatus::Pending(ValidationStatus::Waiting) => Some(TxStatus::Pending( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )), + TxStatus::Pending(ValidationStatus::Reattempting(previous_attempts)) => { + Some(TxStatus::Pending(ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error, clock), + ))) + } + TxStatus::Confirmed { .. } => None, + } + } +} + +impl UpdatableValidationStatus for FailureStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + FailureStatus::RecheckRequired(ValidationStatus::Waiting) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )) + } + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(previous_attempts)) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error.into(), clock), + ), + )) + } + FailureStatus::RetryRequired | FailureStatus::Concluded => None, + } + } +} + +pub struct MismatchReport { + pub noticed_with: TxHashByTable, + pub remaining_hashes: Vec, +} + +pub trait PendingPayableCache { + fn load_cache(&mut self, records: Vec); + fn get_record_by_hash(&mut self, hash: TxHash) -> Option; + fn ensure_empty_cache(&mut self, logger: &Logger); + fn dump_cache(&mut self) -> HashMap; +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct CurrentPendingPayables { + pub(super) sent_payables: HashMap, +} + +impl PendingPayableCache for CurrentPendingPayables { + fn load_cache(&mut self, records: Vec) { + self.sent_payables + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.sent_payables.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.sent_payables.is_empty() { + debug!( + logger, + "Cache misuse - some pending payables left unprocessed: {:?}. Dumping.", + self.sent_payables + ); + } + self.sent_payables.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.sent_payables.drain().collect() + } +} + +impl CurrentPendingPayables { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct RecheckRequiringFailures { + pub(super) failures: HashMap, +} + +impl PendingPayableCache for RecheckRequiringFailures { + fn load_cache(&mut self, records: Vec) { + self.failures + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.failures.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.failures.is_empty() { + debug!( + logger, + "Cache misuse - some tx failures left unprocessed: {:?}. Dumping.", self.failures + ); + } + self.failures.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.failures.drain().collect() + } +} + +impl RecheckRequiringFailures { + pub fn new() -> Self { + Self::default() } } #[derive(Debug, PartialEq, Eq)] pub enum PendingPayableScanResult { NoPendingPayablesLeft(Option), - PaymentRetryRequired, -} - -pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() -} - -pub fn handle_none_status( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Pending transaction {:?} couldn't be confirmed at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let elapsed = elapsed.as_secs(); - if elapsed > max_pending_interval { - error!( - logger, - "Pending transaction {:?} has exceeded the maximum pending time \ - ({}sec) with the age {}sec and the confirmation process is going to be aborted now \ - at the final attempt {}; manual resolution is required from the \ - user to complete the transaction.", - fingerprint.hash, - max_pending_interval, - elapsed, - fingerprint.attempt - ); - scan_report.failures.push(fingerprint.into()) - } else { - scan_report.still_pending.push(fingerprint.into()) - } - scan_report -} - -pub fn handle_status_with_success( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Transaction {:?} has been added to the blockchain; detected locally at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.confirmed.push(fingerprint); - scan_report -} - -//TODO: failures handling is going to need enhancement suggested by GH-693 -pub fn handle_status_with_failure( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - error!( - logger, - "Pending transaction {:?} announced as a failure, interpreting attempt \ - {} after {}ms from the sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.failures.push(fingerprint.into()); - scan_report -} - -pub fn handle_none_receipt( - mut scan_report: PendingPayableScanReport, - payable: PendingPayableFingerprint, - error_msg: &str, - logger: &Logger, -) -> PendingPayableScanReport { - debug!( - logger, - "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", - payable.hash, - error_msg, - payable.attempt, - elapsed_in_ms(payable.timestamp) - ); - - scan_report - .still_pending - .push(PendingPayableId::new(payable.rowid, payable.hash)); - scan_report + PaymentRetryRequired(Either), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Retry { + RetryPayments, + RetryTxStatusCheckOnly, +} + +pub struct TxCaseToBeInterpreted { + pub tx_by_table: TxByTable, + pub tx_receipt_result: TxReceiptResult, +} + +impl TxCaseToBeInterpreted { + pub fn new(tx_by_table: TxByTable, tx_receipt_result: TxReceiptResult) -> Self { + Self { + tx_by_table, + tx_receipt_result, + } + } +} + +#[derive(Debug)] +pub enum TxByTable { + SentPayable(SentTx), + FailedPayable(FailedTx), +} + +impl TxByTable { + pub fn hash(&self) -> TxHash { + match self { + TxByTable::SentPayable(tx) => tx.hash, + TxByTable::FailedPayable(tx) => tx.hash, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum TxHashByTable { + SentPayable(TxHash), + FailedPayable(TxHash), +} + +impl TxHashByTable { + pub fn hash(&self) -> TxHash { + match self { + TxHashByTable::SentPayable(hash) => *hash, + TxHashByTable::FailedPayable(hash) => *hash, + } + } +} + +impl From<&TxByTable> for TxHashByTable { + fn from(tx: &TxByTable) -> Self { + match tx { + TxByTable::SentPayable(tx) => TxHashByTable::SentPayable(tx.hash), + TxByTable::FailedPayable(tx) => TxHashByTable::FailedPayable(tx.hash), + } + } } #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, TxStatus}; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PresortedTxFailure, ReceiptScanReport, + RecheckRequiringFailures, Retry, TxByTable, TxHashByTable, + }; + use crate::accountant::test_utils::{make_failed_tx, make_sent_tx}; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::make_tx_hash; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::ops::Sub; + use std::time::{Duration, SystemTime}; + use std::vec; + + #[test] + fn detected_confirmations_is_empty_works() { + let subject = DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }; + + assert_eq!(subject.is_empty(), true); + } + + #[test] + fn requires_payments_retry() { + // Maximalist approach: exhaustive set of tested variants: + let tx_failures_feedings = vec![ + vec![PresortedTxFailure::NewEntry(make_failed_tx(456))], + vec![PresortedTxFailure::RecheckCompleted(make_tx_hash(123))], + vec![ + PresortedTxFailure::NewEntry(make_failed_tx(123)), + PresortedTxFailure::NewEntry(make_failed_tx(456)), + ], + vec![ + PresortedTxFailure::RecheckCompleted(make_tx_hash(654)), + PresortedTxFailure::RecheckCompleted(make_tx_hash(321)), + ], + vec![ + PresortedTxFailure::NewEntry(make_failed_tx(456)), + PresortedTxFailure::RecheckCompleted(make_tx_hash(654)), + ], + ]; + let tx_receipt_rpc_failures_feeding = vec![ + vec![], + vec![FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ))], + vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + make_tx_hash(12121), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + )], + ]; + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(456)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for tx_failures in &tx_failures_feedings { + for rpc_failures in &tx_receipt_rpc_failures_feeding { + for detected_confirmations in &detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: tx_failures.clone(), + tx_receipt_rpc_failures: rpc_failures.clone(), + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, + Some(Retry::RetryPayments), + "Expected Some(Retry::RetryPayments) but got {:?} for case {:?}", + result, + case + ); + } + } + } + } #[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 - // ) - // }) + fn requires_only_receipt_retrieval_retry() { + let rpc_failure_feedings = vec![ + vec![FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ))], + vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + make_tx_hash(1234), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ), + )], + vec![ + FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ))), + )), + FailedValidationByTable::FailedPayable(FailedValidation::new( + make_tx_hash(1234), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + )), + ], + ]; + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for rpc_failures in &rpc_failure_feedings { + for detected_confirmations in &detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], // This is the determinant + tx_receipt_rpc_failures: rpc_failures.clone(), + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, + Some(Retry::RetryTxStatusCheckOnly), + "Expected Some(Retry::RetryTxStatusCheckOnly) but got {:?} for case {:?}", + result, + case + ); + } + } } #[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) + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for detected_confirmations in detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, None, + "We expected None but got {:?} for case {:?}", + result, case + ); + } + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: reading tx receipts gave no results, \ + but always should" + )] + fn requires_payments_retry_with_no_results_in_whole_summary() { + let report = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + }; + + let _ = report.requires_payments_retry(); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_single_record() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let state_before = subject.sent_payables.clone(); + subject.load_cache(records); + + let first_attempt = subject.get_record_by_hash(tx_hash); + let second_attempt = subject.get_record_by_hash(tx_hash); + + assert_eq!(state_before, hashmap!()); + assert_eq!(first_attempt, Some(sent_tx)); + assert_eq!(second_attempt, None); + assert!( + subject.sent_payables.is_empty(), + "Should be empty but was {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_multiple_records() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx_1 = make_sent_tx(123); + let tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(456); + let tx_hash_2 = sent_tx_2.hash; + let sent_tx_3 = make_sent_tx(789); + let tx_hash_3 = sent_tx_3.hash; + let sent_tx_4 = make_sent_tx(101); + let tx_hash_4 = sent_tx_4.hash; + let nonexistent_tx_hash = make_tx_hash(234); + let records = vec![ + sent_tx_1.clone(), + sent_tx_2.clone(), + sent_tx_3.clone(), + sent_tx_4.clone(), + ]; + + let first_query = subject.get_record_by_hash(tx_hash_1); + subject.load_cache(records); + let second_query = subject.get_record_by_hash(nonexistent_tx_hash); + let third_query = subject.get_record_by_hash(tx_hash_2); + let fourth_query = subject.get_record_by_hash(tx_hash_1); + let fifth_query = subject.get_record_by_hash(tx_hash_4); + let sixth_query = subject.get_record_by_hash(tx_hash_1); + let seventh_query = subject.get_record_by_hash(tx_hash_1); + let eighth_query = subject.get_record_by_hash(tx_hash_3); + + assert_eq!(first_query, None); + assert_eq!(second_query, None); + assert_eq!(third_query, Some(sent_tx_2)); + assert_eq!(fourth_query, Some(sent_tx_1)); + assert_eq!(fifth_query, Some(sent_tx_4)); + assert_eq!(sixth_query, None); + assert_eq!(seventh_query, None); + assert_eq!(eighth_query, Some(sent_tx_3)); + assert!( + subject.sent_payables.is_empty(), + "Expected empty cache, but got {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_ensure_empty_happy_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_happy_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(567); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + let _ = subject.get_record_by_hash(tx_hash); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.sent_payables.is_empty(), + "Should be empty by now but was {:?}", + subject.sent_payables + ); + TestLogHandler::default().exists_no_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some pending payables left unprocessed:" + )); + } + + #[test] + fn pending_payable_cache_ensure_empty_sad_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_sad_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(567); + let tx_timestamp = sent_tx.timestamp; + let records = vec![sent_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.sent_payables.is_empty(), + "Should be empty by now but was {:?}", + subject.sent_payables + ); + TestLogHandler::default().exists_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some pending payables left unprocessed: \ + {{0x0000000000000000000000000000000000000000000000000000000000000237: SentTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ + 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ + {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, status: Pending(Waiting) }}}}. \ + Dumping." + )); + } + + #[test] + fn pending_payable_cache_dump_works() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx_1 = make_sent_tx(567); + let tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(456); + let tx_hash_2 = sent_tx_2.hash; + let sent_tx_3 = make_sent_tx(789); + let tx_hash_3 = sent_tx_3.hash; + let records = vec![sent_tx_1.clone(), sent_tx_2.clone(), sent_tx_3.clone()]; + subject.load_cache(records); + + let result = subject.dump_cache(); + + assert_eq!( + result, + hashmap! ( + tx_hash_1 => sent_tx_1, + tx_hash_2 => sent_tx_2, + tx_hash_3 => sent_tx_3 + ) + ); + } + + #[test] + fn failure_cache_insert_and_get_methods_single_record() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_hash = failed_tx.hash; + let records = vec![failed_tx.clone()]; + let state_before = subject.failures.clone(); + subject.load_cache(records); + + let first_attempt = subject.get_record_by_hash(tx_hash); + let second_attempt = subject.get_record_by_hash(tx_hash); + + assert_eq!(state_before, hashmap!()); + assert_eq!(first_attempt, Some(failed_tx)); + assert_eq!(second_attempt, None); + assert!( + subject.failures.is_empty(), + "Should be empty but was {:?}", + subject.failures + ); + } + + #[test] + fn failure_cache_insert_and_get_methods_multiple_records() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx_1 = make_failed_tx(123); + let tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(456); + let tx_hash_2 = failed_tx_2.hash; + let failed_tx_3 = make_failed_tx(789); + let tx_hash_3 = failed_tx_3.hash; + let failed_tx_4 = make_failed_tx(101); + let tx_hash_4 = failed_tx_4.hash; + let nonexistent_tx_hash = make_tx_hash(234); + let records = vec![ + failed_tx_1.clone(), + failed_tx_2.clone(), + failed_tx_3.clone(), + failed_tx_4.clone(), + ]; + + let first_query = subject.get_record_by_hash(tx_hash_1); + subject.load_cache(records); + let second_query = subject.get_record_by_hash(nonexistent_tx_hash); + let third_query = subject.get_record_by_hash(tx_hash_2); + let fourth_query = subject.get_record_by_hash(tx_hash_1); + let fifth_query = subject.get_record_by_hash(tx_hash_4); + let sixth_query = subject.get_record_by_hash(tx_hash_1); + let seventh_query = subject.get_record_by_hash(tx_hash_1); + let eighth_query = subject.get_record_by_hash(tx_hash_3); + + assert_eq!(first_query, None); + assert_eq!(second_query, None); + assert_eq!(third_query, Some(failed_tx_2)); + assert_eq!(fourth_query, Some(failed_tx_1)); + assert_eq!(fifth_query, Some(failed_tx_4)); + assert_eq!(sixth_query, None); + assert_eq!(seventh_query, None); + assert_eq!(eighth_query, Some(failed_tx_3)); + assert!( + subject.failures.is_empty(), + "Expected empty cache, but got {:?}", + subject.failures + ); + } + + #[test] + fn failure_cache_ensure_empty_happy_path() { + init_test_logging(); + let test_name = "failure_cache_ensure_empty_happy_path"; + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_hash = failed_tx.hash; + let records = vec![failed_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + let _ = subject.get_record_by_hash(tx_hash); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.failures.is_empty(), + "Should be empty by now but was {:?}", + subject.failures + ); + TestLogHandler::default().exists_no_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some tx failures left unprocessed:" + )); + } + + #[test] + fn failure_cache_ensure_empty_sad_path() { + init_test_logging(); + let test_name = "failure_cache_ensure_empty_sad_path"; + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_timestamp = failed_tx.timestamp; + let records = vec![failed_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.failures.is_empty(), + "Should be empty by now but was {:?}", + subject.failures + ); + TestLogHandler::default().exists_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some tx failures left unprocessed: \ + {{0x0000000000000000000000000000000000000000000000000000000000000237: FailedTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ + 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ + {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: \ + RetryRequired }}}}. Dumping." + )); + } + + #[test] + fn failure_cache_dump_works() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx_1 = make_failed_tx(567); + let tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(456); + let tx_hash_2 = failed_tx_2.hash; + let failed_tx_3 = make_failed_tx(789); + let tx_hash_3 = failed_tx_3.hash; + let records = vec![ + failed_tx_1.clone(), + failed_tx_2.clone(), + failed_tx_3.clone(), + ]; + subject.load_cache(records); + + let result = subject.dump_cache(); + + assert_eq!( + result, + hashmap! ( + tx_hash_1 => failed_tx_1, + tx_hash_2 => failed_tx_2, + tx_hash_3 => failed_tx_3 + ) + ); + } + + #[test] + fn failed_validation_new_status_works_for_tx_statuses() { + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(11)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(22)); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_c); + let cases = vec![ + ( + FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ), + Some(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ), + ))), + ), + ( + FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + Some(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + ))), + ), + ]; + + cases.into_iter().for_each(|(input, expected)| { + assert_eq!(input.new_status(&clock), expected); + }); + } + + #[test] + fn failed_validation_new_status_works_for_failure_statuses() { + let timestamp_a = SystemTime::now().sub(Duration::from_secs(222)); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(3333)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(44444)); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b); + let cases = vec![ + ( + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ), + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_a), + )), + )), + ), + ( + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ), + )), + ), + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + ), + )), + ), + ]; + + cases.into_iter().for_each(|(input, expected)| { + assert_eq!(input.new_status(&clock), expected); + }) + } + + #[test] + fn failed_validation_new_status_has_no_effect_on_unexpected_tx_status() { + let validation_failure_clock = ValidationFailureClockMock::default(); + let mal_validated_tx_status = FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Confirmed { + block_hash: "".to_string(), + block_number: 0, + detection: Detection::Normal, + }, + ); + + assert_eq!( + mal_validated_tx_status.new_status(&validation_failure_clock), + None + ); + } + + #[test] + fn failed_validation_new_status_has_no_effect_on_unexpected_failure_status() { + let validation_failure_clock = ValidationFailureClockMock::default(); + let mal_validated_failure_statuses = vec![ + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RetryRequired, + ), + FailedValidation::new( + make_tx_hash(789), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + FailureStatus::Concluded, + ), + ]; + + mal_validated_failure_statuses + .into_iter() + .enumerate() + .for_each(|(idx, failed_validation)| { + let result = failed_validation.new_status(&validation_failure_clock); + assert_eq!( + result, None, + "Failed validation should evaluate to 'None' but was '{:?}' for idx: {}", + result, idx + ) + }); + } + + #[test] + fn tx_hash_by_table_provides_plain_hash() { + let expected_hash_a = make_tx_hash(123); + let a = TxHashByTable::SentPayable(expected_hash_a); + let expected_hash_b = make_tx_hash(654); + let b = TxHashByTable::FailedPayable(expected_hash_b); + + let result_a = a.hash(); + let result_b = b.hash(); + + assert_eq!(result_a, expected_hash_a); + assert_eq!(result_b, expected_hash_b); + } + + #[test] + fn tx_by_table_can_provide_hash() { + let sent_tx = make_sent_tx(123); + let expected_hash_a = sent_tx.hash; + let a = TxByTable::SentPayable(sent_tx); + let failed_tx = make_failed_tx(654); + let expected_hash_b = failed_tx.hash; + let b = TxByTable::FailedPayable(failed_tx); + + let result_a = a.hash(); + let result_b = b.hash(); + + assert_eq!(result_a, expected_hash_a); + assert_eq!(result_b, expected_hash_b); + } + + #[test] + fn tx_by_table_can_be_converted_into_tx_hash_by_table() { + let sent_tx = make_sent_tx(123); + let expected_hash_a = sent_tx.hash; + let a = TxByTable::SentPayable(sent_tx); + let failed_tx = make_failed_tx(654); + let expected_hash_b = failed_tx.hash; + let b = TxByTable::FailedPayable(failed_tx); + + let result_a = TxHashByTable::from(&a); + let result_b = TxHashByTable::from(&b); + + assert_eq!(result_a, TxHashByTable::SentPayable(expected_hash_a)); + assert_eq!(result_b, TxHashByTable::FailedPayable(expected_hash_b)); } } diff --git a/node/src/accountant/scanners/receivable_scanner/mod.rs b/node/src/accountant/scanners/receivable_scanner/mod.rs index b7222df0d7..eff0df95eb 100644 --- a/node/src/accountant/scanners/receivable_scanner/mod.rs +++ b/node/src/accountant/scanners/receivable_scanner/mod.rs @@ -111,7 +111,7 @@ impl ReceivableScanner { { Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", + "Attempt to advance the start block to {} failed due to: {:?}", start_block_number, e ), } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 7a99605b0e..03dad99426 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -44,7 +44,7 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq, Eq)] -pub enum ScanRescheduleAfterEarlyStop { +pub enum ScanReschedulingAfterEarlyStop { Schedule(ScanType), DoNotSchedule, } @@ -245,7 +245,7 @@ pub trait RescheduleScanOnErrorResolver { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop; + ) -> ScanReschedulingAfterEarlyStop; } #[derive(Default)] @@ -258,7 +258,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let reschedule_hint = match scanner { PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) @@ -285,16 +285,16 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } } @@ -304,9 +304,9 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else { unreachable!( "{:?} should be impossible with RetryPayableScanner in automatic mode", @@ -319,12 +319,12 @@ impl RescheduleScanOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -340,7 +340,7 @@ impl RescheduleScanOnErrorResolverReal { // 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) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) } else { unreachable!( "PendingPayableScanner called later than the initial attempt, but \ @@ -359,7 +359,7 @@ impl RescheduleScanOnErrorResolverReal { scanner: PayableSequenceScanner, is_externally_triggered: bool, logger: &Logger, - reschedule_hint: &ScanRescheduleAfterEarlyStop, + reschedule_hint: &ScanReschedulingAfterEarlyStop, ) { let scan_mode = if is_externally_triggered { "Manual" @@ -381,7 +381,7 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, }; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; @@ -634,7 +634,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::DoNotSchedule, + ScanReschedulingAfterEarlyStop::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, @@ -669,7 +669,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); @@ -722,7 +722,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables), "We expected Schedule(PendingPayables) but got {:?} for {:?}", result, scanner @@ -904,7 +904,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, ); diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 3747728abc..c459f72268 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -1,30 +1,29 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod payable_scanner_utils { - use crate::accountant::db_access_objects::utils::ThresholdUtils; - use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; + use crate::accountant::db_access_objects::utils::{ThresholdUtils, TxHash}; + use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; - use crate::accountant::{comma_joined_stringifiable, SentPayables}; + use crate::accountant::{PendingPayable, SentPayables}; use crate::sub_lib::accountant::PaymentThresholds; - use crate::sub_lib::wallet::Wallet; use itertools::Itertools; use masq_lib::logger::Logger; use std::cmp::Ordering; + use std::collections::HashSet; use std::ops::Not; use std::time::SystemTime; use thousands::Separable; - use web3::types::H256; + use web3::types::{Address, 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}; #[derive(Debug, PartialEq, Eq)] pub enum PayableTransactingErrorEnum { LocallyCausedError(PayableTransactionError), - RemotelyCausedErrors(Vec), + RemotelyCausedErrors(HashSet), } #[derive(Debug, PartialEq)] @@ -102,6 +101,7 @@ pub mod payable_scanner_utils { oldest.balance_wei, oldest.age) } + // TODO lifetimes simplification??? pub fn separate_errors<'a, 'b>( sent_payables: &'a SentPayables, logger: &'b Logger, @@ -111,15 +111,28 @@ pub mod payable_scanner_utils { if individual_batch_responses.is_empty() { panic!("Broken code: An empty vector of processed payments claiming to be an Ok value") } - let (oks, err_hashes_opt) = + + let separated_txs_by_result = separate_rpc_results(individual_batch_responses, logger); - let remote_errs_opt = err_hashes_opt.map(RemotelyCausedErrors); + + let remote_errs_opt = if separated_txs_by_result.err_results.is_empty() { + None + } else { + warning!( + logger, + "Please check your blockchain service URL configuration due \ + to detected remote failures" + ); + Some(RemotelyCausedErrors(separated_txs_by_result.err_results)) + }; + let oks = separated_txs_by_result.ok_results; + (oks, remote_errs_opt) } Err(e) => { warning!( logger, - "Any persisted data from failed process will be deleted. Caused by: {}", + "Any persisted data from the failed process will be deleted. Caused by: {}", e ); @@ -128,55 +141,49 @@ pub mod payable_scanner_utils { } } - fn separate_rpc_results<'a, 'b>( + fn separate_rpc_results<'a>( batch_request_responses: &'a [ProcessedPayableFallible], - logger: &'b Logger, - ) -> (Vec<&'a PendingPayable>, Option>) { + logger: &Logger, + ) -> SeparatedTxsByResult<'a> { //TODO maybe we can return not tuple but struct with remote_errors_opt member - let (oks, errs) = batch_request_responses + let init = SeparatedTxsByResult::default(); + batch_request_responses .iter() - .fold((vec![], vec![]), |acc, rpc_result| { - fold_guts(acc, rpc_result, logger) - }); - - let errs_opt = if !errs.is_empty() { Some(errs) } else { None }; - - (oks, errs_opt) - } - - fn add_pending_payable<'a>( - (mut oks, errs): (Vec<&'a PendingPayable>, Vec), - pending_payable: &'a PendingPayable, - ) -> SeparateTxsByResult<'a> { - oks.push(pending_payable); - (oks, errs) + .fold(init, |acc, rpc_result| { + separate_rpc_results_fold_guts(acc, rpc_result, logger) + }) } - fn add_rpc_failure((oks, mut errs): SeparateTxsByResult, hash: H256) -> SeparateTxsByResult { - errs.push(hash); - (oks, errs) + #[derive(Default)] + pub struct SeparatedTxsByResult<'a> { + pub ok_results: Vec<&'a PendingPayable>, + pub err_results: HashSet, } - type SeparateTxsByResult<'a> = (Vec<&'a PendingPayable>, Vec); - - fn fold_guts<'a, 'b>( - acc: SeparateTxsByResult<'a>, + fn separate_rpc_results_fold_guts<'a>( + mut acc: SeparatedTxsByResult<'a>, rpc_result: &'a ProcessedPayableFallible, - logger: &'b Logger, - ) -> SeparateTxsByResult<'a> { + logger: &Logger, + ) -> SeparatedTxsByResult<'a> { match rpc_result { ProcessedPayableFallible::Correct(pending_payable) => { - add_pending_payable(acc, pending_payable) + acc.ok_results.push(pending_payable); + acc } ProcessedPayableFallible::Failed(RpcPayableFailure { rpc_error, recipient_wallet, hash, }) => { - warning!(logger, "Remote transaction failure: '{}' for payment to {} and transaction hash {:?}. \ - Please check your blockchain service URL configuration.", rpc_error, recipient_wallet, hash + warning!( + logger, + "Remote sent payable failure '{}' for wallet {} and tx hash {:?}", + rpc_error, + recipient_wallet, + hash ); - add_rpc_failure(acc, *hash) + acc.err_results.insert(*hash); + acc } } } @@ -194,14 +201,14 @@ pub mod payable_scanner_utils { .duration_since(payable.last_paid_timestamp) .expect("Payable time is corrupt"); format!( - "{} wei owed for {} sec exceeds threshold: {} wei; creditor: {}", + "{} wei owed for {} sec exceeds the threshold {} wei for creditor {}", payable.balance_wei.separate_with_commas(), p_age.as_secs(), threshold_point.separate_with_commas(), payable.wallet ) }) - .join("\n") + .join(".\n") }) } @@ -234,51 +241,23 @@ pub mod payable_scanner_utils { } #[derive(Debug, PartialEq, Eq)] - pub struct PendingPayableMetadata<'a> { - pub recipient: &'a Wallet, + pub struct PendingPayableMissingInDb { + pub recipient: Address, pub hash: H256, - pub rowid_opt: Option, } - impl<'a> PendingPayableMetadata<'a> { - pub fn new( - recipient: &'a Wallet, - hash: H256, - rowid_opt: Option, - ) -> PendingPayableMetadata<'a> { - PendingPayableMetadata { - recipient, - hash, - rowid_opt, - } + impl PendingPayableMissingInDb { + pub fn new(recipient: Address, hash: H256) -> PendingPayableMissingInDb { + PendingPayableMissingInDb { recipient, hash } } } - pub fn mark_pending_payable_fatal_error( - sent_payments: &[&PendingPayable], - nonexistent: &[PendingPayableMetadata], - error: PayableDaoError, - missing_fingerprints_msg_maker: fn(&[PendingPayableMetadata]) -> String, - logger: &Logger, - ) { - if !nonexistent.is_empty() { - error!(logger, "{}", missing_fingerprints_msg_maker(nonexistent)) - }; - panic!( - "Unable to create a mark in the payable table for wallets {} due to {:?}", - comma_joined_stringifiable(sent_payments, |pending_p| pending_p - .recipient_wallet - .to_string()), - error - ) - } - - pub fn err_msg_for_failure_with_expected_but_missing_fingerprints( + pub fn err_msg_for_failure_with_expected_but_missing_sent_tx_record( nonexistent: Vec, serialize_hashes: fn(&[H256]) -> String, ) -> Option { nonexistent.is_empty().not().then_some(format!( - "Ran into failed transactions {} with missing fingerprints. System no longer reliable", + "Ran into failed payables {} with missing records. The system has become unreliable", serialize_hashes(&nonexistent), )) } @@ -333,15 +312,14 @@ mod tests { payables_debug_summary, separate_errors, PayableThresholdsGauge, PayableThresholdsGaugeReal, }; - use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; + use crate::accountant::{checked_conversion, gwei_to_wei, PendingPayable, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; use crate::test_utils::make_wallet; use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use std::time::SystemTime; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; + use std::time::{SystemTime}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -414,7 +392,7 @@ mod tests { init_test_logging(); let error = PayableTransactionError::Sending { msg: "Bad luck".to_string(), - hashes: vec![make_tx_hash(0x7b)], + hashes: hashset![make_tx_hash(0x7b)], }; let sent_payable = SentPayables { payment_procedure_result: Err(error.clone()), @@ -427,8 +405,8 @@ mod tests { assert_eq!(errs, Some(LocallyCausedError(error))); TestLogHandler::new().exists_log_containing( "WARN: test_logger: Any persisted data from \ - failed process will be deleted. Caused by: Sending phase: \"Bad luck\". Signed and hashed \ - transactions: 0x000000000000000000000000000000000000000000000000000000000000007b", + the failed process will be deleted. Caused by: Sending phase: \"Bad luck\". Signed and hashed txs: \ + 0x000000000000000000000000000000000000000000000000000000000000007b", ); } @@ -455,11 +433,14 @@ mod tests { let (oks, errs) = separate_errors(&sent_payable, &Logger::new("test_logger")); assert_eq!(oks, vec![&payable_ok]); - assert_eq!(errs, Some(RemotelyCausedErrors(vec![make_tx_hash(0x315)]))); - TestLogHandler::new().exists_log_containing("WARN: test_logger: Remote transaction failure: \ - 'Got invalid response: That jackass screwed it up' for payment to 0x000000000000000000000000\ - 00000077686f6f61 and transaction hash 0x0000000000000000000000000000000000000000000000000000\ - 000000000315. Please check your blockchain service URL configuration."); + assert_eq!( + errs, + Some(RemotelyCausedErrors(hashset![make_tx_hash(0x315)])) + ); + TestLogHandler::new().exists_log_containing("WARN: test_logger: Remote sent payable \ + failure 'Got invalid response: That jackass screwed it up' for wallet 0x00000000000000000000\ + 000000000077686f6f61 and tx hash 0x000000000000000000000000000000000000000000000000000000000\ + 0000315"); } #[test] @@ -522,10 +503,10 @@ mod tests { payables_debug_summary(&qualified_payables_and_threshold_points, &logger); TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ - 10,002,000,000,000,000 wei owed for 2678400 sec exceeds threshold: \ - 10,000,000,001,152,000 wei; creditor: 0x0000000000000000000000000077616c6c657430\n\ - 999,999,999,000,000,000 wei owed for 86455 sec exceeds threshold: \ - 999,978,993,055,555,580 wei; creditor: 0x0000000000000000000000000077616c6c657431"); + 10,002,000,000,000,000 wei owed for 2678400 sec exceeds the threshold \ + 10,000,000,001,152,000 wei for creditor 0x0000000000000000000000000077616c6c657430.\n\ + 999,999,999,000,000,000 wei owed for 86455 sec exceeds the threshold \ + 999,978,993,055,555,580 wei for creditor 0x0000000000000000000000000077616c6c657431"); } #[test] @@ -665,7 +646,7 @@ mod tests { fn count_total_errors_works_correctly_for_local_error_after_signing() { let error = PayableTransactionError::Sending { msg: "Ouuuups".to_string(), - hashes: vec![make_tx_hash(333), make_tx_hash(666)], + hashes: hashset![make_tx_hash(333), make_tx_hash(666)], }; let sent_payable = Some(LocallyCausedError(error)); @@ -676,7 +657,7 @@ mod tests { #[test] fn count_total_errors_works_correctly_for_remote_errors() { - let sent_payable = Some(RemotelyCausedErrors(vec![ + let sent_payable = Some(RemotelyCausedErrors(hashset![ make_tx_hash(123), make_tx_hash(456), ])); diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 6373250912..ecd0781fe4 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,16 +2,19 @@ #![cfg(test)] +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::scanners::payable_scanner_extension::{ MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, }; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableCache, PendingPayableScanResult, +}; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, - ScanRescheduleAfterEarlyStop, + ScanReschedulingAfterEarlyStop, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::{ @@ -19,8 +22,7 @@ use crate::accountant::scanners::{ Scanner, StartScanError, StartableScanner, }; use crate::accountant::{ - ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, - SentPayables, + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, SentPayables, TxReceiptsMessage, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, OutboundPaymentsInstructions}; @@ -32,6 +34,7 @@ use masq_lib::ui_gateway::NodeToUiMessage; use regex::Regex; use std::any::type_name; use std::cell::RefCell; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use time::{format_description, PrimitiveDateTime}; @@ -367,11 +370,7 @@ pub enum ScannerReplacement { PendingPayable( ReplacementType< PendingPayableScanner, - ScannerMock< - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - >, + ScannerMock, >, ), Receivable( @@ -387,8 +386,8 @@ pub enum MarkScanner<'a> { Started(SystemTime), } -// Cautious: Don't compare to another timestamp on a full match; this timestamp is trimmed in -// nanoseconds down to three digits +// Cautious: Don't compare to another timestamp on an exact match. This timestamp is trimmed in +// nanoseconds down to three digits. Works only for the format bound by TIME_FORMATTING_STRING 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); @@ -444,7 +443,7 @@ pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec>>, - resolve_rescheduling_on_error_results: RefCell>, + resolve_rescheduling_on_error_results: RefCell>, } impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { @@ -454,7 +453,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { self.resolve_rescheduling_on_error_params .lock() .unwrap() @@ -480,7 +479,7 @@ impl RescheduleScanOnErrorResolverMock { } pub fn resolve_rescheduling_on_error_result( self, - result: ScanRescheduleAfterEarlyStop, + result: ScanReschedulingAfterEarlyStop, ) -> Self { self.resolve_rescheduling_on_error_results .borrow_mut() @@ -492,3 +491,70 @@ impl RescheduleScanOnErrorResolverMock { pub fn make_zeroed_consuming_wallet_balances() -> ConsumingWalletBalances { ConsumingWalletBalances::new(0.into(), 0.into()) } + +pub struct PendingPayableCacheMock { + load_cache_params: Arc>>>, + load_cache_results: RefCell>>, + get_record_by_hash_params: Arc>>, + get_record_by_hash_results: RefCell>>, + ensure_empty_cache_params: Arc>>, +} + +impl Default for PendingPayableCacheMock { + fn default() -> Self { + Self { + load_cache_params: Arc::new(Mutex::new(vec![])), + load_cache_results: RefCell::new(vec![]), + get_record_by_hash_params: Arc::new(Mutex::new(vec![])), + get_record_by_hash_results: RefCell::new(vec![]), + ensure_empty_cache_params: Arc::new(Mutex::new(vec![])), + } + } +} + +impl PendingPayableCache for PendingPayableCacheMock { + fn load_cache(&mut self, records: Vec) { + self.load_cache_params.lock().unwrap().push(records); + self.load_cache_results.borrow_mut().remove(0); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.get_record_by_hash_params.lock().unwrap().push(hash); + self.get_record_by_hash_results.borrow_mut().remove(0) + } + + fn ensure_empty_cache(&mut self, _logger: &Logger) { + self.ensure_empty_cache_params.lock().unwrap().push(()); + } + + fn dump_cache(&mut self) -> HashMap { + unimplemented!("not needed yet") + } +} + +impl PendingPayableCacheMock { + pub fn load_cache_params(mut self, params: &Arc>>>) -> Self { + self.load_cache_params = params.clone(); + self + } + + pub fn load_cache_result(self, result: HashMap) -> Self { + self.load_cache_results.borrow_mut().push(result); + self + } + + pub fn get_record_by_hash_params(mut self, params: &Arc>>) -> Self { + self.get_record_by_hash_params = params.clone(); + self + } + + pub fn get_record_by_hash_result(self, result: Option) -> Self { + self.get_record_by_hash_results.borrow_mut().push(result); + self + } + + pub fn ensure_empty_cache_params(mut self, params: &Arc>>) -> Self { + self.ensure_empty_cache_params = params.clone(); + self + } +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 44c888ff71..81b612e477 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -4,18 +4,21 @@ use crate::accountant::db_access_objects::banned_dao::{BannedDao, BannedDaoFactory}; use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureReason, FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::payable_dao::{ - PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, -}; -use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayableDao, PendingPayableDaoError, PendingPayableDaoFactory, TransactionHashes, + MarkPendingPayableID, PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, }; use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDaoError, SentTx, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + SentPayableDao, SentPayableDaoFactory, TxStatus, +}; use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, TxHash, TxIdentifiers, }; @@ -25,15 +28,17 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, }; use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; +use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableCache; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; +use crate::accountant::scanners::test_utils::PendingPayableCacheMock; use crate::accountant::scanners::PayableScanner; -use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; -use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; -use crate::blockchain::test_utils::make_tx_hash; +use crate::accountant::{gwei_to_wei, Accountant}; +use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, TxBlock}; +use crate::blockchain::errors::validation_status::{ValidationFailureClock, ValidationStatus}; +use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::bootstrapper::BootstrapperConfig; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; @@ -45,8 +50,7 @@ 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::System; -use ethereum_types::H256; +use ethereum_types::U64; use masq_lib::logger::Logger; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; @@ -57,6 +61,7 @@ use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::SystemTime; +use web3::types::Address; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_unix_timestamp(SystemTime::now()); @@ -94,13 +99,73 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( } } +pub fn make_sent_tx(num: u64) -> SentTx { + if num == 0 { + panic!("num for generating must be greater than 0"); + } + let params = TxRecordCommonParts::new(num); + SentTx { + hash: params.hash, + receiver_address: params.receiver_address, + amount_minor: params.amount_minor, + timestamp: params.timestamp, + gas_price_minor: params.gas_price_minor, + nonce: params.nonce, + status: TxStatus::Pending(ValidationStatus::Waiting), + } +} + +pub fn make_failed_tx(num: u64) -> FailedTx { + let params = TxRecordCommonParts::new(num); + FailedTx { + hash: params.hash, + receiver_address: params.receiver_address, + amount_minor: params.amount_minor, + timestamp: params.timestamp, + gas_price_minor: params.gas_price_minor, + nonce: params.nonce, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + } +} + +pub fn make_transaction_block(num: u64) -> TxBlock { + TxBlock { + block_hash: make_block_hash(num as u32), + block_number: U64::from(num * num * num), + } +} + +struct TxRecordCommonParts { + hash: TxHash, + receiver_address: Address, + amount_minor: u128, + timestamp: i64, + gas_price_minor: u128, + nonce: u64, +} + +impl TxRecordCommonParts { + fn new(num: u64) -> Self { + Self { + hash: make_tx_hash(num as u32), + receiver_address: make_wallet(&format!("wallet{}", num)).address(), + amount_minor: gwei_to_wei(num * num), + timestamp: to_unix_timestamp(SystemTime::now()) - (num as i64 * 60), + gas_price_minor: gwei_to_wei(num), + nonce: num, + } + } +} + pub struct AccountantBuilder { config_opt: Option, consuming_wallet_opt: Option, logger_opt: Option, payable_dao_factory_opt: Option, receivable_dao_factory_opt: Option, - pending_payable_dao_factory_opt: Option, + sent_payable_dao_factory_opt: Option, + failed_payable_dao_factory_opt: Option, banned_dao_factory_opt: Option, config_dao_factory_opt: Option, } @@ -113,7 +178,8 @@ impl Default for AccountantBuilder { logger_opt: None, payable_dao_factory_opt: None, receivable_dao_factory_opt: None, - pending_payable_dao_factory_opt: None, + sent_payable_dao_factory_opt: None, + failed_payable_dao_factory_opt: None, banned_dao_factory_opt: None, config_dao_factory_opt: None, } @@ -251,7 +317,11 @@ const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::PendingPayableScanner, ]; -const PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ +//TODO Utkarsh should also update this +const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 1] = + [DestinationMarker::PendingPayableScanner]; + +const SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, DestinationMarker::PayableScanner, DestinationMarker::PendingPayableScanner, @@ -278,16 +348,16 @@ impl AccountantBuilder { self } - pub fn pending_payable_daos( + pub fn sent_payable_daos( mut self, - specially_configured_daos: Vec>, + specially_configured_daos: Vec>, ) -> Self { create_or_update_factory!( specially_configured_daos, - PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - pending_payable_dao_factory_opt, - PendingPayableDaoFactoryMock, - PendingPayableDao, + SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, + sent_payable_dao_factory_opt, + SentPayableDaoFactoryMock, + SentPayableDao, self ) } @@ -306,6 +376,27 @@ impl AccountantBuilder { ) } + pub fn failed_payable_daos( + mut self, + mut specially_configured_daos: Vec>, + ) -> Self { + specially_configured_daos.iter_mut().for_each(|dao| { + if let DaoWithDestination::ForPendingPayableScanner(dao) = dao { + let mut extended_queue = vec![vec![]]; + extended_queue.append(&mut dao.retrieve_txs_results.borrow_mut()); + dao.retrieve_txs_results.replace(extended_queue); + } + }); + create_or_update_factory!( + specially_configured_daos, + FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, + failed_payable_dao_factory_opt, + FailedPayableDaoFactoryMock, + FailedPayableDao, + self + ) + } + pub fn receivable_daos( mut self, specially_configured_daos: Vec>, @@ -352,12 +443,15 @@ impl AccountantBuilder { .make_result(ReceivableDaoMock::new()) .make_result(ReceivableDaoMock::new()), ); - let pending_payable_dao_factory = self.pending_payable_dao_factory_opt.unwrap_or( - PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()), + let sent_payable_dao_factory = self.sent_payable_dao_factory_opt.unwrap_or( + SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()), ); + let failed_payable_dao_factory = self + .failed_payable_dao_factory_opt + .unwrap_or(FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new())); let banned_dao_factory = self .banned_dao_factory_opt .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); @@ -368,7 +462,8 @@ impl AccountantBuilder { config, DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), @@ -394,7 +489,8 @@ impl PayableDaoFactory for PayableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied \ + for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -430,7 +526,8 @@ impl ReceivableDaoFactory for ReceivableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only \ + supplied for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -530,7 +627,7 @@ pub struct PayableDaoMock { non_pending_payables_results: RefCell>>, mark_pending_payables_rowids_params: Arc>>>, mark_pending_payables_rowids_results: RefCell>>, - transactions_confirmed_params: Arc>>>, + transactions_confirmed_params: Arc>>>, transactions_confirmed_results: RefCell>>, custom_query_params: Arc>>>, custom_query_result: RefCell>>>, @@ -542,37 +639,36 @@ impl PayableDao for PayableDaoMock { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError> { - self.more_money_payable_parameters - .lock() - .unwrap() - .push((now, wallet.clone(), amount)); + self.more_money_payable_parameters.lock().unwrap().push(( + now, + wallet.clone(), + amount_minor, + )); self.more_money_payable_results.borrow_mut().remove(0) } fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> Result<(), PayableDaoError> { - self.mark_pending_payables_rowids_params - .lock() - .unwrap() - .push( - wallets_and_rowids - .iter() - .map(|(wallet, id)| ((*wallet).clone(), *id)) - .collect(), - ); - self.mark_pending_payables_rowids_results - .borrow_mut() - .remove(0) - } - - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], + _mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError> { + todo!("will be removed in the associated card - GH-662") + // self.mark_pending_payables_rowids_params + // .lock() + // .unwrap() + // .push( + // mark_instructions + // .iter() + // .map(|(wallet, id)| ((*wallet).clone(), *id)) + // .collect(), + // ); + // self.mark_pending_payables_rowids_results + // .borrow_mut() + // .remove(0) + } + + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError> { self.transactions_confirmed_params .lock() .unwrap() @@ -643,10 +739,7 @@ impl PayableDaoMock { self } - pub fn transactions_confirmed_params( - mut self, - params: &Arc>>>, - ) -> Self { + pub fn transactions_confirmed_params(mut self, params: &Arc>>>) -> Self { self.transactions_confirmed_params = params.clone(); self } @@ -694,12 +787,13 @@ impl ReceivableDao for ReceivableDaoMock { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError> { - self.more_money_receivable_parameters - .lock() - .unwrap() - .push((now, wallet.clone(), amount)); + self.more_money_receivable_parameters.lock().unwrap().push(( + now, + wallet.clone(), + amount_minor, + )); self.more_money_receivable_results.borrow_mut().remove(0) } @@ -886,178 +980,169 @@ pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> Boot } #[derive(Default)] -pub struct PendingPayableDaoMock { - fingerprints_rowids_params: Arc>>>, - fingerprints_rowids_results: RefCell>, - delete_fingerprints_params: Arc>>>, - delete_fingerprints_results: RefCell>>, - insert_new_fingerprints_params: Arc, SystemTime)>>>, - insert_new_fingerprints_results: RefCell>>, - increment_scan_attempts_params: Arc>>>, - increment_scan_attempts_result: RefCell>>, - mark_failures_params: Arc>>>, - mark_failures_results: RefCell>>, - return_all_errorless_fingerprints_params: Arc>>, - return_all_errorless_fingerprints_results: RefCell>>, - pub have_return_all_errorless_fingerprints_shut_down_the_system: bool, -} - -impl PendingPayableDao for PendingPayableDaoMock { - fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes { - self.fingerprints_rowids_params +pub struct SentPayableDaoMock { + get_tx_identifiers_params: Arc>>>, + get_tx_identifiers_results: RefCell>, + insert_new_records_params: Arc>>>, + insert_new_records_results: RefCell>>, + retrieve_txs_params: Arc>>>, + retrieve_txs_results: RefCell>>, + confirm_tx_params: Arc>>>, + confirm_tx_results: RefCell>>, + update_statuses_params: Arc>>>, + update_statuses_results: RefCell>>, + replace_records_params: Arc>>>, + replace_records_results: RefCell>>, + delete_records_params: Arc>>>, + delete_records_results: RefCell>>, +} + +impl SentPayableDao for SentPayableDaoMock { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + self.get_tx_identifiers_params .lock() .unwrap() - .push(hashes.to_vec()); - self.fingerprints_rowids_results.borrow_mut().remove(0) + .push(hashes.clone()); + self.get_tx_identifiers_results.borrow_mut().remove(0) } - - fn return_all_errorless_fingerprints(&self) -> Vec { - self.return_all_errorless_fingerprints_params + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + self.insert_new_records_params .lock() .unwrap() - .push(()); - if self.have_return_all_errorless_fingerprints_shut_down_the_system - && self - .return_all_errorless_fingerprints_results - .borrow() - .is_empty() - { - System::current().stop(); - return vec![]; - } - self.return_all_errorless_fingerprints_results - .borrow_mut() - .remove(0) + .push(txs.to_vec()); + self.insert_new_records_results.borrow_mut().remove(0) } - - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError> { - self.insert_new_fingerprints_params + fn retrieve_txs(&self, condition: Option) -> Vec { + self.retrieve_txs_params.lock().unwrap().push(condition); + self.retrieve_txs_results.borrow_mut().remove(0) + } + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { + self.confirm_tx_params .lock() .unwrap() - .push((hashes_and_amounts.to_vec(), batch_wide_timestamp)); - self.insert_new_fingerprints_results.borrow_mut().remove(0) + .push(hash_map.clone()); + self.confirm_tx_results.borrow_mut().remove(0) } - - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.delete_fingerprints_params + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + self.replace_records_params .lock() .unwrap() - .push(ids.to_vec()); - self.delete_fingerprints_results.borrow_mut().remove(0) + .push(new_txs.to_vec()); + self.replace_records_results.borrow_mut().remove(0) } - fn increment_scan_attempts(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.increment_scan_attempts_params + fn update_statuses( + &self, + hash_map: &HashMap, + ) -> Result<(), SentPayableDaoError> { + self.update_statuses_params .lock() .unwrap() - .push(ids.to_vec()); - self.increment_scan_attempts_result.borrow_mut().remove(0) + .push(hash_map.clone()); + self.update_statuses_results.borrow_mut().remove(0) } - fn mark_failures(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.mark_failures_params.lock().unwrap().push(ids.to_vec()); - self.mark_failures_results.borrow_mut().remove(0) + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + self.delete_records_params + .lock() + .unwrap() + .push(hashes.clone()); + self.delete_records_results.borrow_mut().remove(0) } } -impl PendingPayableDaoMock { +impl SentPayableDaoMock { pub fn new() -> Self { - PendingPayableDaoMock::default() + SentPayableDaoMock::default() } - pub fn fingerprints_rowids_params(mut self, params: &Arc>>>) -> Self { - self.fingerprints_rowids_params = params.clone(); + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + self.get_tx_identifiers_params = params.clone(); self } - pub fn fingerprints_rowids_result(self, result: TransactionHashes) -> Self { - self.fingerprints_rowids_results.borrow_mut().push(result); + pub fn get_tx_identifiers_result(self, result: TxIdentifiers) -> Self { + self.get_tx_identifiers_results.borrow_mut().push(result); self } - pub fn insert_fingerprints_params( + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + self.insert_new_records_params = params.clone(); + self + } + + pub fn insert_new_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.insert_new_records_results.borrow_mut().push(result); + self + } + + pub fn retrieve_txs_params( mut self, - params: &Arc, SystemTime)>>>, + params: &Arc>>>, ) -> Self { - self.insert_new_fingerprints_params = params.clone(); + self.retrieve_txs_params = params.clone(); self } - pub fn insert_fingerprints_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.insert_new_fingerprints_results - .borrow_mut() - .push(result); + pub fn retrieve_txs_result(self, result: Vec) -> Self { + self.retrieve_txs_results.borrow_mut().push(result); self } - pub fn delete_fingerprints_params(mut self, params: &Arc>>>) -> Self { - self.delete_fingerprints_params = params.clone(); + pub fn confirm_tx_params(mut self, params: &Arc>>>) -> Self { + self.confirm_tx_params = params.clone(); self } - pub fn delete_fingerprints_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.delete_fingerprints_results.borrow_mut().push(result); + pub fn confirm_tx_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.confirm_tx_results.borrow_mut().push(result); self } - pub fn return_all_errorless_fingerprints_params( - mut self, - params: &Arc>>, - ) -> Self { - self.return_all_errorless_fingerprints_params = params.clone(); + pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { + self.replace_records_params = params.clone(); self } - pub fn return_all_errorless_fingerprints_result( - self, - result: Vec, - ) -> Self { - self.return_all_errorless_fingerprints_results - .borrow_mut() - .push(result); + pub fn replace_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.replace_records_results.borrow_mut().push(result); self } - pub fn mark_failures_params(mut self, params: &Arc>>>) -> Self { - self.mark_failures_params = params.clone(); + pub fn update_statuses_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.update_statuses_params = params.clone(); self } - pub fn mark_failures_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.mark_failures_results.borrow_mut().push(result); + pub fn update_statuses_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.update_statuses_results.borrow_mut().push(result); self } - pub fn increment_scan_attempts_params(mut self, params: &Arc>>>) -> Self { - self.increment_scan_attempts_params = params.clone(); + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + self.delete_records_params = params.clone(); self } - pub fn increment_scan_attempts_result( - self, - result: Result<(), PendingPayableDaoError>, - ) -> Self { - self.increment_scan_attempts_result - .borrow_mut() - .push(result); + pub fn delete_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.delete_records_results.borrow_mut().push(result); self } } -pub struct PendingPayableDaoFactoryMock { +pub struct SentPayableDaoFactoryMock { make_params: Arc>>, - make_results: RefCell>>, + make_results: RefCell>>, } -impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { - fn make(&self) -> Box { +impl SentPayableDaoFactory for SentPayableDaoFactoryMock { + fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "PendingPayableDao Missing. This problem mostly occurs when PendingPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "SentPayableDao Missing. This problem mostly occurs when SentPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -1065,7 +1150,7 @@ impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { } } -impl PendingPayableDaoFactoryMock { +impl SentPayableDaoFactoryMock { pub fn new() -> Self { Self { make_params: Arc::new(Mutex::new(vec![])), @@ -1078,7 +1163,7 @@ impl PendingPayableDaoFactoryMock { self } - pub fn make_result(self, result: PendingPayableDaoMock) -> Self { + pub fn make_result(self, result: SentPayableDaoMock) -> Self { self.make_results.borrow_mut().push(Box::new(result)); self } @@ -1122,12 +1207,12 @@ impl FailedPayableDao for FailedPayableDaoMock { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { self.update_statuses_params .lock() .unwrap() - .push(status_updates); + .push(status_updates.clone()); self.update_statuses_results.borrow_mut().remove(0) } @@ -1209,9 +1294,6 @@ pub struct FailedPayableDaoFactoryMock { impl FailedPayableDaoFactory for FailedPayableDaoFactoryMock { fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!("FailedPayableDao Missing.") - }; self.make_params.lock().unwrap().push(()); self.make_results.borrow_mut().remove(0) } @@ -1238,7 +1320,7 @@ impl FailedPayableDaoFactoryMock { pub struct PayableScannerBuilder { payable_dao: PayableDaoMock, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, payment_thresholds: PaymentThresholds, payment_adjuster: PaymentAdjusterMock, } @@ -1247,7 +1329,7 @@ impl PayableScannerBuilder { pub fn new() -> Self { Self { payable_dao: PayableDaoMock::new(), - pending_payable_dao: PendingPayableDaoMock::new(), + sent_payable_dao: SentPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), payment_adjuster: PaymentAdjusterMock::default(), } @@ -1271,18 +1353,18 @@ impl PayableScannerBuilder { self } - pub fn pending_payable_dao( + pub fn sent_payable_dao( mut self, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, ) -> PayableScannerBuilder { - self.pending_payable_dao = pending_payable_dao; + self.sent_payable_dao = sent_payable_dao; self } pub fn build(self) -> PayableScanner { PayableScanner::new( Box::new(self.payable_dao), - Box::new(self.pending_payable_dao), + Box::new(self.sent_payable_dao), Rc::new(self.payment_thresholds), Box::new(self.payment_adjuster), ) @@ -1291,20 +1373,26 @@ impl PayableScannerBuilder { pub struct PendingPayableScannerBuilder { payable_dao: PayableDaoMock, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, + failed_payable_dao: FailedPayableDaoMock, payment_thresholds: PaymentThresholds, - when_pending_too_long_sec: u64, financial_statistics: FinancialStatistics, + current_sent_payables: Box>, + yet_unproven_failed_payables: Box>, + clock: Box, } impl PendingPayableScannerBuilder { pub fn new() -> Self { Self { payable_dao: PayableDaoMock::new(), - pending_payable_dao: PendingPayableDaoMock::new(), + sent_payable_dao: SentPayableDaoMock::new(), + failed_payable_dao: FailedPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, financial_statistics: FinancialStatistics::default(), + current_sent_payables: Box::new(PendingPayableCacheMock::default()), + yet_unproven_failed_payables: Box::new(PendingPayableCacheMock::default()), + clock: Box::new(ValidationFailureClockMock::default()), } } @@ -1313,24 +1401,46 @@ impl PendingPayableScannerBuilder { self } - pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { - self.pending_payable_dao = pending_payable_dao; + pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { + self.sent_payable_dao = sent_payable_dao; self } - pub fn when_pending_too_long_sec(mut self, interval: u64) -> Self { - self.when_pending_too_long_sec = interval; + pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { + self.failed_payable_dao = failed_payable_dao; + self + } + + pub fn sent_payable_cache(mut self, cache: Box>) -> Self { + self.current_sent_payables = cache; + self + } + + pub fn failed_payable_cache( + mut self, + failures: Box>, + ) -> Self { + self.yet_unproven_failed_payables = failures; + self + } + + pub fn validation_failure_clock(mut self, clock: Box) -> Self { + self.clock = clock; self } pub fn build(self) -> PendingPayableScanner { - PendingPayableScanner::new( + let mut scanner = PendingPayableScanner::new( Box::new(self.payable_dao), - Box::new(self.pending_payable_dao), + Box::new(self.sent_payable_dao), + Box::new(self.failed_payable_dao), Rc::new(self.payment_thresholds), - self.when_pending_too_long_sec, Rc::new(RefCell::new(self.financial_statistics)), - ) + ); + scanner.current_sent_payables = self.current_sent_payables; + scanner.yet_unproven_failed_payables = self.yet_unproven_failed_payables; + scanner.clock = self.clock; + scanner } } @@ -1398,17 +1508,6 @@ pub fn make_custom_payment_thresholds() -> PaymentThresholds { } } -pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { - PendingPayableFingerprint { - rowid: 33, - timestamp: from_unix_timestamp(222_222_222), - hash: make_tx_hash(456), - attempt: 1, - amount: 12345, - process_error: None, - } -} - pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, @@ -1490,10 +1589,10 @@ where { let conn = Connection::open_in_memory().unwrap(); let execute = |sql: &str| conn.execute(sql, []).unwrap(); - execute("create table whatever (exclamations text)"); - execute("insert into whatever (exclamations) values ('Gosh')"); + execute("create table whatever (exclamation text)"); + execute("insert into whatever (exclamation) values ('Gosh')"); - conn.query_row("select exclamations from whatever", [], tested_fn) + conn.query_row("select exclamation from whatever", [], tested_fn) .unwrap(); } diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 9e15f9c5d6..8b24da7221 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -473,7 +473,8 @@ impl ActorFactory for ActorFactoryReal { ) -> AccountantSubs { let data_directory = config.data_directory.as_path(); let payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); - let pending_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let sent_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let failed_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let receivable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let banned_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let config_dao_factory = Box::new(Accountant::dao_factory(data_directory)); @@ -484,7 +485,8 @@ impl ActorFactory for ActorFactoryReal { config, DaoFactories { payable_dao_factory, - pending_payable_dao_factory, + sent_payable_dao_factory, + failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, config_dao_factory, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 421fc6bd55..d1d41337b0 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,17 +1,21 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, PricedQualifiedPayables}; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayablesMessage, +}; use crate::accountant::{ - ReceivedPayments, ResponseSkeleton, ScanError, - SentPayables, SkeletonOptHolder, + ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, TxReceiptResult, }; -use crate::accountant::{ReportTransactionReceipts, RequestTransactionReceipts}; +use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; use crate::actor_system_factory::SubsFactory; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainInterfaceError, PayableTransactionError, }; -use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible; +use crate::blockchain::blockchain_interface::data_structures::{ + ProcessedPayableFallible, StatusReadFromReceiptCheck, +}; use crate::blockchain::blockchain_interface::BlockchainInterface; use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal}; @@ -19,12 +23,10 @@ use crate::db_config::config_dao::ConfigDaoReal; use crate::db_config::persistent_configuration::{ PersistentConfiguration, PersistentConfigurationReal, }; -use crate::sub_lib::blockchain_bridge::{ - BlockchainBridgeSubs, OutboundPaymentsInstructions, -}; +use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, OutboundPaymentsInstructions}; use crate::sub_lib::peer_actors::BindMessage; use crate::sub_lib::utils::{db_connection_launch_panic, handle_ui_crash_request}; -use crate::sub_lib::wallet::{Wallet}; +use crate::sub_lib::wallet::Wallet; use actix::Actor; use actix::Context; use actix::Handler; @@ -33,19 +35,16 @@ use actix::{Addr, Recipient}; use futures::Future; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; +use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; +use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; use std::string::ToString; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -use ethabi::Hash; use web3::types::H256; -use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; -use masq_lib::messages::ScanType; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; pub const DEFAULT_BLOCKCHAIN_SERVICE_URL: &str = "https://0.0.0.0"; @@ -59,12 +58,12 @@ pub struct BlockchainBridge { received_payments_subs_opt: Option>, scan_error_subs_opt: Option>, crashable: bool, - pending_payable_confirmation: TransactionConfirmationTools, + pending_payable_confirmation: TxConfirmationTools, } -struct TransactionConfirmationTools { - new_pp_fingerprints_sub_opt: Option>, - report_transaction_receipts_sub_opt: Option>, +struct TxConfirmationTools { + register_new_pending_payables_sub_opt: Option>, + report_tx_receipts_sub_opt: Option>, } #[derive(PartialEq, Eq)] @@ -88,11 +87,10 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: BindMessage, _ctx: &mut Self::Context) -> Self::Result { self.pending_payable_confirmation - .new_pp_fingerprints_sub_opt = - Some(msg.peer_actors.accountant.init_pending_payable_fingerprints); - self.pending_payable_confirmation - .report_transaction_receipts_sub_opt = - Some(msg.peer_actors.accountant.report_transaction_receipts); + .register_new_pending_payables_sub_opt = + Some(msg.peer_actors.accountant.register_new_pending_payables); + self.pending_payable_confirmation.report_tx_receipts_sub_opt = + Some(msg.peer_actors.accountant.report_transaction_status); self.payable_payments_setup_subs_opt = Some(msg.peer_actors.accountant.report_payable_payments_setup); self.sent_payable_subs_opt = Some(msg.peer_actors.accountant.report_sent_payments); @@ -164,21 +162,14 @@ impl Handler for BlockchainBridge { } #[derive(Debug, Clone, PartialEq, Eq, Message)] -pub struct PendingPayableFingerprintSeeds { - pub batch_wide_timestamp: SystemTime, - pub hashes_and_balances: Vec, +pub struct RegisterNewPendingPayables { + pub new_sent_txs: Vec, } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct PendingPayableFingerprint { - // Sqlite begins counting from 1 - pub rowid: u64, - pub timestamp: SystemTime, - pub hash: H256, - // We have Sqlite begin counting from 1 - pub attempt: u16, - pub amount: u128, - pub process_error: Option, +impl RegisterNewPendingPayables { + pub fn new(new_sent_txs: Vec) -> Self { + Self { new_sent_txs } + } } impl Handler for BlockchainBridge { @@ -204,9 +195,9 @@ impl BlockchainBridge { scan_error_subs_opt: None, crashable, logger: Logger::new("BlockchainBridge"), - pending_payable_confirmation: TransactionConfirmationTools { - new_pp_fingerprints_sub_opt: None, - report_transaction_receipts_sub_opt: None, + pending_payable_confirmation: TxConfirmationTools { + register_new_pending_payables_sub_opt: None, + report_tx_receipts_sub_opt: None, }, } } @@ -394,21 +385,21 @@ impl BlockchainBridge { fn log_status_of_tx_receipts( logger: &Logger, - transaction_receipts_results: &[TransactionReceiptResult], + transaction_receipts_results: &[&TxReceiptResult], ) { logger.debug(|| { let (successful_count, failed_count, pending_count) = transaction_receipts_results.iter().fold( (0, 0, 0), |(success, fail, pending), transaction_receipt| match transaction_receipt { - TransactionReceiptResult::RpcResponse(tx_receipt) => { - match tx_receipt.status { - TxStatus::Failed => (success, fail + 1, pending), - TxStatus::Pending => (success, fail, pending + 1), - TxStatus::Succeeded(_) => (success + 1, fail, pending), + Ok(tx_status) => match tx_status { + StatusReadFromReceiptCheck::Reverted => (success, fail + 1, pending), + StatusReadFromReceiptCheck::Succeeded(_) => { + (success + 1, fail, pending) } - } - TransactionReceiptResult::LocalError(_) => (success, fail, pending + 1), + StatusReadFromReceiptCheck::Pending => (success, fail, pending + 1), + }, + Err(_) => (success, fail, pending + 1), }, ); format!( @@ -425,30 +416,21 @@ impl BlockchainBridge { let logger = self.logger.clone(); let accountant_recipient = self .pending_payable_confirmation - .report_transaction_receipts_sub_opt + .report_tx_receipts_sub_opt .clone() .expect("Accountant is unbound"); - - let transaction_hashes = msg - .pending_payable_fingerprints - .iter() - .map(|finger_print| finger_print.hash) - .collect::>(); Box::new( self.blockchain_interface - .process_transaction_receipts(transaction_hashes) + .process_transaction_receipts(msg.tx_hashes) .map_err(move |e| e.to_string()) - .and_then(move |transaction_receipts_results| { - Self::log_status_of_tx_receipts(&logger, &transaction_receipts_results); - - let pairs = transaction_receipts_results - .into_iter() - .zip(msg.pending_payable_fingerprints.into_iter()) - .collect_vec(); - + .and_then(move |tx_receipt_results| { + Self::log_status_of_tx_receipts( + &logger, + tx_receipt_results.values().collect_vec().as_slice(), + ); accountant_recipient - .try_send(ReportTransactionReceipts { - fingerprints_with_receipts: pairs, + .try_send(TxReceiptsMessage { + results: tx_receipt_results, response_skeleton_opt: msg.response_skeleton_opt, }) .expect("Accountant is dead"); @@ -488,19 +470,19 @@ impl BlockchainBridge { affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { - let new_fingerprints_recipient = self.new_fingerprints_recipient(); + let recipient = self.new_pending_payables_recipient(); let logger = self.logger.clone(); self.blockchain_interface.submit_payables_in_batch( logger, agent, - new_fingerprints_recipient, + recipient, affordable_accounts, ) } - fn new_fingerprints_recipient(&self) -> Recipient { + fn new_pending_payables_recipient(&self) -> Recipient { self.pending_payable_confirmation - .new_pp_fingerprints_sub_opt + .register_new_pending_payables_sub_opt .clone() .expect("Accountant unbound") } @@ -552,18 +534,26 @@ impl SubsFactory for BlockchainBridgeSub 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_unix_timestamp; + use crate::accountant::db_access_objects::sent_payable_dao::TxStatus; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, + }; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint, make_priced_qualified_payables}; + use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::accountant::test_utils::make_priced_qualified_payables; + use crate::accountant::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainAgentBuildError, PayableTransactionError, }; use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible::Correct; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, RetrievedBlockchainTransactions, + BlockchainTransaction, RetrievedBlockchainTransactions, TxBlock, }; + use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; + use crate::blockchain::errors::validation_status::ValidationStatus; use crate::blockchain::test_utils::{ make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; @@ -584,6 +574,7 @@ mod tests { use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; + use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; 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; @@ -597,9 +588,6 @@ mod tests { use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160}; - use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; - use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, QualifiedPayableWithGasPrice}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt}; impl Handler> for BlockchainBridge { type Result = (); @@ -897,18 +885,18 @@ mod tests { system.run(); let time_after = SystemTime::now(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let pending_payable_fingerprint_seeds_msg = - accountant_recording.get_record::(0); + let register_new_pending_payables_msg = + accountant_recording.get_record::(0); let sent_payables_msg = accountant_recording.get_record::(1); + let expected_hash = + H256::from_str("81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c") + .unwrap(); assert_eq!( sent_payables_msg, &SentPayables { payment_procedure_result: Ok(vec![Correct(PendingPayable { - recipient_wallet: account.wallet, - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap() + recipient_wallet: account.wallet.clone(), + hash: expected_hash })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -916,17 +904,26 @@ mod tests { }) } ); - assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp >= time_before); - assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp <= time_after); + let first_actual_sent_tx = ®ister_new_pending_payables_msg.new_sent_txs[0]; assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap(), - amount: account.balance_wei - }] + first_actual_sent_tx.receiver_address, + account.wallet.address() + ); + assert_eq!(first_actual_sent_tx.hash, expected_hash); + assert_eq!(first_actual_sent_tx.amount_minor, account.balance_wei); + assert_eq!(first_actual_sent_tx.gas_price_minor, 111_222_333); + assert_eq!(first_actual_sent_tx.nonce, 0x20); + assert_eq!( + first_actual_sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting) + ); + assert!( + to_unix_timestamp(time_before) <= first_actual_sent_tx.timestamp + && first_actual_sent_tx.timestamp <= to_unix_timestamp(time_after), + "We thought the timestamp was between {:?} and {:?}, but it was {:?}", + time_before, + time_after, + from_unix_timestamp(first_actual_sent_tx.timestamp) ); assert_eq!(accountant_recording.len(), 2); } @@ -945,7 +942,7 @@ mod tests { let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); - let wallet_account = make_wallet("blah"); + let account_wallet = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); let persistent_configuration_mock = PersistentConfigurationMock::default(); let subject = BlockchainBridge::new( @@ -958,7 +955,7 @@ mod tests { let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); let account = PayableAccount { - wallet: wallet_account, + wallet: account_wallet.clone(), balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, @@ -986,8 +983,8 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let pending_payable_fingerprint_seeds_msg = - accountant_recording.get_record::(0); + let actual_register_new_pending_payables_msg = + accountant_recording.get_record::(0); let sent_payables_msg = accountant_recording.get_record::(1); let scan_error_msg = accountant_recording.get_record::(2); assert_sending_error( @@ -998,14 +995,23 @@ mod tests { "Transport error: Error(IncompleteMessage)", ); assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap(), - amount: account.balance_wei - }] + actual_register_new_pending_payables_msg.new_sent_txs[0].receiver_address, + account_wallet.address() + ); + assert_eq!( + actual_register_new_pending_payables_msg.new_sent_txs[0].hash, + H256::from_str("81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c") + .unwrap() + ); + assert_eq!( + actual_register_new_pending_payables_msg.new_sent_txs[0].amount_minor, + account.balance_wei + ); + let number_of_requested_txs = actual_register_new_pending_payables_msg.new_sent_txs.len(); + assert_eq!( + number_of_requested_txs, 1, + "We expected only one sent tx, but got {}", + number_of_requested_txs ); assert_eq!( *scan_error_msg, @@ -1016,7 +1022,8 @@ mod tests { context_id: 4321 }), msg: format!( - "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" + "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". \ + Signed and hashed txs: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) } ); @@ -1058,7 +1065,7 @@ mod tests { let (accountant, _, accountant_recording) = make_recorder(); subject .pending_payable_confirmation - .new_pp_fingerprints_sub_opt = Some(accountant.start().recipient()); + .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject .process_payments(msg.agent, msg.affordable_accounts) @@ -1119,7 +1126,7 @@ mod tests { let (accountant, _, accountant_recording) = make_recorder(); subject .pending_payable_confirmation - .new_pp_fingerprints_sub_opt = Some(accountant.start().recipient()); + .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject .process_payments(msg.agent, msg.affordable_accounts) @@ -1154,21 +1161,13 @@ 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_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); - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 4565, - process_error: None, - }; + let accountant = + accountant.system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)); + let tx_hash_1 = make_tx_hash(123); + let tx_hash_2 = make_tx_hash(456); let first_response = ReceiptResponseBuilder::default() .status(U64::from(1)) - .transaction_hash(hash_1) + .transaction_hash(tx_hash_1) .build(); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) @@ -1189,9 +1188,9 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1205,26 +1204,20 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 1); - let report_transaction_receipt_message = - accountant_recording.get_record::(0); + let tx_receipts_message = accountant_recording.get_record::(0); let mut expected_receipt = TransactionReceipt::default(); - expected_receipt.transaction_hash = hash_1; + expected_receipt.transaction_hash = tx_hash_1; expected_receipt.status = Some(U64::from(1)); assert_eq!( - report_transaction_receipt_message, - &ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(expected_receipt.into()), - pending_payable_fingerprint_1 - ), - ( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash_2, - status: TxStatus::Pending - }), - pending_payable_fingerprint_2 + tx_receipts_message, + &TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(tx_hash_1) => Ok( + expected_receipt.into() ), + TxHashByTable::FailedPayable(tx_hash_2) => Ok( + StatusReadFromReceiptCheck::Pending + ) ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1286,8 +1279,7 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_on_failure_from_remote_process_sends_back_all_good_results_and_logs_abort( - ) { + fn handle_request_transaction_receipts_sends_back_results() { init_test_logging(); let port = find_free_port(); let block_number = U64::from(4545454); @@ -1302,62 +1294,26 @@ mod tests { .begin_batch() .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .raw_response(tx_receipt_response) - .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .err_response( 429, "The requests per second (RPS) of your requests are higher than your plan allows." .to_string(), 7, ) + .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .end_batch() .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!( - ReportTransactionReceipts, - ScanError - )) + .system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)) .start(); - let report_transaction_receipt_recipient: Recipient = + let report_transaction_receipt_recipient: Recipient = accountant_addr.clone().recipient(); let scan_error_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(111334); - let hash_2 = make_tx_hash(100000); - let hash_3 = make_tx_hash(0x1348d); - let hash_4 = make_tx_hash(11111); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.hash = hash_1; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_3 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_3, - attempt: 3, - amount: 4565, - process_error: None, - }; - let fingerprint_4 = PendingPayableFingerprint { - rowid: 450, - timestamp: from_unix_timestamp(230_000_000), - hash: hash_4, - attempt: 1, - amount: 7879, - process_error: None, - }; - let transaction_receipt = TxReceipt { - transaction_hash: Default::default(), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number, - }), - }; + let tx_hash_1 = make_tx_hash(1334); + let tx_hash_2 = make_tx_hash(1000); + let tx_hash_3 = make_tx_hash(1212); + let tx_hash_4 = make_tx_hash(1111); let blockchain_interface = make_blockchain_interface_web3(port); let system = System::new("test_transaction_receipts"); let mut subject = BlockchainBridge::new( @@ -1367,14 +1323,14 @@ mod tests { ); subject .pending_payable_confirmation - .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); + .report_tx_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - fingerprint_1.clone(), - fingerprint_2.clone(), - fingerprint_3.clone(), - fingerprint_4.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::SentPayable(tx_hash_2), + TxHashByTable::SentPayable(tx_hash_3), + TxHashByTable::SentPayable(tx_hash_4), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1388,15 +1344,18 @@ mod tests { assert_eq!(system.run(), 0); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 1); - let report_receipts_msg = accountant_recording.get_record::(0); + let report_receipts_msg = accountant_recording.get_record::(0); assert_eq!( *report_receipts_msg, - ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - (TransactionReceiptResult::RpcResponse(TxReceipt{ transaction_hash: hash_1, status: TxStatus::Pending }), fingerprint_1), - (TransactionReceiptResult::RpcResponse(transaction_receipt), fingerprint_2), - (TransactionReceiptResult::RpcResponse(TxReceipt{ transaction_hash: hash_3, status: TxStatus::Pending }), fingerprint_3), - (TransactionReceiptResult::LocalError("RPC error: Error { code: ServerError(429), message: \"The requests per second (RPS) of your requests are higher than your plan allows.\", data: None }".to_string()), fingerprint_4) + TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: Default::default(), + block_number, + })), + TxHashByTable::SentPayable(tx_hash_3) => Err( + AppRpcError:: Remote(RemoteError::Web3RpcError { code: 429, message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string()})), + TxHashByTable::SentPayable(tx_hash_4) => Ok(StatusReadFromReceiptCheck::Pending), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1410,32 +1369,17 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_if_submit_batch_fails() { + fn handle_request_transaction_receipts_failing_submit_the_batch() { init_test_logging(); let (accountant, _, accountant_recording) = make_recorder(); let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); - let report_transaction_recipient: Recipient = + let report_transaction_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(0x1b2e6); - let fingerprint_1 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_1, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: make_tx_hash(222444), - attempt: 3, - amount: 4565, - process_error: None, - }; + let tx_hash_1 = make_tx_hash(10101); + let tx_hash_2 = make_tx_hash(10102); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); let blockchain_interface = make_blockchain_interface_web3(port); @@ -1446,10 +1390,13 @@ mod tests { ); subject .pending_payable_confirmation - .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); + .report_tx_receipts_sub_opt = Some(report_transaction_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![fingerprint_1, fingerprint_2], + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + ], response_skeleton_opt: None, }; let system = System::new("test"); @@ -2033,10 +1980,11 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); + subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, ScanType::Receivables, @@ -2045,7 +1993,9 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let msg_opt = accountant_recording.get_record_opt::(0); + let received_msg = accountant_recording.get_record::(0); + assert_eq!(received_msg.new_start_block, BlockMarker::Value(0xc8 + 1)); + let msg_opt = accountant_recording.get_record_opt::(1); assert_eq!(msg_opt, None, "We didnt expect a scan error: {:?}", msg_opt); } 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 c93c07b538..7a4d6ddfb6 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 @@ -6,104 +6,12 @@ use crate::blockchain::blockchain_interface::data_structures::errors::Blockchain use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use ethereum_types::{H256, U256, U64}; use futures::Future; -use serde_derive::{Deserialize, Serialize}; 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}; +use web3::types::{Address, BlockNumber, Filter, Log}; use web3::{Error, Web3}; -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TransactionReceiptResult { - RpcResponse(TxReceipt), - LocalError(String), -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TxStatus { - Failed, - Pending, - 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, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct TransactionBlock { - pub block_hash: H256, - pub block_number: U64, -} - -impl From for TxReceipt { - fn from(receipt: TransactionReceipt) -> Self { - let status = match (receipt.status, receipt.block_hash, receipt.block_number) { - (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { - TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }) - } - (Some(status), _, _) if status == U64::from(0) => TxStatus::Failed, - _ => TxStatus::Pending, - }; - - TxReceipt { - transaction_hash: receipt.transaction_hash, - status, - } - } -} - pub struct LowBlockchainIntWeb3 { web3: Web3, web3_batch: Web3>, @@ -222,7 +130,7 @@ mod tests { use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_LITERAL; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::{BlockchainInterfaceError, BlockchainInterface}; - use crate::blockchain::test_utils::make_blockchain_interface_web3; + use crate::blockchain::test_utils::{make_block_hash, make_blockchain_interface_web3, make_tx_hash, TransactionReceiptBuilder}; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use ethereum_types::{H256, U64}; @@ -230,8 +138,8 @@ mod tests { use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; 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::{TransactionBlock, TxReceipt, TxStatus}; + use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, U256}; + use crate::blockchain::blockchain_interface::data_structures::StatusReadFromReceiptCheck; #[test] fn get_transaction_fee_balance_works() { @@ -601,17 +509,17 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_successful_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - Some(U64::from(10)), - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - match tx_receipt.status { - TxStatus::Succeeded(ref block) => { - assert_eq!(block.block_hash, H256::from_low_u64_be(0x1234)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .block_hash(make_block_hash(0x1234)) + .block_number(10.into()) + .build() + .into(); + + match tx_status { + StatusReadFromReceiptCheck::Succeeded(ref block) => { + assert_eq!(block.block_hash, make_block_hash(0x1234)); assert_eq!(block.block_number, U64::from(10)); } _ => panic!("Expected status to be Succeeded"), @@ -620,139 +528,43 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_failed_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(0)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(0)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Failed); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Reverted); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status() { - let tx_receipt: TxReceipt = - create_tx_receipt(None, None, None, H256::from_low_u64_be(0x5678)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status_and_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - None, - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - 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), - })) - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - // 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, - block_number: Option, - transaction_hash: H256, - ) -> TxReceipt { - let receipt = TransactionReceipt { - status, - root: None, - block_hash, - block_number, - cumulative_gas_used: Default::default(), - gas_used: None, - contract_address: None, - transaction_hash, - transaction_index: Default::default(), - logs: vec![], - logs_bloom: Default::default(), - }; - receipt.into() + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } } 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 bb9cde4914..7178d9d906 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,8 +4,9 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; +use std::collections::{HashMap}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; +use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible, StatusReadFromReceiptCheck}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainInterface}; @@ -20,12 +21,15 @@ use actix::Recipient; use ethereum_types::U64; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, PricedQualifiedPayables}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{LowBlockchainIntWeb3, TransactionReceiptResult, TxReceipt, TxStatus}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, RegisterNewPendingPayables}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::LowBlockchainIntWeb3; use crate::blockchain::blockchain_interface::blockchain_interface_web3::utils::{create_blockchain_agent_web3, send_payables_within_batch, BlockchainAgentFutureResult}; - +use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; // TODO We should probably begin to attach these constants to the interfaces more tightly, so that // we aren't baffled by which interface they belong with. I suggest to declare them inside // their inherent impl blocks. They will then need to be preceded by the class name @@ -182,7 +186,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { Box::new( get_gas_price .map_err(BlockchainAgentBuildError::GasPrice) - .and_then(move |gas_price_wei| { + .and_then(move |gas_price_minor| { get_transaction_fee_balance .map_err(move |e| { BlockchainAgentBuildError::TransactionFeeBalance(wallet_address, e) @@ -195,7 +199,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { .and_then(move |masq_token_balance| { let blockchain_agent_future_result = BlockchainAgentFutureResult { - gas_price_wei, + gas_price_minor, transaction_fee_balance, masq_token_balance, }; @@ -213,38 +217,44 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>> - { + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = HashMap, + Error = BlockchainInterfaceError, + >, + > { Box::new( self.lower_interface() - .get_transaction_receipt_in_batch(transaction_hashes.clone()) + .get_transaction_receipt_in_batch(Self::collect_plain_hashes(&tx_hashes)) .map_err(move |e| e) .and_then(move |batch_response| { Ok(batch_response .into_iter() - .zip(transaction_hashes) - .map(|(response, hash)| match response { + .zip(tx_hashes.into_iter()) + .map(|(response, tx_hash)| match response { Ok(result) => { match serde_json::from_value::(result) { Ok(receipt) => { - TransactionReceiptResult::RpcResponse(receipt.into()) + (tx_hash, Ok(StatusReadFromReceiptCheck::from(receipt))) } Err(e) => { if e.to_string().contains("invalid type: null") { - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }) + (tx_hash, Ok(StatusReadFromReceiptCheck::Pending)) } else { - TransactionReceiptResult::LocalError(e.to_string()) + ( + tx_hash, + Err(AppRpcError::Remote( + RemoteError::InvalidResponse(e.to_string()), + )), + ) } } } } - Err(e) => TransactionReceiptResult::LocalError(e.to_string()), + Err(e) => (tx_hash, Err(AppRpcError::from(e))), }) - .collect::>()) + .collect::>()) }), ) } @@ -253,7 +263,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { @@ -274,7 +284,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &web3_batch, consuming_wallet, pending_nonce, - fingerprints_recipient, + new_pending_payables_recipient, affordable_accounts, ) }), @@ -285,7 +295,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub struct HashAndAmount { pub hash: H256, - pub amount: u128, + pub amount_minor: u128, } impl BlockchainInterfaceWeb3 { @@ -431,22 +441,43 @@ impl BlockchainInterfaceWeb3 { Ok(transactions) } } + + fn collect_plain_hashes(hashes_by_table: &[TxHashByTable]) -> Vec { + hashes_by_table + .iter() + .map(|hash_by_table| match hash_by_table { + TxHashByTable::SentPayable(hash) => *hash, + TxHashByTable::FailedPayable(hash) => *hash, + }) + .collect() + } } #[cfg(test)] mod tests { use super::*; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayableWithGasPrice, QualifiedPayablesBeforeGasPriceSelection, + UnpricedQualifiedPayables, + }; + use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; - use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; + use crate::blockchain::blockchain_interface::data_structures::{ + BlockchainTransaction, TxBlock, + }; use crate::blockchain::blockchain_interface::{ BlockchainAgentBuildError, BlockchainInterfaceError, BlockchainInterface, RetrievedBlockchainTransactions, }; - use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; + use crate::blockchain::test_utils::{ + all_chains, make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, + }; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; @@ -462,10 +493,7 @@ mod tests { use std::str::FromStr; use web3::transports::Http; use web3::types::{H256, U256}; - use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayableWithGasPrice}; - use crate::accountant::test_utils::make_payable_account; - use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; #[test] fn constants_are_correct() { @@ -1046,27 +1074,19 @@ mod tests { #[test] fn process_transaction_receipts_works() { let port = find_free_port(); - let tx_hash_1 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0e") - .unwrap(); - let tx_hash_2 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0f") - .unwrap(); - let tx_hash_3 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0a") - .unwrap(); - let tx_hash_4 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0b") - .unwrap(); - let tx_hash_5 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0c") - .unwrap(); - let tx_hash_6 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0d") - .unwrap(); - let tx_hash_vec = vec![ - tx_hash_1, tx_hash_2, tx_hash_3, tx_hash_4, tx_hash_5, tx_hash_6, - ]; + let tx_hash_1 = make_tx_hash(3300); + let tx_hash_2 = make_tx_hash(3401); + let tx_hash_3 = make_tx_hash(3502); + let tx_hash_4 = make_tx_hash(3603); + let tx_hash_5 = make_tx_hash(3704); + let tx_hash_6 = make_tx_hash(3805); + let tx_hbt_1 = TxHashByTable::FailedPayable(tx_hash_1); + let tx_hbt_2 = TxHashByTable::FailedPayable(tx_hash_2); + let tx_hbt_3 = TxHashByTable::SentPayable(tx_hash_3); + let tx_hbt_4 = TxHashByTable::SentPayable(tx_hash_4); + let tx_hbt_5 = TxHashByTable::SentPayable(tx_hash_5); + let tx_hbt_6 = TxHashByTable::SentPayable(tx_hash_6); + let sent_tx_vec = vec![tx_hbt_1, tx_hbt_2, tx_hbt_3, tx_hbt_4, tx_hbt_5, tx_hbt_6]; let block_hash = H256::from_str("6d0abccae617442c26104c2bc63d1bc05e1e002e555aec4ab62a46e826b18f18") .unwrap(); @@ -1108,48 +1128,45 @@ mod tests { let subject = make_blockchain_interface_web3(port); let result = subject - .process_transaction_receipts(tx_hash_vec) + .process_transaction_receipts(sent_tx_vec.clone()) .wait() .unwrap(); - assert_eq!(result[0], TransactionReceiptResult::LocalError("RPC error: Error { code: ServerError(429), message: \"The requests per second (RPS) of your requests are higher than your plan allows.\", data: None }".to_string())); + assert_eq!(result.get(&tx_hbt_1).unwrap(), &Err( + AppRpcError::Remote( + RemoteError::Web3RpcError { + code: 429, + message: + "The requests per second (RPS) of your requests are higher than your plan allows." + .to_string() + } + )) + ); assert_eq!( - result[1], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_2, - status: TxStatus::Pending - }) + result.get(&tx_hbt_2).unwrap(), + &Ok(StatusReadFromReceiptCheck::Pending) ); assert_eq!( - result[2], - TransactionReceiptResult::LocalError( + result.get(&tx_hbt_3).unwrap(), + &Err(AppRpcError::Remote(RemoteError::InvalidResponse( "invalid type: string \"trash\", expected struct Receipt".to_string() - ) + ))) ); assert_eq!( - result[3], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_4, - status: TxStatus::Pending - }) + result.get(&tx_hbt_4).unwrap(), + &Ok(StatusReadFromReceiptCheck::Pending) ); assert_eq!( - result[4], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_5, - status: TxStatus::Failed, - }) + result.get(&tx_hbt_5).unwrap(), + &Ok(StatusReadFromReceiptCheck::Reverted) ); assert_eq!( - result[5], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_6, - status: TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }), - }) - ); + result.get(&tx_hbt_6).unwrap(), + &Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }),) + ) } #[test] @@ -1157,13 +1174,12 @@ mod tests { let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); let subject = make_blockchain_interface_web3(port); - let tx_hash_1 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0e") - .unwrap(); - let tx_hash_2 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0f") - .unwrap(); - let tx_hash_vec = vec![tx_hash_1, tx_hash_2]; + let tx_hash_1 = make_tx_hash(789); + let tx_hash_2 = make_tx_hash(123); + let tx_hash_vec = vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::SentPayable(tx_hash_2), + ]; let error = subject .process_transaction_receipts(tx_hash_vec) @@ -1346,4 +1362,33 @@ mod tests { BlockMarker::Uninitialized ); } + + #[test] + fn collect_plain_hashes_works() { + let hash_sent_tx_1 = make_tx_hash(456); + let hash_sent_tx_2 = make_tx_hash(789); + let hash_sent_tx_3 = make_tx_hash(234); + let hash_failed_tx_1 = make_tx_hash(123); + let hash_failed_tx_2 = make_tx_hash(345); + let inputs = vec![ + TxHashByTable::SentPayable(hash_sent_tx_1), + TxHashByTable::FailedPayable(hash_failed_tx_1), + TxHashByTable::SentPayable(hash_sent_tx_2), + TxHashByTable::SentPayable(hash_sent_tx_3), + TxHashByTable::FailedPayable(hash_failed_tx_2), + ]; + + let result = BlockchainInterfaceWeb3::collect_plain_hashes(&inputs); + + assert_eq!( + result, + vec![ + hash_sent_tx_1, + hash_failed_tx_1, + hash_sent_tx_2, + hash_sent_tx_3, + hash_failed_tx_2 + ] + ); + } } 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 00489febc1..d8e1729f95 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -1,18 +1,21 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. 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::sent_payable_dao::{SentTx, TxStatus}; +use crate::accountant::db_access_objects::utils::{to_unix_timestamp, TxHash}; use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::accountant::PendingPayable; use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ - BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, + BlockchainInterfaceWeb3, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ ProcessedPayableFallible, RpcPayableFailure, }; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use actix::Recipient; @@ -22,66 +25,47 @@ use masq_lib::constants::WALLET_ADDRESS_LENGTH; use masq_lib::logger::Logger; use secp256k1secrets::SecretKey; use serde_json::Value; +use std::collections::HashSet; use std::iter::once; use std::time::SystemTime; use thousands::Separable; use web3::transports::{Batch, Http}; use web3::types::{Bytes, SignedTransaction, TransactionParameters, U256}; -use web3::Error as Web3Error; use web3::Web3; #[derive(Debug)] pub struct BlockchainAgentFutureResult { - pub gas_price_wei: U256, + pub gas_price_minor: U256, pub transaction_fee_balance: U256, pub masq_token_balance: U256, } -pub fn advance_used_nonce(current_nonce: U256) -> U256 { - current_nonce - .checked_add(U256::one()) - .expect("unexpected limits") -} - -fn error_with_hashes( - error: Web3Error, - hashes_and_paid_amounts: Vec, -) -> PayableTransactionError { - let hashes = hashes_and_paid_amounts - .into_iter() - .map(|hash_and_amount| hash_and_amount.hash) - .collect(); - PayableTransactionError::Sending { - msg: error.to_string(), - hashes, - } -} +// TODO using these three vectors like this is dangerous; who guarantees that all three have their +// items sorted in the right order? pub fn merged_output_data( responses: Vec>, - hashes_and_paid_amounts: Vec, + sent_tx_hashes: Vec, accounts: Vec, ) -> Vec { let iterator_with_all_data = responses .into_iter() - .zip(hashes_and_paid_amounts.into_iter()) + .zip(sent_tx_hashes.into_iter()) .zip(accounts.iter()); iterator_with_all_data - .map( - |((rpc_result, hash_and_amount), account)| match rpc_result { - Ok(_rpc_result) => { - // TODO: GH-547: This rpc_result should be validated - ProcessedPayableFallible::Correct(PendingPayable { - recipient_wallet: account.wallet.clone(), - hash: hash_and_amount.hash, - }) - } - Err(rpc_error) => ProcessedPayableFallible::Failed(RpcPayableFailure { - rpc_error, + .map(|((rpc_result, hash), account)| match rpc_result { + Ok(_rpc_result) => { + // TODO: GH-547: This rpc_result should be validated + ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: account.wallet.clone(), - hash: hash_and_amount.hash, - }), - }, - ) + hash, + }) + } + Err(rpc_error) => ProcessedPayableFallible::Failed(RpcPayableFailure { + rpc_error, + recipient_wallet: account.wallet.clone(), + hash, + }), + }) .collect() } @@ -143,11 +127,11 @@ pub fn transmission_log( introduction.chain(body).collect() } -pub fn sign_transaction_data(amount: u128, recipient_wallet: Wallet) -> [u8; 68] { +pub fn sign_transaction_data(amount_minor: u128, recipient_wallet: Wallet) -> [u8; 68] { let mut data = [0u8; 4 + 32 + 32]; data[0..4].copy_from_slice(&TRANSFER_METHOD_ID); data[16..36].copy_from_slice(&recipient_wallet.address().0[..]); - U256::from(amount).to_big_endian(&mut data[36..68]); + U256::from(amount_minor).to_big_endian(&mut data[36..68]); data } @@ -164,11 +148,11 @@ pub fn sign_transaction( web3_batch: &Web3>, recipient_wallet: Wallet, consuming_wallet: Wallet, - amount: u128, + amount_minor: u128, nonce: U256, gas_price_in_wei: u128, ) -> SignedTransaction { - let data = sign_transaction_data(amount, recipient_wallet); + let data = sign_transaction_data(amount_minor, recipient_wallet); let gas_limit = gas_limit(data, chain); // Warning: If you set gas_price or nonce to None in transaction_parameters, sign_transaction // will start making RPC calls which we don't want (Do it at your own risk). @@ -215,7 +199,7 @@ pub fn sign_and_append_payment( consuming_wallet: Wallet, nonce: U256, gas_price_in_wei: u128, -) -> HashAndAmount { +) -> TxHash { let signed_tx = sign_transaction( chain, web3_batch, @@ -227,10 +211,7 @@ pub fn sign_and_append_payment( ); append_signed_transaction_to_batch(web3_batch, signed_tx.raw_transaction); - HashAndAmount { - hash: signed_tx.transaction_hash, - amount: recipient.balance_wei, - } + signed_tx.transaction_hash } pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_transaction: Bytes) { @@ -239,37 +220,51 @@ pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_tr } pub fn sign_and_append_multiple_payments( + now: SystemTime, logger: &Logger, chain: Chain, web3_batch: &Web3>, consuming_wallet: Wallet, - mut pending_nonce: U256, + initial_pending_nonce: U256, accounts: &PricedQualifiedPayables, -) -> Vec { - let mut hash_and_amount_list = vec![]; - accounts.payables.iter().for_each(|payable_pack| { - let payable = &payable_pack.payable; - debug!( - logger, - "Preparing payable future of {} wei to {} with nonce {}", - payable.balance_wei.separate_with_commas(), - payable.wallet, - pending_nonce - ); - - let hash_and_amount = sign_and_append_payment( - chain, - web3_batch, - payable, - consuming_wallet.clone(), - pending_nonce, - payable_pack.gas_price_minor, - ); - - pending_nonce = advance_used_nonce(pending_nonce); - hash_and_amount_list.push(hash_and_amount); - }); - hash_and_amount_list +) -> Vec { + let unix_mow = to_unix_timestamp(now); + accounts + .payables + .iter() + .enumerate() + .map(|(idx, payable_pack)| { + let current_pending_nonce = initial_pending_nonce + U256::from(idx); + let payable = &payable_pack.payable; + + debug!( + logger, + "Preparing tx of {} wei to {} with nonce {}", + payable.balance_wei.separate_with_commas(), + payable.wallet, + current_pending_nonce + ); + + let hash = sign_and_append_payment( + chain, + web3_batch, + payable, + consuming_wallet.clone(), + current_pending_nonce, + payable_pack.gas_price_minor, + ); + + SentTx { + hash, + receiver_address: payable.wallet.address(), + amount_minor: payable.balance_wei, + timestamp: unix_mow, + gas_price_minor: payable_pack.gas_price_minor, + nonce: current_pending_nonce.as_u64(), + status: TxStatus::Pending(ValidationStatus::Waiting), + } + }) + .collect() } #[allow(clippy::too_many_arguments)] @@ -279,7 +274,7 @@ pub fn send_payables_within_batch( web3_batch: &Web3>, consuming_wallet: Wallet, pending_nonce: U256, - new_fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError> + 'static> { @@ -291,7 +286,10 @@ pub fn send_payables_within_batch( chain.rec().num_chain_id, ); - let hashes_and_paid_amounts = sign_and_append_multiple_payments( + let common_timestamp = SystemTime::now(); + + let prepared_sent_txs_records = sign_and_append_multiple_payments( + common_timestamp, logger, chain, web3_batch, @@ -300,15 +298,16 @@ pub fn send_payables_within_batch( &accounts, ); - let timestamp = SystemTime::now(); - let hashes_and_paid_amounts_error = hashes_and_paid_amounts.clone(); - let hashes_and_paid_amounts_ok = hashes_and_paid_amounts.clone(); + let sent_txs_hashes: Vec = prepared_sent_txs_records + .iter() + .map(|sent_tx| sent_tx.hash) + .collect(); + let planned_sent_txs_hashes = HashSet::from_iter(sent_txs_hashes.clone().into_iter()); - new_fingerprints_recipient - .try_send(PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: hashes_and_paid_amounts, - }) + let new_pending_payables_message = RegisterNewPendingPayables::new(prepared_sent_txs_records); + + new_pending_payables_recipient + .try_send(new_pending_payables_message) .expect("Accountant is dead"); info!( @@ -321,11 +320,14 @@ pub fn send_payables_within_batch( web3_batch .transport() .submit_batch() - .map_err(|e| error_with_hashes(e, hashes_and_paid_amounts_error)) + .map_err(move |e| PayableTransactionError::Sending { + msg: e.to_string(), + hashes: planned_sent_txs_hashes, + }) .and_then(move |batch_response| { Ok(merged_output_data( batch_response, - hashes_and_paid_amounts_ok, + sent_txs_hashes, accounts.into(), )) }), @@ -346,7 +348,7 @@ pub fn create_blockchain_agent_web3( masq_token_balance_in_minor_units, ); Box::new(BlockchainAgentWeb3::new( - blockchain_agent_future_result.gas_price_wei.as_u128(), + blockchain_agent_future_result.gas_price_minor.as_u128(), gas_limit_const_part, wallet, cons_wallet_balances, @@ -431,13 +433,8 @@ mod tests { let mut batch_result = web3_batch.eth().transport().submit_batch().wait().unwrap(); assert_eq!( result, - HashAndAmount { - hash: H256::from_str( - "94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2" - ) - .unwrap(), - amount: account.balance_wei - } + H256::from_str("94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2") + .unwrap() ); assert_eq!( batch_result.pop().unwrap().unwrap(), @@ -448,9 +445,10 @@ mod tests { } #[test] - fn send_and_append_multiple_payments_works() { + fn sign_and_append_multiple_payments_works() { + let now = SystemTime::now(); let port = find_free_port(); - let logger = Logger::new("send_and_append_multiple_payments_works"); + let logger = Logger::new("sign_and_append_multiple_payments_works"); let (_event_loop_handle, transport) = Http::with_max_parallel( &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), REQUESTS_IN_PARALLEL, @@ -463,11 +461,12 @@ mod tests { let account_1 = make_payable_account(1); let account_2 = make_payable_account(2); let accounts = make_priced_qualified_payables(vec![ - (account_1, 111_111_111), - (account_2, 222_222_222), + (account_1.clone(), 111_234_111), + (account_2.clone(), 222_432_222), ]); - let result = sign_and_append_multiple_payments( + let mut result = sign_and_append_multiple_payments( + now, &logger, chain, &web3_batch, @@ -476,25 +475,47 @@ mod tests { &accounts, ); + let first_actual_sent_tx = result.remove(0); + let second_actual_sent_tx = result.remove(0); + assert_prepared_sent_tx_record( + first_actual_sent_tx, + now, + account_1, + "0x6b85347ff8edf8b126dffb85e7517ac7af1b23eace4ed5ad099d783fd039b1ee", + 1, + 111_234_111, + ); + assert_prepared_sent_tx_record( + second_actual_sent_tx, + now, + account_2, + "0x3dac025697b994920c9cd72ab0d2df82a7caaa24d44e78b7c04e223299819d54", + 2, + 222_432_222, + ); + } + + fn assert_prepared_sent_tx_record( + actual_sent_tx: SentTx, + now: SystemTime, + account_1: PayableAccount, + expected_tx_hash_including_prefix: &str, + expected_nonce: u64, + expected_gas_price_minor: u128, + ) { + assert_eq!(actual_sent_tx.receiver_address, account_1.wallet.address()); assert_eq!( - result, - vec![ - HashAndAmount { - hash: H256::from_str( - "374b7d023f4ac7d99e612d82beda494b0747116e9b9dc975b33b865f331ee934" - ) - .unwrap(), - amount: 1000000000 - }, - HashAndAmount { - hash: H256::from_str( - "5708afd876bc2573f9db984ec6d0e7f8ef222dd9f115643c9b9056d8bef8bbd9" - ) - .unwrap(), - amount: 2000000000 - } - ] + actual_sent_tx.hash, + H256::from_str(&expected_tx_hash_including_prefix[2..]).unwrap() + ); + assert_eq!(actual_sent_tx.amount_minor, account_1.balance_wei); + assert_eq!(actual_sent_tx.gas_price_minor, expected_gas_price_minor); + assert_eq!(actual_sent_tx.nonce, expected_nonce); + assert_eq!( + actual_sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting) ); + assert_eq!(actual_sent_tx.timestamp, to_unix_timestamp(now)); } #[test] @@ -626,16 +647,7 @@ mod tests { pending_payable_opt: None, }, ]; - let fingerprint_inputs = vec![ - HashAndAmount { - hash: make_tx_hash(444), - amount: 2_345_678, - }, - HashAndAmount { - hash: make_tx_hash(333), - amount: 6_543_210, - }, - ]; + let tx_hashes = vec![make_tx_hash(444), make_tx_hash(333)]; let responses = vec![ Ok(Value::String(String::from("blah"))), Err(web3::Error::Rpc(Error { @@ -645,7 +657,7 @@ mod tests { })), ]; - let result = merged_output_data(responses, fingerprint_inputs, accounts.to_vec()); + let result = merged_output_data(responses, tx_hashes, accounts.to_vec()); assert_eq!( result, @@ -679,13 +691,13 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let pending_nonce: U256 = 1.into(); + let pending_nonce: U256 = 3.into(); let web3_batch = Web3::new(Batch::new(transport)); let (accountant, _, accountant_recording) = make_recorder(); let logger = Logger::new(test_name); let chain = DEFAULT_CHAIN; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let new_fingerprints_recipient = accountant.start().recipient(); + let new_pending_payables_recipient = accountant.start().recipient(); let system = System::new(test_name); let timestamp_before = SystemTime::now(); @@ -695,7 +707,7 @@ mod tests { &web3_batch, consuming_wallet.clone(), pending_nonce, - new_fingerprints_recipient, + new_pending_payables_recipient, accounts.clone(), ) .wait(); @@ -703,12 +715,30 @@ mod tests { System::current().stop(); system.run(); let timestamp_after = SystemTime::now(); + assert_eq!(result, expected_result); let accountant_recording_result = accountant_recording.lock().unwrap(); - let ppfs_message = - accountant_recording_result.get_record::(0); + let rnpp_message = accountant_recording_result.get_record::(0); assert_eq!(accountant_recording_result.len(), 1); - assert!(timestamp_before <= ppfs_message.batch_wide_timestamp); - assert!(timestamp_after >= ppfs_message.batch_wide_timestamp); + let nonces = 3_64..(accounts.payables.len() as u64 + 3); + rnpp_message + .new_sent_txs + .iter() + .zip(accounts.payables.iter()) + .zip(nonces) + .for_each(|((tx, payable_account), nonce)| { + assert_eq!( + tx.receiver_address, + payable_account.payable.wallet.address() + ); + assert_eq!(tx.amount_minor, payable_account.payable.balance_wei); + assert_eq!(tx.gas_price_minor, payable_account.gas_price_minor); + assert_eq!(tx.nonce, nonce); + assert_eq!(tx.status, TxStatus::Pending(ValidationStatus::Waiting)); + assert!( + timestamp_before <= from_unix_timestamp(tx.timestamp) + && from_unix_timestamp(tx.timestamp) <= timestamp_after + ); + }); let tlh = TestLogHandler::new(); tlh.exists_log_containing( &format!("DEBUG: {test_name}: Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", @@ -721,7 +751,6 @@ mod tests { "INFO: {test_name}: {}", transmission_log(chain, &accounts, pending_nonce) )); - assert_eq!(result, expected_result); } #[test] @@ -740,14 +769,14 @@ mod tests { Correct(PendingPayable { recipient_wallet: account_1.wallet.clone(), hash: H256::from_str( - "6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2", + "0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3", ) .unwrap(), }), Correct(PendingPayable { recipient_wallet: account_2.wallet.clone(), hash: H256::from_str( - "b67a61b29c0c48d8b63a64fda73b3247e8e2af68082c710325675d4911e113d4", + "6b485dbd4d769b5a19fa57058d612fad99cdd78769db6b3be129f981c42657ac", ) .unwrap(), }), @@ -775,9 +804,9 @@ mod tests { let port = find_free_port(); let expected_result = Err(Sending { msg: format!("Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", os_code, os_msg).to_string(), - hashes: vec![ - H256::from_str("ec7ac48060b75889f949f5e8d301b386198218e60e2635c95cb6b0934a0887ea").unwrap(), - H256::from_str("c2d5059db0ec2fbf15f83d9157eeb0d793d6242de5e73a607935fb5660e7e925").unwrap() + hashes: hashset![ + H256::from_str("5bbe90ad19d86b69ee49879cec4b3f8b769223e6a872aae0be88773de2fc3beb").unwrap(), + H256::from_str("a1b609dbe9cc77ad586dbe4e5c1079d6ad76020a353c960928d6daeafd43f366").unwrap() ], }); @@ -818,7 +847,7 @@ mod tests { data: None, }), recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), + hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -827,7 +856,7 @@ mod tests { data: None, }), recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), + hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), }), ]); @@ -861,7 +890,7 @@ mod tests { let expected_result = Ok(vec![ Correct(PendingPayable { recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), + hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -870,7 +899,7 @@ mod tests { data: None, }), recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), + hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), }), ]); @@ -885,15 +914,6 @@ mod tests { ); } - #[test] - fn advance_used_nonce_works() { - let initial_nonce = U256::from(55); - - let result = advance_used_nonce(initial_nonce); - - assert_eq!(result, U256::from(56)) - } - #[test] #[should_panic( expected = "Consuming wallet doesn't contain a secret key: Signature(\"Cannot sign with non-keypair wallet: Address(0x000000000000000000006261645f77616c6c6574).\")" diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index ffdfa4c209..1d01532ec3 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -1,11 +1,13 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::comma_joined_stringifiable; -use itertools::Either; +use crate::accountant::db_access_objects::utils::TxHash; +use itertools::{Either, Itertools}; +use std::collections::HashSet; use std::fmt; use std::fmt::{Display, Formatter}; use variant_count::VariantCount; -use web3::types::{Address, H256}; +use web3::types::Address; const BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED: &str = "Uninitialized blockchain interface. To avoid \ being delinquency-banned, you should restart the Node with a value for blockchain-service-url"; @@ -39,7 +41,10 @@ pub enum PayableTransactionError { TransactionID(BlockchainInterfaceError), UnusableWallet(String), Signing(String), - Sending { msg: String, hashes: Vec }, + Sending { + msg: String, + hashes: HashSet, + }, UninitializedInterface, } @@ -61,12 +66,15 @@ impl Display for PayableTransactionError { msg ), Self::Signing(msg) => write!(f, "Signing phase: \"{}\"", msg), - Self::Sending { msg, hashes } => write!( - f, - "Sending phase: \"{}\". Signed and hashed transactions: {}", - msg, - comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) - ), + Self::Sending { msg, hashes } => { + let hashes = hashes.iter().map(|hash| *hash).sorted().collect_vec(); + write!( + f, + "Sending phase: \"{}\". Signed and hashed txs: {}", + msg, + comma_joined_stringifiable(&hashes, |hash| format!("{:?}", hash)) + ) + } Self::UninitializedInterface => { write!(f, "{}", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED) } @@ -180,7 +188,7 @@ mod tests { ), PayableTransactionError::Sending { msg: "Sending to cosmos belongs elsewhere".to_string(), - hashes: vec![make_tx_hash(0x6f), make_tx_hash(0xde)], + hashes: hashset![make_tx_hash(0x6f), make_tx_hash(0xde)], }, PayableTransactionError::UninitializedInterface, ]; @@ -202,7 +210,7 @@ mod tests { LEDGER wallet, stupid.\"", "Signing phase: \"You cannot sign with just three crosses here, clever boy\"", "Sending phase: \"Sending to cosmos belongs elsewhere\". Signed and hashed \ - transactions: 0x000000000000000000000000000000000000000000000000000000000000006f, \ + txs: 0x000000000000000000000000000000000000000000000000000000000000006f, \ 0x00000000000000000000000000000000000000000000000000000000000000de", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED ]) diff --git a/node/src/blockchain/blockchain_interface/data_structures/mod.rs b/node/src/blockchain/blockchain_interface/data_structures/mod.rs index a33a1f889a..1e8c918de1 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -2,12 +2,16 @@ pub mod errors; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::PendingPayable; use crate::blockchain::blockchain_bridge::BlockMarker; use crate::sub_lib::wallet::Wallet; +use ethereum_types::U64; +use serde_derive::{Deserialize, Serialize}; use std::fmt; -use std::fmt::Formatter; -use web3::types::H256; +use std::fmt::{Display, Formatter}; +use web3::types::{TransactionReceipt, H256}; use web3::Error; #[derive(Clone, Debug, Eq, PartialEq)] @@ -37,7 +41,7 @@ pub struct RetrievedBlockchainTransactions { pub struct RpcPayableFailure { pub rpc_error: Error, pub recipient_wallet: Wallet, - pub hash: H256, + pub hash: TxHash, } #[derive(Debug, PartialEq, Clone)] @@ -45,3 +49,90 @@ pub enum ProcessedPayableFallible { Correct(PendingPayable), Failed(RpcPayableFailure), } + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RetrievedTxStatus { + pub tx_hash: TxHashByTable, + pub status: StatusReadFromReceiptCheck, +} + +impl RetrievedTxStatus { + pub fn new(tx_hash: TxHashByTable, status: StatusReadFromReceiptCheck) -> Self { + Self { tx_hash, status } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum StatusReadFromReceiptCheck { + Reverted, + Succeeded(TxBlock), + Pending, +} + +impl Display for StatusReadFromReceiptCheck { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StatusReadFromReceiptCheck::Reverted => { + write!(f, "Reverted") + } + StatusReadFromReceiptCheck::Succeeded(block) => { + write!( + f, + "Succeeded({},{:?})", + block.block_number, block.block_hash + ) + } + StatusReadFromReceiptCheck::Pending => write!(f, "Pending"), + } + } +} + +impl From for StatusReadFromReceiptCheck { + fn from(receipt: TransactionReceipt) -> Self { + match (receipt.status, receipt.block_hash, receipt.block_number) { + (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { + StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }) + } + (Some(status), _, _) if status == U64::from(0) => StatusReadFromReceiptCheck::Reverted, + _ => StatusReadFromReceiptCheck::Pending, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Ord, PartialOrd, Serialize, Deserialize)] +pub struct TxBlock { + pub block_hash: H256, + pub block_number: U64, +} + +#[cfg(test)] +mod tests { + use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, + }; + use ethereum_types::{H256, U64}; + + #[test] + fn tx_status_display_works() { + // Test Failed + assert_eq!(StatusReadFromReceiptCheck::Reverted.to_string(), "Reverted"); + + // Test Pending + assert_eq!(StatusReadFromReceiptCheck::Pending.to_string(), "Pending"); + + // Test Succeeded + let block_number = U64::from(12345); + let block_hash = H256::from_low_u64_be(0xabcdef); + let succeeded = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }); + assert_eq!( + succeeded.to_string(), + format!("Succeeded({},0x{:x})", block_number, block_hash) + ); + } +} diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index eb736b2a38..09961776e3 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -4,20 +4,27 @@ pub mod blockchain_interface_web3; pub mod data_structures; pub mod lower_level_interface; -use actix::Recipient; -use ethereum_types::H256; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; +use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_bridge::{ + BlockMarker, BlockScanRange, RegisterNewPendingPayables, +}; +use crate::blockchain::blockchain_interface::data_structures::errors::{ + BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError, +}; +use crate::blockchain::blockchain_interface::data_structures::{ + ProcessedPayableFallible, RetrievedBlockchainTransactions, +}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::sub_lib::wallet::Wallet; +use actix::Recipient; use futures::Future; use masq_lib::blockchains::chains::Chain; -use web3::types::Address; use masq_lib::logger::Logger; -use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; +use std::collections::HashMap; +use web3::types::Address; pub trait BlockchainInterface { fn contract_address(&self) -> Address; @@ -40,14 +47,19 @@ pub trait BlockchainInterface { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>>; + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = HashMap, + Error = BlockchainInterfaceError, + >, + >; fn submit_payables_in_batch( &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>>; diff --git a/node/src/blockchain/errors/internal_errors.rs b/node/src/blockchain/errors/internal_errors.rs index fb6a4bf631..9982d06679 100644 --- a/node/src/blockchain/errors/internal_errors.rs +++ b/node/src/blockchain/errors/internal_errors.rs @@ -7,7 +7,7 @@ pub enum InternalError { PendingTooLongNotReplaced, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum InternalErrorKind { PendingTooLongNotReplaced, } diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index 5cd1a6f3ce..e406a96b11 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -14,7 +14,7 @@ pub enum BlockchainError { Internal(InternalError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum BlockchainErrorKind { AppRpc(AppRpcErrorKind), Internal(InternalErrorKind), diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index e717fbf25e..bf78fa53b0 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -53,13 +53,13 @@ impl From for AppRpcError { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AppRpcErrorKind { Local(LocalErrorKind), Remote(RemoteErrorKind), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum LocalErrorKind { Decoder, Internal, @@ -68,7 +68,7 @@ pub enum LocalErrorKind { Transport, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum RemoteErrorKind { InvalidResponse, Unreachable, diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index f3b354931d..2ce57f2612 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -229,6 +229,55 @@ pub fn transport_error_message() -> String { } } +pub struct TransactionReceiptBuilder { + status_opt: Option, + block_hash_opt: Option, + block_number_opt: Option, + transaction_hash: H256, +} + +impl TransactionReceiptBuilder { + pub fn new(transaction_hash: H256) -> Self { + Self { + status_opt: None, + block_hash_opt: None, + block_number_opt: None, + transaction_hash, + } + } + + pub fn status(mut self, status: U64) -> Self { + self.status_opt = Some(status); + self + } + + pub fn block_hash(mut self, block_hash: H256) -> Self { + self.block_hash_opt = Some(block_hash); + self + } + + pub fn block_number(mut self, block_number: U64) -> Self { + self.block_number_opt = Some(block_number); + self + } + + pub fn build(self) -> TransactionReceipt { + TransactionReceipt { + status: self.status_opt, + root: None, + block_hash: self.block_hash_opt, + block_number: self.block_number_opt, + cumulative_gas_used: Default::default(), + gas_used: None, + contract_address: None, + transaction_hash: self.transaction_hash, + transaction_index: Default::default(), + logs: vec![], + logs_bloom: Default::default(), + } + } +} + #[derive(Default)] pub struct ValidationFailureClockMock { now_results: RefCell>, 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 4b5bbb50a5..204ab49a56 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 @@ -150,7 +150,7 @@ mod tests { conn: &dyn ConnectionWrapper, transaction_hash_opt: Option, wallet: &Wallet, - amount: i64, + amount_minor: i64, timestamp: SystemTime, ) { let hash_str = transaction_hash_opt @@ -159,7 +159,7 @@ mod tests { let mut stm = conn.prepare("insert into payable (wallet_address, balance, last_paid_timestamp, pending_payment_transaction) values (?,?,?,?)").unwrap(); let params: &[&dyn ToSql] = &[ &wallet, - &amount, + &amount_minor, &to_unix_timestamp(timestamp), if !hash_str.is_empty() { &hash_str diff --git a/node/src/database/rusqlite_wrappers.rs b/node/src/database/rusqlite_wrappers.rs index ec867482f7..2177a250b9 100644 --- a/node/src/database/rusqlite_wrappers.rs +++ b/node/src/database/rusqlite_wrappers.rs @@ -5,15 +5,15 @@ use crate::masq_lib::utils::ExpectValue; use rusqlite::{Connection, Error, Statement, ToSql, Transaction}; use std::fmt::Debug; -// We were challenged multiple times to device mocks for testing stubborn, hard to tame, data +// We were challenged multiple times to devise mocks for testing stubborn, hard to tame, data // structures from the 'rusqlite' library. After all, we've adopted two of them, the Connection, // that came first, and the Transaction to come much later. Of these, only the former complies // with the standard policy we follow for mock designs. // // The delay until the second one became a thing, even though we would've been glad having it -// on hand much earlier, was caused by vacuum of ideas on how we could create a mock of these +// on hand much earlier, was caused by a vacuum of ideas on how we could create a mock of these // parameters and have it accepted by the compiler. Passing a lot of time, we came up with a hybrid, -// at least. That said, it has costed us a considerably high price of giving up on simplicity. +// at least. That said, it has cost us a considerably high price of giving up on simplicity. // // The firmest blocker of the design has always rooted in a relationship of serialized lifetimes, // affecting each other, that has been so hard to maintain right. Yet the choices made @@ -74,12 +74,12 @@ impl ConnectionWrapperReal { } } -// Whole point of this outer wrapper, that is common to both the real and mock transactions, is to +// The whole point of this outer wrapper that is common to both the real and mock transactions is to // make a chance to deconstruct all components of a transaction in place. It plays a crucial role -// during the final commit. Note that an usual mock based on the direct use of a trait object +// during the final commit. Note that a usual mock based on the direct use of a trait object // cannot be consumed by any of its methods because of the Rust rules for trait objects. They say // clearly that we can access it via '&self', '&mut self' but not 'self'. However, to have a thing -// consume itself we need to be provided with the full ownership. +// consume itself, we need to be provided with the full ownership. // // Leaving remains of an already committed transaction around would expose us to a risk. Let's // imagine somebody trying to make use of it the second time, while the inner element providing diff --git a/node/src/database/test_utils/transaction_wrapper_mock.rs b/node/src/database/test_utils/transaction_wrapper_mock.rs index d0577c72f6..5b9a717e9e 100644 --- a/node/src/database/test_utils/transaction_wrapper_mock.rs +++ b/node/src/database/test_utils/transaction_wrapper_mock.rs @@ -137,7 +137,7 @@ impl TransactionInnerWrapper for TransactionInnerWrapperMock { // is to be formed. // With that said, we're relieved to have at least one working solution now. Speaking of the 'prepare' -// method, an error would be hardly needed because the production code simply unwraps the results by +// method, an error would hardly be needed because the production code simply unwraps the results by // using 'expect'. That is a function excluded from the requirement of writing tests for. // The 'Statement' produced by this method must be better understood. The 'prepare' method has @@ -199,12 +199,12 @@ impl SetupForProdCodeAndAlteredStmts { // necessary base. If the continuity is broken the later statement might not work. If // we record some changes on the transaction, other changes tried to be done from // a different connection might meet a different state of the database and thwart the - // efforts. (This behaviour probably depends on the global setup of the db). + // efforts. (This behavior probably depends on the global setup of the db). // // // Also imagine a 'Statement' that wouldn't cause an error whereupon any potential // rollback of this txn should best drag off both the prod code and altered statements - // all together, disappearing. If we did not use this txn some of the changes would stay. + // all together, disappearing. If we did not use this txn some changes would stay. { self.txn_bearing_prod_code_stmts_opt .as_ref() diff --git a/node/src/stream_handler_pool.rs b/node/src/stream_handler_pool.rs index 470f0c44f6..5772e9cf8c 100644 --- a/node/src/stream_handler_pool.rs +++ b/node/src/stream_handler_pool.rs @@ -1760,7 +1760,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); @@ -1927,7 +1927,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index f1f174e6e4..e8d3477e6b 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -1,15 +1,15 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; +use crate::accountant::db_access_objects::failed_payable_dao::FailedPayableDaoFactory; 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::db_access_objects::sent_payable_dao::SentPayableDaoFactory; use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::{ - checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, - SentPayables, + checked_conversion, Accountant, ReceivedPayments, ScanError, SentPayables, TxReceiptsMessage, }; use crate::actor_system_factory::SubsFactory; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::db_config::config_dao::ConfigDaoFactory; use crate::sub_lib::neighborhood::ConfigChangeMsg; use crate::sub_lib::peer_actors::{BindMessage, StartMessage}; @@ -71,7 +71,8 @@ impl PaymentThresholds { pub struct DaoFactories { pub payable_dao_factory: Box, - pub pending_payable_dao_factory: Box, + pub sent_payable_dao_factory: Box, + pub failed_payable_dao_factory: Box, pub receivable_dao_factory: Box, pub banned_dao_factory: Box, pub config_dao_factory: Box, @@ -100,8 +101,8 @@ pub struct AccountantSubs { pub report_services_consumed: Recipient, pub report_payable_payments_setup: Recipient, pub report_inbound_payments: Recipient, - pub init_pending_payable_fingerprints: Recipient, - pub report_transaction_receipts: Recipient, + pub register_new_pending_payables: Recipient, + pub report_transaction_status: Recipient, pub report_sent_payments: Recipient, pub scan_errors: Recipient, pub ui_message_sub: Recipient, diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 588eb87e68..546149ae66 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -570,6 +570,18 @@ pub mod unshared_test_utils { pub assertions: Box, } + pub fn capture_digits_with_separators_from_str( + surveyed_str: &str, + length_between_separators: usize, + separator: char, + ) -> Vec { + let regex = + format!("(\\d{{1,{length_between_separators}}}(?:{separator}\\d{{{length_between_separators}}})+)"); + let re = regex::Regex::new(®ex).unwrap(); + let captures = re.captures_iter(surveyed_str); + captures.map(|capture| capture[1].to_string()).collect() + } + pub fn assert_on_initialization_with_panic_on_migration(data_dir: &Path, act: &A) where A: Fn(&Path) + ?Sized, @@ -934,8 +946,7 @@ pub mod unshared_test_utils { ) -> Box { if self.panic_on_schedule_attempt { panic!( - "Message scheduling request for {:?} and interval {}ms, thought not \ - expected", + "Message scheduling request for {:?} and interval {}ms, thought not expected", msg, interval.as_millis() ); diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 6633ee9485..ed35378c21 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -7,8 +7,8 @@ use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::accountant::{ReportTransactionReceipts, ScanForPendingPayables, ScanForRetryPayables}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::accountant::{ScanForPendingPayables, ScanForRetryPayables, TxReceiptsMessage}; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::daemon::crash_notification::CrashNotification; use crate::daemon::DaemonBindMessage; @@ -153,7 +153,7 @@ recorder_message_handler_t_m_p!(NodeFromUiMessage); recorder_message_handler_t_m_p!(NodeToUiMessage); recorder_message_handler_t_m_p!(NoLookupIncipientCoresPackage); recorder_message_handler_t_p!(OutboundPaymentsInstructions); -recorder_message_handler_t_m_p!(PendingPayableFingerprintSeeds); +recorder_message_handler_t_m_p!(RegisterNewPendingPayables); recorder_message_handler_t_m_p!(PoolBindMessage); recorder_message_handler_t_m_p!(QualifiedPayablesMessage); recorder_message_handler_t_m_p!(ReceivedPayments); @@ -162,7 +162,7 @@ recorder_message_handler_t_m_p!(RemoveStreamMsg); recorder_message_handler_t_m_p!(ReportExitServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportRoutingServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportServicesConsumedMessage); -recorder_message_handler_t_m_p!(ReportTransactionReceipts); +recorder_message_handler_t_m_p!(TxReceiptsMessage); recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); @@ -529,8 +529,8 @@ pub fn make_accountant_subs_from_recorder(addr: &Addr) -> AccountantSu report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), - init_pending_payable_fingerprints: recipient!(addr, PendingPayableFingerprintSeeds), - report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), + register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), + report_transaction_status: recipient!(addr, TxReceiptsMessage), report_sent_payments: recipient!(addr, SentPayables), scan_errors: recipient!(addr, ScanError), ui_message_sub: recipient!(addr, NodeFromUiMessage),