diff --git a/masq_lib/src/utils.rs b/masq_lib/src/utils.rs index 9355d0624..ad4197aad 100644 --- a/masq_lib/src/utils.rs +++ b/masq_lib/src/utils.rs @@ -520,10 +520,30 @@ macro_rules! hashset { }; } +#[macro_export(local_inner_macros)] +macro_rules! btreeset { + () => { + ::std::collections::BTreeSet::new() + }; + ($($val:expr,)+) => { + btreeset!($($val),+) + }; + ($($value:expr),+) => { + { + let mut _bts = ::std::collections::BTreeSet::new(); + $( + let _ = _bts.insert($value); + )* + _bts + } + }; +} + #[cfg(test)] mod tests { use super::*; use itertools::Itertools; + use std::collections::BTreeSet; use std::collections::{BTreeMap, HashMap, HashSet}; use std::env::current_dir; use std::fmt::Write; @@ -956,4 +976,45 @@ mod tests { assert_eq!(hashset_of_string, expected_hashset_of_string); assert_eq!(hashset_with_duplicate, expected_hashset_with_duplicate); } + + #[test] + fn btreeset_macro_works() { + let empty_btreeset: BTreeSet = btreeset!(); + let btreeset_with_one_element = btreeset!(2); + let btreeset_with_multiple_elements = btreeset!(2, 20, 42); + let btreeset_with_trailing_comma = btreeset!(2, 20,); + let btreeset_of_string = btreeset!("val_a", "val_b"); + let btreeset_with_duplicate = btreeset!(2, 2); + + let expected_empty_btreeset: BTreeSet = BTreeSet::new(); + let mut expected_btreeset_with_one_element = BTreeSet::new(); + expected_btreeset_with_one_element.insert(2); + let mut expected_btreeset_with_multiple_elements = BTreeSet::new(); + expected_btreeset_with_multiple_elements.insert(2); + expected_btreeset_with_multiple_elements.insert(20); + expected_btreeset_with_multiple_elements.insert(42); + let mut expected_btreeset_with_trailing_comma = BTreeSet::new(); + expected_btreeset_with_trailing_comma.insert(2); + expected_btreeset_with_trailing_comma.insert(20); + let mut expected_btreeset_of_string = BTreeSet::new(); + expected_btreeset_of_string.insert("val_a"); + expected_btreeset_of_string.insert("val_b"); + let mut expected_btreeset_with_duplicate = BTreeSet::new(); + expected_btreeset_with_duplicate.insert(2); + assert_eq!(empty_btreeset, expected_empty_btreeset); + assert_eq!( + btreeset_with_one_element, + expected_btreeset_with_one_element + ); + assert_eq!( + btreeset_with_multiple_elements, + expected_btreeset_with_multiple_elements + ); + assert_eq!( + btreeset_with_trailing_comma, + expected_btreeset_with_trailing_comma + ); + assert_eq!(btreeset_of_string, expected_btreeset_of_string); + assert_eq!(btreeset_with_duplicate, expected_btreeset_with_duplicate); + } } diff --git a/multinode_integration_tests/tests/bookkeeping_test.rs b/multinode_integration_tests/tests/bookkeeping_test.rs index 6c7552eae..ea5c8ae90 100644 --- a/multinode_integration_tests/tests/bookkeeping_test.rs +++ b/multinode_integration_tests/tests/bookkeeping_test.rs @@ -40,7 +40,7 @@ fn provided_and_consumed_services_are_recorded_in_databases() { ); // get all payables from originating node - let payables = non_pending_payables(&originating_node); + let payables = retrieve_payables(&originating_node); // Waiting until the serving nodes have finished accruing their receivables thread::sleep(Duration::from_secs(10)); @@ -79,9 +79,9 @@ fn provided_and_consumed_services_are_recorded_in_databases() { }); } -fn non_pending_payables(node: &MASQRealNode) -> Vec { +fn retrieve_payables(node: &MASQRealNode) -> Vec { let payable_dao = payable_dao(node.name()); - payable_dao.non_pending_payables() + payable_dao.retrieve_payables(None) } fn receivables(node: &MASQRealNode) -> Vec { diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index 5d682fea4..9b369e192 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -225,7 +225,7 @@ fn verify_bill_payment() { } let now = Instant::now(); - while !consuming_payable_dao.non_pending_payables().is_empty() + while !consuming_payable_dao.retrieve_payables(None).is_empty() && now.elapsed() < Duration::from_secs(10) { thread::sleep(Duration::from_millis(400)); @@ -400,7 +400,7 @@ fn verify_pending_payables() { ); let now = Instant::now(); - while !consuming_payable_dao.non_pending_payables().is_empty() + while !consuming_payable_dao.retrieve_payables(None).is_empty() && now.elapsed() < Duration::from_secs(10) { thread::sleep(Duration::from_millis(400)); @@ -437,7 +437,7 @@ fn verify_pending_payables() { .tmb(0), ); - assert!(consuming_payable_dao.non_pending_payables().is_empty()); + assert!(consuming_payable_dao.retrieve_payables(None).is_empty()); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), "Found 3 pending payables to process", 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 7a6e509bc..7d4644ffa 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -1,19 +1,21 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_access_objects::utils::{ - DaoFactoryReal, TxHash, TxIdentifiers, TxRecordWithHash, VigilantRusqliteFlatten, + sql_values_of_failed_tx, DaoFactoryReal, TxHash, TxIdentifiers, VigilantRusqliteFlatten, }; +use crate::accountant::db_access_objects::Transaction; 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::accountant::{join_with_commas, join_with_separator}; +use crate::blockchain::errors::rpc_errors::{AppRpcError, AppRpcErrorKind}; use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; -use itertools::Itertools; use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; use std::fmt::{Display, Formatter}; use std::str::FromStr; use web3::types::Address; +use web3::Error as Web3Error; #[derive(Debug, PartialEq, Eq)] pub enum FailedPayableDaoError { @@ -24,7 +26,7 @@ pub enum FailedPayableDaoError { SqlExecutionFailed(String), } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum FailureReason { Submission(AppRpcErrorKind), Reverted, @@ -49,7 +51,7 @@ impl FromStr for FailureReason { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] pub enum FailureStatus { RetryRequired, RecheckRequired(ValidationStatus), @@ -73,7 +75,7 @@ impl FromStr for FailureStatus { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct FailedTx { pub hash: TxHash, pub receiver_address: Address, @@ -85,16 +87,58 @@ pub struct FailedTx { pub status: FailureStatus, } -impl TxRecordWithHash for FailedTx { +impl Transaction for FailedTx { fn hash(&self) -> TxHash { self.hash } + + fn receiver_address(&self) -> Address { + self.receiver_address + } + + fn amount(&self) -> u128 { + self.amount_minor + } + + fn timestamp(&self) -> i64 { + self.timestamp + } + + fn gas_price_wei(&self) -> u128 { + self.gas_price_minor + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn is_failed(&self) -> bool { + true + } } -#[derive(Debug, PartialEq, Eq)] +impl From<(&SentTx, &Web3Error)> for FailedTx { + fn from((sent_tx, error): (&SentTx, &Web3Error)) -> Self { + let app_rpc_error = AppRpcError::from(error.clone()); + let error_kind = AppRpcErrorKind::from(&app_rpc_error); + Self { + 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: FailureReason::Submission(error_kind), + status: FailureStatus::RetryRequired, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum FailureRetrieveCondition { ByTxHash(Vec), ByStatus(FailureStatus), + ByReceiverAddresses(BTreeSet
), EveryRecheckRequiredRecord, } @@ -105,12 +149,19 @@ impl Display for FailureRetrieveCondition { write!( f, "WHERE tx_hash IN ({})", - comma_joined_stringifiable(hashes, |hash| format!("'{:?}'", hash)) + join_with_commas(hashes, |hash| format!("'{:?}'", hash)) ) } FailureRetrieveCondition::ByStatus(status) => { write!(f, "WHERE status = '{}'", status) } + FailureRetrieveCondition::ByReceiverAddresses(addresses) => { + write!( + f, + "WHERE receiver_address IN ({})", + join_with_commas(addresses, |address| format!("'{:?}'", address)) + ) + } FailureRetrieveCondition::EveryRecheckRequiredRecord => { write!(f, "WHERE status LIKE 'RecheckRequired%'") } @@ -119,16 +170,16 @@ impl Display for FailureRetrieveCondition { } pub trait FailedPayableDao { - fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers; //TODO potentially atomically - fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError>; - fn retrieve_txs(&self, condition: Option) -> Vec; + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), FailedPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> BTreeSet; fn update_statuses( &self, status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError>; //TODO potentially atomically - fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError>; + fn delete_records(&self, hashes: &BTreeSet) -> Result<(), FailedPayableDaoError>; } #[derive(Debug)] @@ -143,11 +194,10 @@ impl<'a> FailedPayableDaoReal<'a> { } impl FailedPayableDao for FailedPayableDaoReal<'_> { - fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { - let hashes_vec: Vec = hashes.iter().copied().collect(); + fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers { let sql = format!( "SELECT tx_hash, rowid FROM failed_payable WHERE tx_hash IN ({})", - comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)) + join_with_commas(hashes, |hash| format!("'{:?}'", hash)) ); let mut stmt = self @@ -167,12 +217,12 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { .collect() } - fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError> { + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), FailedPayableDaoError> { if txs.is_empty() { return Err(FailedPayableDaoError::EmptyInput); } - let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); + let unique_hashes: BTreeSet = txs.iter().map(|tx| tx.hash).collect(); if unique_hashes.len() != txs.len() { return Err(FailedPayableDaoError::InvalidInput(format!( "Duplicate hashes found in the input. Input Transactions: {:?}", @@ -201,26 +251,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { reason, \ status ) VALUES {}", - comma_joined_stringifiable(txs, |tx| { - 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_minor_checked); - format!( - "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}', '{}')", - tx.hash, - tx.receiver_address, - amount_high_b, - amount_low_b, - tx.timestamp, - gas_price_wei_high_b, - gas_price_wei_low_b, - tx.nonce, - tx.reason, - tx.status - ) - }) + join_with_commas(txs, |tx| sql_values_of_failed_tx(tx)) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -239,7 +270,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { } } - fn retrieve_txs(&self, condition: Option) -> Vec { + fn retrieve_txs(&self, condition: Option) -> BTreeSet { let raw_sql = "SELECT tx_hash, \ receiver_address, \ amount_high_b, \ @@ -308,13 +339,12 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { return Err(FailedPayableDaoError::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 case_statements = join_with_separator( + status_updates, + |(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status), + " ", + ); + let tx_hashes = join_with_commas(status_updates.keys(), |hash| format!("'{:?}'", hash)); let sql = format!( "UPDATE failed_payable \ @@ -341,15 +371,14 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { } } - fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError> { + fn delete_records(&self, hashes: &BTreeSet) -> Result<(), FailedPayableDaoError> { if hashes.is_empty() { return Err(FailedPayableDaoError::EmptyInput); } - let hashes_vec: Vec = hashes.iter().cloned().collect(); let sql = format!( "DELETE FROM failed_payable WHERE tx_hash IN ({})", - comma_joined_stringifiable(&hashes_vec, |hash| { format!("'{:?}'", hash) }) + join_with_commas(hashes, |hash| { format!("'{:?}'", hash) }) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -390,27 +419,28 @@ mod tests { Concluded, RecheckRequired, RetryRequired, }; use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailedTx, FailureReason, 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, TxRecordWithHash}; - use crate::accountant::test_utils::make_failed_tx; + use crate::accountant::db_access_objects::utils::current_unix_timestamp; + use crate::accountant::db_access_objects::Transaction; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; 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, ValidationFailureClockMock}; + use crate::blockchain::test_utils::{make_address, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; use crate::database::test_utils::ConnectionWrapperMock; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; - use std::collections::{HashMap, HashSet}; + use std::collections::{BTreeSet, HashMap}; use std::ops::Add; use std::str::FromStr; use std::time::{Duration, SystemTime}; @@ -425,19 +455,21 @@ mod tests { let tx1 = FailedTxBuilder::default() .hash(make_tx_hash(1)) .reason(Reverted) + .nonce(1) .build(); let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) + .nonce(2) .reason(PendingTooLong) .build(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let txs = vec![tx1, tx2]; + let hashset = BTreeSet::from([tx1.clone(), tx2.clone()]); - let result = subject.insert_new_records(&txs); + let result = subject.insert_new_records(&hashset); let retrieved_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); - assert_eq!(retrieved_txs, txs); + assert_eq!(retrieved_txs, BTreeSet::from([tx2, tx1])); } #[test] @@ -450,7 +482,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let empty_input = vec![]; + let empty_input = BTreeSet::new(); let result = subject.insert_new_records(&empty_input); @@ -470,29 +502,31 @@ mod tests { let tx1 = FailedTxBuilder::default() .hash(hash) .status(RetryRequired) + .nonce(1) .build(); let tx2 = FailedTxBuilder::default() .hash(hash) .status(RecheckRequired(ValidationStatus::Waiting)) + .nonce(2) .build(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let result = subject.insert_new_records(&vec![tx1, tx2]); + let result = subject.insert_new_records(&BTreeSet::from([tx1, tx2])); assert_eq!( result, Err(FailedPayableDaoError::InvalidInput( "Duplicate hashes found in the input. Input Transactions: \ - [FailedTx { \ + {FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ - nonce: 0, reason: PendingTooLong, status: RetryRequired }, \ + amount_minor: 0, timestamp: 1719990000, gas_price_minor: 0, \ + nonce: 1, reason: PendingTooLong, status: RetryRequired }, \ FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ - nonce: 0, reason: PendingTooLong, status: RecheckRequired(Waiting) }]" + amount_minor: 0, timestamp: 1719990000, gas_price_minor: 0, \ + nonce: 2, reason: PendingTooLong, status: RecheckRequired(Waiting) }}" .to_string() )) ); @@ -517,9 +551,9 @@ mod tests { .status(RecheckRequired(ValidationStatus::Waiting)) .build(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let initial_insertion_result = subject.insert_new_records(&vec![tx1]); + let initial_insertion_result = subject.insert_new_records(&BTreeSet::from([tx1])); - let result = subject.insert_new_records(&vec![tx2]); + let result = subject.insert_new_records(&BTreeSet::from([tx2])); assert_eq!(initial_insertion_result, Ok(())); assert_eq!( @@ -546,7 +580,7 @@ mod tests { let tx = FailedTxBuilder::default().build(); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.insert_new_records(&vec![tx]); + let result = subject.insert_new_records(&BTreeSet::from([tx])); assert_eq!( result, @@ -566,7 +600,7 @@ mod tests { let tx = FailedTxBuilder::default().build(); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.insert_new_records(&vec![tx]); + let result = subject.insert_new_records(&BTreeSet::from([tx])); assert_eq!( result, @@ -587,13 +621,17 @@ mod tests { let present_hash = make_tx_hash(1); let absent_hash = make_tx_hash(2); let another_present_hash = make_tx_hash(3); - let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); - let present_tx = FailedTxBuilder::default().hash(present_hash).build(); + let hashset = BTreeSet::from([present_hash, absent_hash, another_present_hash]); + let present_tx = FailedTxBuilder::default() + .hash(present_hash) + .nonce(1) + .build(); let another_present_tx = FailedTxBuilder::default() .hash(another_present_hash) + .nonce(2) .build(); subject - .insert_new_records(&vec![present_tx, another_present_tx]) + .insert_new_records(&BTreeSet::from([present_tx, another_present_tx])) .unwrap(); let result = subject.get_tx_identifiers(&hashset); @@ -708,34 +746,58 @@ mod tests { FailureRetrieveCondition::ByStatus(RetryRequired).to_string(), "WHERE status = '\"RetryRequired\"'" ); + assert_eq!( + FailureRetrieveCondition::ByReceiverAddresses(BTreeSet::from([make_address(1), make_address(2)])) + .to_string(), + "WHERE receiver_address IN ('0x0000000000000000000003000000000003000000', '0x0000000000000000000006000000000006000000')" + ) } #[test] - fn can_retrieve_all_txs() { - let home_dir = - ensure_node_home_directory_exists("failed_payable_dao", "can_retrieve_all_txs"); + fn can_retrieve_all_txs_ordered_by_timestamp_and_nonce() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "can_retrieve_all_txs_ordered_by_timestamp_and_nonce", + ); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let tx1 = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .timestamp(1000) + .nonce(1) + .build(); let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) + .timestamp(1000) + .nonce(2) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .timestamp(1001) .nonce(1) .build(); - let tx3 = FailedTxBuilder::default().hash(make_tx_hash(3)).build(); + let tx4 = FailedTxBuilder::default() + .hash(make_tx_hash(4)) + .timestamp(1001) + .nonce(2) + .build(); + subject - .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .insert_new_records(&BTreeSet::from([tx2.clone(), tx4.clone()])) + .unwrap(); + subject + .insert_new_records(&BTreeSet::from([tx1.clone(), tx3.clone()])) .unwrap(); - subject.insert_new_records(&vec![tx3.clone()]).unwrap(); let result = subject.retrieve_txs(None); - assert_eq!(result, vec![tx1, tx2, tx3]); + assert_eq!(result, BTreeSet::from([tx4, tx3, tx2, tx1])); } #[test] - fn can_retrieve_unchecked_pending_too_long_txs() { + fn can_retrieve_txs_to_retry() { let home_dir = ensure_node_home_directory_exists( "failed_payable_dao", "can_retrieve_unchecked_pending_too_long_txs", @@ -747,18 +809,22 @@ mod tests { let now = current_unix_timestamp(); let tx1 = FailedTxBuilder::default() .hash(make_tx_hash(1)) - .reason(PendingTooLong) + .nonce(1) .timestamp(now - 3600) + .reason(PendingTooLong) .status(RetryRequired) .build(); let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) - .reason(Reverted) + .nonce(2) .timestamp(now - 3600) + .reason(Reverted) .status(RetryRequired) .build(); let tx3 = FailedTxBuilder::default() .hash(make_tx_hash(3)) + .nonce(3) + .timestamp(now - 3000) .reason(PendingTooLong) .status(RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new( @@ -771,17 +837,72 @@ mod tests { .build(); let tx4 = FailedTxBuilder::default() .hash(make_tx_hash(4)) + .nonce(4) .reason(PendingTooLong) .status(Concluded) .timestamp(now - 3000) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3, tx4]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone(), tx3, tx4])) .unwrap(); let result = subject.retrieve_txs(Some(FailureRetrieveCondition::ByStatus(RetryRequired))); - assert_eq!(result, vec![tx1, tx2]); + assert_eq!(result, BTreeSet::from([tx2, tx1])); + } + + #[test] + fn can_retrieve_txs_by_receiver_addresses() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "can_retrieve_txs_by_receiver_addresses", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let address1 = make_address(1); + let address2 = make_address(2); + let address3 = make_address(3); + let address4 = make_address(4); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .receiver_address(address1) + .nonce(1) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .receiver_address(address2) + .nonce(2) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .receiver_address(address3) + .nonce(3) + .build(); + let tx4 = FailedTxBuilder::default() + .hash(make_tx_hash(4)) + .receiver_address(address4) + .nonce(4) + .build(); + subject + .insert_new_records(&BTreeSet::from([ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + ])) + .unwrap(); + + let result = subject.retrieve_txs(Some(FailureRetrieveCondition::ByReceiverAddresses( + BTreeSet::from([address1, address2, address3]), + ))); + + assert_eq!(result.len(), 3); + assert!(result.contains(&tx1)); + assert!(result.contains(&tx2)); + assert!(result.contains(&tx3)); + assert!(!result.contains(&tx4)); } #[test] @@ -792,28 +913,41 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); + let hash1 = make_tx_hash(1); + let hash2 = make_tx_hash(2); + let hash3 = make_tx_hash(3); + let hash4 = make_tx_hash(4); let tx1 = FailedTxBuilder::default() - .hash(make_tx_hash(1)) + .hash(hash1) .reason(Reverted) .status(RetryRequired) + .nonce(4) .build(); let tx2 = FailedTxBuilder::default() - .hash(make_tx_hash(2)) + .hash(hash2) .reason(PendingTooLong) .status(RecheckRequired(ValidationStatus::Waiting)) + .nonce(3) .build(); let tx3 = FailedTxBuilder::default() - .hash(make_tx_hash(3)) + .hash(hash3) .reason(PendingTooLong) .status(RetryRequired) + .nonce(2) .build(); let tx4 = FailedTxBuilder::default() - .hash(make_tx_hash(4)) + .hash(hash4) .reason(PendingTooLong) .status(RecheckRequired(ValidationStatus::Waiting)) + .nonce(1) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .insert_new_records(&BTreeSet::from([ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + ])) .unwrap(); let timestamp = SystemTime::now(); let clock = ValidationFailureClockMock::default() @@ -834,24 +968,28 @@ mod tests { ]); let result = subject.update_statuses(&hashmap); - let updated_txs = subject.retrieve_txs(None); + let find_tx = |tx_hash| updated_txs.iter().find(|tx| tx.hash == tx_hash).unwrap(); + let updated_tx1 = find_tx(hash1); + let updated_tx2 = find_tx(hash2); + let updated_tx3 = find_tx(hash3); + let updated_tx4 = find_tx(hash4); assert_eq!(result, Ok(())); assert_eq!(tx1.status, RetryRequired); - assert_eq!(updated_txs[0].status, Concluded); + assert_eq!(updated_tx1.status, Concluded); assert_eq!(tx2.status, RecheckRequired(ValidationStatus::Waiting)); assert_eq!( - updated_txs[1].status, + updated_tx2.status, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &clock ))) ); assert_eq!(tx3.status, RetryRequired); - assert_eq!(updated_txs[2].status, Concluded); + assert_eq!(updated_tx3.status, Concluded); assert_eq!(tx4.status, RecheckRequired(ValidationStatus::Waiting)); assert_eq!( - updated_txs[3].status, + updated_tx4.status, RecheckRequired(ValidationStatus::Waiting) ); assert_eq!(updated_txs.len(), 4); @@ -900,20 +1038,37 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let tx1 = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); - let tx2 = FailedTxBuilder::default().hash(make_tx_hash(2)).build(); - let tx3 = FailedTxBuilder::default().hash(make_tx_hash(3)).build(); - let tx4 = FailedTxBuilder::default().hash(make_tx_hash(4)).build(); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .nonce(1) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .nonce(2) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .nonce(3) + .build(); + let tx4 = FailedTxBuilder::default() + .hash(make_tx_hash(4)) + .nonce(4) + .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .insert_new_records(&BTreeSet::from([ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + ])) .unwrap(); - let hashset = HashSet::from([tx1.hash, tx3.hash]); + let hashset = BTreeSet::from([tx1.hash, tx3.hash]); let result = subject.delete_records(&hashset); let remaining_records = subject.retrieve_txs(None); assert_eq!(result, Ok(())); - assert_eq!(remaining_records, vec![tx2, tx4]); + assert_eq!(remaining_records, BTreeSet::from([tx4, tx2])); } #[test] @@ -927,7 +1082,7 @@ mod tests { .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let result = subject.delete_records(&HashSet::new()); + let result = subject.delete_records(&BTreeSet::new()); assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); } @@ -943,7 +1098,7 @@ mod tests { .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); let non_existent_hash = make_tx_hash(999); - let hashset = HashSet::from([non_existent_hash]); + let hashset = BTreeSet::from([non_existent_hash]); let result = subject.delete_records(&hashset); @@ -963,10 +1118,10 @@ mod tests { let present_hash = make_tx_hash(1); let absent_hash = make_tx_hash(2); let tx = FailedTxBuilder::default().hash(present_hash).build(); - subject.insert_new_records(&vec![tx]).unwrap(); - let hashset = HashSet::from([present_hash, absent_hash]); + subject.insert_new_records(&BTreeSet::from([tx])).unwrap(); + let set = BTreeSet::from([present_hash, absent_hash]); - let result = subject.delete_records(&hashset); + let result = subject.delete_records(&set); assert_eq!( result, @@ -984,7 +1139,7 @@ mod tests { ); let wrapped_conn = make_read_only_db_connection(home_dir); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let hashes = HashSet::from([make_tx_hash(1)]); + let hashes = BTreeSet::from([make_tx_hash(1)]); let result = subject.delete_records(&hashes); @@ -997,12 +1152,33 @@ 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(); + fn transaction_trait_methods_for_failed_tx() { + let hash = make_tx_hash(1); + let receiver_address = make_address(1); + let amount = 1000; + let timestamp = 1625247600; + let gas_price_wei = 2000; + let nonce = 42; + let reason = FailureReason::Reverted; + let status = FailureStatus::RetryRequired; + + let failed_tx = FailedTx { + hash, + receiver_address, + amount_minor: amount, + timestamp, + gas_price_minor: gas_price_wei, + nonce, + reason, + status, + }; - assert_eq!(hash_from_trait, hash); + assert_eq!(failed_tx.receiver_address(), receiver_address); + assert_eq!(failed_tx.hash(), hash); + assert_eq!(failed_tx.amount(), amount); + assert_eq!(failed_tx.timestamp(), timestamp); + assert_eq!(failed_tx.gas_price_wei(), gas_price_wei); + assert_eq!(failed_tx.nonce(), nonce); + assert_eq!(failed_tx.is_failed(), true); } } diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index 0141e8796..a8c8e225e 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -1,10 +1,23 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::utils::TxHash; +use web3::types::Address; + pub mod banned_dao; pub mod failed_payable_dao; pub mod 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 test_utils; pub mod utils; + +pub trait Transaction { + fn hash(&self) -> TxHash; + fn receiver_address(&self) -> Address; + fn amount(&self) -> u128; + fn timestamp(&self) -> i64; + fn gas_price_wei(&self) -> u128; + fn nonce(&self) -> u64; + fn is_failed(&self) -> bool; +} diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index cff264a58..f9a723f54 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -1,10 +1,11 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; 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, RowId, TopStmConfig, TxHash, VigilantRusqliteFlatten, + from_unix_timestamp, sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, + CustomQuery, DaoFactoryReal, RangeStmConfig, RowId, TopStmConfig, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; use crate::accountant::db_big_integer::big_int_db_processor::{ @@ -12,20 +13,20 @@ use crate::accountant::db_big_integer::big_int_db_processor::{ 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::accountant::{checked_conversion, join_with_commas, sign_conversion, PendingPayableId}; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; use ethabi::Address; #[cfg(test)] -use ethereum_types::{BigEndianHash, U256}; +use ethereum_types::{BigEndianHash, H256, U256}; use itertools::Either; use masq_lib::utils::ExpectValue; #[cfg(test)] use rusqlite::OptionalExtension; use rusqlite::{Error, Row}; -use std::fmt::Debug; +use std::collections::BTreeSet; +use std::fmt::{Debug, Display, Formatter}; use std::time::SystemTime; -use web3::types::H256; #[derive(Debug, PartialEq, Eq)] pub enum PayableDaoError { @@ -41,6 +42,34 @@ pub struct PayableAccount { pub pending_payable_opt: Option, } +impl From<&FailedTx> for PayableAccount { + fn from(failed_tx: &FailedTx) -> Self { + PayableAccount { + wallet: Wallet::from(failed_tx.receiver_address), + balance_wei: failed_tx.amount_minor, + last_paid_timestamp: from_unix_timestamp(failed_tx.timestamp), + pending_payable_opt: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PayableRetrieveCondition { + ByAddresses(BTreeSet
), +} + +impl Display for PayableRetrieveCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PayableRetrieveCondition::ByAddresses(addresses) => write!( + f, + "AND wallet_address IN ({})", + join_with_commas(addresses, |hash| format!("'{:?}'", hash)) + ), + } + } +} + pub trait PayableDao: Debug + Send { fn more_money_payable( &self, @@ -56,7 +85,10 @@ pub trait PayableDao: Debug + Send { fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError>; - fn non_pending_payables(&self) -> Vec; + fn retrieve_payables( + &self, + condition_opt: Option, + ) -> Vec; fn custom_query(&self, custom_query: CustomQuery) -> Option>; @@ -170,11 +202,19 @@ impl PayableDao for PayableDaoReal { }) } - fn non_pending_payables(&self) -> Vec { - let sql = "\ + fn retrieve_payables( + &self, + condition_opt: Option, + ) -> Vec { + let raw_sql = "\ select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp from \ - payable where pending_payable_rowid is null"; - let mut stmt = self.conn.prepare(sql).expect("Internal error"); + payable where pending_payable_rowid is null" + .to_string(); + let sql = match condition_opt { + None => raw_sql, + Some(condition) => format!("{} {}", raw_sql, condition), + }; + let mut stmt = self.conn.prepare(&sql).expect("Internal error"); stmt.query_map([], |row| { let wallet_result: Result = row.get(0); let high_b_result: Result = row.get(1); @@ -366,7 +406,7 @@ impl TableNameDAO for PayableDaoReal { // 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::join_with_commas; // use crate::accountant::db_access_objects::payable_dao::{MarkPendingPayableID, PayableDaoError}; // use crate::accountant::db_access_objects::utils::{ // update_rows_and_return_valid_count, VigilantRusqliteFlatten, @@ -504,7 +544,7 @@ impl TableNameDAO for PayableDaoReal { // pairs: &[(W, R)], // rowid_pretty_writer: fn(&R) -> Box, // ) -> String { -// comma_joined_stringifiable(pairs, |(wallet, rowid)| { +// join_with_commas(pairs, |(wallet, rowid)| { // format!( // "( Wallet: {}, Rowid: {} )", // wallet, @@ -517,13 +557,15 @@ impl TableNameDAO for PayableDaoReal { #[cfg(test)] mod tests { use super::*; + use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition::ByAddresses; use crate::accountant::db_access_objects::sent_payable_dao::SentTx; + use crate::accountant::db_access_objects::test_utils::make_sent_tx; use crate::accountant::db_access_objects::utils::{ - current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, + current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, TxHash, }; use crate::accountant::gwei_to_wei; use crate::accountant::test_utils::{ - assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_sent_tx, + assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, trick_rusqlite_with_read_only_conn, }; use crate::blockchain::test_utils::make_tx_hash; @@ -932,7 +974,7 @@ mod tests { // TODO argument will be eliminated in GH-662 None, ); - let mut sent_tx = make_sent_tx((idx as u64 + 1) * 1234); + let mut sent_tx = make_sent_tx((idx as u32 + 1) * 1234); sent_tx.hash = test_inputs.hash; sent_tx.amount_minor = test_inputs.balance_change; sent_tx.receiver_address = test_inputs.receiver_wallet; @@ -1140,10 +1182,10 @@ mod tests { } #[test] - fn non_pending_payables_should_return_an_empty_vec_when_the_database_is_empty() { + fn retrieve_payables_should_return_an_empty_vec_when_the_database_is_empty() { let home_dir = ensure_node_home_directory_exists( "payable_dao", - "non_pending_payables_should_return_an_empty_vec_when_the_database_is_empty", + "retrieve_payables_should_return_an_empty_vec_when_the_database_is_empty", ); let subject = PayableDaoReal::new( DbInitializerReal::default() @@ -1151,16 +1193,16 @@ mod tests { .unwrap(), ); - let result = subject.non_pending_payables(); + let result = subject.retrieve_payables(None); assert_eq!(result, vec![]); } #[test] - fn non_pending_payables_should_return_payables_with_no_pending_transaction() { + fn retrieve_payables_should_return_payables_with_no_pending_transaction() { let home_dir = ensure_node_home_directory_exists( "payable_dao", - "non_pending_payables_should_return_payables_with_no_pending_transaction", + "retrieve_payables_should_return_payables_with_no_pending_transaction", ); let subject = PayableDaoReal::new( DbInitializerReal::default() @@ -1185,7 +1227,7 @@ mod tests { insert("0x0000000000000000000000000000000000626172", Some(16)); insert(&make_wallet("barfoo").to_string(), None); - let result = subject.non_pending_payables(); + let result = subject.retrieve_payables(None); assert_eq!( result, @@ -1206,6 +1248,59 @@ mod tests { ); } + #[test] + fn retrieve_payables_should_return_payables_by_addresses() { + let home_dir = ensure_node_home_directory_exists( + "payable_dao", + "retrieve_payables_should_return_payables_by_addresses", + ); + let subject = PayableDaoReal::new( + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(), + ); + let mut flags = OpenFlags::empty(); + flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE); + let conn = Connection::open_with_flags(&home_dir.join(DATABASE_FILE), flags).unwrap(); + let conn = ConnectionWrapperReal::new(conn); + let insert = |wallet: &str, pending_payable_rowid: Option| { + insert_payable_record_fn( + &conn, + wallet, + 1234567890123456, + 111_111_111, + pending_payable_rowid, + ); + }; + let wallet1 = make_wallet("foobar"); + let wallet2 = make_wallet("barfoo"); + insert("0x0000000000000000000000000000000000666f6f", Some(15)); + insert(&wallet1.to_string(), None); + insert("0x0000000000000000000000000000000000626172", None); + insert(&wallet2.to_string(), None); + let set = BTreeSet::from([wallet1.address(), wallet2.address()]); + + let result = subject.retrieve_payables(Some(ByAddresses(set))); + + assert_eq!( + result, + vec![ + PayableAccount { + wallet: wallet2, + balance_wei: 1234567890123456 as u128, + last_paid_timestamp: from_unix_timestamp(111_111_111), + pending_payable_opt: None + }, + PayableAccount { + wallet: wallet1, + balance_wei: 1234567890123456 as u128, + last_paid_timestamp: from_unix_timestamp(111_111_111), + pending_payable_opt: None + }, + ] + ); + } + #[test] fn custom_query_handles_empty_table_in_top_records_mode() { let main_test_setup = |_conn: &dyn ConnectionWrapper, _insert: InsertPayableHelperFn| {}; @@ -1651,4 +1746,15 @@ mod tests { main_setup_fn(conn.as_ref(), &insert_payable_record_fn); PayableDaoReal::new(conn) } + + #[test] + fn payable_retrieve_condition_to_str_works() { + let address_1 = make_wallet("first").address(); + let address_2 = make_wallet("second").address(); + assert_eq!( + PayableRetrieveCondition::ByAddresses(BTreeSet::from([address_1, address_2])) + .to_string(), + "AND wallet_address IN ('0x0000000000000000000000000000006669727374', '0x00000000000000000000000000007365636f6e64')" + ); + } } 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 a82bafdce..d0edbfa34 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -1,10 +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, TxRecordWithHash, + sql_values_of_sent_tx, DaoFactoryReal, TxHash, TxIdentifiers, }; +use crate::accountant::db_access_objects::Transaction; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::accountant::{checked_conversion, join_with_commas, join_with_separator}; use crate::blockchain::blockchain_interface::data_structures::TxBlock; use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; @@ -12,7 +13,8 @@ use ethereum_types::H256; use itertools::Itertools; use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashMap}; use std::fmt::{Display, Formatter}; use std::str::FromStr; use web3::types::Address; @@ -26,7 +28,7 @@ pub enum SentPayableDaoError { SqlExecutionFailed(String), } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct SentTx { pub hash: TxHash, pub receiver_address: Address, @@ -37,10 +39,34 @@ pub struct SentTx { pub status: TxStatus, } -impl TxRecordWithHash for SentTx { +impl Transaction for SentTx { fn hash(&self) -> TxHash { self.hash } + + fn receiver_address(&self) -> Address { + self.receiver_address + } + + fn amount(&self) -> u128 { + self.amount_minor + } + + fn timestamp(&self) -> i64 { + self.timestamp + } + + fn gas_price_wei(&self) -> u128 { + self.gas_price_minor + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn is_failed(&self) -> bool { + false + } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -53,6 +79,41 @@ pub enum TxStatus { }, } +impl PartialOrd for TxStatus { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for TxStatus { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (TxStatus::Pending(status1), TxStatus::Pending(status2)) => status1.cmp(status2), + (TxStatus::Pending(_), TxStatus::Confirmed { .. }) => Ordering::Greater, + (TxStatus::Confirmed { .. }, TxStatus::Pending(_)) => Ordering::Less, + ( + TxStatus::Confirmed { + block_hash: block_hash1, + block_number: block_num1, + detection: detection1, + }, + TxStatus::Confirmed { + block_hash: block_hash2, + block_number: block_num2, + detection: detection2, + }, + ) => block_hash1 + .cmp(block_hash2) + .then_with(|| block_num1.cmp(block_num2)) + .then_with(|| detection1.cmp(detection2)), + } + } +} + impl FromStr for TxStatus { type Err = String; @@ -71,7 +132,7 @@ impl Display for TxStatus { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] pub enum Detection { Normal, Reclaim, @@ -87,10 +148,10 @@ impl From for TxStatus { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum RetrieveCondition { IsPending, - ByHash(Vec), + ByHash(BTreeSet), ByNonce(Vec), } @@ -104,14 +165,14 @@ impl Display for RetrieveCondition { write!( f, "WHERE tx_hash IN ({})", - comma_joined_stringifiable(tx_hashes, |hash| format!("'{:?}'", hash)) + join_with_commas(tx_hashes, |hash| format!("'{:?}'", hash)) ) } RetrieveCondition::ByNonce(nonces) => { write!( f, "WHERE nonce IN ({})", - comma_joined_stringifiable(nonces, |nonce| nonce.to_string()) + join_with_commas(nonces, |nonce| nonce.to_string()) ) } } @@ -119,18 +180,18 @@ impl Display for RetrieveCondition { } pub trait SentPayableDao { - fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; - fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError>; - fn retrieve_txs(&self, condition: Option) -> Vec; + fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers; + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> BTreeSet; //TODO potentially atomically fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError>; - fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError>; + fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError>; fn update_statuses( &self, hash_map: &HashMap, ) -> Result<(), SentPayableDaoError>; //TODO potentially atomically - fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; + fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError>; } #[derive(Debug)] @@ -145,11 +206,10 @@ impl<'a> SentPayableDaoReal<'a> { } impl SentPayableDao for SentPayableDaoReal<'_> { - fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { - let hashes_vec: Vec = hashes.iter().copied().collect(); + fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers { let sql = format!( "SELECT tx_hash, rowid FROM sent_payable WHERE tx_hash IN ({})", - comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)) + join_with_commas(hashes, |hash| format!("'{:?}'", hash)) ); let mut stmt = self @@ -169,12 +229,12 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError> { if txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); + let unique_hashes: BTreeSet = txs.iter().map(|tx| tx.hash).collect(); if unique_hashes.len() != txs.len() { return Err(SentPayableDaoError::InvalidInput(format!( "Duplicate hashes found in the input. Input Transactions: {:?}", @@ -202,25 +262,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { nonce, \ status \ ) VALUES {}", - comma_joined_stringifiable(txs, |tx| { - 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); - format!( - "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}')", - tx.hash, - tx.receiver_address, - amount_high_b, - amount_low_b, - tx.timestamp, - gas_price_wei_high_b, - gas_price_wei_low_b, - tx.nonce, - tx.status - ) - }) + join_with_commas(txs, |tx| sql_values_of_sent_tx(tx)) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -239,7 +281,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } - fn retrieve_txs(&self, condition_opt: Option) -> Vec { + fn retrieve_txs(&self, condition_opt: Option) -> BTreeSet { 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(); @@ -318,16 +360,17 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Ok(()) } - fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError> { if new_txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } let build_case = |value_fn: fn(&SentTx) -> String| { - new_txs - .iter() - .map(|tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx))) - .join(" ") + join_with_separator( + new_txs, + |tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx)), + " ", + ) }; let tx_hash_cases = build_case(|tx| format!("'{:?}'", tx.hash)); @@ -355,7 +398,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { }); let status_cases = build_case(|tx| format!("'{}'", tx.status)); - let nonces = comma_joined_stringifiable(new_txs, |tx| tx.nonce.to_string()); + let nonces = join_with_commas(new_txs, |tx| tx.nonce.to_string()); let sql = format!( "UPDATE sent_payable \ @@ -413,7 +456,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .iter() .map(|(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status)) .join(" "); - let tx_hashes = comma_joined_stringifiable(&status_updates.keys().collect_vec(), |hash| { + let tx_hashes = join_with_commas(&status_updates.keys().collect_vec(), |hash| { format!("'{:?}'", hash) }); @@ -442,15 +485,14 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } - fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError> { if hashes.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - let hashes_vec: Vec = hashes.iter().cloned().collect(); let sql = format!( "DELETE FROM sent_payable WHERE tx_hash IN ({})", - comma_joined_stringifiable(&hashes_vec, |hash| { format!("'{:?}'", hash) }) + join_with_commas(hashes, |hash| { format!("'{:?}'", hash) }) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -492,22 +534,21 @@ mod tests { }; use crate::accountant::db_access_objects::sent_payable_dao::{ Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, - TxStatus, + SentTx, TxStatus, }; use crate::accountant::db_access_objects::test_utils::{ - make_read_only_db_connection, TxBuilder, + make_read_only_db_connection, make_sent_tx, TxBuilder, }; - use crate::accountant::db_access_objects::utils::TxRecordWithHash; - use crate::accountant::test_utils::make_sent_tx; + use crate::accountant::db_access_objects::Transaction; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::blockchain::blockchain_interface::data_structures::TxBlock; + use crate::blockchain::errors::internal_errors::InternalErrorKind; 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::blockchain::test_utils::{make_address, make_block_hash, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; @@ -515,7 +556,8 @@ mod tests { use ethereum_types::{H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; - use std::collections::{HashMap, HashSet}; + use std::cmp::Ordering; + use std::collections::{BTreeSet, HashMap}; use std::ops::{Add, Sub}; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -547,7 +589,7 @@ mod tests { ))) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); - let txs = vec![tx1, tx2]; + let txs = BTreeSet::from([tx1, tx2]); let result = subject.insert_new_records(&txs); @@ -566,7 +608,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let empty_input = vec![]; + let empty_input = BTreeSet::new(); let result = subject.insert_new_records(&empty_input); @@ -599,14 +641,14 @@ mod tests { .build(); let subject = SentPayableDaoReal::new(wrapped_conn); - let result = subject.insert_new_records(&vec![tx1, tx2]); + let result = subject.insert_new_records(&BTreeSet::from([tx1, tx2])); assert_eq!( result, Err(SentPayableDaoError::InvalidInput( "Duplicate hashes found in the input. Input Transactions: \ - [SentTx { \ - hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + {\ + SentTx { hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ nonce: 0, status: Pending(Waiting) }, \ @@ -616,8 +658,9 @@ mod tests { amount_minor: 0, timestamp: 1749204020, gas_price_minor: 0, \ nonce: 0, status: Confirmed { block_hash: \ \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ - block_number: 7890123, detection: Reclaim } }]" - .to_string() + block_number: 7890123, detection: Reclaim } }\ + }" + .to_string() )) ); } @@ -635,9 +678,9 @@ mod tests { let tx1 = TxBuilder::default().hash(hash).build(); let tx2 = TxBuilder::default().hash(hash).build(); let subject = SentPayableDaoReal::new(wrapped_conn); - let initial_insertion_result = subject.insert_new_records(&vec![tx1]); + let initial_insertion_result = subject.insert_new_records(&BTreeSet::from([tx1])); - let result = subject.insert_new_records(&vec![tx2]); + let result = subject.insert_new_records(&BTreeSet::from([tx2])); assert_eq!(initial_insertion_result, Ok(())); assert_eq!( @@ -664,7 +707,7 @@ mod tests { let tx = TxBuilder::default().build(); let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.insert_new_records(&vec![tx]); + let result = subject.insert_new_records(&BTreeSet::from([tx])); assert_eq!( result, @@ -684,7 +727,7 @@ mod tests { let wrapped_conn = make_read_only_db_connection(home_dir); let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.insert_new_records(&vec![tx]); + let result = subject.insert_new_records(&BTreeSet::from([tx])); assert_eq!( result, @@ -705,11 +748,11 @@ mod tests { let present_hash = make_tx_hash(1); let absent_hash = make_tx_hash(2); let another_present_hash = make_tx_hash(3); - let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); + let hashset = BTreeSet::from([present_hash, absent_hash, another_present_hash]); let present_tx = TxBuilder::default().hash(present_hash).build(); let another_present_tx = TxBuilder::default().hash(another_present_hash).build(); subject - .insert_new_records(&vec![present_tx, another_present_tx]) + .insert_new_records(&BTreeSet::from([present_tx, another_present_tx])) .unwrap(); let result = subject.get_tx_identifiers(&hashset); @@ -722,11 +765,12 @@ mod tests { #[test] fn retrieve_condition_display_works() { assert_eq!(IsPending.to_string(), "WHERE status LIKE '%\"Pending\":%'"); + // 0x0000000000000000000000000000000000000000000000000000000123456789 assert_eq!( - ByHash(vec![ + ByHash(BTreeSet::from([ H256::from_low_u64_be(0x123456789), H256::from_low_u64_be(0x987654321), - ]) + ])) .to_string(), "WHERE tx_hash IN (\ '0x0000000000000000000000000000000000000000000000000000000123456789', \ @@ -748,13 +792,15 @@ mod tests { let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone()])) + .unwrap(); + subject + .insert_new_records(&BTreeSet::from([tx3.clone()])) .unwrap(); - subject.insert_new_records(&vec![tx3.clone()]).unwrap(); let result = subject.retrieve_txs(None); - assert_eq!(result, vec![tx1, tx2, tx3]); + assert_eq!(result, BTreeSet::from([tx1, tx2, tx3])); } #[test] @@ -789,12 +835,12 @@ mod tests { }) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone(), tx3])) .unwrap(); let result = subject.retrieve_txs(Some(RetrieveCondition::IsPending)); - assert_eq!(result, vec![tx1, tx2]); + assert_eq!(result, BTreeSet::from([tx1, tx2])); } #[test] @@ -809,12 +855,50 @@ mod tests { let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); subject - .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2, tx3.clone()])) .unwrap(); - let result = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx3.hash]))); + let result = subject.retrieve_txs(Some(ByHash(BTreeSet::from([tx1.hash, tx3.hash])))); - assert_eq!(result, vec![tx1, tx3]); + assert_eq!(result, BTreeSet::from([tx1, tx3])); + } + + #[test] + fn retrieve_txs_by_hash_returns_only_existing_transactions() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "retrieve_txs_by_hash_returns_only_existing_transactions", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).nonce(3).build(); + subject + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone(), tx3.clone()])) + .unwrap(); + let mut query_hashes = BTreeSet::new(); + query_hashes.insert(make_tx_hash(1)); // Exists + query_hashes.insert(make_tx_hash(2)); // Exists + query_hashes.insert(make_tx_hash(4)); // Does not exist + query_hashes.insert(make_tx_hash(5)); // Does not exist + + let result = subject.retrieve_txs(Some(RetrieveCondition::ByHash(query_hashes))); + + assert_eq!(result.len(), 2, "Should only return 2 transactions"); + assert!(result.contains(&tx1), "Should contain tx1"); + assert!(result.contains(&tx2), "Should contain tx2"); + assert!(!result.contains(&tx3), "Should not contain tx3"); + assert!( + result.iter().all(|tx| tx.hash != make_tx_hash(4)), + "Should not contain hash 4" + ); + assert!( + result.iter().all(|tx| tx.hash != make_tx_hash(5)), + "Should not contain hash 5" + ); } #[test] @@ -838,12 +922,12 @@ mod tests { .nonce(35) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2, tx3.clone()])) .unwrap(); let result = subject.retrieve_txs(Some(ByNonce(vec![33, 35]))); - assert_eq!(result, vec![tx1, tx3]); + assert_eq!(result, BTreeSet::from([tx1, tx3])); } #[test] @@ -853,14 +937,17 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); - let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let hash1 = make_tx_hash(1); + let hash2 = make_tx_hash(2); + let tx1 = TxBuilder::default().hash(hash1).build(); + let tx2 = TxBuilder::default().hash(hash2).build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone()])) .unwrap(); - 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 updated_pre_assert_txs = + subject.retrieve_txs(Some(ByHash(BTreeSet::from([hash1, hash2])))); + let pre_assert_status_tx1 = updated_pre_assert_txs.get(&tx1).unwrap().status.clone(); + let pre_assert_status_tx2 = updated_pre_assert_txs.get(&tx2).unwrap().status.clone(); let confirmed_tx_block_1 = TxBlock { block_hash: make_block_hash(3), block_number: U64::from(1), @@ -876,14 +963,16 @@ mod tests { let result = subject.confirm_txs(&hash_map); - let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); + let updated_txs = subject.retrieve_txs(Some(ByHash(BTreeSet::from([tx1.hash, tx2.hash])))); + let updated_tx1 = updated_txs.iter().find(|tx| tx.hash == hash1).unwrap(); + let updated_tx2 = updated_txs.iter().find(|tx| tx.hash == hash2).unwrap(); assert_eq!(result, Ok(())); assert_eq!( pre_assert_status_tx1, TxStatus::Pending(ValidationStatus::Waiting) ); assert_eq!( - updated_txs[0].status, + updated_tx1.status, TxStatus::Confirmed { block_hash: format!("{:?}", confirmed_tx_block_1.block_hash), block_number: confirmed_tx_block_1.block_number.as_u64(), @@ -895,7 +984,7 @@ mod tests { TxStatus::Pending(ValidationStatus::Waiting) ); assert_eq!( - updated_txs[1].status, + updated_tx2.status, TxStatus::Confirmed { block_hash: format!("{:?}", confirmed_tx_block_2.block_hash), block_number: confirmed_tx_block_2.block_number.as_u64(), @@ -916,7 +1005,7 @@ mod tests { let subject = SentPayableDaoReal::new(wrapped_conn); let existent_hash = make_tx_hash(1); let tx = TxBuilder::default().hash(existent_hash).build(); - subject.insert_new_records(&vec![tx]).unwrap(); + subject.insert_new_records(&BTreeSet::from([tx])).unwrap(); let hash_map = HashMap::new(); let result = subject.confirm_txs(&hash_map); @@ -937,7 +1026,7 @@ mod tests { let existent_hash = make_tx_hash(1); let non_existent_hash = make_tx_hash(999); let tx = TxBuilder::default().hash(existent_hash).build(); - subject.insert_new_records(&vec![tx]).unwrap(); + subject.insert_new_records(&BTreeSet::from([tx])).unwrap(); let hash_map = HashMap::from([ ( existent_hash, @@ -1005,15 +1094,20 @@ mod tests { let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); let tx4 = TxBuilder::default().hash(make_tx_hash(4)).build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .insert_new_records(&BTreeSet::from([ + tx1.clone(), + tx2.clone(), + tx3.clone(), + tx4.clone(), + ])) .unwrap(); - let hashset = HashSet::from([tx1.hash, tx3.hash]); + let hashset = BTreeSet::from([tx1.hash, tx3.hash]); let result = subject.delete_records(&hashset); let remaining_records = subject.retrieve_txs(None); assert_eq!(result, Ok(())); - assert_eq!(remaining_records, vec![tx2, tx4]); + assert_eq!(remaining_records, BTreeSet::from([tx2, tx4])); } #[test] @@ -1027,7 +1121,7 @@ mod tests { .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let result = subject.delete_records(&HashSet::new()); + let result = subject.delete_records(&BTreeSet::new()); assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); } @@ -1043,7 +1137,7 @@ mod tests { .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); let non_existent_hash = make_tx_hash(999); - let hashset = HashSet::from([non_existent_hash]); + let hashset = BTreeSet::from([non_existent_hash]); let result = subject.delete_records(&hashset); @@ -1063,8 +1157,8 @@ mod tests { let present_hash = make_tx_hash(1); let absent_hash = make_tx_hash(2); let tx = TxBuilder::default().hash(present_hash).build(); - subject.insert_new_records(&vec![tx]).unwrap(); - let hashset = HashSet::from([present_hash, absent_hash]); + subject.insert_new_records(&BTreeSet::from([tx])).unwrap(); + let hashset = BTreeSet::from([present_hash, absent_hash]); let result = subject.delete_records(&hashset); @@ -1084,7 +1178,7 @@ mod tests { ); let wrapped_conn = make_read_only_db_connection(home_dir); let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); - let hashes = HashSet::from([make_tx_hash(1)]); + let hashes = BTreeSet::from([make_tx_hash(1)]); let result = subject.delete_records(&hashes); @@ -1116,7 +1210,7 @@ mod tests { let mut tx3 = make_sent_tx(123); tx3.status = TxStatus::Pending(ValidationStatus::Waiting); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone(), tx3.clone()])) .unwrap(); let hashmap = HashMap::from([ ( @@ -1157,17 +1251,26 @@ mod tests { let result = subject.update_statuses(&hashmap); - let updated_txs = subject.retrieve_txs(None); + let updated_txs: Vec<_> = subject.retrieve_txs(None).into_iter().collect(); assert_eq!(result, Ok(())); assert_eq!( updated_txs[0].status, + TxStatus::Confirmed { + block_hash: "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + } + ); + assert_eq!( + updated_txs[1].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, + updated_txs[2].status, TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( @@ -1183,15 +1286,6 @@ mod tests { ) )) ); - 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) } @@ -1250,7 +1344,7 @@ mod tests { let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); let tx3 = TxBuilder::default().hash(make_tx_hash(3)).nonce(3).build(); subject - .insert_new_records(&vec![tx1.clone(), tx2, tx3]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2, tx3])) .unwrap(); let new_tx2 = TxBuilder::default() .hash(make_tx_hash(22)) @@ -1271,11 +1365,11 @@ mod tests { .nonce(3) .build(); - let result = subject.replace_records(&[new_tx2.clone(), new_tx3.clone()]); + let result = subject.replace_records(&BTreeSet::from([new_tx2.clone(), new_tx3.clone()])); let retrieved_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); - assert_eq!(retrieved_txs, vec![tx1, new_tx2, new_tx3]); + assert_eq!(retrieved_txs, BTreeSet::from([tx1, new_tx2, new_tx3])); } #[test] @@ -1294,7 +1388,7 @@ mod tests { let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); let tx3 = TxBuilder::default().hash(make_tx_hash(3)).nonce(3).build(); - let _ = subject.replace_records(&[tx1, tx2, tx3]); + let _ = subject.replace_records(&BTreeSet::from([tx1, tx2, tx3])); let captured_params = prepare_params.lock().unwrap(); let sql = &captured_params[0]; @@ -1326,9 +1420,11 @@ mod tests { let subject = SentPayableDaoReal::new(wrapped_conn); let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); - subject.insert_new_records(&vec![tx1, tx2]).unwrap(); + subject + .insert_new_records(&BTreeSet::from([tx1, tx2])) + .unwrap(); - let result = subject.replace_records(&[]); + let result = subject.replace_records(&BTreeSet::new()); assert_eq!(result, Err(EmptyInput)); } @@ -1346,7 +1442,7 @@ mod tests { let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone()])) .unwrap(); let new_tx2 = TxBuilder::default() .hash(make_tx_hash(22)) @@ -1367,7 +1463,7 @@ mod tests { .nonce(3) .build(); - let result = subject.replace_records(&[new_tx2, new_tx3]); + let result = subject.replace_records(&BTreeSet::from([new_tx2, new_tx3])); assert_eq!( result, @@ -1389,7 +1485,7 @@ mod tests { let subject = SentPayableDaoReal::new(wrapped_conn); let tx = TxBuilder::default().hash(make_tx_hash(1)).nonce(42).build(); - let result = subject.replace_records(&[tx]); + let result = subject.replace_records(&BTreeSet::from([tx])); assert_eq!(result, Err(SentPayableDaoError::NoChange)); } @@ -1404,7 +1500,7 @@ mod tests { let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); let tx = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); - let result = subject.replace_records(&[tx]); + let result = subject.replace_records(&BTreeSet::from([tx])); assert_eq!( result, @@ -1479,12 +1575,93 @@ mod tests { } #[test] - fn tx_record_with_hash_is_implemented_for_sent_tx() { - let sent_tx = make_sent_tx(1234); - let hash = sent_tx.hash; + fn tx_status_ordering_works() { + let tx_status_1 = TxStatus::Pending(ValidationStatus::Waiting); + let tx_status_2 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), + &ValidationFailureClockReal::default(), + ))); + let tx_status_3 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &ValidationFailureClockReal::default(), + ))); + let tx_status_4 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + &ValidationFailureClockReal::default(), + ))); + let tx_status_5 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(1)), + block_number: 123456, + detection: Detection::Normal, + }; + let tx_status_6 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(2)), + block_number: 6543, + detection: Detection::Normal, + }; + let tx_status_7 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(1)), + block_number: 123456, + detection: Detection::Reclaim, + }; + let tx_status_1_identical = tx_status_1.clone(); + let tx_status_6_identical = tx_status_6.clone(); + + let mut set = BTreeSet::new(); + vec![ + tx_status_1.clone(), + tx_status_2.clone(), + tx_status_3.clone(), + tx_status_4.clone(), + tx_status_5.clone(), + tx_status_6.clone(), + tx_status_7.clone(), + ] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); - let hash_from_trait = sent_tx.hash(); + let expected_order = vec![ + tx_status_5, + tx_status_7, + tx_status_6.clone(), + tx_status_3, + tx_status_2, + tx_status_4, + tx_status_1.clone(), + ]; + assert_eq!(set.into_iter().collect::>(), expected_order); + assert_eq!(tx_status_1.cmp(&tx_status_1_identical), Ordering::Equal); + assert_eq!(tx_status_6.cmp(&tx_status_6_identical), Ordering::Equal); + } + + #[test] + fn transaction_trait_methods_for_tx() { + let hash = make_tx_hash(1); + let receiver_address = make_address(1); + let amount_minor = 1000; + let timestamp = 1625247600; + let gas_price_minor = 2000; + let nonce = 42; + let status = TxStatus::Pending(ValidationStatus::Waiting); + + let tx = SentTx { + hash, + receiver_address, + amount_minor, + timestamp, + gas_price_minor, + nonce, + status, + }; - assert_eq!(hash_from_trait, hash); + assert_eq!(tx.receiver_address(), receiver_address); + assert_eq!(tx.hash(), hash); + assert_eq!(tx.amount(), amount_minor); + assert_eq!(tx.timestamp(), timestamp); + assert_eq!(tx.gas_price_wei(), gas_price_minor); + assert_eq!(tx.nonce(), nonce); + assert_eq!(tx.is_failed(), false); } } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index e395aa2de..fca96ed7f 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -6,7 +6,9 @@ use crate::accountant::db_access_objects::failed_payable_dao::{ }; use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplate; use crate::blockchain::errors::validation_status::ValidationStatus; +use crate::blockchain::test_utils::{make_address, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; @@ -36,6 +38,11 @@ impl TxBuilder { self } + pub fn receiver_address(mut self, receiver_address: Address) -> Self { + self.receiver_address_opt = Some(receiver_address); + self + } + pub fn timestamp(mut self, timestamp: i64) -> Self { self.timestamp_opt = Some(timestamp); self @@ -46,6 +53,14 @@ impl TxBuilder { self } + pub fn template(mut self, signable_tx_template: SignableTxTemplate) -> Self { + self.receiver_address_opt = Some(signable_tx_template.receiver_address); + self.amount_opt = Some(signable_tx_template.amount_in_wei); + self.gas_price_wei_opt = Some(signable_tx_template.gas_price_wei); + self.nonce_opt = Some(signable_tx_template.nonce); + self + } + pub fn status(mut self, status: TxStatus) -> Self { self.status_opt = Some(status); self @@ -88,11 +103,26 @@ impl FailedTxBuilder { self } + pub fn receiver_address(mut self, receiver_address: Address) -> Self { + self.receiver_address_opt = Some(receiver_address); + self + } + + pub fn amount(mut self, amount: u128) -> Self { + self.amount_opt = Some(amount); + self + } + pub fn timestamp(mut self, timestamp: i64) -> Self { self.timestamp_opt = Some(timestamp); self } + pub fn gas_price_wei(mut self, gas_price_wei: u128) -> Self { + self.gas_price_wei_opt = Some(gas_price_wei); + self + } + pub fn nonce(mut self, nonce: u64) -> Self { self.nonce_opt = Some(nonce); self @@ -103,6 +133,14 @@ impl FailedTxBuilder { self } + pub fn template(mut self, signable_tx_template: SignableTxTemplate) -> Self { + self.receiver_address_opt = Some(signable_tx_template.receiver_address); + self.amount_opt = Some(signable_tx_template.amount_in_wei); + self.gas_price_wei_opt = Some(signable_tx_template.gas_price_wei); + self.nonce_opt = Some(signable_tx_template.nonce); + self + } + pub fn status(mut self, failure_status: FailureStatus) -> Self { self.status_opt = Some(failure_status); self @@ -113,7 +151,7 @@ impl FailedTxBuilder { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), amount_minor: self.amount_opt.unwrap_or_default(), - timestamp: self.timestamp_opt.unwrap_or_default(), + timestamp: self.timestamp_opt.unwrap_or_else(|| 1719990000), gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), reason: self @@ -126,6 +164,61 @@ impl FailedTxBuilder { } } +pub fn make_failed_tx(n: u32) -> FailedTx { + let n = n % 0xfff; + FailedTxBuilder::default() + .hash(make_tx_hash(n)) + .timestamp(((n * 12) as i64).pow(2)) + .receiver_address(make_address(n.pow(2))) + .gas_price_wei((n as u128).pow(3)) + .amount((n as u128).pow(4)) + .nonce(n as u64) + .build() +} + +pub fn make_sent_tx(n: u32) -> SentTx { + let n = n % 0xfff; + TxBuilder::default() + .hash(make_tx_hash(n)) + .timestamp(((n * 12) as i64).pow(2)) + .template(SignableTxTemplate { + receiver_address: make_address(n), + amount_in_wei: (n as u128).pow(4), + gas_price_wei: (n as u128).pow(3), + nonce: n as u64, + }) + .build() +} + +pub fn assert_on_sent_txs(actual: Vec, expected: Vec) { + assert_eq!(actual.len(), expected.len()); + + actual.iter().zip(expected).for_each(|(st1, st2)| { + assert_eq!(st1.hash, st2.hash); + assert_eq!(st1.receiver_address, st2.receiver_address); + assert_eq!(st1.amount_minor, st2.amount_minor); + assert_eq!(st1.gas_price_minor, st2.gas_price_minor); + assert_eq!(st1.nonce, st2.nonce); + assert_eq!(st1.status, st2.status); + assert!((st1.timestamp - st2.timestamp).abs() < 10); + }) +} + +pub fn assert_on_failed_txs(actual: Vec, expected: Vec) { + assert_eq!(actual.len(), expected.len()); + + actual.iter().zip(expected).for_each(|(f1, f2)| { + assert_eq!(f1.hash, f2.hash); + assert_eq!(f1.receiver_address, f2.receiver_address); + assert_eq!(f1.amount_minor, f2.amount_minor); + assert_eq!(f1.gas_price_minor, f2.gas_price_minor); + assert_eq!(f1.nonce, f2.nonce); + assert_eq!(f1.reason, f2.reason); + assert_eq!(f1.status, f2.status); + assert!((f1.timestamp - f2.timestamp).abs() < 10); + }) +} + pub fn make_read_only_db_connection(home_dir: PathBuf) -> ConnectionWrapperReal { { DbInitializerReal::default() diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 21c9cdc83..98c14ac3e 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -1,7 +1,9 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, gwei_to_wei, sign_conversion}; use crate::database::db_initializer::{ @@ -46,8 +48,45 @@ pub fn from_unix_timestamp(unix_timestamp: i64) -> SystemTime { SystemTime::UNIX_EPOCH + interval } -pub trait TxRecordWithHash { - fn hash(&self) -> TxHash; +pub fn sql_values_of_failed_tx(failed_tx: &FailedTx) -> String { + let amount_checked = checked_conversion::(failed_tx.amount_minor); + let gas_price_wei_checked = checked_conversion::(failed_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); + format!( + "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}', '{}')", + failed_tx.hash, + failed_tx.receiver_address, + amount_high_b, + amount_low_b, + failed_tx.timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + failed_tx.nonce, + failed_tx.reason, + failed_tx.status + ) +} + +pub fn sql_values_of_sent_tx(sent_tx: &SentTx) -> String { + let amount_checked = checked_conversion::(sent_tx.amount_minor); + let gas_price_wei_checked = checked_conversion::(sent_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); + format!( + "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}')", + sent_tx.hash, + sent_tx.receiver_address, + amount_high_b, + amount_low_b, + sent_tx.timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + sent_tx.nonce, + sent_tx.status + ) } pub struct DaoFactoryReal { diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index b9a3d093b..20512a135 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -22,23 +22,22 @@ use crate::accountant::db_access_objects::utils::{ use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, }; -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +use crate::accountant::scanners::payable_scanner::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, }; +use crate::accountant::scanners::payable_scanner::utils::NextScanToRun; use crate::accountant::scanners::pending_payable_scanner::utils::{ - PendingPayableScanResult, Retry, TxHashByTable, + PendingPayableScanResult, 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, StatusReadFromReceiptCheck, + BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck, }; use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::bootstrapper::BootstrapperConfig; @@ -76,7 +75,7 @@ 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; +use std::collections::{BTreeMap, BTreeSet}; #[cfg(test)] use std::default::Default; use std::fmt::Display; @@ -84,7 +83,6 @@ use std::ops::{Div, Mul}; use std::path::Path; use std::rc::Rc; use std::time::SystemTime; -use web3::types::H256; pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours @@ -100,7 +98,7 @@ pub struct Accountant { scan_schedulers: ScanSchedulers, financial_statistics: Rc>, outbound_payments_instructions_sub_opt: Option>, - qualified_payables_sub_opt: Option>, + qualified_payables_sub_opt: Option>, retrieve_transactions_sub_opt: Option>, request_transaction_receipts_sub_opt: Option>, report_inbound_payments_sub_opt: Option>, @@ -145,13 +143,20 @@ pub type TxReceiptResult = Result; #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct TxReceiptsMessage { - pub results: HashMap, + pub results: BTreeMap, pub response_skeleton_opt: Option, } -#[derive(Debug, Message, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum PayableScanType { + New, + Retry, +} + +#[derive(Debug, Message, PartialEq, Eq, Clone)] pub struct SentPayables { - pub payment_procedure_result: Result, PayableTransactionError>, + pub payment_procedure_result: Result, + pub payable_scan_type: PayableScanType, pub response_skeleton_opt: Option, } @@ -319,7 +324,6 @@ impl Handler for Accountant { type 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) => { if let Some(node_to_ui_msg) = ui_msg_opt { @@ -336,34 +340,37 @@ impl Handler for Accountant { .schedule_new_payable_scan(ctx, &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"), - }, + PendingPayableScanResult::PaymentRetryRequired(response_skeleton_opt) => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + PendingPayableScanResult::ProcedureShouldBeRepeated(ui_msg_opt) => { + if let Some(node_to_ui_msg) = ui_msg_opt { + info!( + self.logger, + "Re-running the pending payable scan is recommended, as some \ + parts did not finish last time." + ); + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + // The repetition must be triggered by an external impulse + } else { + self.scan_schedulers + .pending_payable + .schedule(ctx, &self.logger) + } + } }; } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle( - &mut self, - msg: BlockchainAgentWithContextMessage, - _ctx: &mut Self::Context, - ) -> Self::Result { + fn handle(&mut self, msg: PricedTemplatesMessage, _ctx: &mut Self::Context) -> Self::Result { self.handle_payable_payment_setup(msg) } } @@ -375,16 +382,7 @@ impl Handler for Accountant { let scan_result = self.scanners.finish_payable_scan(msg, &self.logger); match scan_result.ui_response_opt { - None => match scan_result.result { - OperationOutcome::NewPendingPayable => self - .scan_schedulers - .pending_payable - .schedule(ctx, &self.logger), - OperationOutcome::Failure => self - .scan_schedulers - .payable - .schedule_new_payable_scan(ctx, &self.logger), - }, + None => self.schedule_next_automatic_scan(scan_result.result, ctx), Some(node_to_ui_msg) => { self.ui_message_sub_opt .as_ref() @@ -393,8 +391,8 @@ impl Handler for Accountant { .expect("UIGateway is dead"); // Externally triggered scans are not allowed to provoke an unwinding scan sequence - // with intervals. The only exception is the PendingPayableScanner and retry- - // payable scanner, which are ever meant to run in a tight tandem. + // with intervals. The only exception is the PendingPayableScanner that is always + // followed by the retry-payable scanner in a tight tandem. } } } @@ -597,7 +595,7 @@ impl Accountant { report_routing_service_provided: recipient!(addr, ReportRoutingServiceProvidedMessage), report_exit_service_provided: recipient!(addr, ReportExitServiceProvidedMessage), report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), - report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), + report_payable_payments_setup: recipient!(addr, PricedTemplatesMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), report_transaction_status: recipient!(addr, TxReceiptsMessage), @@ -810,7 +808,7 @@ impl Accountant { }) } - fn handle_payable_payment_setup(&mut self, msg: BlockchainAgentWithContextMessage) { + fn handle_payable_payment_setup(&mut self, msg: PricedTemplatesMessage) { let blockchain_bridge_instructions = match self .scanners .try_skipping_payable_adjustment(msg, &self.logger) @@ -963,7 +961,7 @@ impl Accountant { &mut self, response_skeleton_opt: Option, ) -> ScanReschedulingAfterEarlyStop { - let result: Result = + let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( consuming_wallet, @@ -996,7 +994,7 @@ impl Accountant { &mut self, response_skeleton_opt: Option, ) { - let result: Result = + let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( consuming_wallet, @@ -1159,12 +1157,35 @@ impl Accountant { } } + fn schedule_next_automatic_scan( + &self, + next_scan_to_run: NextScanToRun, + ctx: &mut Context, + ) { + match next_scan_to_run { + NextScanToRun::PendingPayableScan => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + NextScanToRun::NewPayableScan => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + NextScanToRun::RetryPayableScan => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, None, &self.logger), + } + } + 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)) + join_with_commas(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } - match self.sent_payable_dao.insert_new_records(&msg.new_sent_txs) { + let sent_txs: BTreeSet = msg.new_sent_txs.iter().cloned().collect(); + + match self.sent_payable_dao.insert_new_records(&sent_txs) { Ok(_) => debug!( self.logger, "Registered new pending payables for: {}", @@ -1212,11 +1233,23 @@ impl PendingPayableId { } } -pub fn comma_joined_stringifiable(collection: &[T], stringify: F) -> String +pub fn join_with_separator(collection: I, stringify: F, separator: &str) -> String +where + F: Fn(&T) -> String, + I: IntoIterator, +{ + collection + .into_iter() + .map(|item| stringify(&item)) + .join(separator) +} + +pub fn join_with_commas(collection: I, stringify: F) -> String where - F: FnMut(&T) -> String, + F: Fn(&T) -> String, + I: IntoIterator, { - collection.iter().map(stringify).join(", ") + join_with_separator(collection, stringify, ", ") } pub fn sign_conversion>(num: T) -> Result { @@ -1258,19 +1291,24 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::{ Detection, SentPayableDaoError, TxStatus, }; + use crate::accountant::db_access_objects::test_utils::{ + make_failed_tx, make_sent_tx, TxBuilder, + }; use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, }; use crate::accountant::payment_adjuster::Adjustment; - use crate::accountant::scanners::payable_scanner_extension::msgs::UnpricedQualifiedPayables; - use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::pending_payable_scanner::utils::{TxByTable, TxHashByTable}; + use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::{ + make_priced_new_tx_templates, make_retry_tx_template, + }; + use crate::accountant::scanners::payable_scanner::utils::PayableScanResult; + use crate::accountant::scanners::pending_payable_scanner::utils::TxByTable; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, }; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ - OperationOutcome, PayableScanResult, - }; use crate::accountant::scanners::test_utils::{ MarkScanner, NewPayableScanDynIntervalComputerMock, PendingPayableCacheMock, ReplacementType, RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement, @@ -1280,18 +1318,16 @@ mod tests { ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_failed_tx, make_payable_account, - make_qualified_and_unqualified_payables, make_sent_tx, make_transaction_block, - BannedDaoFactoryMock, ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, + bc_from_earning_wallet, bc_from_wallets, make_payable_account, + make_qualified_and_unqualified_payables, make_transaction_block, BannedDaoFactoryMock, + ConfigDaoFactoryMock, DaoWithDestination, FailedPayableDaoFactoryMock, FailedPayableDaoMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, - PayableScannerBuilder, PaymentAdjusterMock, PendingPayableScannerBuilder, - ReceivableDaoFactoryMock, ReceivableDaoMock, SentPayableDaoFactoryMock, SentPayableDaoMock, - }; - use crate::accountant::test_utils::{ - make_priced_qualified_payables, make_unpriced_qualified_payables_for_retry_mode, + PaymentAdjusterMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, + ReceivableDaoMock, SentPayableDaoFactoryMock, SentPayableDaoMock, }; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; + use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; use crate::blockchain::blockchain_interface::data_structures::{ StatusReadFromReceiptCheck, TxBlock, }; @@ -1344,11 +1380,13 @@ mod tests { use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; use std::any::TypeId; + use std::collections::BTreeSet; use std::ops::Sub; use std::sync::Arc; use std::sync::Mutex; use std::time::{Duration, UNIX_EPOCH}; use std::vec; + use web3::types::H256; impl Handler> for Accountant { type Result = (); @@ -1372,8 +1410,8 @@ mod tests { fn new_calls_factories_properly() { let config = make_bc_with_defaults(DEFAULT_CHAIN); let 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 sent_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![])); @@ -1389,7 +1427,8 @@ mod tests { .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; + .make_result(FailedPayableDaoMock::new()) // For Payable Scanner + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::new())); // For PendingPayableScanner; let receivable_dao_factory = ReceivableDaoFactoryMock::new() .make_params(&receivable_dao_factory_params_arc) .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1423,7 +1462,7 @@ mod tests { ); assert_eq!( *failed_payable_dao_factory_params_arc.lock().unwrap(), - vec![()] + vec![(), ()] ); assert_eq!( *receivable_dao_factory_params_arc.lock().unwrap(), @@ -1443,16 +1482,17 @@ mod tests { .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()), // For PendingPayable Scanner ); + let failed_payable_dao_factory = Box::new( + FailedPayableDaoFactoryMock::new() + .make_result(FailedPayableDaoMock::new()) // For Payable Scanner + .make_result(FailedPayableDaoMock::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; + .make_result(SentPayableDaoMock::new()), + ); // For PendingPayable Scanner let receivable_dao_factory = Box::new( ReceivableDaoFactoryMock::new() .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1581,7 +1621,7 @@ mod tests { pending_payable_opt: None, }; let payable_dao = - PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); + PayableDaoMock::new().retrieve_payables_result(vec![payable_account.clone()]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) @@ -1589,7 +1629,7 @@ mod tests { .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge - .system_stop_conditions(match_lazily_every_type_id!(QualifiedPayablesMessage)); + .system_stop_conditions(match_lazily_every_type_id!(InitialTemplatesMessage)); let blockchain_bridge_addr = blockchain_bridge.start(); // Important subject.scan_schedulers.automatic_scans_enabled = false; @@ -1612,10 +1652,11 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let expected_new_tx_templates = NewTxTemplates::from(&vec![payable_account]); assert_eq!( - blockchain_bridge_recording.get_record::(0), - &QualifiedPayablesMessage { - qualified_payables: UnpricedQualifiedPayables::from(vec![payable_account]), + blockchain_bridge_recording.get_record::(0), + &InitialTemplatesMessage { + initial_templates: Either::Left(expected_new_tx_templates), consuming_wallet, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1626,29 +1667,27 @@ mod tests { } #[test] - fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { + fn sent_payables_with_response_skeleton_results_in_scan_response_to_ui_gateway() { let config = bc_from_earning_wallet(make_wallet("earning_wallet")); - 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() - .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) + let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); + let subject = AccountantBuilder::default() .payable_daos(vec![ForPayableScanner(payable_dao)]) + .sent_payable_daos(vec![DaoWithDestination::ForPayableScanner( + sent_payable_dao, + )]) .bootstrapper_config(config) .build(); - // Making sure we would get a panic if another scan was scheduled - subject.scan_schedulers.pending_payable.handle = - Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { - recipient_wallet: make_wallet("blah"), - hash: tx_hash, - })]), + let sent_payables = SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![make_sent_tx(1)], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::New, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1656,7 +1695,7 @@ mod tests { }; subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - subject_addr.try_send(sent_payable).unwrap(); + subject_addr.try_send(sent_payables).unwrap(); System::current().stop(); system.run(); @@ -1703,12 +1742,12 @@ mod tests { let system = System::new("test"); let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp); - let qualified_payables = make_priced_qualified_payables(vec![ + let priced_new_templates = make_priced_new_tx_templates(vec![ (account_1, 1_000_000_001), (account_2, 1_000_000_002), ]); - let msg = BlockchainAgentWithContextMessage { - qualified_payables: qualified_payables.clone(), + let msg = PricedTemplatesMessage { + priced_templates: Either::Left(priced_new_templates.clone()), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1723,8 +1762,8 @@ mod tests { let (blockchain_agent_with_context_msg_actual, logger_clone) = is_adjustment_required_params.remove(0); assert_eq!( - blockchain_agent_with_context_msg_actual.qualified_payables, - qualified_payables.clone() + blockchain_agent_with_context_msg_actual.priced_templates, + Either::Left(priced_new_templates.clone()) ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -1744,8 +1783,8 @@ mod tests { let payments_instructions = blockchain_bridge_recording.get_record::(0); assert_eq!( - payments_instructions.affordable_accounts, - qualified_payables + payments_instructions.priced_templates, + Either::Left(priced_new_templates.clone()) ); assert_eq!( payments_instructions.response_skeleton_opt, @@ -1810,12 +1849,12 @@ mod tests { let agent_id_stamp_first_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_first_phase); - let initial_unadjusted_accounts = make_priced_qualified_payables(vec![ + let initial_unadjusted_accounts = make_priced_new_tx_templates(vec![ (unadjusted_account_1.clone(), 111_222_333), (unadjusted_account_2.clone(), 222_333_444), ]); - let msg = BlockchainAgentWithContextMessage { - qualified_payables: initial_unadjusted_accounts.clone(), + let msg = PricedTemplatesMessage { + priced_templates: Either::Left(initial_unadjusted_accounts.clone()), agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; @@ -1824,12 +1863,12 @@ mod tests { let agent_id_stamp_second_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_second_phase); - let affordable_accounts = make_priced_qualified_payables(vec![ + let affordable_accounts = make_priced_new_tx_templates(vec![ (adjusted_account_1.clone(), 111_222_333), (adjusted_account_2.clone(), 222_333_444), ]); let payments_instructions = OutboundPaymentsInstructions { - affordable_accounts: affordable_accounts.clone(), + priced_templates: Either::Left(affordable_accounts.clone()), agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; @@ -1862,8 +1901,8 @@ mod tests { assert_eq!( actual_prepared_adjustment .original_setup_msg - .qualified_payables, - initial_unadjusted_accounts + .priced_templates, + Either::Left(initial_unadjusted_accounts) ); assert_eq!( actual_prepared_adjustment @@ -1888,8 +1927,8 @@ mod tests { agent_id_stamp_second_phase ); assert_eq!( - payments_instructions.affordable_accounts, - affordable_accounts + payments_instructions.priced_templates, + Either::Left(affordable_accounts) ); assert_eq!( payments_instructions.response_skeleton_opt, @@ -1909,8 +1948,10 @@ mod tests { }); 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 sent_payable_dao = + SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::from([sent_tx])); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) @@ -2000,7 +2041,7 @@ mod tests { block_number: 78901234.into(), }; let tx_receipts_msg = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + results: btreemap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( StatusReadFromReceiptCheck::Succeeded(tx_block), )], response_skeleton_opt, @@ -2042,8 +2083,7 @@ mod tests { let mut payable_account = make_payable_account(123); payable_account.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei); payable_account.last_paid_timestamp = from_unix_timestamp(past_timestamp_unix); - let payable_dao = - PayableDaoMock::default().non_pending_payables_result(vec![payable_account]); + let payable_dao = PayableDaoMock::default().retrieve_payables_result(vec![payable_account]); let subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(make_paying_wallet(b"consuming")) @@ -2188,28 +2228,41 @@ 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, - }); 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 payable_dao_for_payable_scanner = + PayableDaoMock::default().retrieve_payables_result(vec![]); + let payable_dao_for_pending_payable_scanner = + PayableDaoMock::default().transactions_confirmed_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()]) + let sent_payable_dao_for_payable_scanner = SentPayableDaoMock::default() + // TODO should be removed with GH-701 + .insert_new_records_result(Ok(())); + let sent_payable_dao_for_pending_payable_scanner = SentPayableDaoMock::default() + .retrieve_txs_result(BTreeSet::from([sent_tx.clone()])) .delete_records_params(&delete_records_params_arc) .delete_records_result(Ok(())); - let failed_payable_dao = FailedPayableDaoMock::default() + let failed_tx = make_failed_tx(123); + let failed_payable_dao_for_payable_scanner = + FailedPayableDaoMock::default().retrieve_txs_result(btreeset!(failed_tx)); + let failed_payable_dao_for_pending_payable_scanner = 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)]) - .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) - .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) + .payable_daos(vec![ + ForPayableScanner(payable_dao_for_payable_scanner), + ForPendingPayableScanner(payable_dao_for_pending_payable_scanner), + ]) + .sent_payable_daos(vec![ + ForPayableScanner(sent_payable_dao_for_payable_scanner), + ForPendingPayableScanner(sent_payable_dao_for_pending_payable_scanner), + ]) + .failed_payable_daos(vec![ + ForPayableScanner(failed_payable_dao_for_payable_scanner), + ForPendingPayableScanner(failed_payable_dao_for_pending_payable_scanner), + ]) .build(); subject.scan_schedulers.automatic_scans_enabled = false; let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -2222,27 +2275,31 @@ mod tests { .build_and_provide_addresses(); let subject_addr = subject.start(); let system = System::new("test"); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 4555, + context_id: 5566, + }); let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + results: btreemap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( StatusReadFromReceiptCheck::Reverted ),], response_skeleton_opt }, &subject_addr ); + let sent_payables = SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![make_sent_tx(1)], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::New, + response_skeleton_opt, + }; let second_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( - QualifiedPayablesMessage, - SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( - PendingPayable { - recipient_wallet: make_wallet("abc"), - hash: make_tx_hash(789) - } - )]), - response_skeleton_opt - }, + InitialTemplatesMessage, + sent_payables, &subject_addr ); peer_addresses @@ -2262,9 +2319,12 @@ mod tests { 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]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([expected_failed_tx])] + ); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![tx_hash]]); + assert_eq!(*delete_records_params, vec![BTreeSet::from([tx_hash])]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2282,10 +2342,9 @@ mod tests { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); - let (qualified_payables, _, all_non_pending_payables) = + let (qualified_payables, _, retrieved_payables) = make_qualified_and_unqualified_payables(now, &payment_thresholds); - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let payable_dao = PayableDaoMock::new().retrieve_payables_result(retrieved_payables); let system = System::new("accountant_sends_qualified_payable_msg_when_qualified_payable_found"); let consuming_wallet = make_paying_wallet(b"consuming"); @@ -2317,11 +2376,12 @@ mod tests { system.run(); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recorder.len(), 1); - let message = blockchain_bridge_recorder.get_record::(0); + let message = blockchain_bridge_recorder.get_record::(0); + let expected_new_tx_templates = NewTxTemplates::from(&qualified_payables); assert_eq!( message, - &QualifiedPayablesMessage { - qualified_payables: UnpricedQualifiedPayables::from(qualified_payables), + &InitialTemplatesMessage { + initial_templates: Either::Left(expected_new_tx_templates), consuming_wallet, response_skeleton_opt: None, } @@ -2396,11 +2456,10 @@ mod tests { .build(); let consuming_wallet = make_wallet("abc"); subject.consuming_wallet_opt = Some(consuming_wallet.clone()); - let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: make_unpriced_qualified_payables_for_retry_mode(vec![ - (make_payable_account(789), 111_222_333), - (make_payable_account(888), 222_333_444), - ]), + let retry_tx_templates = + RetryTxTemplates(vec![make_retry_tx_template(1), make_retry_tx_template(2)]); + let qualified_payables_msg = InitialTemplatesMessage { + initial_templates: Either::Right(retry_tx_templates), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; @@ -2448,7 +2507,7 @@ mod tests { start_scan_params ); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - let message = blockchain_bridge_recorder.get_record::(0); + let message = blockchain_bridge_recorder.get_record::(0); assert_eq!(message, &qualified_payables_msg); assert_eq!(blockchain_bridge_recorder.len(), 1); assert_using_the_same_logger(&actual_logger, test_name, None) @@ -2804,16 +2863,14 @@ mod tests { let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); let tx_hash = make_tx_hash(456); + let retry_tx_templates = RetryTxTemplates(vec![make_retry_tx_template(1)]); let payable_scanner = ScannerMock::new() .scan_started_at_result(None) .scan_started_at_result(None) // These values belong to the RetryPayableScanner .start_scan_params(&scan_params.payable_start_scan) - .start_scan_result(Ok(QualifiedPayablesMessage { - qualified_payables: make_unpriced_qualified_payables_for_retry_mode(vec![( - make_payable_account(123), - 555_666_777, - )]), + .start_scan_result(Ok(InitialTemplatesMessage { + initial_templates: Either::Right(retry_tx_templates), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, })) @@ -2821,7 +2878,7 @@ mod tests { // Important .finish_scan_result(PayableScanResult { ui_response_opt: None, - result: OperationOutcome::NewPendingPayable, + result: NextScanToRun::PendingPayableScan, }); let pending_payable_scanner = ScannerMock::new() .scan_started_at_result(None) @@ -2831,9 +2888,7 @@ mod tests { response_skeleton_opt: None, })) .finish_scan_params(&scan_params.pending_payable_finish_scan) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( - Either::Left(Retry::RetryPayments), - )); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(None)); let receivable_scanner = ScannerMock::new() .scan_started_at_result(None) .start_scan_params(&scan_params.receivable_start_scan) @@ -2851,16 +2906,21 @@ mod tests { let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); let expected_tx_receipts_msg = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok( + results: btreemap![TxHashByTable::SentPayable(tx_hash) => Ok( StatusReadFromReceiptCheck::Reverted, )], response_skeleton_opt: None, }; + let sent_tx = TxBuilder::default() + .hash(make_tx_hash(890)) + .receiver_address(make_wallet("bcd").address()) + .build(); let expected_sent_payables = SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { - recipient_wallet: make_wallet("bcd"), - hash: make_tx_hash(890), - })]), + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![sent_tx], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::New, response_skeleton_opt: None, }; let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( @@ -2869,7 +2929,7 @@ mod tests { &subject_addr ); let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( - QualifiedPayablesMessage, + InitialTemplatesMessage, expected_sent_payables.clone(), &subject_addr ); @@ -2953,7 +3013,7 @@ mod tests { ReceivedPayments, Option, >, - payable_scanner: ScannerMock, + payable_scanner: ScannerMock, ) -> (Accountant, Duration, Duration) { let mut subject = make_subject_and_inject_scanners( test_name, @@ -3001,7 +3061,7 @@ mod tests { test_name: &str, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, config: BootstrapperConfig, - payable_scanner: ScannerMock, + payable_scanner: ScannerMock, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, TxReceiptsMessage, @@ -3066,7 +3126,7 @@ mod tests { ReceivedPayments, Option, >, - payable_scanner: ScannerMock, + payable_scanner: ScannerMock, ) -> Accountant { let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) @@ -3368,8 +3428,9 @@ mod tests { #[test] fn initial_pending_payable_scan_if_some_payables_found() { let sent_payable_dao = - SentPayableDaoMock::default().retrieve_txs_result(vec![make_sent_tx(789)]); - let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); + SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::from([make_sent_tx(789)])); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) @@ -3395,8 +3456,9 @@ mod tests { #[test] fn initial_pending_payable_scan_if_no_payables_found() { - let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![]); - let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) @@ -3565,23 +3627,28 @@ mod tests { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge.start(); let payable_account = make_payable_account(123); - let unpriced_qualified_payables = - UnpricedQualifiedPayables::from(vec![payable_account.clone()]); - let priced_qualified_payables = - make_priced_qualified_payables(vec![(payable_account, 123_456_789)]); + let new_tx_templates = NewTxTemplates::from(&vec![payable_account.clone()]); + let priced_new_tx_templates = + make_priced_new_tx_templates(vec![(payable_account, 123_456_789)]); let consuming_wallet = make_paying_wallet(b"consuming"); - let counter_msg_1 = BlockchainAgentWithContextMessage { - qualified_payables: priced_qualified_payables.clone(), + let counter_msg_1 = PricedTemplatesMessage { + priced_templates: Either::Left(priced_new_tx_templates.clone()), agent: Box::new(BlockchainAgentMock::default()), 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 sent_tx = TxBuilder::default() + .hash(transaction_hash) + .receiver_address(creditor_wallet.address()) + .build(); let counter_msg_2 = SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( - PendingPayable::new(creditor_wallet, transaction_hash), - )]), + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![sent_tx], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::New, response_skeleton_opt: None, }; let tx_status = StatusReadFromReceiptCheck::Succeeded(TxBlock { @@ -3589,15 +3656,15 @@ mod tests { block_number: 4444444444u64.into(), }); let counter_msg_3 = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], + results: btreemap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], response_skeleton_opt: None, }; let request_transaction_receipts_msg = RequestTransactionReceipts { tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, }; - let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: unpriced_qualified_payables, + let qualified_payables_msg = InitialTemplatesMessage { + initial_templates: Either::Left(new_tx_templates), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; @@ -3615,7 +3682,7 @@ mod tests { let subject_addr = subject.start(); let set_up_counter_msgs = SetUpCounterMsgs::new(vec![ setup_for_counter_msg_triggered_via_type_id!( - QualifiedPayablesMessage, + InitialTemplatesMessage, counter_msg_1, &subject_addr ), @@ -3662,13 +3729,13 @@ mod tests { assert_using_the_same_logger(&logger, test_name, Some("start scan pending payable")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let actual_qualified_payables_msg = - blockchain_bridge_recording.get_record::(0); + blockchain_bridge_recording.get_record::(0); assert_eq!(actual_qualified_payables_msg, &qualified_payables_msg); let actual_outbound_payment_instructions_msg = blockchain_bridge_recording.get_record::(1); assert_eq!( - actual_outbound_payment_instructions_msg.affordable_accounts, - priced_qualified_payables + actual_outbound_payment_instructions_msg.priced_templates, + Either::Left(priced_new_tx_templates) ); let actual_requested_receipts_1 = blockchain_bridge_recording.get_record::(2); @@ -3700,7 +3767,7 @@ mod tests { test_name: &str, blockchain_bridge_addr: &Addr, consuming_wallet: &Wallet, - qualified_payables_msg: &QualifiedPayablesMessage, + qualified_payables_msg: &InitialTemplatesMessage, request_transaction_receipts: &RequestTransactionReceipts, start_scan_pending_payable_params_arc: &Arc< Mutex, Logger, String)>>, @@ -3726,7 +3793,7 @@ mod tests { .start_scan_result(Ok(qualified_payables_msg.clone())) .finish_scan_result(PayableScanResult { ui_response_opt: None, - result: OperationOutcome::NewPendingPayable, + result: NextScanToRun::PendingPayableScan, }); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { @@ -3895,8 +3962,8 @@ mod tests { }, ]; let payable_dao = PayableDaoMock::new() - .non_pending_payables_result(accounts.clone()) - .non_pending_payables_result(vec![]); + .retrieve_payables_result(accounts.clone()) + .retrieve_payables_result(vec![]); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); let system = System::new( "scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve", @@ -3976,7 +4043,7 @@ mod tests { }, ]; let payable_dao = - PayableDaoMock::default().non_pending_payables_result(qualified_payables.clone()); + PayableDaoMock::default().retrieve_payables_result(qualified_payables.clone()); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge.start(); let system = @@ -4000,11 +4067,12 @@ mod tests { System::current().stop(); system.run(); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); - let message = blockchain_bridge_recordings.get_record::(0); + let message = blockchain_bridge_recordings.get_record::(0); + let new_tx_templates = NewTxTemplates::from(&qualified_payables); assert_eq!( message, - &QualifiedPayablesMessage { - qualified_payables: UnpricedQualifiedPayables::from(qualified_payables), + &InitialTemplatesMessage { + initial_templates: Either::Left(new_tx_templates), consuming_wallet, response_skeleton_opt: None, } @@ -4045,8 +4113,8 @@ mod tests { let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_lazily_every_type_id!( - QualifiedPayablesMessage, - QualifiedPayablesMessage + InitialTemplatesMessage, + InitialTemplatesMessage )) .start(); let qualified_payables_sub = blockchain_bridge_addr.clone().recipient(); @@ -4055,8 +4123,8 @@ mod tests { let payable_1 = qualified_payables.remove(0); let payable_2 = qualified_payables.remove(0); let payable_dao = PayableDaoMock::new() - .non_pending_payables_result(vec![payable_1.clone()]) - .non_pending_payables_result(vec![payable_2.clone()]); + .retrieve_payables_result(vec![payable_1.clone()]) + .retrieve_payables_result(vec![payable_2.clone()]); let mut config = bc_from_earning_wallet(make_wallet("mine")); config.payment_thresholds_opt = Some(payment_thresholds); let system = System::new(test_name); @@ -4097,7 +4165,8 @@ mod tests { // the first message. Now we reset the state by ending the first scan by a failure and see // that the third scan request is going to be accepted willingly again. addr.try_send(SentPayables { - payment_procedure_result: Err(PayableTransactionError::Signing("blah".to_string())), + payment_procedure_result: Err("blah".to_string()), + payable_scan_type: PayableScanType::New, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1122, context_id: 7788, @@ -4107,13 +4176,13 @@ mod tests { addr.try_send(message_after.clone()).unwrap(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording.lock().unwrap(); - let first_message_actual: &QualifiedPayablesMessage = + let first_message_actual: &InitialTemplatesMessage = blockchain_bridge_recording.get_record(0); assert_eq!( first_message_actual.response_skeleton_opt, message_before.response_skeleton_opt ); - let second_message_actual: &QualifiedPayablesMessage = + let second_message_actual: &InitialTemplatesMessage = blockchain_bridge_recording.get_record(1); assert_eq!( second_message_actual.response_skeleton_opt, @@ -4267,7 +4336,7 @@ mod tests { let now = SystemTime::now(); let bootstrapper_config = bc_from_earning_wallet(make_wallet("hi")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); - let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); + let payable_dao_mock = PayableDaoMock::new().retrieve_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc) .more_money_receivable_result(Ok(())); @@ -4314,7 +4383,7 @@ mod tests { let consuming_wallet = make_wallet("our consuming wallet"); let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("our earning wallet")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); - let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); + let payable_dao_mock = PayableDaoMock::new().retrieve_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() @@ -4359,7 +4428,7 @@ mod tests { let earning_wallet = make_wallet("our earning wallet"); let config = bc_from_earning_wallet(earning_wallet.clone()); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); - let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); + let payable_dao_mock = PayableDaoMock::new().retrieve_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() @@ -4404,7 +4473,7 @@ mod tests { let now = SystemTime::now(); let config = bc_from_earning_wallet(make_wallet("hi")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); - let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); + let payable_dao_mock = PayableDaoMock::new().retrieve_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc) .more_money_receivable_result(Ok(())); @@ -4451,7 +4520,7 @@ mod tests { let consuming_wallet = make_wallet("my consuming wallet"); let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("my earning wallet")); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); - let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); + let payable_dao_mock = PayableDaoMock::new().retrieve_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() @@ -4496,7 +4565,7 @@ mod tests { let earning_wallet = make_wallet("my earning wallet"); let config = bc_from_earning_wallet(earning_wallet.clone()); let more_money_receivable_parameters_arc = Arc::new(Mutex::new(vec![])); - let payable_dao_mock = PayableDaoMock::new().non_pending_payables_result(vec![]); + let payable_dao_mock = PayableDaoMock::new().retrieve_payables_result(vec![]); let receivable_dao_mock = ReceivableDaoMock::new() .more_money_receivable_parameters(&more_money_receivable_parameters_arc); let subject = AccountantBuilder::default() @@ -4631,7 +4700,7 @@ mod tests { ) -> Arc>> { let more_money_payable_parameters_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new() - .non_pending_payables_result(vec![]) + .retrieve_payables_result(vec![]) .more_money_payable_result(Ok(())) .more_money_payable_params(more_money_payable_parameters_arc.clone()); let subject = AccountantBuilder::default() @@ -4891,18 +4960,23 @@ mod tests { #[test] fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { - let get_tx_identifiers_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 inserted_new_records_params_arc = Arc::new(Mutex::new(vec![])); let expected_hash = H256::from("transaction_hash".keccak256()); - let expected_rowid = 45623; - 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 payable_dao = PayableDaoMock::new(); + let sent_payable_dao = SentPayableDaoMock::new() + .insert_new_records_params(&inserted_new_records_params_arc) + .insert_new_records_result(Ok(())); + // let expected_rowid = 45623; + // 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)]) .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .build(); let pending_payable_interval = Duration::from_millis(55); @@ -4911,11 +4985,19 @@ mod tests { NotifyLaterHandleMock::default() .notify_later_params(&pending_payable_notify_later_params_arc), ); - let expected_payable = PendingPayable::new(expected_wallet.clone(), expected_hash.clone()); + 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.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + let expected_tx = TxBuilder::default().hash(expected_hash.clone()).build(); let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( - expected_payable.clone(), - )]), + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![expected_tx.clone()], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::New, response_skeleton_opt: None, }; let addr = subject.start(); @@ -4924,28 +5006,88 @@ mod tests { System::current().stop(); system.run(); - let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); - assert_eq!(*get_tx_identifiers_params, vec![hashset!(expected_hash)]); + let inserted_new_records_params = inserted_new_records_params_arc.lock().unwrap(); + assert_eq!( + inserted_new_records_params[0], + BTreeSet::from([expected_tx]) + ); let pending_payable_notify_later_params = pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( *pending_payable_notify_later_params, vec![(ScanForPendingPayables::default(), pending_payable_interval)] ); - // The accountant is unbound here. We don't use the bind message. It means we can prove - // none of those other scan requests could have been sent (especially ScanForNewPayables, - // ScanForRetryPayables) } #[test] - fn no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted( - ) { + fn accountant_finishes_processing_of_retry_payables_and_schedules_pending_payable_scanner() { + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let inserted_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let expected_hash = H256::from("transaction_hash".keccak256()); + let payable_dao = PayableDaoMock::new(); + let sent_payable_dao = SentPayableDaoMock::new() + .insert_new_records_params(&inserted_new_records_params_arc) + .insert_new_records_result(Ok(())); + let failed_payble_dao = FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::new()); + let system = System::new( + "accountant_finishes_processing_of_retry_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)]) + .failed_payable_daos(vec![ForPayableScanner(failed_payble_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; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), + ); + 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.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + let expected_tx = TxBuilder::default().hash(expected_hash.clone()).build(); + let sent_payable = SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![expected_tx.clone()], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::Retry, + response_skeleton_opt: None, + }; + let addr = subject.start(); + + addr.try_send(sent_payable).expect("unexpected actix error"); + + System::current().stop(); + system.run(); + let inserted_new_records_params = inserted_new_records_params_arc.lock().unwrap(); + assert_eq!( + inserted_new_records_params[0], + BTreeSet::from([expected_tx]) + ); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payable_notify_later_params, + vec![(ScanForPendingPayables::default(), pending_payable_interval)] + ); + } + + #[test] + fn retry_payable_scan_is_requested_to_be_repeated() { init_test_logging(); - let test_name = "no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted"; + let test_name = "retry_payable_scan_is_requested_to_be_repeated"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); - let payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); + let consuming_wallet = make_paying_wallet(b"paying wallet"); let mut subject = AccountantBuilder::default() + .consuming_wallet(consuming_wallet.clone()) .logger(Logger::new(test_name)) .build(); subject @@ -4955,28 +5097,23 @@ mod tests { .finish_scan_params(&finish_scan_params_arc) .finish_scan_result(PayableScanResult { ui_response_opt: None, - result: OperationOutcome::Failure, + result: NextScanToRun::RetryPayableScan, }), ))); - // Important. Otherwise, the scan would've been handled through a different endpoint and - // gone for a very long time - subject - .scan_schedulers - .payable - .inner - .lock() - .unwrap() - .last_new_payable_scan_timestamp = SystemTime::now(); - subject.scan_schedulers.payable.new_payable_notify_later = Box::new( - NotifyLaterHandleMock::default().notify_later_params(&payable_notify_later_params_arc), - ); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + 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 sent_payable = SentPayables { - payment_procedure_result: Err(PayableTransactionError::Sending { - msg: "booga".to_string(), - hashes: hashset![make_tx_hash(456)], + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![], + failed_txs: vec![make_failed_tx(1), make_failed_tx(2)], }), + payable_scan_type: PayableScanType::New, response_skeleton_opt: None, }; let addr = subject.start(); @@ -4990,21 +5127,22 @@ mod tests { let (actual_sent_payable, logger) = finish_scan_params.remove(0); assert_eq!(actual_sent_payable, sent_payable,); assert_using_the_same_logger(&logger, test_name, None); - let mut payable_notify_later_params = payable_notify_later_params_arc.lock().unwrap(); - let (scheduled_msg, _interval) = payable_notify_later_params.remove(0); - assert_eq!(scheduled_msg, ScanForNewPayables::default()); + let mut payable_notify_params = retry_payable_notify_params_arc.lock().unwrap(); + let scheduled_msg = payable_notify_params.remove(0); + assert_eq!(scheduled_msg, ScanForRetryPayables::default()); assert!( - payable_notify_later_params.is_empty(), + payable_notify_params.is_empty(), "Should be empty but {:?}", - payable_notify_later_params + payable_notify_params ); } #[test] - fn accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed() { + fn accountant_in_automatic_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed() + { init_test_logging(); let test_name = - "accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed"; + "accountant_in_automatic_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default() @@ -5012,9 +5150,7 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( - Either::Left(Retry::RetryPayments), - )); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(None)); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( @@ -5039,11 +5175,7 @@ mod tests { status: StatusReadFromReceiptCheck::Reverted, }, ]); - let response_skeleton_opt = Some(ResponseSkeleton { - client_id: 45, - context_id: 7, - }); - msg.response_skeleton_opt = response_skeleton_opt; + msg.response_skeleton_opt = None; let subject_addr = subject.start(); subject_addr.try_send(msg.clone()).unwrap(); @@ -5057,17 +5189,17 @@ mod tests { assert_eq!( *retry_payable_notify_params, vec![ScanForRetryPayables { - response_skeleton_opt + response_skeleton_opt: None }] ); assert_using_the_same_logger(&logger, test_name, None) } #[test] - fn accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed() { + fn accountant_reschedules_pending_p_scanner_in_automatic_mode_after_receipt_fetching_failed() { init_test_logging(); let test_name = - "accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed"; + "accountant_reschedules_pending_p_scanner_in_automatic_mode_after_receipt_fetching_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() @@ -5075,9 +5207,7 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( - Either::Left(Retry::RetryTxStatusCheckOnly), - )); + .finish_scan_result(PendingPayableScanResult::ProcedureShouldBeRepeated(None)); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( @@ -5097,7 +5227,7 @@ mod tests { ); let system = System::new(test_name); let msg = TxReceiptsMessage { - results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + results: btreemap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), response_skeleton_opt: None, }; let subject_addr = subject.start(); @@ -5124,39 +5254,96 @@ mod tests { } #[test] - fn accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected() - { + fn accountant_reschedules_pending_p_scanner_in_manual_mode_after_receipt_fetching_failed() { init_test_logging(); + let test_name = + "accountant_reschedules_pending_p_scanner_in_manual_mode_after_receipt_fetching_failed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let ui_gateway = ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let expected_node_to_ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(1234), + body: UiScanResponse {}.tmb(54), + }; + 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::ProcedureShouldBeRepeated(Some( + expected_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()); + let interval = Duration::from_secs(20); + subject.scan_schedulers.pending_payable.interval = interval; + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.ui_message_sub_opt = Some(ui_gateway.start().recipient()); + let system = System::new(test_name); + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 54, + }; + let msg = TxReceiptsMessage { + results: btreemap!(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 node_to_ui_msg = ui_gateway_recording.get_record::(0); + assert_eq!(node_to_ui_msg, &expected_node_to_ui_msg); + assert_eq!(ui_gateway_recording.len(), 1); + assert_using_the_same_logger(&logger, test_name, None); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Re-running the pending payable scan is recommended, as some parts \ + did not finish last time." + )); + } + + #[test] + fn accountant_in_manual_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed() { + init_test_logging(); let test_name = - "accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected"; + "accountant_in_manual_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed"; + let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); 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()), - )); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(Some( + response_skeleton, + ))); 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()); + Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify_later = @@ -5164,22 +5351,26 @@ mod tests { 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))), + results: btreemap!(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::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 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); + let retry_payable_notify_params = retry_payable_notify_params_arc.lock().unwrap(); + assert_eq!( + *retry_payable_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt: Some(response_skeleton) + }] + ); assert_using_the_same_logger(&logger, test_name, None) } @@ -5443,7 +5634,7 @@ mod tests { seeds: Vec, ) -> (TxReceiptsMessage, Vec) { let (tx_receipt_results, tx_record_vec) = seeds.into_iter().enumerate().fold( - (hashmap![], vec![]), + (btreemap![], 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; @@ -5470,7 +5661,7 @@ mod tests { ) -> (TxHashByTable, TxReceiptResult, TxByTable) { match tx_hash { TxHashByTable::SentPayable(hash) => { - let mut sent_tx = make_sent_tx(1 + idx); + let mut sent_tx = make_sent_tx((1 + idx) as u32); sent_tx.hash = hash; if let StatusReadFromReceiptCheck::Succeeded(block) = &status { @@ -5486,7 +5677,7 @@ mod tests { (tx_hash, result, record_by_table) } TxHashByTable::FailedPayable(hash) => { - let mut failed_tx = make_failed_tx(1 + idx); + let mut failed_tx = make_failed_tx(1 + idx as u32); failed_tx.hash = hash; let result = Ok(status); @@ -5528,7 +5719,10 @@ mod tests { System::current().stop(); assert_eq!(system.run(), 0); 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]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([sent_tx_1, sent_tx_2])] + ); TestLogHandler::new().exists_log_containing(&format!( "DEBUG: {test_name}: Registered new pending payables for: \ 0x000000000000000000000000000000000000000000000000000000000006c81c, \ @@ -5567,7 +5761,10 @@ mod tests { let _ = subject.register_new_pending_sent_tx(msg); 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]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([sent_tx_1, sent_tx_2])] + ); TestLogHandler::new().exists_log_containing(&format!( "ERROR: {test_name}: Failed to save new pending payable records for \ 0x00000000000000000000000000000000000000000000000000000000000001c8, \ @@ -6717,6 +6914,7 @@ pub mod exportable_test_parts { check_if_source_code_is_attached, ensure_node_home_directory_exists, ShouldWeRunTheTest, }; use regex::Regex; + use std::collections::BTreeSet; use std::env::current_dir; use std::fs::File; use std::io::{BufRead, BufReader}; @@ -6893,4 +7091,27 @@ pub mod exportable_test_parts { // We didn't blow up, it recognized the functions. // This is an example of the error: "no such function: slope_drop_high_bytes" } + + #[test] + fn join_with_separator_works() { + // With a Vec + let vec = vec![1, 2, 3]; + let result_vec = join_with_separator(vec, |&num| num.to_string(), ", "); + assert_eq!(result_vec, "1, 2, 3".to_string()); + + // With a HashSet + let set = BTreeSet::from([1, 2, 3]); + let result_set = join_with_separator(set, |&num| num.to_string(), ", "); + assert_eq!(result_set, "1, 2, 3".to_string()); + + // With a slice + let slice = &[1, 2, 3]; + let result_slice = join_with_separator(slice.to_vec(), |&num| num.to_string(), ", "); + assert_eq!(result_slice, "1, 2, 3".to_string()); + + // With an array + let array = [1, 2, 3]; + let result_array = join_with_separator(array.to_vec(), |&num| num.to_string(), ", "); + assert_eq!(result_array, "1, 2, 3".to_string()); + } } diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index 5062fc1ab..b8318895d 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; +use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::PreparedAdjustment; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use masq_lib::logger::Logger; use std::time::SystemTime; @@ -9,7 +9,7 @@ use std::time::SystemTime; pub trait PaymentAdjuster { fn search_for_indispensable_adjustment( &self, - msg: &BlockchainAgentWithContextMessage, + msg: &PricedTemplatesMessage, logger: &Logger, ) -> Result, AnalysisError>; @@ -28,7 +28,7 @@ pub struct PaymentAdjusterReal {} impl PaymentAdjuster for PaymentAdjusterReal { fn search_for_indispensable_adjustment( &self, - _msg: &BlockchainAgentWithContextMessage, + _msg: &PricedTemplatesMessage, _logger: &Logger, ) -> Result, AnalysisError> { Ok(None) @@ -71,9 +71,11 @@ pub enum AnalysisError {} #[cfg(test)] mod tests { use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; - use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::{make_payable_account, make_priced_qualified_payables}; + use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::make_priced_new_tx_templates; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; + use itertools::Either; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; @@ -83,8 +85,9 @@ mod tests { let test_name = "search_for_indispensable_adjustment_always_returns_none"; let payable = make_payable_account(123); let agent = BlockchainAgentMock::default(); - let setup_msg = BlockchainAgentWithContextMessage { - qualified_payables: make_priced_qualified_payables(vec![(payable, 111_111_111)]), + let priced_new_tx_templates = make_priced_new_tx_templates(vec![(payable, 111_111_111)]); + let setup_msg = PricedTemplatesMessage { + priced_templates: Either::Left(priced_new_tx_templates), agent: Box::new(agent), response_skeleton_opt: None, }; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 1d69ab3c9..a11813615 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,69 +1,54 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod payable_scanner_extension; +pub mod payable_scanner; pub mod pending_payable_scanner; pub mod receivable_scanner; pub mod scan_schedulers; -pub mod scanners_utils; pub mod test_utils; -use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; -use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ - LocallyCausedError, RemotelyCausedErrors, +use crate::accountant::payment_adjuster::PaymentAdjusterReal; +use crate::accountant::scanners::payable_scanner::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, }; -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::scanners::payable_scanner::payment_adjuster_integration::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::utils::{NextScanToRun, PayableScanResult}; +use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PayableScanner}; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::pending_payable_scanner::{ + ExtendedPendingPayablePrivateScanner, PendingPayableScanner, +}; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::{ - comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, - TxReceiptsMessage, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, - ScanForReceivables, SentPayables, + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, ScanError, ScanForNewPayables, + ScanForReceivables, ScanForRetryPayables, SentPayables, TxReceiptsMessage, +}; +use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use crate::db_config::persistent_configuration::PersistentConfigurationReal; +use crate::sub_lib::accountant::{ + DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, }; -use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; -use crate::sub_lib::accountant::{DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; -use actix::{Message}; -use itertools::{Either, Itertools}; +use actix::Message; +use itertools::Either; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; -use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; -use masq_lib::utils::ExpectValue; +use masq_lib::messages::ScanType; +use masq_lib::ui_gateway::NodeToUiMessage; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::rc::Rc; -use std::time::{SystemTime}; +use std::time::SystemTime; use time::format_description::parse; use time::OffsetDateTime; use variant_count::VariantCount; -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; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; -use crate::accountant::scanners::receivable_scanner::ReceivableScanner; -use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; -use crate::db_config::persistent_configuration::{PersistentConfigurationReal}; // Leave the individual scanner objects private! pub struct Scanners { payable: Box, aware_of_unresolved_pending_payable: bool, initial_pending_payable_scan: bool, - pending_payable: Box< - dyn PrivateScanner< - ScanForPendingPayables, - RequestTransactionReceipts, - TxReceiptsMessage, - PendingPayableScanResult, - >, - >, + pending_payable: Box, receivable: Box< dyn PrivateScanner< ScanForReceivables, @@ -83,6 +68,7 @@ impl Scanners { let payable = Box::new(PayableScanner::new( dao_factories.payable_dao_factory.make(), dao_factories.sent_payable_dao_factory.make(), + dao_factories.failed_payable_dao_factory.make(), Rc::clone(&payment_thresholds), Box::new(PaymentAdjusterReal::new()), )); @@ -122,7 +108,7 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, automatic_scans_enabled: bool, - ) -> Result { + ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( @@ -153,7 +139,7 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { + ) -> Result { if let Some(started_at) = self.payable.scan_started_at() { unreachable!( "Guards should ensure that no payable scanner can run if the pending payable \ @@ -247,10 +233,9 @@ impl Scanners { pub fn finish_payable_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { let scan_result = self.payable.finish_scan(msg, logger); - match scan_result.result { - OperationOutcome::NewPendingPayable => self.aware_of_unresolved_pending_payable = true, - OperationOutcome::Failure => (), - }; + if scan_result.result == NextScanToRun::PendingPayableScan { + self.aware_of_unresolved_pending_payable = true + } scan_result } @@ -286,22 +271,12 @@ 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); + self.pending_payable.empty_caches(logger) } pub fn try_skipping_payable_adjustment( &self, - msg: BlockchainAgentWithContextMessage, + msg: PricedTemplatesMessage, logger: &Logger, ) -> Result, String> { self.payable.try_skipping_payment_adjustment(msg, logger) @@ -333,15 +308,15 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result + ) -> Result where TriggerMessage: Message, (dyn MultistageDualPayableScanner + 'a): - StartableScanner, + StartableScanner, { <(dyn MultistageDualPayableScanner + 'a) as StartableScanner< TriggerMessage, - QualifiedPayablesMessage, + InitialTemplatesMessage, >>::start_scan(scanner, wallet, timestamp, response_skeleton_opt, logger) } @@ -411,7 +386,6 @@ where fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); fn mark_as_ended(&mut self, logger: &Logger); - as_any_ref_in_trait!(); as_any_mut_in_trait!(); } @@ -473,430 +447,6 @@ macro_rules! time_marking_methods { }; } -pub struct PayableScanner { - pub payable_threshold_gauge: Box, - pub common: ScannerCommon, - pub payable_dao: Box, - pub sent_payable_dao: Box, - pub payment_adjuster: Box, -} - -impl MultistageDualPayableScanner for PayableScanner {} - -impl StartableScanner for PayableScanner { - fn start_scan( - &mut self, - consuming_wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.mark_as_started(timestamp); - info!(logger, "Scanning for new payables"); - let all_non_pending_payables = self.payable_dao.non_pending_payables(); - - debug!( - logger, - "{}", - investigate_debt_extremes(timestamp, &all_non_pending_payables) - ); - - let qualified_payables = - self.sniff_out_alarming_payables_and_maybe_log_them(all_non_pending_payables, logger); - - match qualified_payables.is_empty() { - true => { - self.mark_as_ended(logger); - Err(StartScanError::NothingToProcess) - } - false => { - info!( - logger, - "Chose {} qualified debts to pay", - qualified_payables.len() - ); - let qualified_payables = UnpricedQualifiedPayables::from(qualified_payables); - let outgoing_msg = QualifiedPayablesMessage::new( - qualified_payables, - consuming_wallet.clone(), - response_skeleton_opt, - ); - Ok(outgoing_msg) - } - } - } -} - -impl StartableScanner for PayableScanner { - fn start_scan( - &mut self, - _consuming_wallet: &Wallet, - _timestamp: SystemTime, - _response_skeleton_opt: Option, - _logger: &Logger, - ) -> Result { - todo!("Complete me under GH-605") - // 1. Find the failed payables - // 2. Look into the payable DAO to update the amount - // 3. Prepare UnpricedQualifiedPayables - } -} - -impl Scanner for PayableScanner { - fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> PayableScanResult { - let (sent_payables, err_opt) = separate_errors(&message, logger); - debug!( - logger, - "{}", - debugging_summary_after_error_separation(&sent_payables, &err_opt) - ); - - if !sent_payables.is_empty() { - self.check_on_missing_sent_tx_records(&sent_payables); - } - - self.handle_sent_payable_errors(err_opt, logger); - - self.mark_as_ended(logger); - - let ui_response_opt = - message - .response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }); - - let result = if !sent_payables.is_empty() { - OperationOutcome::NewPendingPayable - } else { - OperationOutcome::Failure - }; - - PayableScanResult { - ui_response_opt, - result, - } - } - - time_marking_methods!(Payables); - - as_any_ref_in_trait_impl!(); -} - -impl SolvencySensitivePaymentInstructor for PayableScanner { - fn try_skipping_payment_adjustment( - &self, - msg: BlockchainAgentWithContextMessage, - logger: &Logger, - ) -> Result, String> { - match self - .payment_adjuster - .search_for_indispensable_adjustment(&msg, logger) - { - Ok(None) => Ok(Either::Left(OutboundPaymentsInstructions::new( - msg.qualified_payables, - msg.agent, - msg.response_skeleton_opt, - ))), - Ok(Some(adjustment)) => Ok(Either::Right(PreparedAdjustment::new(msg, adjustment))), - Err(_e) => todo!("be implemented with GH-711"), - } - } - - fn perform_payment_adjustment( - &self, - setup: PreparedAdjustment, - logger: &Logger, - ) -> OutboundPaymentsInstructions { - let now = SystemTime::now(); - self.payment_adjuster.adjust_payments(setup, now, logger) - } -} - -impl PayableScanner { - pub fn new( - payable_dao: Box, - sent_payable_dao: Box, - payment_thresholds: Rc, - payment_adjuster: Box, - ) -> Self { - Self { - common: ScannerCommon::new(payment_thresholds), - payable_dao, - sent_payable_dao, - payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), - payment_adjuster, - } - } - - fn sniff_out_alarming_payables_and_maybe_log_them( - &self, - non_pending_payables: Vec, - logger: &Logger, - ) -> Vec { - fn pass_payables_and_drop_points( - qp_tp: impl Iterator, - ) -> Vec { - let (payables, _) = qp_tp.unzip::<_, _, Vec, Vec<_>>(); - payables - } - - let qualified_payables_and_points_uncollected = - non_pending_payables.into_iter().flat_map(|account| { - self.payable_exceeded_threshold(&account, SystemTime::now()) - .map(|threshold_point| (account, threshold_point)) - }); - match logger.debug_enabled() { - false => pass_payables_and_drop_points(qualified_payables_and_points_uncollected), - true => { - let qualified_and_points_collected = - qualified_payables_and_points_uncollected.collect_vec(); - payables_debug_summary(&qualified_and_points_collected, logger); - pass_payables_and_drop_points(qualified_and_points_collected.into_iter()) - } - } - } - - fn payable_exceeded_threshold( - &self, - payable: &PayableAccount, - now: SystemTime, - ) -> Option { - let debt_age = now - .duration_since(payable.last_paid_timestamp) - .expect("Internal error") - .as_secs(); - - if self.payable_threshold_gauge.is_innocent_age( - debt_age, - self.common.payment_thresholds.maturity_threshold_sec, - ) { - return None; - } - - if self.payable_threshold_gauge.is_innocent_balance( - payable.balance_wei, - gwei_to_wei(self.common.payment_thresholds.permanent_debt_allowed_gwei), - ) { - return None; - } - - let threshold = self - .payable_threshold_gauge - .calculate_payout_threshold_in_gwei(&self.common.payment_thresholds, debt_age); - if payable.balance_wei > threshold { - Some(threshold) - } else { - None - } - } - - fn 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::>(); - - if hashset_with_hashes_to_eliminate_duplicates.len() != actual_sent_payables_len { - panic!( - "Found duplicates in the recent sent txs: {:?}", - just_baked_sent_payables - ); - } - - 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_address = sent_payables_hashmap - .remove(&hash) - .expectv("wallet") - .address(); - PendingPayableMissingInDb::new(wallet_address, hash) - }) - .collect() - } - - fn check_on_missing_sent_tx_records(&self, sent_payments: &[&PendingPayable]) { - fn missing_record_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { - format!( - "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 - )) - ) - } - - 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, - ) { - fn decide_on_tx_error_handling( - err: &PayableTransactingErrorEnum, - ) -> Option<&HashSet> { - match err { - LocallyCausedError(PayableTransactionError::Sending { hashes, .. }) - | 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_sent_tx_records( - &self, - hashes_of_failed: &HashSet, - logger: &Logger, - ) { - fn serialize_hashes(hashes: &[TxHash]) -> String { - comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) - } - - 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_sent_tx_in_db.is_empty() { - let hashes = existent_sent_tx_in_db - .keys() - .copied() - .sorted() - .collect_vec(); - warning!( - logger, - "Deleting sent payable records for {}", - serialize_hashes(&hashes) - ); - 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: sent payable record deletion for txs {} failed \ - due to {:?}", - serialize_hashes(&hashes), - e - ) - } - } - if let Some(msg) = missing_fgp_err_msg_opt { - panic!("{}", msg) - }; - } -} - #[derive(Debug, PartialEq, Eq, Clone, VariantCount)] pub enum StartScanError { NothingToProcess, @@ -1019,48 +569,45 @@ 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::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::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; + use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; + use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::{ + RetryTxTemplate, RetryTxTemplates, }; + use crate::accountant::scanners::payable_scanner::utils::PayableScanResult; + use crate::accountant::scanners::payable_scanner::PayableScanner; 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, + CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, TxHashByTable, }; + use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; + use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::scanners::test_utils::{ - assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, + assert_timestamps_from_str, parse_system_time_from_str, + trim_expected_timestamp_to_three_digits_nanos, MarkScanner, NullScanner, PendingPayableCacheMock, ReplacementType, ScannerReplacement, }; use crate::accountant::scanners::{ - ManulTriggerError, PayableScanner, PendingPayableScanner, ReceivableScanner, Scanner, - ScannerCommon, Scanners, StartScanError, StartableScanner, + ManulTriggerError, 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, + make_custom_payment_thresholds, make_qualified_and_unqualified_payables, + make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, + FailedPayableDaoFactoryMock, FailedPayableDaoMock, PayableDaoFactoryMock, PayableDaoMock, + PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, + ReceivableScannerBuilder, SentPayableDaoFactoryMock, SentPayableDaoMock, }; use crate::accountant::{ - gwei_to_wei, PendingPayable, ReceivedPayments, RequestTransactionReceipts, ScanError, - ScanForRetryPayables, SentPayables, TxReceiptsMessage, + PayableScanType, ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, ScanError, + 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, + BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck, TxBlock, }; use crate::blockchain::errors::rpc_errors::{ AppRpcError, AppRpcErrorKind, RemoteError, RemoteErrorKind, @@ -1074,12 +621,11 @@ mod tests { use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ DaoFactories, DetailedScanType, 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 actix::Message; use ethereum_types::U64; use itertools::Either; use masq_lib::logger::Logger; @@ -1089,12 +635,12 @@ mod tests { use regex::Regex; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; + use std::collections::BTreeSet; 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::Error; impl Scanners { pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { @@ -1181,10 +727,11 @@ mod tests { 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 failed_payable_dao_factory = FailedPayableDaoFactoryMock::new() + .make_result(FailedPayableDaoMock::new()) + .make_result(FailedPayableDaoMock::new()); + let receivable_dao = ReceivableDaoMock::new(); + let receivable_dao_factory = ReceivableDaoFactoryMock::new().make_result(receivable_dao); 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() @@ -1289,10 +836,9 @@ mod tests { let test_name = "new_payable_scanner_can_initiate_a_scan"; let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); - let (qualified_payable_accounts, _, all_non_pending_payables) = + let (qualified_payable_accounts, _, retrieved_payables) = make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let payable_dao = PayableDaoMock::new().retrieve_payables_result(retrieved_payables); let mut subject = make_dull_subject(); let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) @@ -1310,16 +856,11 @@ mod tests { let timestamp = subject.payable.scan_started_at(); assert_eq!(timestamp, Some(now)); let qualified_payables_count = qualified_payable_accounts.len(); - let expected_unpriced_qualified_payables = UnpricedQualifiedPayables { - payables: qualified_payable_accounts - .into_iter() - .map(|payable| QualifiedPayablesBeforeGasPriceSelection::new(payable, None)) - .collect::>(), - }; + let expected_tx_templates = NewTxTemplates::from(&qualified_payable_accounts); assert_eq!( result, - Ok(QualifiedPayablesMessage { - qualified_payables: expected_unpriced_qualified_payables, + Ok(InitialTemplatesMessage { + initial_templates: Either::Left(expected_tx_templates), consuming_wallet, response_skeleton_opt: None, }) @@ -1336,12 +877,11 @@ mod tests { #[test] fn new_payable_scanner_cannot_be_initiated_if_it_is_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + let (_, _, retrieved_payables) = make_qualified_and_unqualified_payables( SystemTime::now(), &PaymentThresholds::default(), ); - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let payable_dao = PayableDaoMock::new().retrieve_payables_result(retrieved_payables); let mut subject = make_dull_subject(); let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) @@ -1382,7 +922,7 @@ mod tests { let (_, unqualified_payable_accounts, _) = make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = - PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); + PayableDaoMock::new().retrieve_payables_result(unqualified_payable_accounts); let mut subject = make_dull_subject(); subject.payable = Box::new( PayableScannerBuilder::new() @@ -1405,73 +945,63 @@ mod tests { #[test] fn retry_payable_scanner_can_initiate_a_scan() { - // - // Setup Part: - // DAOs: PayableDao, FailedPayableDao - // Fetch data from FailedPayableDao (inject it into Payable Scanner -- allow the change in production code). - // Scanners constructor will require to create it with the Factory -- try it - // Configure it such that it returns at least 2 failed tx - // Once I get those 2 records, I should get hold of those identifiers used in the Payable DAO - // Update the new balance for those transactions - // Modify Payable DAO and add another method, that will return just the corresponding payments - // The account which I get from the PayableDAO can go straight to the QualifiedPayableBeforePriceSelection - - todo!("this must be set up under GH-605"); - // TODO make sure the QualifiedPayableRawPack will express the difference from - // the NewPayable scanner: The QualifiedPayablesBeforeGasPriceSelection needs to carry - // `Some()` instead of None - // init_test_logging(); - // let test_name = "retry_payable_scanner_can_initiate_a_scan"; - // let consuming_wallet = make_paying_wallet(b"consuming wallet"); - // let now = SystemTime::now(); - // let (qualified_payable_accounts, _, all_non_pending_payables) = - // make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); - // let payable_dao = - // PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - // let mut subject = make_dull_subject(); - // let payable_scanner = PayableScannerBuilder::new() - // .payable_dao(payable_dao) - // .build(); - // subject.payable = Box::new(payable_scanner); - // - // let result = subject.start_retry_payable_scan_guarded( - // &consuming_wallet, - // now, - // None, - // &Logger::new(test_name), - // ); - // - // let timestamp = subject.payable.scan_started_at(); - // assert_eq!(timestamp, Some(now)); - // assert_eq!( - // result, - // Ok(QualifiedPayablesMessage { - // qualified_payables: todo!(""), - // consuming_wallet, - // response_skeleton_opt: None, - // }) - // ); - // TestLogHandler::new().assert_logs_match_in_order(vec![ - // &format!("INFO: {test_name}: Scanning for retry-required payables"), - // &format!( - // "INFO: {test_name}: Chose {} qualified debts to pay", - // qualified_payable_accounts.len() - // ), - // ]) + init_test_logging(); + let test_name = "retry_payable_scanner_can_initiate_a_scan"; + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + let response_skeleton = ResponseSkeleton { + client_id: 24, + context_id: 42, + }; + let (_, _, retrieved_payables) = + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); + let failed_tx = make_failed_tx(1); + let payable_dao = PayableDaoMock::new().retrieve_payables_result(retrieved_payables); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([failed_tx.clone()])); + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + subject.payable = Box::new(payable_scanner); + + let result = subject.start_retry_payable_scan_guarded( + &consuming_wallet, + now, + Some(response_skeleton), + &Logger::new(test_name), + ); + + let timestamp = subject.payable.scan_started_at(); + let expected_template = RetryTxTemplate::from(&failed_tx); + assert_eq!(timestamp, Some(now)); + assert_eq!( + result, + Ok(InitialTemplatesMessage { + initial_templates: Either::Right(RetryTxTemplates(vec![expected_template])), + consuming_wallet, + response_skeleton_opt: Some(response_skeleton), + }) + ); + let tlh = TestLogHandler::new(); + tlh.exists_log_containing(&format!("INFO: {test_name}: Scanning for retry payables")); } #[test] fn retry_payable_scanner_panics_in_case_scan_is_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + let (_, _, retrieved_payables) = make_qualified_and_unqualified_payables( SystemTime::now(), &PaymentThresholds::default(), ); - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let payable_dao = PayableDaoMock::new().retrieve_payables_result(retrieved_payables); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = make_dull_subject(); let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); subject.payable = Box::new(payable_scanner); let before = SystemTime::now(); @@ -1483,7 +1013,7 @@ mod tests { ); let caught_panic = catch_unwind(AssertUnwindSafe(|| { - let _: Result = subject + let _: Result = subject .start_retry_payable_scan_guarded( &consuming_wallet, SystemTime::now(), @@ -1495,9 +1025,9 @@ mod tests { let after = SystemTime::now(); let panic_msg = caught_panic.downcast_ref::().unwrap(); - let expected_needle_1 = "internal error: entered unreachable code: Guard for pending \ - payables should've prevented running the tandem of scanners if the payable scanner was \ - still running. It started "; + let expected_needle_1 = "internal error: entered unreachable code: \ + Guards should ensure that no payable scanner can run if the pending payable \ + repetitive sequence is still ongoing. However, some other payable scan intruded at"; assert!( panic_msg.contains(expected_needle_1), "We looked for {} but the actual string doesn't contain it: {}", @@ -1522,8 +1052,10 @@ mod tests { after: SystemTime, ) { let system_times = parse_system_time_from_str(panic_msg); + let before = trim_expected_timestamp_to_three_digits_nanos(before); let first_actual = system_times[0]; let second_actual = system_times[1]; + let after = trim_expected_timestamp_to_three_digits_nanos(after); assert!( before <= first_actual @@ -1539,715 +1071,86 @@ mod tests { } #[test] - #[should_panic(expected = "Complete me with GH-605")] - fn retry_payable_scanner_panics_in_case_no_qualified_payable_is_found() { - let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let now = SystemTime::now(); - let (_, unqualified_payable_accounts, _) = - make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); - let mut subject = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .build(); - - let _ = Scanners::start_correct_payable_scanner::( - &mut subject, - &consuming_wallet, - now, - None, - &Logger::new("test"), + fn finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err( + ) { + test_finish_payable_scan_keeps_aware_flag_false_on_error(PayableScanType::New, "new_scan"); + test_finish_payable_scan_keeps_aware_flag_false_on_error( + PayableScanType::Retry, + "retry_scan", ); } - #[test] - fn payable_scanner_handles_sent_payable_message() { + fn test_finish_payable_scan_keeps_aware_flag_false_on_error( + payable_scan_type: PayableScanType, + test_name_str: &str, + ) { init_test_logging(); - let test_name = "payable_scanner_handles_sent_payable_message"; - let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payables_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"); - let correct_pending_payable_1 = - PendingPayable::new(correct_payable_wallet_1.clone(), correct_payable_hash_1); - let failure_payable_hash_2 = make_tx_hash(0xde); - let failure_payable_rowid_2 = 126; - let failure_payable_wallet_2 = make_wallet("hihihi"); - let failure_payable_2 = RpcPayableFailure { - rpc_error: Error::InvalidResponse( - "Ged rid of your illiteracy before you send your garbage!".to_string(), - ), - recipient_wallet: failure_payable_wallet_2, - hash: failure_payable_hash_2, - }; - let correct_payable_hash_3 = make_tx_hash(0x14d); - let correct_payable_rowid_3 = 127; - 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 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) - .sent_payable_dao(sent_payable_dao) - .build(); - let logger = Logger::new(test_name); + let test_name = format!( + "finish_payable_scan_keeps_the_aware_of_unresolved_\ + pending_payable_flag_as_false_in_case_of_err_for_\ + {test_name_str}" + ); let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(correct_pending_payable_1), - ProcessedPayableFallible::Failed(failure_payable_2), - ProcessedPayableFallible::Correct(correct_pending_payable_3), - ]), + payment_procedure_result: Err("Some error".to_string()), + payable_scan_type, response_skeleton_opt: None, }; - payable_scanner.mark_as_started(SystemTime::now()); + let logger = Logger::new(&test_name); + let payable_scanner = PayableScannerBuilder::new().build(); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; - let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); + subject.finish_payable_scan(sent_payable, &logger); - let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; - assert_eq!( - payable_scan_result, - PayableScanResult { - ui_response_opt: None, - result: OperationOutcome::NewPendingPayable - } - ); - assert_eq!(is_scan_running, false); assert_eq!(aware_of_unresolved_pending_payable_before, false); - assert_eq!(aware_of_unresolved_pending_payable_after, true); - let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); - assert_eq!( - *get_tx_identifiers_params, - vec![ - hashset![correct_payable_hash_1, correct_payable_hash_3], - hashset![failure_payable_hash_2] - ] - ); - let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!( - *delete_records_params, - vec![hashset![failure_payable_hash_2]] - ); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let log_handler = TestLogHandler::new(); - log_handler.assert_logs_contain_in_order(vec![ - &format!( - "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!( - "WARN: {test_name}: Deleting sent payable records for \ - 0x00000000000000000000000000000000000000000000000000000000000000de" - ), - ]); - log_handler.exists_log_matching(&format!( - "INFO: {test_name}: The Payables scan ended in \\d+ms." - )); - } - - #[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 = "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 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(); - - subject.check_for_missing_records(&pending_payables_ref); - } - - #[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( - PayableDaoError::SignConversion(9999999999999), + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Local error occurred before transaction signing. Error: Some error" )); - let mut subject = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .sent_payable_dao(sent_payable_dao) - .build(); - let sent_payables = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(payable_1), - ProcessedPayableFallible::Correct(payable_2), - ]), - response_skeleton_opt: None, - }; - - subject.finish_scan(sent_payables, &Logger::new(test_name)); } #[test] - fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist() { + fn finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found_in_retry_mode( + ) { init_test_logging(); - let test_name = - "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_sent_tx_rowid = 3; - let second_sent_tx_rowid = 5; - let system = System::new(test_name); - 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 test_name = "finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found_in_retry_mode"; + let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let payable_scanner = PayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_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: hashset![hash_tx_1, hash_tx_2], - }), - response_skeleton_opt: None, - }; let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let aware_of_unresolved_pending_payable_before = - subject.aware_of_unresolved_pending_payable; - - let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); - - let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; - System::current().stop(); - system.run(); - assert_eq!( - payable_scan_result, - PayableScanResult { - ui_response_opt: None, - result: OperationOutcome::Failure - } - ); - assert_eq!(aware_of_unresolved_pending_payable_before, false); - assert_eq!(aware_of_unresolved_pending_payable_after, false); - let 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 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 - } - - #[test] - fn payable_scanner_handles_error_born_too_early_to_see_transaction_hash() { - init_test_logging(); - let test_name = "payable_scanner_handles_error_born_too_early_to_see_transaction_hash"; - let sent_payable = SentPayables { - payment_procedure_result: Err(PayableTransactionError::Signing( - "Some error".to_string(), - )), + let sent_payables = SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![make_sent_tx(1)], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::Retry, response_skeleton_opt: None, }; - let payable_scanner = PayableScannerBuilder::new().build(); - let mut subject = make_dull_subject(); - subject.payable = Box::new(payable_scanner); let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; - subject.finish_payable_scan(sent_payable, &Logger::new(test_name)); + subject.finish_payable_scan(sent_payables, &logger); let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; assert_eq!(aware_of_unresolved_pending_payable_before, false); - assert_eq!(aware_of_unresolved_pending_payable_after, false); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!( - "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" - )); - log_handler.exists_log_containing(&format!( - "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_sent_tx_record_for_failed_payments_but_panics_at_their_deletion() { - let test_name = - "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; - let hash_2 = make_tx_hash(0x315); - let sent_payable = SentPayables { - payment_procedure_result: Err(PayableTransactionError::Sending { - msg: "blah".to_string(), - hashes: hashset![hash_1, hash_2], - }), - response_skeleton_opt: None, - }; - 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() - .sent_payable_dao(sent_payable_dao) - .build(); - - let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { - subject.finish_scan(sent_payable, &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, - "Database corrupt: sent payable record deletion for txs \ - 0x000000000000000000000000000000000000000000000000000000000000007b, 0x00000000000000000000\ - 00000000000000000000000000000000000000000315 failed due to SqlExecutionFailed(\"I overslept \ - since my brain thinks the alarm is just a lullaby\")"); - let log_handler = TestLogHandler::new(); - // 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_sent_tx_records_but_deletion_of_some_works() { - init_test_logging(); - let test_name = - "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 sent_payable_dao = SentPayableDaoMock::default() - .get_tx_identifiers_result(hashmap!(hash_1 => 333)) - .delete_records_result(Ok(())); - let mut subject = PayableScannerBuilder::new() - .sent_payable_dao(sent_payable_dao) - .build(); - let sent_payable = SentPayables { - payment_procedure_result: Err(PayableTransactionError::Sending { - msg: "SQLite migraine".to_string(), - hashes: hashset![hash_1, hash_2, hash_3], - }), - response_skeleton_opt: None, - }; - - let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { - subject.finish_scan(sent_payable, &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, - "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 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_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_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 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() - .sent_payable_dao(sent_payable_dao) - .build(); - let failed_payment_1 = RpcPayableFailure { - rpc_error: Error::Unreachable, - recipient_wallet: make_wallet("abc"), - hash: existent_record_hash, - }; - let failed_payment_2 = RpcPayableFailure { - rpc_error: Error::Internal, - recipient_wallet: make_wallet("def"), - hash: nonexistent_record_hash, - }; - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Failed(failed_payment_1), - ProcessedPayableFallible::Failed(failed_payment_2), - ]), - response_skeleton_opt: None, - }; - - let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { - subject.finish_scan(sent_payable, &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, - "Database corrupt: sent payable record deletion for txs \ - 0x000000000000000000000000000000000000000000000000000000000000b26e failed due to \ - SqlExecutionFailed(\"Another failure. Really???\")" - ); + assert_eq!(aware_of_unresolved_pending_payable_after, true); let log_handler = TestLogHandler::new(); 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 \ - payables 0x00000000000000000000000000000000000000000000000000000000000004d2 with missing \ - records. The system has become unreliable" + "DEBUG: {test_name}: Processed retried txs while sending to RPC: \ + Total: 1, Sent to RPC: 1, Failed to send: 0." )); } - #[test] - fn payable_is_found_innocent_by_age_and_returns() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result(true); - let mut subject = PayableScannerBuilder::new().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - let now = SystemTime::now(); - let debt_age_s = 111_222; - let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); - let mut payable = make_payable_account(111); - payable.last_paid_timestamp = last_paid_timestamp; - - let result = subject.payable_exceeded_threshold(&payable, now); - - assert_eq!(result, None); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age_returned, threshold_value) = is_innocent_age_params.remove(0); - assert!(is_innocent_age_params.is_empty()); - assert_eq!(debt_age_returned, debt_age_s); - assert_eq!( - threshold_value, - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec - ) - // No panic and so no other method was called, which means an early return - } - - #[test] - fn payable_is_found_innocent_by_balance_and_returns() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result(false) - .is_innocent_balance_params(&is_innocent_balance_params_arc) - .is_innocent_balance_result(true); - let mut subject = PayableScannerBuilder::new().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - let now = SystemTime::now(); - let debt_age_s = 3_456; - let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); - let mut payable = make_payable_account(222); - payable.last_paid_timestamp = last_paid_timestamp; - payable.balance_wei = 123456; - - let result = subject.payable_exceeded_threshold(&payable, now); - - assert_eq!(result, None); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age_returned, _) = is_innocent_age_params.remove(0); - assert!(is_innocent_age_params.is_empty()); - assert_eq!(debt_age_returned, debt_age_s); - let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); - assert_eq!( - *is_innocent_balance_params, - vec![( - 123456_u128, - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) - )] - ) - //no other method was called (absence of panic), and that means we returned early - } - - #[test] - fn threshold_calculation_depends_on_user_defined_payment_thresholds() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); - let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); - let balance = gwei_to_wei(5555_u64); - let now = SystemTime::now(); - let debt_age_s = 1111 + 1; - let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); - let payable_account = PayableAccount { - wallet: make_wallet("hi"), - balance_wei: balance, - last_paid_timestamp, - pending_payable_opt: None, - }; - let custom_payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1111, - payment_grace_period_sec: 2222, - permanent_debt_allowed_gwei: 3333, - debt_threshold_gwei: 4444, - threshold_interval_sec: 5555, - unban_below_gwei: 5555, - }; - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result( - debt_age_s <= custom_payment_thresholds.maturity_threshold_sec as u64, - ) - .is_innocent_balance_params(&is_innocent_balance_params_arc) - .is_innocent_balance_result( - balance <= gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei), - ) - .calculate_payout_threshold_in_gwei_params(&calculate_payable_threshold_params_arc) - .calculate_payout_threshold_in_gwei_result(4567898); //made up value - let mut subject = PayableScannerBuilder::new() - .payment_thresholds(custom_payment_thresholds) - .build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - - let result = subject.payable_exceeded_threshold(&payable_account, now); - - assert_eq!(result, Some(4567898)); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age_returned_innocent, curve_derived_time) = is_innocent_age_params.remove(0); - assert_eq!(*is_innocent_age_params, vec![]); - assert_eq!(debt_age_returned_innocent, debt_age_s); - assert_eq!( - curve_derived_time, - custom_payment_thresholds.maturity_threshold_sec as u64 - ); - let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); - assert_eq!( - *is_innocent_balance_params, - vec![( - payable_account.balance_wei, - gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei) - )] - ); - let mut calculate_payable_curves_params = - calculate_payable_threshold_params_arc.lock().unwrap(); - let (payment_thresholds, debt_age_returned_curves) = - calculate_payable_curves_params.remove(0); - assert_eq!(*calculate_payable_curves_params, vec![]); - assert_eq!(debt_age_returned_curves, debt_age_s); - assert_eq!(payment_thresholds, custom_payment_thresholds) - } - - #[test] - fn payable_with_debt_under_the_slope_is_marked_unqualified() { - init_test_logging(); - let now = SystemTime::now(); - let payment_thresholds = PaymentThresholds::default(); - let debt = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); - let time = to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; - let unqualified_payable_account = vec![PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: debt, - last_paid_timestamp: from_unix_timestamp(time), - pending_payable_opt: None, - }]; - let subject = PayableScannerBuilder::new() - .payment_thresholds(payment_thresholds) - .build(); - let test_name = - "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; - let logger = Logger::new(test_name); - - let result = subject - .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); - - assert_eq!(result, vec![]); - TestLogHandler::new() - .exists_no_log_containing(&format!("DEBUG: {}: Paying qualified debts", test_name)); - } - - #[test] - fn payable_with_debt_above_the_slope_is_qualified() { - init_test_logging(); - let payment_thresholds = PaymentThresholds::default(); - let debt = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); - let time = (payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec - - 1) as i64; - let qualified_payable = PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: debt, - last_paid_timestamp: from_unix_timestamp(time), - pending_payable_opt: None, - }; - let subject = PayableScannerBuilder::new() - .payment_thresholds(payment_thresholds) - .build(); - let test_name = "payable_with_debt_above_the_slope_is_qualified"; - let logger = Logger::new(test_name); - - let result = subject.sniff_out_alarming_payables_and_maybe_log_them( - vec![qualified_payable.clone()], - &logger, - ); - - assert_eq!(result, vec![qualified_payable]); - TestLogHandler::new().exists_log_matching(&format!( - "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 - )); - } - - #[test] - fn non_pending_payables_turn_into_an_empty_vector_if_all_unqualified() { - init_test_logging(); - let test_name = "non_pending_payables_turn_into_an_empty_vector_if_all_unqualified"; - let now = SystemTime::now(); - let payment_thresholds = PaymentThresholds::default(); - let unqualified_payable_account = vec![PayableAccount { - wallet: make_wallet("wallet1"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), - last_paid_timestamp: from_unix_timestamp( - to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, - ), - pending_payable_opt: None, - }]; - let subject = PayableScannerBuilder::new() - .payment_thresholds(payment_thresholds) - .build(); - let logger = Logger::new(test_name); - - let result = subject - .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); - - assert_eq!(result, vec![]); - TestLogHandler::new() - .exists_no_log_containing(&format!("DEBUG: {test_name}: Paying qualified debts")); - } - #[test] fn pending_payable_scanner_can_initiate_a_scan() { init_test_logging(); @@ -2257,9 +1160,10 @@ mod tests { 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 sent_payable_dao = + SentPayableDaoMock::new().retrieve_txs_result(btreeset![sent_tx.clone()]); let failed_payable_dao = - FailedPayableDaoMock::new().retrieve_txs_result(vec![failed_tx.clone()]); + FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([failed_tx.clone()])); let mut subject = make_dull_subject(); let pending_payable_scanner = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) @@ -2305,9 +1209,9 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = make_dull_subject(); let sent_payable_dao = - SentPayableDaoMock::new().retrieve_txs_result(vec![make_sent_tx(123)]); + SentPayableDaoMock::new().retrieve_txs_result(btreeset![make_sent_tx(123)]); let failed_payable_dao = - FailedPayableDaoMock::new().retrieve_txs_result(vec![make_failed_tx(456)]); + FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([make_failed_tx(456)])); let pending_payable_scanner = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) .failed_payable_dao(failed_payable_dao) @@ -2564,7 +1468,7 @@ mod tests { .validation_failure_clock(Box::new(validation_failure_clock)) .build(); let msg = TxReceiptsMessage { - results: hashmap![ + results: btreemap![ 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), @@ -2580,10 +1484,7 @@ mod tests { let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - assert_eq!( - result, - PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) - ); + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); sent_tx_1.status = TxStatus::Confirmed { block_hash: format!("{:?}", tx_block_1.block_hash), @@ -2595,13 +1496,16 @@ mod tests { 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]]); + assert_eq!(*replace_records_params, vec![btreeset![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!( *insert_new_records_params, - vec![vec![expected_failure_for_tx_3, expected_failure_for_tx_6]] + vec![btreeset![ + expected_failure_for_tx_3, + expected_failure_for_tx_6 + ]] ); let update_statuses_pending_payable_params = update_statuses_pending_payable_params_arc.lock().unwrap(); @@ -2641,7 +1545,7 @@ mod tests { fn pending_payable_scanner_handles_empty_report_transaction_receipts_message() { let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); let msg = TxReceiptsMessage { - results: hashmap![], + results: btreemap![], response_skeleton_opt: None, }; pending_payable_scanner.mark_as_started(SystemTime::now()); diff --git a/node/src/accountant/scanners/payable_scanner/finish_scan.rs b/node/src/accountant/scanners/payable_scanner/finish_scan.rs new file mode 100644 index 000000000..900bf9b56 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/finish_scan.rs @@ -0,0 +1,258 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::scanners::payable_scanner::utils::PayableScanResult; +use crate::accountant::scanners::payable_scanner::PayableScanner; +use crate::accountant::scanners::Scanner; +use crate::accountant::SentPayables; +use crate::time_marking_methods; +use masq_lib::logger::Logger; +use masq_lib::messages::ScanType; +use std::time::SystemTime; + +impl Scanner for PayableScanner { + fn finish_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { + // TODO as for GH-701, here there should be this check, but later on, when it comes to + // GH-655, the need for this check passes and it will go away. Until then it should be + // present, though. + // if !sent_payables.is_empty() { + // self.check_on_missing_sent_tx_records(&sent_payables); + // } + + self.process_message(&msg, logger); + + self.mark_as_ended(logger); + + PayableScanResult { + ui_response_opt: Self::generate_ui_response(msg.response_skeleton_opt), + result: Self::determine_next_scan_to_run(&msg), + } + } + + time_marking_methods!(Payables); + + as_any_ref_in_trait_impl!(); +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; + use crate::accountant::db_access_objects::test_utils::{ + make_failed_tx, make_sent_tx, FailedTxBuilder, + }; + use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; + use crate::accountant::scanners::payable_scanner::utils::{NextScanToRun, PayableScanResult}; + use crate::accountant::scanners::Scanner; + use crate::accountant::test_utils::{FailedPayableDaoMock, SentPayableDaoMock}; + use crate::accountant::{join_with_separator, PayableScanType, ResponseSkeleton, SentPayables}; + use crate::blockchain::blockchain_interface::data_structures::BatchResults; + use crate::blockchain::errors::validation_status::ValidationStatus::Waiting; + use crate::blockchain::test_utils::make_tx_hash; + use masq_lib::logger::Logger; + use masq_lib::messages::{ToMessageBody, UiScanResponse}; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; + use std::collections::BTreeSet; + use std::sync::{Arc, Mutex}; + use std::time::SystemTime; + + #[test] + fn new_payable_scan_finishes_as_expected() { + init_test_logging(); + let test_name = "new_payable_scan_finishes_as_expected"; + let sent_payable_insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let failed_tx_1 = make_failed_tx(1); + let failed_tx_2 = make_failed_tx(2); + let sent_tx_1 = make_sent_tx(1); + let sent_tx_2 = make_sent_tx(2); + let batch_results = BatchResults { + sent_txs: vec![sent_tx_1.clone(), sent_tx_2.clone()], + failed_txs: vec![failed_tx_1.clone(), failed_tx_2.clone()], + }; + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 5678, + }; + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&sent_payable_insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&failed_payable_insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let mut subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + subject.mark_as_started(SystemTime::now()); + let sent_payables = SentPayables { + payment_procedure_result: Ok(batch_results), + payable_scan_type: PayableScanType::New, + response_skeleton_opt: Some(response_skeleton), + }; + let logger = Logger::new(test_name); + + let result = subject.finish_scan(sent_payables, &logger); + + let sent_payable_insert_new_records_params = + sent_payable_insert_new_records_params_arc.lock().unwrap(); + let failed_payable_insert_new_records_params = + failed_payable_insert_new_records_params_arc.lock().unwrap(); + assert_eq!(sent_payable_insert_new_records_params.len(), 1); + assert_eq!( + sent_payable_insert_new_records_params[0], + BTreeSet::from([sent_tx_1, sent_tx_2]) + ); + assert_eq!(failed_payable_insert_new_records_params.len(), 1); + assert!(failed_payable_insert_new_records_params[0].contains(&failed_tx_1)); + assert!(failed_payable_insert_new_records_params[0].contains(&failed_tx_2)); + assert_eq!( + result, + PayableScanResult { + ui_response_opt: Some(NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }), + result: NextScanToRun::PendingPayableScan, + } + ); + TestLogHandler::new().exists_log_matching(&format!( + "INFO: {test_name}: The Payables scan ended in \\d+ms." + )); + } + + #[test] + fn retry_payable_scan_finishes_as_expected() { + init_test_logging(); + let test_name = "retry_payable_scan_finishes_as_expected"; + let sent_payable_insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_update_statuses_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&sent_payable_insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_txs = vec![make_sent_tx(1), make_sent_tx(2)]; + let failed_txs = vec![make_failed_tx(1), make_failed_tx(2)]; + let prev_failed_txs: BTreeSet = sent_txs + .iter() + .enumerate() + .map(|(i, tx)| { + let i = (i + 1) * 10; + FailedTxBuilder::default() + .hash(make_tx_hash(i as u32)) + .nonce(i as u64) + .receiver_address(tx.receiver_address) + .build() + }) + .collect(); + let failed_paybale_dao = FailedPayableDaoMock::default() + .update_statuses_params(&failed_payable_update_statuses_params_arc) + .retrieve_txs_result(prev_failed_txs) + .update_statuses_result(Ok(())); + let mut subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_paybale_dao) + .build(); + subject.mark_as_started(SystemTime::now()); + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 5678, + }; + let sent_payables = SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: sent_txs.clone(), + failed_txs: failed_txs.clone(), + }), + payable_scan_type: PayableScanType::Retry, + response_skeleton_opt: Some(response_skeleton), + }; + let logger = Logger::new(test_name); + + let result = subject.finish_scan(sent_payables, &logger); + + let sent_payable_insert_new_records_params = + sent_payable_insert_new_records_params_arc.lock().unwrap(); + let failed_payable_update_statuses_params = + failed_payable_update_statuses_params_arc.lock().unwrap(); + assert_eq!(sent_payable_insert_new_records_params.len(), 1); + assert_eq!( + sent_payable_insert_new_records_params[0], + sent_txs.iter().cloned().collect::>() + ); + assert_eq!(failed_payable_update_statuses_params.len(), 1); + let updated_statuses = failed_payable_update_statuses_params[0].clone(); + assert_eq!(updated_statuses.len(), 2); + assert_eq!( + updated_statuses.get(&make_tx_hash(10)).unwrap(), + &FailureStatus::RecheckRequired(Waiting) + ); + assert_eq!( + updated_statuses.get(&make_tx_hash(20)).unwrap(), + &FailureStatus::RecheckRequired(Waiting) + ); + assert_eq!( + result, + PayableScanResult { + ui_response_opt: Some(NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }), + result: NextScanToRun::PendingPayableScan, + } + ); + let tlh = TestLogHandler::new(); + tlh.exists_log_containing(&format!( + "WARN: {test_name}: While retrying, 2 transactions with hashes: {} have failed.", + join_with_separator(failed_txs, |failed_tx| format!("{:?}", failed_tx.hash), ",") + )); + tlh.exists_log_matching(&format!( + "INFO: {test_name}: The Payables scan ended in \\d+ms." + )); + } + + #[test] + fn payable_scanner_with_error_works_as_expected() { + test_execute_payable_scanner_finish_scan_with_an_error(PayableScanType::New, "new"); + test_execute_payable_scanner_finish_scan_with_an_error(PayableScanType::Retry, "retry"); + } + + fn test_execute_payable_scanner_finish_scan_with_an_error( + payable_scan_type: PayableScanType, + suffix: &str, + ) { + init_test_logging(); + let test_name = &format!("test_execute_payable_scanner_finish_scan_with_an_error_{suffix}"); + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 5678, + }; + let mut subject = PayableScannerBuilder::new().build(); + subject.mark_as_started(SystemTime::now()); + let sent_payables = SentPayables { + payment_procedure_result: Err("Any error".to_string()), + payable_scan_type, + response_skeleton_opt: Some(response_skeleton), + }; + let logger = Logger::new(test_name); + + let result = subject.finish_scan(sent_payables, &logger); + + assert_eq!( + result, + PayableScanResult { + ui_response_opt: Some(NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }), + result: match payable_scan_type { + PayableScanType::New => NextScanToRun::NewPayableScan, + PayableScanType::Retry => NextScanToRun::RetryPayableScan, + }, + } + ); + let tlh = TestLogHandler::new(); + tlh.exists_log_containing(&format!( + "WARN: {test_name}: Local error occurred before transaction signing. Error: Any error" + )); + tlh.exists_log_matching(&format!( + "INFO: {test_name}: The Payables scan ended in \\d+ms." + )); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs new file mode 100644 index 000000000..b82d2c46e --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -0,0 +1,1065 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +mod finish_scan; +pub mod msgs; +mod start_scan; +pub mod test_utils; +pub mod tx_templates; + +pub mod payment_adjuster_integration; +pub mod utils; + +use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCondition::ByStatus; +use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedTx, FailureReason, FailureRetrieveCondition, FailureStatus, +}; +use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition::ByAddresses; +use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, SentTx}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::payment_adjuster::PaymentAdjuster; +use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; +use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::SolvencySensitivePaymentInstructor; +use crate::accountant::scanners::payable_scanner::utils::{ + batch_stats, calculate_occurences, filter_receiver_addresses_from_txs, generate_status_updates, + payables_debug_summary, NextScanToRun, PayableScanResult, PayableThresholdsGauge, + PayableThresholdsGaugeReal, PendingPayableMissingInDb, +}; +use crate::accountant::scanners::{Scanner, ScannerCommon, StartableScanner}; +use crate::accountant::{ + gwei_to_wei, join_with_commas, join_with_separator, PayableScanType, PendingPayable, + ResponseSkeleton, ScanForNewPayables, ScanForRetryPayables, SentPayables, +}; +use crate::blockchain::blockchain_interface::data_structures::BatchResults; +use crate::blockchain::errors::validation_status::ValidationStatus; +use crate::sub_lib::accountant::PaymentThresholds; +use crate::sub_lib::wallet::Wallet; +use itertools::Itertools; +use masq_lib::logger::Logger; +use masq_lib::messages::{ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use masq_lib::utils::ExpectValue; +use std::collections::{BTreeSet, HashMap}; +use std::rc::Rc; +use std::time::SystemTime; +use web3::types::Address; + +pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: + StartableScanner + + StartableScanner + + SolvencySensitivePaymentInstructor + + Scanner +{ +} + +pub struct PayableScanner { + pub payable_threshold_gauge: Box, + pub common: ScannerCommon, + pub payable_dao: Box, + pub sent_payable_dao: Box, + pub failed_payable_dao: Box, + pub payment_adjuster: Box, +} + +impl MultistageDualPayableScanner for PayableScanner {} + +impl PayableScanner { + pub fn new( + payable_dao: Box, + sent_payable_dao: Box, + failed_payable_dao: Box, + payment_thresholds: Rc, + payment_adjuster: Box, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + payable_dao, + sent_payable_dao, + failed_payable_dao, + payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), + payment_adjuster, + } + } + + pub fn sniff_out_alarming_payables_and_maybe_log_them( + &self, + retrieve_payables: Vec, + logger: &Logger, + ) -> Vec { + fn pass_payables_and_drop_points( + qp_tp: impl Iterator, + ) -> Vec { + let (payables, _) = qp_tp.unzip::<_, _, Vec, Vec<_>>(); + payables + } + + let qualified_payables_and_points_uncollected = + retrieve_payables.into_iter().flat_map(|account| { + self.payable_exceeded_threshold(&account, SystemTime::now()) + .map(|threshold_point| (account, threshold_point)) + }); + match logger.debug_enabled() { + false => pass_payables_and_drop_points(qualified_payables_and_points_uncollected), + true => { + let qualified_and_points_collected = + qualified_payables_and_points_uncollected.collect_vec(); + payables_debug_summary(&qualified_and_points_collected, logger); + pass_payables_and_drop_points(qualified_and_points_collected.into_iter()) + } + } + } + + fn payable_exceeded_threshold( + &self, + payable: &PayableAccount, + now: SystemTime, + ) -> Option { + let debt_age = now + .duration_since(payable.last_paid_timestamp) + .expect("Internal error") + .as_secs(); + + if self.payable_threshold_gauge.is_innocent_age( + debt_age, + self.common.payment_thresholds.maturity_threshold_sec, + ) { + return None; + } + + if self.payable_threshold_gauge.is_innocent_balance( + payable.balance_wei, + gwei_to_wei(self.common.payment_thresholds.permanent_debt_allowed_gwei), + ) { + return None; + } + + let threshold = self + .payable_threshold_gauge + .calculate_payout_threshold_in_gwei(&self.common.payment_thresholds, debt_age); + if payable.balance_wei > threshold { + Some(threshold) + } else { + None + } + } + + fn 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::>(); + + if hashset_with_hashes_to_eliminate_duplicates.len() != actual_sent_payables_len { + panic!( + "Found duplicates in the recent sent txs: {:?}", + just_baked_sent_payables + ); + } + + 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 = hashset_with_hashes_to_eliminate_duplicates + .difference(&hashes_from_db) + .copied(); + + let mut sent_payables_hashmap = just_baked_sent_payables + .iter() + .map(|payable| (payable.hash, &payable.recipient_wallet)) + .collect::>(); + missing_sent_payables_hashes + .map(|hash| { + let wallet_address = sent_payables_hashmap + .remove(&hash) + .expectv("wallet") + .address(); + PendingPayableMissingInDb::new(wallet_address, hash) + }) + .collect() + } + + // TODO this should be used when Utkarsh picks the card GH-701 where he postponed the fix of saving the SentTxs + #[allow(dead_code)] + fn check_on_missing_sent_tx_records(&self, sent_payments: &[&PendingPayable]) { + fn missing_record_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { + format!( + "Expected sent-payable records for {} were not found. The system has become unreliable", + join_with_commas(nonexistent, |missing_sent_tx_ids| format!( + "(tx: {:?}, to wallet: {:?})", + missing_sent_tx_ids.hash, missing_sent_tx_ids.recipient + )) + ) + } + + 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)) + } + } + + fn determine_next_scan_to_run(msg: &SentPayables) -> NextScanToRun { + match &msg.payment_procedure_result { + Ok(batch_results) => { + if batch_results.sent_txs.is_empty() { + if batch_results.failed_txs.is_empty() { + return NextScanToRun::NewPayableScan; + } else { + return NextScanToRun::RetryPayableScan; + } + } + + NextScanToRun::PendingPayableScan + } + Err(_e) => match msg.payable_scan_type { + PayableScanType::New => NextScanToRun::NewPayableScan, + PayableScanType::Retry => NextScanToRun::RetryPayableScan, + }, + } + } + + fn process_message(&self, msg: &SentPayables, logger: &Logger) { + match &msg.payment_procedure_result { + Ok(batch_results) => match msg.payable_scan_type { + PayableScanType::New => { + self.handle_batch_results_for_new_scan(batch_results, logger) + } + PayableScanType::Retry => { + self.handle_batch_results_for_retry_scan(batch_results, logger) + } + }, + Err(local_error) => Self::log_local_error(local_error, logger), + } + } + + fn handle_batch_results_for_new_scan(&self, batch_results: &BatchResults, logger: &Logger) { + let (sent, failed) = calculate_occurences(batch_results); + debug!( + logger, + "Processed new txs while sending to RPC: {}", + batch_stats(sent, failed), + ); + if sent > 0 { + self.insert_records_in_sent_payables(&batch_results.sent_txs); + } + if failed > 0 { + self.insert_records_in_failed_payables(&batch_results.failed_txs); + } + } + + fn handle_batch_results_for_retry_scan(&self, batch_results: &BatchResults, logger: &Logger) { + let (sent, failed) = calculate_occurences(batch_results); + debug!( + logger, + "Processed retried txs while sending to RPC: {}", + batch_stats(sent, failed), + ); + + if sent > 0 { + self.insert_records_in_sent_payables(&batch_results.sent_txs); + self.update_statuses_of_prev_txs(&batch_results.sent_txs); + } + if failed > 0 { + // TODO: Would it be a good ides to update Retry attempt of previous tx? + Self::log_failed_txs_during_retry(&batch_results.failed_txs, logger); + } + } + + fn update_statuses_of_prev_txs(&self, sent_txs: &[SentTx]) { + // TODO: We can do better here, possibly by creating a relationship between failed and sent txs + // Also, consider the fact that some txs will be with PendingTooLong status, what should we do with them? + let retrieved_txs = self.retrieve_failed_txs_by_receiver_addresses(sent_txs); + let (pending_too_long, other_reasons): (BTreeSet<_>, BTreeSet<_>) = retrieved_txs + .into_iter() + .partition(|tx| matches!(tx.reason, FailureReason::PendingTooLong)); + if !pending_too_long.is_empty() { + self.update_failed_txs( + &pending_too_long, + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ); + } + if !other_reasons.is_empty() { + self.update_failed_txs(&other_reasons, FailureStatus::Concluded); + } + } + + fn retrieve_failed_txs_by_receiver_addresses(&self, sent_txs: &[SentTx]) -> BTreeSet { + let receiver_addresses = filter_receiver_addresses_from_txs(sent_txs.iter()); + self.failed_payable_dao + .retrieve_txs(Some(FailureRetrieveCondition::ByReceiverAddresses( + receiver_addresses, + ))) + } + + fn update_failed_txs(&self, failed_txs: &BTreeSet, status: FailureStatus) { + let status_updates = generate_status_updates(failed_txs, status); + self.failed_payable_dao + .update_statuses(&status_updates) + .unwrap_or_else(|e| panic!("Failed to conclude txs in database: {:?}", e)); + } + + fn log_failed_txs_during_retry(failed_txs: &[FailedTx], logger: &Logger) { + warning!( + logger, + "While retrying, {} transactions with hashes: {} have failed.", + failed_txs.len(), + join_with_separator(failed_txs, |failed_tx| format!("{:?}", failed_tx.hash), ",") + ) + } + + fn log_local_error(local_error: &str, logger: &Logger) { + warning!( + logger, + "Local error occurred before transaction signing. Error: {}", + local_error + ) + } + + fn insert_records_in_sent_payables(&self, sent_txs: &[SentTx]) { + self.sent_payable_dao + .insert_new_records(&sent_txs.iter().cloned().collect()) + .unwrap_or_else(|e| { + panic!( + "Failed to insert transactions into the SentPayable table. Error: {:?}", + e + ) + }); + } + + fn insert_records_in_failed_payables(&self, failed_txs: &[FailedTx]) { + self.failed_payable_dao + .insert_new_records(&failed_txs.iter().cloned().collect()) + .unwrap_or_else(|e| { + panic!( + "Failed to insert transactions into the FailedPayable table. Error: {:?}", + e + ) + }); + } + + fn generate_ui_response( + response_skeleton_opt: Option, + ) -> Option { + response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) + } + + fn get_txs_to_retry(&self) -> BTreeSet { + self.failed_payable_dao + .retrieve_txs(Some(ByStatus(RetryRequired))) + } + + fn find_amount_from_payables( + &self, + txs_to_retry: &BTreeSet, + ) -> HashMap { + let addresses = filter_receiver_addresses_from_txs(txs_to_retry.iter()); + self.payable_dao + .retrieve_payables(Some(ByAddresses(addresses))) + .into_iter() + .map(|payable| (payable.wallet.address(), payable.balance_wei)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::accountant::db_access_objects::failed_payable_dao::FailedPayableDaoError; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError; + use crate::accountant::db_access_objects::test_utils::{ + make_failed_tx, make_sent_tx, FailedTxBuilder, TxBuilder, + }; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; + use crate::accountant::test_utils::{ + make_payable_account, FailedPayableDaoMock, PayableThresholdsGaugeMock, SentPayableDaoMock, + }; + use crate::blockchain::test_utils::make_tx_hash; + use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; + use crate::test_utils::make_wallet; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + #[test] + fn generate_ui_response_works_correctly() { + assert_eq!(PayableScanner::generate_ui_response(None), None); + assert_eq!( + PayableScanner::generate_ui_response(Some(ResponseSkeleton { + client_id: 1234, + context_id: 5678 + })), + Some(NodeToUiMessage { + target: MessageTarget::ClientId(1234), + body: UiScanResponse {}.tmb(5678), + }) + ); + } + + #[test] + fn determine_next_scan_to_run_works() { + // Error + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Err("Any error".to_string()), + payable_scan_type: PayableScanType::New, + response_skeleton_opt: None, + }), + NextScanToRun::NewPayableScan + ); + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Err("Any error".to_string()), + payable_scan_type: PayableScanType::Retry, + response_skeleton_opt: None, + }), + NextScanToRun::RetryPayableScan + ); + + // BatchResults is empty + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::New, + response_skeleton_opt: None, + }), + NextScanToRun::NewPayableScan + ); + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::Retry, + response_skeleton_opt: None, + }), + NextScanToRun::NewPayableScan + ); + + // Only FailedTxs + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![], + failed_txs: vec![make_failed_tx(1), make_failed_tx(2)], + }), + payable_scan_type: PayableScanType::New, + response_skeleton_opt: None, + }), + NextScanToRun::RetryPayableScan + ); + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![], + failed_txs: vec![make_failed_tx(1), make_failed_tx(2)], + }), + payable_scan_type: PayableScanType::Retry, + response_skeleton_opt: None, + }), + NextScanToRun::RetryPayableScan + ); + + // Only SentTxs + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![make_sent_tx(1), make_sent_tx(2)], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::New, + response_skeleton_opt: None, + }), + NextScanToRun::PendingPayableScan + ); + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![make_sent_tx(1), make_sent_tx(2)], + failed_txs: vec![], + }), + payable_scan_type: PayableScanType::Retry, + response_skeleton_opt: None, + }), + NextScanToRun::PendingPayableScan + ); + + // Both SentTxs and FailedTxs are present + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![make_sent_tx(1), make_sent_tx(2)], + failed_txs: vec![make_failed_tx(1), make_failed_tx(2)], + }), + payable_scan_type: PayableScanType::New, + response_skeleton_opt: None, + }), + NextScanToRun::PendingPayableScan + ); + assert_eq!( + PayableScanner::determine_next_scan_to_run(&SentPayables { + payment_procedure_result: Ok(BatchResults { + sent_txs: vec![make_sent_tx(1), make_sent_tx(2)], + failed_txs: vec![make_failed_tx(1), make_failed_tx(2)], + }), + payable_scan_type: PayableScanType::Retry, + response_skeleton_opt: None, + }), + NextScanToRun::PendingPayableScan + ); + } + + #[test] + fn update_statuses_of_prev_txs_updates_statuses_correctly() { + let retrieve_txs_params = Arc::new(Mutex::new(vec![])); + let update_statuses_params = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(1); + let tx_hash_2 = make_tx_hash(2); + let failed_payable_dao = FailedPayableDaoMock::default() + .retrieve_txs_params(&retrieve_txs_params) + .retrieve_txs_result(BTreeSet::from([ + FailedTxBuilder::default() + .hash(tx_hash_1) + .reason(FailureReason::PendingTooLong) + .build(), + FailedTxBuilder::default() + .hash(tx_hash_2) + .reason(FailureReason::Reverted) + .build(), + ])) + .update_statuses_params(&update_statuses_params) + .update_statuses_result(Ok(())) + .update_statuses_result(Ok(())); + let subject = PayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let sent_txs = vec![make_sent_tx(1), make_sent_tx(2)]; + + subject.update_statuses_of_prev_txs(&sent_txs); + + let update_params = update_statuses_params.lock().unwrap(); + assert_eq!(update_params.len(), 2); + assert_eq!( + update_params[0], + hashmap!(tx_hash_1 => FailureStatus::RecheckRequired(ValidationStatus::Waiting)) + ); + assert_eq!( + update_params[1], + hashmap!(tx_hash_2 => FailureStatus::Concluded) + ); + } + + #[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 = "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 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(); + + subject.check_for_missing_records(&pending_payables_ref); + } + + #[test] + fn payable_is_found_innocent_by_age_and_returns() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result(true); + let mut subject = PayableScannerBuilder::new().build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + let now = SystemTime::now(); + let debt_age_s = 111_222; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let mut payable = make_payable_account(111); + payable.last_paid_timestamp = last_paid_timestamp; + + let result = subject.payable_exceeded_threshold(&payable, now); + + assert_eq!(result, None); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned, threshold_value) = is_innocent_age_params.remove(0); + assert!(is_innocent_age_params.is_empty()); + assert_eq!(debt_age_returned, debt_age_s); + assert_eq!( + threshold_value, + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + ) + // No panic and so no other method was called, which means an early return + } + + #[test] + fn payable_is_found_innocent_by_balance_and_returns() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result(false) + .is_innocent_balance_params(&is_innocent_balance_params_arc) + .is_innocent_balance_result(true); + let mut subject = PayableScannerBuilder::new().build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + let now = SystemTime::now(); + let debt_age_s = 3_456; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let mut payable = make_payable_account(222); + payable.last_paid_timestamp = last_paid_timestamp; + payable.balance_wei = 123456; + + let result = subject.payable_exceeded_threshold(&payable, now); + + assert_eq!(result, None); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned, _) = is_innocent_age_params.remove(0); + assert!(is_innocent_age_params.is_empty()); + assert_eq!(debt_age_returned, debt_age_s); + let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); + assert_eq!( + *is_innocent_balance_params, + vec![( + 123456_u128, + gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) + )] + ) + //no other method was called (absence of panic), and that means we returned early + } + + #[test] + fn threshold_calculation_depends_on_user_defined_payment_thresholds() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); + let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); + let balance = gwei_to_wei(5555_u64); + let now = SystemTime::now(); + let debt_age_s = 1111 + 1; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let payable_account = PayableAccount { + wallet: make_wallet("hi"), + balance_wei: balance, + last_paid_timestamp, + pending_payable_opt: None, + }; + let custom_payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 1111, + payment_grace_period_sec: 2222, + permanent_debt_allowed_gwei: 3333, + debt_threshold_gwei: 4444, + threshold_interval_sec: 5555, + unban_below_gwei: 5555, + }; + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result( + debt_age_s <= custom_payment_thresholds.maturity_threshold_sec as u64, + ) + .is_innocent_balance_params(&is_innocent_balance_params_arc) + .is_innocent_balance_result( + balance <= gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei), + ) + .calculate_payout_threshold_in_gwei_params(&calculate_payable_threshold_params_arc) + .calculate_payout_threshold_in_gwei_result(4567898); //made up value + let mut subject = PayableScannerBuilder::new() + .payment_thresholds(custom_payment_thresholds) + .build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + + let result = subject.payable_exceeded_threshold(&payable_account, now); + + assert_eq!(result, Some(4567898)); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned_innocent, curve_derived_time) = is_innocent_age_params.remove(0); + assert_eq!(*is_innocent_age_params, vec![]); + assert_eq!(debt_age_returned_innocent, debt_age_s); + assert_eq!( + curve_derived_time, + custom_payment_thresholds.maturity_threshold_sec as u64 + ); + let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); + assert_eq!( + *is_innocent_balance_params, + vec![( + payable_account.balance_wei, + gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei) + )] + ); + let mut calculate_payable_curves_params = + calculate_payable_threshold_params_arc.lock().unwrap(); + let (payment_thresholds, debt_age_returned_curves) = + calculate_payable_curves_params.remove(0); + assert_eq!(*calculate_payable_curves_params, vec![]); + assert_eq!(debt_age_returned_curves, debt_age_s); + assert_eq!(payment_thresholds, custom_payment_thresholds) + } + + #[test] + fn payable_with_debt_under_the_slope_is_marked_unqualified() { + init_test_logging(); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let debt = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); + let time = to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; + let unqualified_payable_account = vec![PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: debt, + last_paid_timestamp: from_unix_timestamp(time), + pending_payable_opt: None, + }]; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let test_name = + "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; + let logger = Logger::new(test_name); + + let result = subject + .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); + + assert_eq!(result, vec![]); + TestLogHandler::new() + .exists_no_log_containing(&format!("DEBUG: {}: Paying qualified debts", test_name)); + } + + #[test] + fn payable_with_debt_above_the_slope_is_qualified() { + init_test_logging(); + let payment_thresholds = PaymentThresholds::default(); + let debt = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); + let time = (payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec + - 1) as i64; + let qualified_payable = PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: debt, + last_paid_timestamp: from_unix_timestamp(time), + pending_payable_opt: None, + }; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let test_name = "payable_with_debt_above_the_slope_is_qualified"; + let logger = Logger::new(test_name); + + let result = subject.sniff_out_alarming_payables_and_maybe_log_them( + vec![qualified_payable.clone()], + &logger, + ); + + assert_eq!(result, vec![qualified_payable]); + TestLogHandler::new().exists_log_matching(&format!( + "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 + )); + } + + #[test] + fn retrieved_payables_turn_into_an_empty_vector_if_all_unqualified() { + init_test_logging(); + let test_name = "retrieved_payables_turn_into_an_empty_vector_if_all_unqualified"; + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let unqualified_payable_account = vec![PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + ), + pending_payable_opt: None, + }]; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let logger = Logger::new(test_name); + + let result = subject + .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); + + assert_eq!(result, vec![]); + TestLogHandler::new() + .exists_no_log_containing(&format!("DEBUG: {test_name}: Paying qualified debts")); + } + + #[test] + fn insert_records_in_sent_payables_inserts_records_successfully() { + let insert_new_records_params = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params) + .insert_new_records_result(Ok(())); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .build(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let sent_txs = vec![tx1.clone(), tx2.clone()]; + + subject.insert_records_in_sent_payables(&sent_txs); + + let params = insert_new_records_params.lock().unwrap(); + assert_eq!(params.len(), 1); + assert_eq!(params[0], sent_txs.into_iter().collect()); + } + + #[test] + fn insert_records_in_sent_payables_panics_on_error() { + let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Err( + SentPayableDaoError::PartialExecution("Test error".to_string()), + )); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .build(); + let tx = TxBuilder::default().hash(make_tx_hash(1)).build(); + let sent_txs = vec![tx]; + + let result = catch_unwind(AssertUnwindSafe(|| { + subject.insert_records_in_sent_payables(&sent_txs); + })) + .unwrap_err(); + + let panic_msg = result.downcast_ref::().unwrap(); + assert!(panic_msg.contains("Failed to insert transactions into the SentPayable table")); + assert!(panic_msg.contains("Test error")); + } + + #[test] + fn insert_records_in_failed_payables_inserts_records_successfully() { + let insert_new_records_params = Arc::new(Mutex::new(vec![])); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params) + .insert_new_records_result(Ok(())); + let subject = PayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let failed_tx1 = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let failed_tx2 = FailedTxBuilder::default().hash(make_tx_hash(2)).build(); + let failed_txs = vec![failed_tx1.clone(), failed_tx2.clone()]; + + subject.insert_records_in_failed_payables(&failed_txs); + + let params = insert_new_records_params.lock().unwrap(); + assert_eq!(params.len(), 1); + assert_eq!(params[0], BTreeSet::from([failed_tx1, failed_tx2])); + } + + #[test] + fn insert_records_in_failed_payables_panics_on_error() { + let failed_payable_dao = FailedPayableDaoMock::default().insert_new_records_result(Err( + FailedPayableDaoError::PartialExecution("Test error".to_string()), + )); + let subject = PayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let failed_tx = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let failed_txs = vec![failed_tx]; + + let result = catch_unwind(AssertUnwindSafe(|| { + subject.insert_records_in_failed_payables(&failed_txs); + })) + .unwrap_err(); + + let panic_msg = result.downcast_ref::().unwrap(); + assert!(panic_msg.contains("Failed to insert transactions into the FailedPayable table")); + assert!(panic_msg.contains("Test error")); + } + + #[test] + fn handle_batch_results_for_new_scan_does_not_perform_any_operation_when_sent_txs_is_empty() { + let insert_new_records_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let insert_new_records_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_sent_tx_params_arc); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_failed_tx_params_arc) + .insert_new_records_result(Ok(())); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let batch_results = BatchResults { + sent_txs: vec![], + failed_txs: vec![make_failed_tx(1)], + }; + + subject.handle_batch_results_for_new_scan(&batch_results, &Logger::new("test")); + + assert_eq!( + insert_new_records_failed_tx_params_arc + .lock() + .unwrap() + .len(), + 1 + ); + assert!(insert_new_records_sent_tx_params_arc + .lock() + .unwrap() + .is_empty()); + } + + #[test] + fn handle_batch_results_for_new_scan_does_not_perform_any_operation_when_failed_txs_is_empty() { + let insert_new_records_params_failed = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_failed); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let batch_results = BatchResults { + sent_txs: vec![make_sent_tx(1)], + failed_txs: vec![], + }; + + subject.handle_batch_results_for_new_scan(&batch_results, &Logger::new("test")); + + assert!(insert_new_records_params_failed.lock().unwrap().is_empty()); + } + + #[test] + fn handle_batch_results_for_retry_scan_does_not_perform_any_operation_when_sent_txs_is_empty() { + let insert_new_records_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let retrieve_txs_params = Arc::new(Mutex::new(vec![])); + let update_statuses_params = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_sent_tx_params_arc); + let failed_payable_dao = FailedPayableDaoMock::default() + .retrieve_txs_params(&retrieve_txs_params) + .update_statuses_params(&update_statuses_params); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let batch_results = BatchResults { + sent_txs: vec![], + failed_txs: vec![make_failed_tx(1)], + }; + + subject.handle_batch_results_for_retry_scan(&batch_results, &Logger::new("test")); + + assert!(insert_new_records_sent_tx_params_arc + .lock() + .unwrap() + .is_empty()); + assert!(retrieve_txs_params.lock().unwrap().is_empty()); + assert!(update_statuses_params.lock().unwrap().is_empty()); + } + + #[test] + fn handle_retry_logs_no_warn_when_failed_txs_exist() { + init_test_logging(); + let test_name = "handle_retry_logs_no_warn_when_failed_txs_exist"; + let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .retrieve_txs_result(BTreeSet::from([make_failed_tx(1)])) + .update_statuses_result(Ok(())) + .update_statuses_result(Ok(())); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let batch_results = BatchResults { + sent_txs: vec![make_sent_tx(1)], + failed_txs: vec![], + }; + + subject.handle_batch_results_for_retry_scan(&batch_results, &Logger::new(test_name)); + + let tlh = TestLogHandler::new(); + tlh.exists_no_log_containing(&format!("WARN: {test_name}")); + } + + #[test] + fn update_failed_txs_panics_on_error() { + let failed_payable_dao = FailedPayableDaoMock::default().update_statuses_result(Err( + FailedPayableDaoError::SqlExecutionFailed("I slept too much".to_string()), + )); + let subject = PayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let failed_tx = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let failed_txs = BTreeSet::from([failed_tx]); + + let result = catch_unwind(AssertUnwindSafe(|| { + subject.update_failed_txs(&failed_txs, FailureStatus::Concluded); + })) + .unwrap_err(); + + let panic_msg = result.downcast_ref::().unwrap(); + assert!(panic_msg.contains( + "Failed to conclude txs in database: SqlExecutionFailed(\"I slept too much\")" + )); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner/msgs.rs new file mode 100644 index 000000000..5379d26f5 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/msgs.rs @@ -0,0 +1,69 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; +use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; +use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_bridge::MsgInterpretableAsDetailedScanType; +use crate::sub_lib::accountant::DetailedScanType; +use crate::sub_lib::wallet::Wallet; +use actix::Message; +use itertools::Either; + +#[derive(Debug, Message, PartialEq, Eq, Clone)] +pub struct InitialTemplatesMessage { + pub initial_templates: Either, + pub consuming_wallet: Wallet, + pub response_skeleton_opt: Option, +} + +impl MsgInterpretableAsDetailedScanType for InitialTemplatesMessage { + fn detailed_scan_type(&self) -> DetailedScanType { + match self.initial_templates { + Either::Left(_) => DetailedScanType::NewPayables, + Either::Right(_) => DetailedScanType::RetryPayables, + } + } +} + +#[derive(Message)] +pub struct PricedTemplatesMessage { + pub priced_templates: Either, + pub agent: Box, + pub response_skeleton_opt: Option, +} + +impl SkeletonOptHolder for InitialTemplatesMessage { + fn skeleton_opt(&self) -> Option { + self.response_skeleton_opt + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; + use crate::blockchain::blockchain_bridge::MsgInterpretableAsDetailedScanType; + use crate::sub_lib::accountant::DetailedScanType; + use crate::test_utils::make_wallet; + use itertools::Either; + + #[test] + fn detailed_scan_type_is_implemented_for_initial_templates_message() { + let msg_a = InitialTemplatesMessage { + initial_templates: Either::Left(NewTxTemplates(vec![])), + consuming_wallet: make_wallet("abc"), + response_skeleton_opt: None, + }; + let msg_b = InitialTemplatesMessage { + initial_templates: Either::Right(RetryTxTemplates(vec![])), + consuming_wallet: make_wallet("abc"), + response_skeleton_opt: None, + }; + + assert_eq!(msg_a.detailed_scan_type(), DetailedScanType::NewPayables); + assert_eq!(msg_b.detailed_scan_type(), DetailedScanType::RetryPayables); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/payment_adjuster_integration.rs b/node/src/accountant/scanners/payable_scanner/payment_adjuster_integration.rs new file mode 100644 index 000000000..d3d38e3a9 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/payment_adjuster_integration.rs @@ -0,0 +1,60 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::payment_adjuster::Adjustment; +use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; +use crate::accountant::scanners::payable_scanner::PayableScanner; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use itertools::Either; +use masq_lib::logger::Logger; +use std::time::SystemTime; + +pub struct PreparedAdjustment { + pub original_setup_msg: PricedTemplatesMessage, + pub adjustment: Adjustment, +} + +pub trait SolvencySensitivePaymentInstructor { + fn try_skipping_payment_adjustment( + &self, + msg: PricedTemplatesMessage, + logger: &Logger, + ) -> Result, String>; + + fn perform_payment_adjustment( + &self, + setup: PreparedAdjustment, + logger: &Logger, + ) -> OutboundPaymentsInstructions; +} + +impl SolvencySensitivePaymentInstructor for PayableScanner { + fn try_skipping_payment_adjustment( + &self, + msg: PricedTemplatesMessage, + logger: &Logger, + ) -> Result, String> { + match self + .payment_adjuster + .search_for_indispensable_adjustment(&msg, logger) + { + Ok(None) => Ok(Either::Left(OutboundPaymentsInstructions::new( + msg.priced_templates, + msg.agent, + msg.response_skeleton_opt, + ))), + Ok(Some(adjustment)) => Ok(Either::Right(PreparedAdjustment { + original_setup_msg: msg, + adjustment, + })), + Err(_e) => todo!("be implemented with GH-711"), + } + } + + fn perform_payment_adjustment( + &self, + setup: PreparedAdjustment, + logger: &Logger, + ) -> OutboundPaymentsInstructions { + let now = SystemTime::now(); + self.payment_adjuster.adjust_payments(setup, now, logger) + } +} diff --git a/node/src/accountant/scanners/payable_scanner/start_scan.rs b/node/src/accountant/scanners/payable_scanner/start_scan.rs new file mode 100644 index 000000000..35cbd3ab2 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/start_scan.rs @@ -0,0 +1,189 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; +use crate::accountant::scanners::payable_scanner::utils::investigate_debt_extremes; +use crate::accountant::scanners::payable_scanner::PayableScanner; +use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner}; +use crate::accountant::{ResponseSkeleton, ScanForNewPayables, ScanForRetryPayables}; +use crate::sub_lib::wallet::Wallet; +use itertools::Either; +use masq_lib::logger::Logger; +use std::time::SystemTime; + +impl StartableScanner for PayableScanner { + fn start_scan( + &mut self, + consuming_wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.mark_as_started(timestamp); + info!(logger, "Scanning for new payables"); + let retrieved_payables = self.payable_dao.retrieve_payables(None); + + debug!( + logger, + "{}", + investigate_debt_extremes(timestamp, &retrieved_payables) + ); + + let qualified_payables = + self.sniff_out_alarming_payables_and_maybe_log_them(retrieved_payables, logger); + + match qualified_payables.is_empty() { + true => { + self.mark_as_ended(logger); + Err(StartScanError::NothingToProcess) + } + false => { + info!( + logger, + "Chose {} qualified debts to pay", + qualified_payables.len() + ); + let new_tx_templates = NewTxTemplates::from(&qualified_payables); + Ok(InitialTemplatesMessage { + initial_templates: Either::Left(new_tx_templates), + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt, + }) + } + } + } +} + +impl StartableScanner for PayableScanner { + fn start_scan( + &mut self, + consuming_wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.mark_as_started(timestamp); + info!(logger, "Scanning for retry payables"); + let failed_txs = self.get_txs_to_retry(); + let amount_from_payables = self.find_amount_from_payables(&failed_txs); + let retry_tx_templates = RetryTxTemplates::new(&failed_txs, &amount_from_payables); + + Ok(InitialTemplatesMessage { + initial_templates: Either::Right(retry_tx_templates), + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::PendingTooLong; + use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCondition::ByStatus; + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; + use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition; + use crate::accountant::db_access_objects::test_utils::FailedTxBuilder; + use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::{ + RetryTxTemplate, RetryTxTemplates, + }; + use crate::accountant::scanners::Scanners; + use crate::accountant::test_utils::{ + make_payable_account, FailedPayableDaoMock, PayableDaoMock, + }; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::{make_paying_wallet, make_wallet}; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::collections::BTreeSet; + use std::sync::{Arc, Mutex}; + use std::time::SystemTime; + + #[test] + fn start_scan_for_retry_works() { + init_test_logging(); + let test_name = "start_scan_for_retry_works"; + let logger = Logger::new(test_name); + let retrieve_txs_params_arc = Arc::new(Mutex::new(vec![])); + let retrieve_payables_params_arc = Arc::new(Mutex::new(vec![])); + let timestamp = SystemTime::now(); + let consuming_wallet = make_paying_wallet(b"consuming"); + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }; + let payable_account_1 = make_payable_account(42); + let receiver_address_1 = payable_account_1.wallet.address(); + let receiever_wallet_2 = make_wallet("absent in payable dao"); + let receiver_address_2 = receiever_wallet_2.address(); + let failed_tx_1 = FailedTxBuilder::default() + .nonce(1) + .hash(make_tx_hash(1)) + .receiver_address(receiver_address_1) + .reason(PendingTooLong) + .status(RetryRequired) + .build(); + let failed_tx_2 = FailedTxBuilder::default() + .nonce(2) + .hash(make_tx_hash(2)) + .receiver_address(receiver_address_2) + .reason(PendingTooLong) + .status(RetryRequired) + .build(); + let expected_addresses = BTreeSet::from([receiver_address_1, receiver_address_2]); + let failed_payable_dao = FailedPayableDaoMock::new() + .retrieve_txs_params(&retrieve_txs_params_arc) + .retrieve_txs_result(BTreeSet::from([failed_tx_1.clone(), failed_tx_2.clone()])); + let payable_dao = PayableDaoMock::new() + .retrieve_payables_params(&retrieve_payables_params_arc) + .retrieve_payables_result(vec![payable_account_1.clone()]); // the second record is absent + let mut subject = PayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .payable_dao(payable_dao) + .build(); + + let result = Scanners::start_correct_payable_scanner::( + &mut subject, + &consuming_wallet, + timestamp, + Some(response_skeleton), + &logger, + ); + + let scan_started_at = subject.scan_started_at(); + let failed_payables_retrieve_txs_params = retrieve_txs_params_arc.lock().unwrap(); + let retrieve_payables_params = retrieve_payables_params_arc.lock().unwrap(); + let expected_tx_templates = { + let mut tx_template_1 = RetryTxTemplate::from(&failed_tx_1); + tx_template_1.base.amount_in_wei = + tx_template_1.base.amount_in_wei + payable_account_1.balance_wei; + + let tx_template_2 = RetryTxTemplate::from(&failed_tx_2); + + RetryTxTemplates(vec![tx_template_1, tx_template_2]) + }; + assert_eq!( + result, + Ok(InitialTemplatesMessage { + initial_templates: Either::Right(expected_tx_templates), + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: Some(response_skeleton), + }) + ); + assert_eq!(scan_started_at, Some(timestamp)); + assert_eq!( + failed_payables_retrieve_txs_params[0], + Some(ByStatus(FailureStatus::RetryRequired)) + ); + assert_eq!(failed_payables_retrieve_txs_params.len(), 1); + assert_eq!( + retrieve_payables_params[0], + Some(PayableRetrieveCondition::ByAddresses(expected_addresses)) + ); + assert_eq!(retrieve_payables_params.len(), 1); + TestLogHandler::new() + .exists_log_containing(&format!("INFO: {test_name}: Scanning for retry payables")); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/test_utils.rs b/node/src/accountant/scanners/payable_scanner/test_utils.rs new file mode 100644 index 000000000..4dcc8d67d --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/test_utils.rs @@ -0,0 +1,97 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +#![cfg(test)] + +use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; +use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::PayableScanner; +use crate::accountant::test_utils::{ + FailedPayableDaoMock, PayableDaoMock, PaymentAdjusterMock, SentPayableDaoMock, +}; +use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; +use crate::sub_lib::accountant::PaymentThresholds; +use std::rc::Rc; + +pub struct PayableScannerBuilder { + payable_dao: PayableDaoMock, + sent_payable_dao: SentPayableDaoMock, + failed_payable_dao: FailedPayableDaoMock, + payment_thresholds: PaymentThresholds, + payment_adjuster: PaymentAdjusterMock, +} + +impl PayableScannerBuilder { + pub fn new() -> Self { + Self { + payable_dao: PayableDaoMock::new(), + sent_payable_dao: SentPayableDaoMock::new(), + failed_payable_dao: FailedPayableDaoMock::new(), + payment_thresholds: PaymentThresholds::default(), + payment_adjuster: PaymentAdjusterMock::default(), + } + } + + pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> PayableScannerBuilder { + self.payable_dao = payable_dao; + self + } + + pub fn sent_payable_dao( + mut self, + sent_payable_dao: SentPayableDaoMock, + ) -> PayableScannerBuilder { + self.sent_payable_dao = sent_payable_dao; + self + } + + pub fn failed_payable_dao( + mut self, + failed_payable_dao: FailedPayableDaoMock, + ) -> PayableScannerBuilder { + self.failed_payable_dao = failed_payable_dao; + self + } + + pub fn payment_adjuster( + mut self, + payment_adjuster: PaymentAdjusterMock, + ) -> PayableScannerBuilder { + self.payment_adjuster = payment_adjuster; + self + } + + pub fn payment_thresholds(mut self, payment_thresholds: PaymentThresholds) -> Self { + self.payment_thresholds = payment_thresholds; + self + } + + pub fn build(self) -> PayableScanner { + PayableScanner::new( + Box::new(self.payable_dao), + Box::new(self.sent_payable_dao), + Box::new(self.failed_payable_dao), + Rc::new(self.payment_thresholds), + Box::new(self.payment_adjuster), + ) + } +} + +impl Clone for PricedTemplatesMessage { + fn clone(&self) -> Self { + let original_agent_id = self.agent.arbitrary_id_stamp(); + let cloned_agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(original_agent_id); + Self { + priced_templates: self.priced_templates.clone(), + agent: Box::new(cloned_agent), + response_skeleton_opt: self.response_skeleton_opt, + } + } +} + +impl Clone for PreparedAdjustment { + fn clone(&self) -> Self { + Self { + original_setup_msg: self.original_setup_msg.clone(), + adjustment: self.adjustment.clone(), + } + } +} diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/mod.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/mod.rs new file mode 100644 index 000000000..84adbe5e3 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/mod.rs @@ -0,0 +1,3 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod new; +pub mod retry; diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs new file mode 100644 index 000000000..aceb532b0 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs @@ -0,0 +1,217 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; +use std::ops::{Deref, DerefMut}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NewTxTemplate { + pub base: BaseTxTemplate, +} + +impl From<&PayableAccount> for NewTxTemplate { + fn from(payable_account: &PayableAccount) -> Self { + Self { + base: BaseTxTemplate::from(payable_account), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct NewTxTemplates(pub Vec); + +impl From> for NewTxTemplates { + fn from(new_tx_template_vec: Vec) -> Self { + Self(new_tx_template_vec) + } +} + +impl Deref for NewTxTemplates { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for NewTxTemplates { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl IntoIterator for NewTxTemplates { + type Item = NewTxTemplate; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator for NewTxTemplates { + fn from_iter>(iter: I) -> Self { + NewTxTemplates(iter.into_iter().collect()) + } +} + +impl From<&Vec> for NewTxTemplates { + fn from(payable_accounts: &Vec) -> Self { + Self(payable_accounts.iter().map(NewTxTemplate::from).collect()) + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::payable_dao::PayableAccount; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::{ + NewTxTemplate, NewTxTemplates, + }; + use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; + use crate::blockchain::test_utils::make_address; + use crate::test_utils::make_wallet; + use std::time::SystemTime; + + #[test] + fn new_tx_template_can_be_created_from_payable_account() { + let wallet = make_wallet("some wallet"); + let balance_wei = 1_000_000; + let payable_account = PayableAccount { + wallet: wallet.clone(), + balance_wei, + last_paid_timestamp: SystemTime::now(), + pending_payable_opt: None, + }; + + let new_tx_template = NewTxTemplate::from(&payable_account); + + assert_eq!(new_tx_template.base.receiver_address, wallet.address()); + assert_eq!(new_tx_template.base.amount_in_wei, balance_wei); + } + + #[test] + fn new_tx_templates_can_be_created_from_vec_using_into() { + let template1 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1000, + }, + }; + let template2 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 2000, + }, + }; + let templates_vec = vec![template1.clone(), template2.clone()]; + + let templates: NewTxTemplates = templates_vec.into(); + + assert_eq!(templates.len(), 2); + assert_eq!(templates[0], template1); + assert_eq!(templates[1], template2); + } + + #[test] + fn new_tx_templates_deref_provides_access_to_inner_vector() { + let template1 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1000, + }, + }; + let template2 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 2000, + }, + }; + + let templates = NewTxTemplates(vec![template1.clone(), template2.clone()]); + + assert_eq!(templates.len(), 2); + assert_eq!(templates[0], template1); + assert_eq!(templates[1], template2); + assert!(!templates.is_empty()); + assert!(templates.contains(&template1)); + assert_eq!( + templates + .iter() + .map(|template| template.base.amount_in_wei) + .sum::(), + 3000 + ); + } + + #[test] + fn new_tx_templates_into_iter_consumes_and_iterates() { + let template1 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1000, + }, + }; + let template2 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 2000, + }, + }; + let templates = NewTxTemplates(vec![template1.clone(), template2.clone()]); + + let collected: Vec = templates.into_iter().collect(); + + assert_eq!(collected.len(), 2); + assert_eq!(collected[0], template1); + assert_eq!(collected[1], template2); + } + + #[test] + fn new_tx_templates_can_be_created_from_payable_accounts() { + let wallet1 = make_wallet("wallet1"); + let wallet2 = make_wallet("wallet2"); + let payable_accounts = vec![ + PayableAccount { + wallet: wallet1.clone(), + balance_wei: 1000, + last_paid_timestamp: SystemTime::now(), + pending_payable_opt: None, + }, + PayableAccount { + wallet: wallet2.clone(), + balance_wei: 2000, + last_paid_timestamp: SystemTime::now(), + pending_payable_opt: None, + }, + ]; + + let new_tx_templates = NewTxTemplates::from(&payable_accounts); + + assert_eq!(new_tx_templates.len(), 2); + assert_eq!(new_tx_templates[0].base.receiver_address, wallet1.address()); + assert_eq!(new_tx_templates[0].base.amount_in_wei, 1000); + assert_eq!(new_tx_templates[1].base.receiver_address, wallet2.address()); + assert_eq!(new_tx_templates[1].base.amount_in_wei, 2000); + } + + #[test] + fn new_tx_templates_can_be_created_from_iterator() { + let template1 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1000, + }, + }; + let template2 = NewTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 2000, + }, + }; + + let templates = NewTxTemplates::from_iter(vec![template1.clone(), template2.clone()]); + + assert_eq!(templates.len(), 2); + assert_eq!(templates[0], template1); + assert_eq!(templates[1], template2); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs new file mode 100644 index 000000000..9990635cd --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs @@ -0,0 +1,216 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; +use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; +use std::collections::{BTreeSet, HashMap}; +use std::ops::{Deref, DerefMut}; +use web3::types::Address; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RetryTxTemplate { + pub base: BaseTxTemplate, + pub prev_gas_price_wei: u128, + pub prev_nonce: u64, +} + +impl RetryTxTemplate { + pub fn new(failed_tx: &FailedTx, payable_scan_amount_opt: Option) -> Self { + let mut retry_template = RetryTxTemplate::from(failed_tx); + + if let Some(payable_scan_amount) = payable_scan_amount_opt { + retry_template.base.amount_in_wei += payable_scan_amount; + } + + retry_template + } +} + +impl From<&FailedTx> for RetryTxTemplate { + fn from(failed_tx: &FailedTx) -> Self { + RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: failed_tx.receiver_address, + amount_in_wei: failed_tx.amount_minor, + }, + prev_gas_price_wei: failed_tx.gas_price_minor, + prev_nonce: failed_tx.nonce, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RetryTxTemplates(pub Vec); + +impl RetryTxTemplates { + pub fn new( + txs_to_retry: &BTreeSet, + amounts_from_payables: &HashMap, + ) -> Self { + Self( + txs_to_retry + .iter() + .map(|tx_to_retry| { + let payable_scan_amount_opt = amounts_from_payables + .get(&tx_to_retry.receiver_address) + .copied(); + RetryTxTemplate::new(tx_to_retry, payable_scan_amount_opt) + }) + .collect(), + ) + } +} + +impl From> for RetryTxTemplates { + fn from(retry_tx_templates: Vec) -> Self { + Self(retry_tx_templates) + } +} + +impl Deref for RetryTxTemplates { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for RetryTxTemplates { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl IntoIterator for RetryTxTemplates { + type Item = RetryTxTemplate; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::{ + RetryTxTemplate, RetryTxTemplates, + }; + use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; + use crate::blockchain::test_utils::{make_address, make_tx_hash}; + + #[test] + fn retry_tx_template_can_be_created_from_failed_tx() { + let receiver_address = make_address(42); + let amount_in_wei = 1_000_000; + let gas_price = 20_000_000_000; + let nonce = 123; + let tx_hash = make_tx_hash(789); + let failed_tx = FailedTx { + hash: tx_hash, + receiver_address, + amount_minor: amount_in_wei, + gas_price_minor: gas_price, + nonce, + timestamp: 1234567, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + }; + + let retry_tx_template = RetryTxTemplate::from(&failed_tx); + + assert_eq!(retry_tx_template.base.receiver_address, receiver_address); + assert_eq!(retry_tx_template.base.amount_in_wei, amount_in_wei); + assert_eq!(retry_tx_template.prev_gas_price_wei, gas_price); + assert_eq!(retry_tx_template.prev_nonce, nonce); + } + + #[test] + fn retry_tx_templates_can_be_created_from_vec_using_into() { + let template1 = RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1000, + }, + prev_gas_price_wei: 20_000_000_000, + prev_nonce: 5, + }; + let template2 = RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 2000, + }, + prev_gas_price_wei: 25_000_000_000, + prev_nonce: 6, + }; + let templates_vec = vec![template1.clone(), template2.clone()]; + + let templates: RetryTxTemplates = templates_vec.into(); + + assert_eq!(templates.len(), 2); + assert_eq!(templates[0], template1); + assert_eq!(templates[1], template2); + } + + #[test] + fn retry_tx_templates_deref_provides_access_to_inner_vector() { + let template1 = RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1000, + }, + prev_gas_price_wei: 20_000_000_000, + prev_nonce: 5, + }; + let template2 = RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 2000, + }, + prev_gas_price_wei: 25_000_000_000, + prev_nonce: 6, + }; + + let templates = RetryTxTemplates(vec![template1.clone(), template2.clone()]); + + assert_eq!(templates.len(), 2); + assert_eq!(templates[0], template1); + assert_eq!(templates[1], template2); + assert!(!templates.is_empty()); + assert!(templates.contains(&template1)); + assert_eq!( + templates + .iter() + .map(|template| template.base.amount_in_wei) + .sum::(), + 3000 + ); + } + + #[test] + fn retry_tx_templates_into_iter_consumes_and_iterates() { + let template1 = RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1000, + }, + prev_gas_price_wei: 20_000_000_000, + prev_nonce: 5, + }; + let template2 = RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 2000, + }, + prev_gas_price_wei: 25_000_000_000, + prev_nonce: 6, + }; + let templates = RetryTxTemplates(vec![template1.clone(), template2.clone()]); + + let collected: Vec = templates.into_iter().collect(); + + assert_eq!(collected.len(), 2); + assert_eq!(collected[0], template1); + assert_eq!(collected[1], template2); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs new file mode 100644 index 000000000..ca8dfa870 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs @@ -0,0 +1,48 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use web3::types::Address; + +pub mod initial; +pub mod priced; +pub mod signable; +pub mod test_utils; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct BaseTxTemplate { + pub receiver_address: Address, + pub amount_in_wei: u128, +} + +impl From<&PayableAccount> for BaseTxTemplate { + fn from(payable_account: &PayableAccount) -> Self { + Self { + receiver_address: payable_account.wallet.address(), + amount_in_wei: payable_account.balance_wei, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::accountant::db_access_objects::payable_dao::PayableAccount; + use crate::test_utils::make_wallet; + use std::time::SystemTime; + + #[test] + fn base_tx_template_can_be_created_from_payable_account() { + let wallet = make_wallet("some wallet"); + let balance_wei = 1_000_000; + let payable_account = PayableAccount { + wallet: wallet.clone(), + balance_wei, + last_paid_timestamp: SystemTime::now(), + pending_payable_opt: None, + }; + + let base_tx_template = BaseTxTemplate::from(&payable_account); + + assert_eq!(base_tx_template.receiver_address, wallet.address()); + assert_eq!(base_tx_template.amount_in_wei, balance_wei); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/priced/mod.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/mod.rs new file mode 100644 index 000000000..84adbe5e3 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/mod.rs @@ -0,0 +1,3 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod new; +pub mod retry; diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs new file mode 100644 index 000000000..6de54e4c9 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs @@ -0,0 +1,107 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::join_with_separator; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::{ + NewTxTemplate, NewTxTemplates, +}; +use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; +use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; +use masq_lib::logger::Logger; +use std::ops::Deref; +use thousands::Separable; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PricedNewTxTemplate { + pub base: BaseTxTemplate, + pub computed_gas_price_wei: u128, +} + +impl PricedNewTxTemplate { + pub fn new(unpriced_tx_template: NewTxTemplate, computed_gas_price_wei: u128) -> Self { + Self { + base: unpriced_tx_template.base, + computed_gas_price_wei, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PricedNewTxTemplates(pub Vec); + +// TODO: GH-703: Consider design changes here +impl Deref for PricedNewTxTemplates { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromIterator for PricedNewTxTemplates { + fn from_iter>(iter: I) -> Self { + PricedNewTxTemplates(iter.into_iter().collect()) + } +} + +impl PricedNewTxTemplates { + pub fn new( + unpriced_new_tx_templates: NewTxTemplates, + computed_gas_price_wei: u128, + ) -> PricedNewTxTemplates { + let updated_tx_templates = unpriced_new_tx_templates + .into_iter() + .map(|new_tx_template| { + PricedNewTxTemplate::new(new_tx_template, computed_gas_price_wei) + }) + .collect(); + + PricedNewTxTemplates(updated_tx_templates) + } + + pub fn from_initial_with_logging( + initial_templates: NewTxTemplates, + latest_gas_price_wei: u128, + ceil: u128, + logger: &Logger, + ) -> Self { + let computed_gas_price_wei = increase_gas_price_by_margin(latest_gas_price_wei); + + let safe_gas_price_wei = if computed_gas_price_wei > ceil { + warning!( + logger, + "{}", + Self::log_ceiling_crossed(&initial_templates, computed_gas_price_wei, ceil) + ); + + ceil + } else { + computed_gas_price_wei + }; + + Self::new(initial_templates, safe_gas_price_wei) + } + + fn log_ceiling_crossed( + templates: &NewTxTemplates, + computed_gas_price_wei: u128, + ceil: u128, + ) -> String { + format!( + "The computed gas price {} wei is above the ceil value of {} wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + {}", + computed_gas_price_wei.separate_with_commas(), + ceil.separate_with_commas(), + join_with_separator( + templates.iter(), + |tx_template| format!("{:?}", tx_template.base.receiver_address), + "\n" + ) + ) + } + + pub fn total_gas_price(&self) -> u128 { + self.iter() + .map(|new_tx_template| new_tx_template.computed_gas_price_wei) + .sum() + } +} diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs new file mode 100644 index 000000000..48e41f4b9 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs @@ -0,0 +1,169 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::join_with_separator; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::{ + RetryTxTemplate, RetryTxTemplates, +}; +use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; +use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; +use masq_lib::logger::Logger; +use std::ops::{Deref, DerefMut}; +use thousands::Separable; +use web3::types::Address; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PricedRetryTxTemplate { + pub base: BaseTxTemplate, + pub prev_nonce: u64, + pub computed_gas_price_wei: u128, +} + +impl PricedRetryTxTemplate { + pub fn new(initial: RetryTxTemplate, computed_gas_price_wei: u128) -> Self { + Self { + base: initial.base, + prev_nonce: initial.prev_nonce, + computed_gas_price_wei, + } + } + + fn create_and_update_log_data( + retry_tx_template: RetryTxTemplate, + latest_gas_price_wei: u128, + ceil: u128, + log_builder: &mut RetryLogBuilder, + ) -> PricedRetryTxTemplate { + let receiver = retry_tx_template.base.receiver_address; + let computed_gas_price_wei = + Self::compute_gas_price(retry_tx_template.prev_gas_price_wei, latest_gas_price_wei); + + let safe_gas_price_wei = if computed_gas_price_wei > ceil { + log_builder.push(receiver, computed_gas_price_wei); + ceil + } else { + computed_gas_price_wei + }; + + PricedRetryTxTemplate::new(retry_tx_template, safe_gas_price_wei) + } + + fn compute_gas_price(latest_gas_price_wei: u128, prev_gas_price_wei: u128) -> u128 { + let gas_price_wei = latest_gas_price_wei.max(prev_gas_price_wei); + + increase_gas_price_by_margin(gas_price_wei) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PricedRetryTxTemplates(pub Vec); + +// TODO: GH-703: Consider design changes here +impl Deref for PricedRetryTxTemplates { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// TODO: GH-703: Consider design changes here +impl DerefMut for PricedRetryTxTemplates { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FromIterator for PricedRetryTxTemplates { + fn from_iter>(iter: I) -> Self { + PricedRetryTxTemplates(iter.into_iter().collect()) + } +} + +impl PricedRetryTxTemplates { + pub fn from_initial_with_logging( + initial_templates: RetryTxTemplates, + latest_gas_price_wei: u128, + ceil: u128, + logger: &Logger, + ) -> Self { + let mut log_builder = RetryLogBuilder::new(initial_templates.len(), ceil); + + let templates = initial_templates + .into_iter() + .map(|retry_tx_template| { + PricedRetryTxTemplate::create_and_update_log_data( + retry_tx_template, + latest_gas_price_wei, + ceil, + &mut log_builder, + ) + }) + .collect(); + + if let Some(log_msg) = log_builder.build() { + warning!(logger, "{}", log_msg) + } + + templates + } + + pub fn total_gas_price(&self) -> u128 { + self.iter() + .map(|retry_tx_template| retry_tx_template.computed_gas_price_wei) + .sum() + } + + pub fn reorder_by_nonces(mut self, latest_nonce: u64) -> Self { + // TODO: This algorithm could be made more robust by including un-realistic permutations of tx nonces + self.sort_by_key(|template| template.prev_nonce); + + let split_index = self + .iter() + .position(|template| template.prev_nonce == latest_nonce) + .unwrap_or(0); + + let (left, right) = self.split_at(split_index); + + Self([right, left].concat()) + } +} + +pub struct RetryLogBuilder { + log_data: Vec<(Address, u128)>, + ceil: u128, +} + +impl RetryLogBuilder { + fn new(capacity: usize, ceil: u128) -> Self { + Self { + log_data: Vec::with_capacity(capacity), + ceil, + } + } + + fn push(&mut self, address: Address, gas_price: u128) { + self.log_data.push((address, gas_price)); + } + + fn build(&self) -> Option { + if self.log_data.is_empty() { + None + } else { + Some(format!( + "The computed gas price(s) in wei is \ + above the ceil value of {} wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + {}", + self.ceil.separate_with_commas(), + join_with_separator( + &self.log_data, + |(address, gas_price)| format!( + "{:?} with gas price {}", + address, + gas_price.separate_with_commas() + ), + "\n" + ) + )) + } + } +} diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs new file mode 100644 index 000000000..d1ae97ebe --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs @@ -0,0 +1,253 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ + PricedNewTxTemplate, PricedNewTxTemplates, +}; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::{ + PricedRetryTxTemplate, PricedRetryTxTemplates, +}; +use itertools::{Either, Itertools}; +use std::ops::Deref; +use web3::types::Address; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SignableTxTemplate { + pub receiver_address: Address, + pub amount_in_wei: u128, + pub gas_price_wei: u128, + pub nonce: u64, +} + +impl From<(&PricedNewTxTemplate, u64)> for SignableTxTemplate { + fn from((priced_new_tx_template, nonce): (&PricedNewTxTemplate, u64)) -> Self { + SignableTxTemplate { + receiver_address: priced_new_tx_template.base.receiver_address, + amount_in_wei: priced_new_tx_template.base.amount_in_wei, + gas_price_wei: priced_new_tx_template.computed_gas_price_wei, + nonce, + } + } +} + +impl From<(&PricedRetryTxTemplate, u64)> for SignableTxTemplate { + fn from((priced_retry_tx_template, nonce): (&PricedRetryTxTemplate, u64)) -> Self { + SignableTxTemplate { + receiver_address: priced_retry_tx_template.base.receiver_address, + amount_in_wei: priced_retry_tx_template.base.amount_in_wei, + gas_price_wei: priced_retry_tx_template.computed_gas_price_wei, + nonce, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SignableTxTemplates(pub Vec); + +impl FromIterator for SignableTxTemplates { + fn from_iter>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl From<(PricedNewTxTemplates, u64)> for SignableTxTemplates { + fn from((priced_new_tx_templates, latest_nonce): (PricedNewTxTemplates, u64)) -> Self { + priced_new_tx_templates + .iter() + .enumerate() + .map(|(i, template)| SignableTxTemplate::from((template, latest_nonce + i as u64))) + .collect() + } +} + +impl From<(PricedRetryTxTemplates, u64)> for SignableTxTemplates { + fn from((priced_retry_tx_templates, latest_nonce): (PricedRetryTxTemplates, u64)) -> Self { + priced_retry_tx_templates + .reorder_by_nonces(latest_nonce) + .iter() + .enumerate() + .map(|(i, template)| SignableTxTemplate::from((template, latest_nonce + i as u64))) + .collect() + } +} + +impl SignableTxTemplates { + pub fn new( + priced_tx_templates: Either, + latest_nonce: u64, + ) -> Self { + match priced_tx_templates { + Either::Left(priced_new_tx_templates) => { + Self::from((priced_new_tx_templates, latest_nonce)) + } + Either::Right(priced_retry_tx_templates) => { + Self::from((priced_retry_tx_templates, latest_nonce)) + } + } + } + + pub fn nonce_range(&self) -> (u64, u64) { + let sorted: Vec<&SignableTxTemplate> = self + .iter() + .sorted_by_key(|template| template.nonce) + .collect(); + let first = sorted.first().map_or(0, |template| template.nonce); + let last = sorted.last().map_or(0, |template| template.nonce); + + (first, last) + } + + pub fn largest_amount(&self) -> u128 { + self.iter() + .map(|signable_tx_template| signable_tx_template.amount_in_wei) + .max() + .expect("there aren't any templates") + } +} + +// TODO: GH-703: Consider design changes here +impl Deref for SignableTxTemplates { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + + use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::{ + make_priced_new_tx_template, make_priced_retry_tx_template, make_signable_tx_template, + }; + use itertools::Either; + + #[test] + fn signable_tx_templates_can_be_created_from_priced_new_tx_templates() { + let nonce = 10; + let priced_new_tx_templates = PricedNewTxTemplates(vec![ + make_priced_new_tx_template(1), + make_priced_new_tx_template(2), + make_priced_new_tx_template(3), + make_priced_new_tx_template(4), + make_priced_new_tx_template(5), + ]); + + let result = SignableTxTemplates::new(Either::Left(priced_new_tx_templates.clone()), nonce); + + priced_new_tx_templates + .iter() + .zip(result.iter()) + .enumerate() + .for_each(|(i, (priced, signable))| { + assert_eq!( + signable.receiver_address, priced.base.receiver_address, + "Element {i}: receiver_address mismatch", + ); + assert_eq!( + signable.amount_in_wei, priced.base.amount_in_wei, + "Element {i}: amount_in_wei mismatch", + ); + assert_eq!( + signable.gas_price_wei, priced.computed_gas_price_wei, + "Element {i}: gas_price_wei mismatch", + ); + assert_eq!( + signable.nonce, + nonce + i as u64, + "Element {i}: nonce mismatch", + ); + }); + } + + #[test] + fn signable_tx_templates_can_be_created_from_priced_retry_tx_templates() { + let nonce = 10; + let retries = PricedRetryTxTemplates(vec![ + make_priced_retry_tx_template(12), + make_priced_retry_tx_template(6), + make_priced_retry_tx_template(10), + make_priced_retry_tx_template(8), + make_priced_retry_tx_template(11), + ]); + + let result = SignableTxTemplates::new(Either::Right(retries.clone()), nonce); + + let expected_order = vec![2, 4, 0, 1, 3]; + result + .iter() + .zip(expected_order.into_iter()) + .enumerate() + .for_each(|(i, (signable, tx_order))| { + assert_eq!( + signable.receiver_address, retries[tx_order].base.receiver_address, + "Element {} (tx_order {}): receiver_address mismatch", + i, tx_order + ); + assert_eq!( + signable.nonce, + nonce + i as u64, + "Element {} (tx_order {}): nonce mismatch", + i, + tx_order + ); + assert_eq!( + signable.amount_in_wei, retries[tx_order].base.amount_in_wei, + "Element {} (tx_order {}): amount_in_wei mismatch", + i, tx_order + ); + assert_eq!( + signable.gas_price_wei, retries[tx_order].computed_gas_price_wei, + "Element {} (tx_order {}): gas_price_wei mismatch", + i, tx_order + ); + }); + } + + #[test] + fn test_largest_amount() { + let templates = SignableTxTemplates(vec![ + make_signable_tx_template(1), + make_signable_tx_template(2), + make_signable_tx_template(3), + ]); + + assert_eq!(templates.largest_amount(), 3000); + } + + #[test] + #[should_panic(expected = "there aren't any templates")] + fn largest_amount_panics_for_empty_templates() { + let empty_templates = SignableTxTemplates(vec![]); + + let _ = empty_templates.largest_amount(); + } + + #[test] + fn test_nonce_range() { + // Test case 1: Empty templates + let empty_templates = SignableTxTemplates(vec![]); + assert_eq!(empty_templates.nonce_range(), (0, 0)); + + // Test case 2: Single template + let single_template = SignableTxTemplates(vec![make_signable_tx_template(5)]); + assert_eq!(single_template.nonce_range(), (5, 5)); + + // Test case 3: Multiple templates in order + let ordered_templates = SignableTxTemplates(vec![ + make_signable_tx_template(1), + make_signable_tx_template(2), + make_signable_tx_template(3), + ]); + assert_eq!(ordered_templates.nonce_range(), (1, 3)); + + // Test case 4: Multiple templates out of order + let unordered_templates = SignableTxTemplates(vec![ + make_signable_tx_template(3), + make_signable_tx_template(1), + make_signable_tx_template(2), + ]); + assert_eq!(unordered_templates.nonce_range(), (1, 3)); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs new file mode 100644 index 000000000..b91eaed76 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs @@ -0,0 +1,108 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +#![cfg(test)] + +use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplate; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ + PricedNewTxTemplate, PricedNewTxTemplates, +}; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplate; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplate; +use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; +use crate::accountant::test_utils::make_payable_account; +use crate::blockchain::test_utils::make_address; +use masq_lib::constants::DEFAULT_GAS_PRICE; +use web3::types::Address; + +pub fn make_priced_new_tx_templates(vec: Vec<(PayableAccount, u128)>) -> PricedNewTxTemplates { + vec.iter() + .map(|(payable_account, gas_price_wei)| PricedNewTxTemplate { + base: BaseTxTemplate::from(payable_account), + computed_gas_price_wei: *gas_price_wei, + }) + .collect() +} + +pub fn make_priced_new_tx_template(n: u64) -> PricedNewTxTemplate { + PricedNewTxTemplate { + base: BaseTxTemplate::from(&make_payable_account(n)), + computed_gas_price_wei: DEFAULT_GAS_PRICE as u128, + } +} + +pub fn make_priced_retry_tx_template(prev_nonce: u64) -> PricedRetryTxTemplate { + PricedRetryTxTemplate { + base: BaseTxTemplate::from(&make_payable_account(prev_nonce)), + prev_nonce, + computed_gas_price_wei: DEFAULT_GAS_PRICE as u128, + } +} + +pub fn make_signable_tx_template(nonce: u64) -> SignableTxTemplate { + SignableTxTemplate { + receiver_address: make_address(1), + amount_in_wei: nonce as u128 * 1_000, + gas_price_wei: nonce as u128 * 1_000_000, + nonce, + } +} + +pub fn make_retry_tx_template(n: u32) -> RetryTxTemplate { + RetryTxTemplateBuilder::new() + .receiver_address(make_address(n)) + .amount_in_wei(n as u128 * 1_000) + .prev_gas_price_wei(n as u128 * 1_000_000) + .prev_nonce(n as u64) + .build() +} + +#[derive(Default)] +pub struct RetryTxTemplateBuilder { + receiver_address_opt: Option
, + amount_in_wei_opt: Option, + prev_gas_price_wei_opt: Option, + prev_nonce_opt: Option, +} + +impl RetryTxTemplateBuilder { + pub fn new() -> Self { + RetryTxTemplateBuilder::default() + } + + pub fn receiver_address(mut self, address: Address) -> Self { + self.receiver_address_opt = Some(address); + self + } + + pub fn amount_in_wei(mut self, amount: u128) -> Self { + self.amount_in_wei_opt = Some(amount); + self + } + + pub fn prev_gas_price_wei(mut self, gas_price: u128) -> Self { + self.prev_gas_price_wei_opt = Some(gas_price); + self + } + + pub fn prev_nonce(mut self, nonce: u64) -> Self { + self.prev_nonce_opt = Some(nonce); + self + } + + pub fn payable_account(mut self, payable_account: &PayableAccount) -> Self { + self.receiver_address_opt = Some(payable_account.wallet.address()); + self.amount_in_wei_opt = Some(payable_account.balance_wei); + self + } + + pub fn build(self) -> RetryTxTemplate { + RetryTxTemplate { + base: BaseTxTemplate { + receiver_address: self.receiver_address_opt.unwrap_or_else(|| make_address(0)), + amount_in_wei: self.amount_in_wei_opt.unwrap_or(0), + }, + prev_gas_price_wei: self.prev_gas_price_wei_opt.unwrap_or(0), + prev_nonce: self.prev_nonce_opt.unwrap_or(0), + } + } +} diff --git a/node/src/accountant/scanners/payable_scanner/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs new file mode 100644 index 000000000..3ace3b8b6 --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -0,0 +1,512 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; +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::Transaction; +use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; +use crate::accountant::{join_with_commas, PendingPayable}; +use crate::blockchain::blockchain_interface::data_structures::BatchResults; +use crate::sub_lib::accountant::PaymentThresholds; +use crate::sub_lib::wallet::Wallet; +use itertools::{Either, Itertools}; +use masq_lib::logger::Logger; +use masq_lib::ui_gateway::NodeToUiMessage; +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashMap}; +use std::ops::Not; +use std::time::SystemTime; +use thousands::Separable; +use web3::types::{Address, H256}; + +#[derive(Debug, PartialEq, Eq)] +pub struct PayableScanResult { + pub ui_response_opt: Option, + pub result: NextScanToRun, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum NextScanToRun { + PendingPayableScan, + NewPayableScan, + RetryPayableScan, +} + +pub fn filter_receiver_addresses_from_txs<'a, T, I>(transactions: I) -> BTreeSet
+where + T: 'a + Transaction, + I: Iterator, +{ + transactions.map(|tx| tx.receiver_address()).collect() +} + +pub fn generate_status_updates( + failed_txs: &BTreeSet, + status: FailureStatus, +) -> HashMap { + failed_txs + .iter() + .map(|tx| (tx.hash, status.clone())) + .collect() +} + +pub fn calculate_occurences(batch_results: &BatchResults) -> (usize, usize) { + (batch_results.sent_txs.len(), batch_results.failed_txs.len()) +} + +pub fn batch_stats(sent_txs_len: usize, failed_txs_len: usize) -> String { + format!( + "Total: {total}, Sent to RPC: {sent_txs_len}, Failed to send: {failed_txs_len}.", + total = sent_txs_len + failed_txs_len + ) +} + +pub fn initial_templates_msg_stats(msg: &InitialTemplatesMessage) -> String { + let (len, scan_type) = match &msg.initial_templates { + Either::Left(new_templates) => (new_templates.len(), "new"), + Either::Right(retry_templates) => (retry_templates.len(), "retry"), + }; + + format!("Found {} {} txs to process", len, scan_type) +} + +//debugging purposes only +pub fn investigate_debt_extremes( + timestamp: SystemTime, + retrieved_payables: &[PayableAccount], +) -> String { + #[derive(Clone, Copy, Default)] + struct PayableInfo { + balance_wei: u128, + age: u64, + } + fn bigger(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { + match payable_1.balance_wei.cmp(&payable_2.balance_wei) { + Ordering::Greater => payable_1, + Ordering::Less => payable_2, + Ordering::Equal => { + if payable_1.age == payable_2.age { + payable_1 + } else { + older(payable_1, payable_2) + } + } + } + } + fn older(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { + match payable_1.age.cmp(&payable_2.age) { + Ordering::Greater => payable_1, + Ordering::Less => payable_2, + Ordering::Equal => { + if payable_1.balance_wei == payable_2.balance_wei { + payable_1 + } else { + bigger(payable_1, payable_2) + } + } + } + } + + if retrieved_payables.is_empty() { + return "Payable scan found no debts".to_string(); + } + let (biggest, oldest) = retrieved_payables + .iter() + .map(|payable| PayableInfo { + balance_wei: payable.balance_wei, + age: timestamp + .duration_since(payable.last_paid_timestamp) + .expect("Payable time is corrupt") + .as_secs(), + }) + .fold( + Default::default(), + |(so_far_biggest, so_far_oldest): (PayableInfo, PayableInfo), payable| { + ( + bigger(so_far_biggest, payable), + older(so_far_oldest, payable), + ) + }, + ); + format!("Payable scan found {} debts; the biggest is {} owed for {}sec, the oldest is {} owed for {}sec", + retrieved_payables.len(), biggest.balance_wei, biggest.age, + oldest.balance_wei, oldest.age) +} + +pub fn payables_debug_summary(qualified_accounts: &[(PayableAccount, u128)], logger: &Logger) { + if qualified_accounts.is_empty() { + return; + } + debug!(logger, "Paying qualified debts:\n{}", { + let now = SystemTime::now(); + qualified_accounts + .iter() + .map(|(payable, threshold_point)| { + let p_age = now + .duration_since(payable.last_paid_timestamp) + .expect("Payable time is corrupt"); + format!( + "{} wei owed for {} sec exceeds the threshold {} wei for creditor {}", + payable.balance_wei.separate_with_commas(), + p_age.as_secs(), + threshold_point.separate_with_commas(), + payable.wallet + ) + }) + .join("\n") + }) +} + +#[derive(Debug, PartialEq, Eq)] +pub struct PendingPayableMissingInDb { + pub recipient: Address, + pub hash: H256, +} + +impl PendingPayableMissingInDb { + pub fn new(recipient: Address, hash: H256) -> Self { + PendingPayableMissingInDb { recipient, hash } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct PendingPayableMetadata<'a> { + pub recipient: &'a Wallet, + 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, + } + } +} + +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 {:?}", + join_with_commas(sent_payments, |pending_p| pending_p + .recipient_wallet + .to_string()), + error + ) +} + +pub fn err_msg_for_failure_with_expected_but_missing_fingerprints( + 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", + serialize_hashes(&nonexistent), + )) +} + +pub fn separate_rowids_and_hashes(ids_of_payments: Vec<(u64, H256)>) -> (Vec, Vec) { + ids_of_payments.into_iter().unzip() +} + +pub trait PayableThresholdsGauge { + fn is_innocent_age(&self, age: u64, limit: u64) -> bool; + fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool; + fn calculate_payout_threshold_in_gwei( + &self, + payment_thresholds: &PaymentThresholds, + x: u64, + ) -> u128; + as_any_ref_in_trait!(); +} + +#[derive(Default)] +pub struct PayableThresholdsGaugeReal {} + +impl PayableThresholdsGauge for PayableThresholdsGaugeReal { + fn is_innocent_age(&self, age: u64, limit: u64) -> bool { + age <= limit + } + + fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool { + balance <= limit + } + + fn calculate_payout_threshold_in_gwei( + &self, + payment_thresholds: &PaymentThresholds, + debt_age: u64, + ) -> u128 { + ThresholdUtils::calculate_finite_debt_limit_by_age(payment_thresholds, debt_age) + } + as_any_ref_in_trait_impl!(); +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::payable_dao::PayableAccount; + use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::payable_scanner::utils::{ + investigate_debt_extremes, payables_debug_summary, PayableThresholdsGauge, + PayableThresholdsGaugeReal, + }; + use crate::accountant::scanners::receivable_scanner::utils::balance_and_age; + use crate::accountant::{checked_conversion, gwei_to_wei}; + 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; + + #[test] + fn investigate_debt_extremes_picks_the_most_relevant_records() { + let now = SystemTime::now(); + let now_t = to_unix_timestamp(now); + let same_amount_significance = 2_000_000; + let same_age_significance = from_unix_timestamp(now_t - 30000); + let payables = &[ + PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: same_amount_significance, + last_paid_timestamp: from_unix_timestamp(now_t - 5000), + pending_payable_opt: None, + }, + //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked + PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: same_amount_significance, + last_paid_timestamp: from_unix_timestamp(now_t - 10000), + pending_payable_opt: None, + }, + //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen + PayableAccount { + wallet: make_wallet("wallet3"), + balance_wei: 100, + last_paid_timestamp: same_age_significance, + pending_payable_opt: None, + }, + PayableAccount { + wallet: make_wallet("wallet2"), + balance_wei: 330, + last_paid_timestamp: same_age_significance, + pending_payable_opt: None, + }, + ]; + + let result = investigate_debt_extremes(now, payables); + + assert_eq!(result, "Payable scan found 4 debts; the biggest is 2000000 owed for 10000sec, the oldest is 330 owed for 30000sec") + } + + #[test] + fn balance_and_age_is_calculated_as_expected() { + let now = SystemTime::now(); + let offset = 1000; + let receivable_account = ReceivableAccount { + wallet: make_wallet("wallet0"), + balance_wei: 10_000_000_000, + last_received_timestamp: from_unix_timestamp(to_unix_timestamp(now) - offset), + }; + + let (balance, age) = balance_and_age(now, &receivable_account); + + assert_eq!(balance, "10"); + assert_eq!(age.as_secs(), offset as u64); + } + + #[test] + fn payables_debug_summary_displays_nothing_for_no_qualified_payments() { + init_test_logging(); + let logger = + Logger::new("payables_debug_summary_displays_nothing_for_no_qualified_payments"); + + payables_debug_summary(&vec![], &logger); + + TestLogHandler::new().exists_no_log_containing( + "DEBUG: payables_debug_summary_stays_\ + inert_if_no_qualified_payments: Paying qualified debts:", + ); + } + + #[test] + fn payables_debug_summary_prints_pretty_summary() { + init_test_logging(); + let now = to_unix_timestamp(SystemTime::now()); + let payment_thresholds = PaymentThresholds { + threshold_interval_sec: 2_592_000, + debt_threshold_gwei: 1_000_000_000, + payment_grace_period_sec: 86_400, + maturity_threshold_sec: 86_400, + permanent_debt_allowed_gwei: 10_000_000, + unban_below_gwei: 10_000_000, + }; + let qualified_payables_and_threshold_points = vec![ + ( + PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2000), + last_paid_timestamp: from_unix_timestamp( + now - checked_conversion::( + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec, + ), + ), + pending_payable_opt: None, + }, + 10_000_000_001_152_000_u128, + ), + ( + PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1), + last_paid_timestamp: from_unix_timestamp( + now - checked_conversion::( + payment_thresholds.maturity_threshold_sec + 55, + ), + ), + pending_payable_opt: None, + }, + 999_978_993_055_555_580, + ), + ]; + let logger = Logger::new("test"); + + payables_debug_summary(&qualified_payables_and_threshold_points, &logger); + + TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ + 10,002,000,000,000,000 wei owed for 2678400 sec exceeds 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] + fn payout_sloped_segment_in_payment_thresholds_goes_along_proper_line() { + let payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 333, + payment_grace_period_sec: 444, + permanent_debt_allowed_gwei: 4444, + debt_threshold_gwei: 8888, + threshold_interval_sec: 1111111, + unban_below_gwei: 0, + }; + let higher_corner_timestamp = payment_thresholds.maturity_threshold_sec; + let middle_point_timestamp = payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec / 2; + let lower_corner_timestamp = + payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec; + let tested_fn = |payment_thresholds: &PaymentThresholds, time| { + PayableThresholdsGaugeReal {} + .calculate_payout_threshold_in_gwei(payment_thresholds, time) as i128 + }; + + let higher_corner_point = tested_fn(&payment_thresholds, higher_corner_timestamp); + let middle_point = tested_fn(&payment_thresholds, middle_point_timestamp); + let lower_corner_point = tested_fn(&payment_thresholds, lower_corner_timestamp); + + let allowed_imprecision = WEIS_IN_GWEI; + let ideal_template_higher: i128 = gwei_to_wei(payment_thresholds.debt_threshold_gwei); + let ideal_template_middle: i128 = gwei_to_wei( + (payment_thresholds.debt_threshold_gwei + - payment_thresholds.permanent_debt_allowed_gwei) + / 2 + + payment_thresholds.permanent_debt_allowed_gwei, + ); + let ideal_template_lower: i128 = + gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei); + assert!( + higher_corner_point <= ideal_template_higher + allowed_imprecision + && ideal_template_higher - allowed_imprecision <= higher_corner_point, + "ideal: {}, real: {}", + ideal_template_higher, + higher_corner_point + ); + assert!( + middle_point <= ideal_template_middle + allowed_imprecision + && ideal_template_middle - allowed_imprecision <= middle_point, + "ideal: {}, real: {}", + ideal_template_middle, + middle_point + ); + assert!( + lower_corner_point <= ideal_template_lower + allowed_imprecision + && ideal_template_lower - allowed_imprecision <= lower_corner_point, + "ideal: {}, real: {}", + ideal_template_lower, + lower_corner_point + ) + } + + #[test] + fn is_innocent_age_works_for_age_smaller_than_innocent_age() { + let payable_age = 999; + + let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_age_works_for_age_equal_to_innocent_age() { + let payable_age = 1000; + + let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_age_works_for_excessive_age() { + let payable_age = 1001; + + let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); + + assert_eq!(result, false) + } + + #[test] + fn is_innocent_balance_works_for_balance_smaller_than_innocent_balance() { + let payable_balance = 999; + + let result = + PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_balance_works_for_balance_equal_to_innocent_balance() { + let payable_balance = 1000; + + let result = + PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); + + assert_eq!(result, true) + } + + #[test] + fn is_innocent_balance_works_for_excessive_balance() { + let payable_balance = 1001; + + let result = + PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); + + assert_eq!(result, false) + } +} diff --git a/node/src/accountant/scanners/payable_scanner_extension/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs deleted file mode 100644 index 1d1e8cb0b..000000000 --- a/node/src/accountant/scanners/payable_scanner_extension/mod.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -pub mod msgs; -pub mod test_utils; - -use crate::accountant::payment_adjuster::Adjustment; -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, -}; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; -use crate::accountant::scanners::{Scanner, StartableScanner}; -use crate::accountant::{ScanForNewPayables, ScanForRetryPayables, SentPayables}; -use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use itertools::Either; -use masq_lib::logger::Logger; - -pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: - StartableScanner - + StartableScanner - + SolvencySensitivePaymentInstructor - + Scanner -{ -} - -pub(in crate::accountant::scanners) trait SolvencySensitivePaymentInstructor { - fn try_skipping_payment_adjustment( - &self, - msg: BlockchainAgentWithContextMessage, - logger: &Logger, - ) -> Result, String>; - - fn perform_payment_adjustment( - &self, - setup: PreparedAdjustment, - logger: &Logger, - ) -> OutboundPaymentsInstructions; -} - -pub struct PreparedAdjustment { - pub original_setup_msg: BlockchainAgentWithContextMessage, - pub adjustment: Adjustment, -} - -impl PreparedAdjustment { - pub fn new( - original_setup_msg: BlockchainAgentWithContextMessage, - adjustment: Adjustment, - ) -> Self { - Self { - original_setup_msg, - adjustment, - } - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; - - impl Clone for PreparedAdjustment { - fn clone(&self) -> Self { - Self { - original_setup_msg: self.original_setup_msg.clone(), - adjustment: self.adjustment.clone(), - } - } - } -} diff --git a/node/src/accountant/scanners/payable_scanner_extension/msgs.rs b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs deleted file mode 100644 index 1e9dbe59d..000000000 --- a/node/src/accountant/scanners/payable_scanner_extension/msgs.rs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::sub_lib::wallet::Wallet; -use actix::Message; -use std::fmt::Debug; - -#[derive(Debug, Message, PartialEq, Eq, Clone)] -pub struct QualifiedPayablesMessage { - pub qualified_payables: UnpricedQualifiedPayables, - pub consuming_wallet: Wallet, - pub response_skeleton_opt: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct UnpricedQualifiedPayables { - pub payables: Vec, -} - -impl From> for UnpricedQualifiedPayables { - fn from(qualified_payable: Vec) -> Self { - UnpricedQualifiedPayables { - payables: qualified_payable - .into_iter() - .map(|payable| QualifiedPayablesBeforeGasPriceSelection::new(payable, None)) - .collect(), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct QualifiedPayablesBeforeGasPriceSelection { - pub payable: PayableAccount, - pub previous_attempt_gas_price_minor_opt: Option, -} - -impl QualifiedPayablesBeforeGasPriceSelection { - pub fn new( - payable: PayableAccount, - previous_attempt_gas_price_minor_opt: Option, - ) -> Self { - Self { - payable, - previous_attempt_gas_price_minor_opt, - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct PricedQualifiedPayables { - pub payables: Vec, -} - -impl Into> for PricedQualifiedPayables { - fn into(self) -> Vec { - self.payables - .into_iter() - .map(|qualified_payable| qualified_payable.payable) - .collect() - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct QualifiedPayableWithGasPrice { - pub payable: PayableAccount, - pub gas_price_minor: u128, -} - -impl QualifiedPayableWithGasPrice { - pub fn new(payable: PayableAccount, gas_price_minor: u128) -> Self { - Self { - payable, - gas_price_minor, - } - } -} - -impl QualifiedPayablesMessage { - pub(in crate::accountant) fn new( - qualified_payables: UnpricedQualifiedPayables, - consuming_wallet: Wallet, - response_skeleton_opt: Option, - ) -> Self { - Self { - qualified_payables, - consuming_wallet, - response_skeleton_opt, - } - } -} - -impl SkeletonOptHolder for QualifiedPayablesMessage { - fn skeleton_opt(&self) -> Option { - self.response_skeleton_opt - } -} - -#[derive(Message)] -pub struct BlockchainAgentWithContextMessage { - pub qualified_payables: PricedQualifiedPayables, - pub agent: Box, - pub response_skeleton_opt: Option, -} - -impl BlockchainAgentWithContextMessage { - pub fn new( - qualified_payables: PricedQualifiedPayables, - agent: Box, - response_skeleton_opt: Option, - ) -> Self { - Self { - qualified_payables, - agent, - response_skeleton_opt, - } - } -} - -#[cfg(test)] -mod tests { - - use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - - impl Clone for BlockchainAgentWithContextMessage { - fn clone(&self) -> Self { - let original_agent_id = self.agent.arbitrary_id_stamp(); - let cloned_agent = - BlockchainAgentMock::default().set_arbitrary_id_stamp(original_agent_id); - Self { - qualified_payables: self.qualified_payables.clone(), - agent: Box::new(cloned_agent), - response_skeleton_opt: self.response_skeleton_opt, - } - } - } -} diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 70c043909..7e179ac9d 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -11,20 +11,21 @@ use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoEr 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::db_access_objects::utils::TxHash; +use crate::accountant::db_access_objects::Transaction; 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, + FailedValidationByTable, 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, + join_with_commas, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables, + TxReceiptResult, TxReceiptsMessage, }; use crate::blockchain::blockchain_interface::data_structures::TxBlock; use crate::blockchain::errors::validation_status::{ @@ -38,7 +39,7 @@ 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::collections::{BTreeSet, HashMap}; use std::fmt::Display; use std::rc::Rc; use std::str::FromStr; @@ -46,6 +47,20 @@ use std::time::SystemTime; use thousands::Separable; use web3::types::H256; +pub(in crate::accountant::scanners) trait ExtendedPendingPayablePrivateScanner: + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + TxReceiptsMessage, + PendingPayableScanResult, + > + CachesEmptiableScanner +{ +} + +pub trait CachesEmptiableScanner { + fn empty_caches(&mut self, logger: &Logger); +} + pub struct PendingPayableScanner { pub common: ScannerCommon, pub payable_dao: Box, @@ -57,6 +72,8 @@ pub struct PendingPayableScanner { pub clock: Box, } +impl ExtendedPendingPayablePrivateScanner for PendingPayableScanner {} + impl PrivateScanner< ScanForPendingPayables, @@ -78,30 +95,16 @@ impl StartableScanner logger: &Logger, ) -> Result { self.mark_as_started(timestamp); - info!(logger, "Scanning for pending payable"); - let pending_tx_hashes_opt = self.handle_pending_payables(); - let failure_hashes_opt = self.handle_unproven_failures(); + info!(logger, "Scanning for pending payable"); - if pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() { + let tx_hashes = self.harvest_tables(logger).map_err(|e| { 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(); + e + })?; Ok(RequestTransactionReceipts { - tx_hashes: all_hashes, + tx_hashes, response_skeleton_opt, }) } @@ -133,6 +136,13 @@ impl Scanner for PendingPayableScan as_any_mut_in_trait_impl!(); } +impl CachesEmptiableScanner for PendingPayableScanner { + fn empty_caches(&mut self, logger: &Logger) { + self.current_sent_payables.ensure_empty_cache(logger); + self.yet_unproven_failed_payables.ensure_empty_cache(logger); + } +} + impl PendingPayableScanner { pub fn new( payable_dao: Box, @@ -153,40 +163,86 @@ impl PendingPayableScanner { } } - fn handle_pending_payables(&mut self) -> Option> { + fn harvest_tables(&mut self, logger: &Logger) -> Result, StartScanError> { + let pending_tx_hashes_opt = self.harvest_pending_payables(); + let failure_hashes_opt = self.harvest_unproven_failures(); + + if Self::is_there_nothing_to_process( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + ) { + return Err(StartScanError::NothingToProcess); + } + + Self::log_records_for_receipt_check( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + logger, + ); + + Ok(Self::merge_hashes( + pending_tx_hashes_opt, + failure_hashes_opt, + )) + } + + fn harvest_pending_payables(&mut self) -> Option> { let pending_txs = self .sent_payable_dao - .retrieve_txs(Some(RetrieveCondition::IsPending)); + .retrieve_txs(Some(RetrieveCondition::IsPending)) + .into_iter() + .collect_vec(); if pending_txs.is_empty() { return None; } - let pending_tx_hashes = Self::get_wrapped_hashes(&pending_txs, TxHashByTable::SentPayable); + let pending_tx_hashes = Self::wrap_hashes(&pending_txs, TxHashByTable::SentPayable); self.current_sent_payables.load_cache(pending_txs); Some(pending_tx_hashes) } - fn handle_unproven_failures(&mut self) -> Option> { + fn harvest_unproven_failures(&mut self) -> Option> { let failures = self .failed_payable_dao - .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)); + .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)) + .into_iter() + .collect_vec(); if failures.is_empty() { return None; } - let failure_hashes = Self::get_wrapped_hashes(&failures, TxHashByTable::FailedPayable); + let failure_hashes = Self::wrap_hashes(&failures, TxHashByTable::FailedPayable); self.yet_unproven_failed_payables.load_cache(failures); Some(failure_hashes) } - fn get_wrapped_hashes( + fn is_there_nothing_to_process( + pending_tx_hashes_opt: Option<&Vec>, + failure_hashes_opt: Option<&Vec>, + ) -> bool { + pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() + } + + fn merge_hashes( + pending_tx_hashes_opt: Option>, + failure_hashes_opt: Option>, + ) -> Vec { + let failures = failure_hashes_opt.unwrap_or_default(); + pending_tx_hashes_opt + .unwrap_or_default() + .into_iter() + .chain(failures) + .collect() + } + + fn wrap_hashes( records: &[Record], wrap_the_hash: fn(TxHash) -> TxHashByTable, ) -> Vec where - Record: TxRecordWithHash, + Record: Transaction, { records .iter() @@ -208,14 +264,18 @@ impl PendingPayableScanner { 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)) + match retry { + Retry::RetryPayments => { + PendingPayableScanResult::PaymentRetryRequired(response_skeleton_opt) + } + Retry::RetryTxStatusCheckOnly => { + 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::ProcedureShouldBeRepeated(ui_msg_opt) + } } } else { let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { @@ -238,7 +298,7 @@ impl PendingPayableScanner { let interpretable_data = self.prepare_cases_to_interpret(msg, logger); TxReceiptInterpreter::default().compose_receipt_scan_report( interpretable_data, - &self, + self, logger, ) } @@ -248,28 +308,23 @@ impl PendingPayableScanner { msg: TxReceiptsMessage, logger: &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) - } - Either::Right(mut mismatch_report) => { - mismatch_report.remaining_hashes.push(tx_hash_by_table); - Either::Right(mismatch_report) - } - }, - ); + let init: Either, TxHashByTable> = Either::Left(vec![]); + let either = + msg.results + .into_iter() + .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) + } + Either::Right(missing_entry) => Either::Right(missing_entry), + }, + ); let cases = match either { Either::Left(cases) => cases, - Either::Right(mismatch_report) => self.panic_dump(mismatch_report), + Either::Right(missing_entry) => self.panic_dump(missing_entry), }; self.current_sent_payables.ensure_empty_cache(logger); @@ -283,7 +338,7 @@ impl PendingPayableScanner { mut cases: Vec, receipt_result: TxReceiptResult, looked_up_hash: TxHashByTable, - ) -> Either, MismatchReport> { + ) -> Either, TxHashByTable> { match looked_up_hash { TxHashByTable::SentPayable(tx_hash) => { match self.current_sent_payables.get_record_by_hash(tx_hash) { @@ -294,10 +349,7 @@ impl PendingPayableScanner { )); Either::Left(cases) } - None => Either::Right(MismatchReport { - noticed_with: looked_up_hash, - remaining_hashes: vec![], - }), + None => Either::Right(looked_up_hash), } } TxHashByTable::FailedPayable(tx_hash) => { @@ -312,16 +364,13 @@ impl PendingPayableScanner { )); Either::Left(cases) } - None => Either::Right(MismatchReport { - noticed_with: looked_up_hash, - remaining_hashes: vec![], - }), + None => Either::Right(looked_up_hash), } } } } - fn panic_dump(&mut self, mismatch_report: MismatchReport) -> ! { + fn panic_dump(&mut self, missing_entry: TxHashByTable) -> ! { fn rearrange(hashmap: HashMap) -> Vec { hashmap .into_iter() @@ -332,12 +381,10 @@ impl PendingPayableScanner { 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, + the remaining values. Pending payables: {:?}. Unproven failures: {:?}.", + missing_entry, rearrange(self.current_sent_payables.dump_cache()), rearrange(self.yet_unproven_failed_payables.dump_cache()), - mismatch_report.remaining_hashes ) } @@ -369,7 +416,7 @@ impl PendingPayableScanner { self.add_to_the_total_of_paid_payable(&reclaimed, logger) } - fn isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> HashSet { + fn isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> BTreeSet { reclaimed.iter().map(|(tx_hash, _)| *tx_hash).collect() } @@ -408,7 +455,9 @@ impl PendingPayableScanner { hashes_and_blocks: &[(TxHash, TxBlock)], logger: &Logger, ) { - match self.sent_payable_dao.replace_records(sent_txs_to_reclaim) { + let btreeset: BTreeSet = sent_txs_to_reclaim.iter().cloned().collect(); + + match self.sent_payable_dao.replace_records(&btreeset) { Ok(_) => { debug!(logger, "Replaced records for txs being reclaimed") } @@ -416,7 +465,7 @@ impl PendingPayableScanner { 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, _)| { + join_with_commas(hashes_and_blocks, |(tx_hash, _)| { format!("{:?}", tx_hash) }), e @@ -432,7 +481,7 @@ impl PendingPayableScanner { info!( logger, "Reclaimed txs {} as confirmed on-chain", - comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, tx_block)| { + join_with_commas(hashes_and_blocks, |(tx_hash, tx_block)| { format!("{:?} (block {})", tx_hash, tx_block.block_number) }) ) @@ -440,7 +489,7 @@ impl PendingPayableScanner { Err(e) => { panic!( "Unable to delete failed tx records {} to finish the reclaims due to: {:?}", - comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + join_with_commas(hashes_and_blocks, |(tx_hash, _)| { format!("{:?}", tx_hash) }), e @@ -497,7 +546,7 @@ impl PendingPayableScanner { panic!( "Unable to complete the tx confirmation by the adjustment of the payable accounts \ {} due to: {:?}", - comma_joined_stringifiable( + join_with_commas( &confirmed_txs .iter() .map(|tx| tx.receiver_address) @@ -513,7 +562,7 @@ impl PendingPayableScanner { ) -> ! { panic!( "Unable to update sent payable records {} by their tx blocks due to: {:?}", - comma_joined_stringifiable( + join_with_commas( &tx_hashes_and_tx_blocks.keys().sorted().collect_vec(), |tx_hash| format!("{:?}", tx_hash) ), @@ -574,14 +623,14 @@ impl PendingPayableScanner { } fn add_new_failures(&self, new_failures: Vec, logger: &Logger) { - fn prepare_hashset(failures: &[FailedTx]) -> HashSet { + fn prepare_btreeset(failures: &[FailedTx]) -> BTreeSet { 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)) + join_with_commas(new_failures, |failure| format!("{:?}", failure.hash)) ) } @@ -589,17 +638,22 @@ impl PendingPayableScanner { return; } - if let Err(e) = self.failed_payable_dao.insert_new_records(&new_failures) { + let new_failures_btree_set: BTreeSet = new_failures.iter().cloned().collect(); + + if let Err(e) = self + .failed_payable_dao + .insert_new_records(&new_failures_btree_set) + { panic!( "Unable to persist failed txs {} due to: {:?}", - comma_joined_stringifiable(&new_failures, |failure| format!("{:?}", failure.hash)), + join_with_commas(&new_failures, |failure| format!("{:?}", failure.hash)), e ) } match self .sent_payable_dao - .delete_records(&prepare_hashset(&new_failures)) + .delete_records(&prepare_btreeset(&new_failures)) { Ok(_) => { log_procedure_finished(logger, &new_failures); @@ -607,10 +661,7 @@ impl PendingPayableScanner { Err(e) => { panic!( "Unable to purge sent payable records for failed txs {} due to: {:?}", - comma_joined_stringifiable(&new_failures, |failure| format!( - "{:?}", - failure.hash - )), + join_with_commas(&new_failures, |failure| format!("{:?}", failure.hash)), e ) } @@ -621,7 +672,7 @@ impl PendingPayableScanner { fn prepare_hashmap(rechecks_completed: &[TxHash]) -> HashMap { rechecks_completed .iter() - .map(|tx_hash| (tx_hash.clone(), FailureStatus::Concluded)) + .map(|tx_hash| (*tx_hash, FailureStatus::Concluded)) .collect() } @@ -637,19 +688,13 @@ impl PendingPayableScanner { debug!( logger, "Concluded failures that had required rechecks: {}.", - comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( - "{:?}", - tx_hash - )) + join_with_commas(&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 - )), + join_with_commas(&rechecks_completed, |tx_hash| format!("{:?}", tx_hash)), e ) } @@ -693,7 +738,7 @@ impl PendingPayableScanner { logger, "Pending-tx statuses were processed in the db for validation failure \ of txs {}", - comma_joined_stringifiable(&sent_payable_failures, |failure| { + join_with_commas(&sent_payable_failures, |failure| { format!("{:?}", failure.tx_hash) }) ) @@ -728,10 +773,9 @@ impl PendingPayableScanner { 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) } - ) + join_with_commas(&failed_txs_validation_failures, |failure| { + format!("{:?}", failure.tx_hash) + }) ) } Err(e) => { @@ -778,7 +822,7 @@ impl PendingPayableScanner { ) } - fn log_records_found_for_receipt_check( + fn log_records_for_receipt_check( pending_tx_hashes_opt: Option<&Vec>, failure_hashes_opt: Option<&Vec>, logger: &Logger, @@ -805,6 +849,7 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::{ Detection, SentPayableDaoError, TxStatus, }; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::accountant::scanners::pending_payable_scanner::utils::{ CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, @@ -815,10 +860,10 @@ mod tests { 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, + make_transaction_block, FailedPayableDaoMock, PayableDaoMock, PendingPayableScannerBuilder, + SentPayableDaoMock, }; - use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; + use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, TxReceiptsMessage}; use crate::blockchain::blockchain_interface::data_structures::{ StatusReadFromReceiptCheck, TxBlock, }; @@ -831,11 +876,12 @@ mod tests { 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 itertools::Itertools; use masq_lib::logger::Logger; + use masq_lib::messages::{ToMessageBody, UiScanResponse}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::Regex; - use std::collections::HashMap; + use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; + use std::collections::{BTreeSet, HashMap}; use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; @@ -852,9 +898,9 @@ mod tests { 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()]); + .retrieve_txs_result(btreeset![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()]); + .retrieve_txs_result(btreeset![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) @@ -944,7 +990,7 @@ mod tests { 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![ + results: btreemap![ 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)), @@ -955,10 +1001,7 @@ mod tests { let result = subject.finish_scan(msg, &logger); - assert_eq!( - result, - PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) - ); + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)); let get_record_by_hash_failed_payable_cache_params = get_record_by_hash_failed_payable_cache_params_arc .lock() @@ -985,11 +1028,10 @@ mod tests { #[test] 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 sent_tx_hash_1 = make_tx_hash(0x890); 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 sent_tx_hash_2 = make_tx_hash(0x123); 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; @@ -1005,7 +1047,7 @@ mod tests { 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( + results: btreemap![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)), @@ -1018,24 +1060,13 @@ mod tests { 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 expected = "Looking up 'SentPayable(0x00000000000000000000000000000000000000000000\ + 00000000000000000123)' in the cache, the record could not be found. Dumping the remaining \ + values. Pending payables: [SentTx { hash: 0x0000000000000000000000000000000000000000000000\ + 000000000000000890, receiver_address: 0x0000000000000000000558000000000558000000, \ + amount_minor: 43237380096, timestamp: 29942784, gas_price_minor: 94818816, nonce: 456, \ + status: Pending(Waiting) }]. Unproven failures: []."; + assert_eq!(panic_msg, expected); } #[test] @@ -1046,7 +1077,7 @@ mod tests { 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 failed_tx_hash_2 = make_tx_hash(987); 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(); @@ -1056,7 +1087,7 @@ mod tests { 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), + results: btreemap![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))), @@ -1068,27 +1099,109 @@ mod tests { 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 expected = "Looking up 'FailedPayable(0x000000000000000000000000000000000000000000\ + 00000000000000000003db)' in the cache, the record could not be found. Dumping the remaining \ + values. Pending payables: [SentTx { hash: 0x000000000000000000000000000000000000000000000000\ + 00000000000001c8, receiver_address: 0x0000000000000000000558000000000558000000, amount_minor: \ + 43237380096, timestamp: 29942784, gas_price_minor: 94818816, nonce: 456, status: \ + Pending(Waiting) }, SentTx { hash: 0x0000000000000000000000000000000000000000000000000000000\ + 000000315, receiver_address: 0x000000000000000000093f00000000093f000000, amount_minor: \ + 387532395441, timestamp: 89643024, gas_price_minor: 491169069, nonce: 789, status: \ + Pending(Waiting) }]. Unproven failures: []."; + assert_eq!(panic_msg, expected); + } + + #[test] + fn compose_scan_result_all_payments_resolved_in_automatic_mode() { + let result = PendingPayableScanner::compose_scan_result(None, None); + + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ) + } + + #[test] + fn compose_scan_result_all_payments_resolved_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + None, + Some(ResponseSkeleton { + client_id: 2222, + context_id: 22, + }), + ); + + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(Some(NodeToUiMessage { + target: MessageTarget::ClientId(2222), + body: UiScanResponse {}.tmb(22) + })) + ) + } + + #[test] + fn compose_scan_result_payments_retry_required_in_automatic_mode() { + let result = PendingPayableScanner::compose_scan_result(Some(Retry::RetryPayments), None); + + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)) + } + + #[test] + fn compose_scan_result_payments_retry_required_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + Some(Retry::RetryPayments), + Some(ResponseSkeleton { + client_id: 1234, + context_id: 21, + }), ); + + assert_eq!( + result, + PendingPayableScanResult::PaymentRetryRequired(Some(ResponseSkeleton { + client_id: 1234, + context_id: 21 + })) + ) + } + + #[test] + fn compose_scan_result_only_scan_procedure_should_be_repeated_in_automatic_mode() { + let result = + PendingPayableScanner::compose_scan_result(Some(Retry::RetryTxStatusCheckOnly), None); + + assert_eq!( + result, + PendingPayableScanResult::ProcedureShouldBeRepeated(None) + ) + } + + #[test] + fn compose_scan_result_only_scan_procedure_should_be_repeated_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + Some(Retry::RetryTxStatusCheckOnly), + Some(ResponseSkeleton { + client_id: 4455, + context_id: 12, + }), + ); + + assert_eq!( + result, + PendingPayableScanResult::ProcedureShouldBeRepeated(Some(NodeToUiMessage { + target: MessageTarget::ClientId(4455), + body: UiScanResponse {}.tmb(12) + })) + ) } #[test] 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 sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(btreeset![]); + let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(btreeset![]); let mut subject = PendingPayableScannerBuilder::new() .failed_payable_dao(failed_payable_dao) .sent_payable_dao(sent_payable_dao) @@ -1150,10 +1263,10 @@ mod tests { let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); assert_eq!( *insert_new_records_params, - vec![vec![failed_tx_1, failed_tx_2]] + vec![btreeset![failed_tx_1, failed_tx_2]] ); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![hash_1, hash_2]]); + assert_eq!(*delete_records_params, vec![btreeset![hash_1, hash_2]]); TestLogHandler::new().exists_log_containing(&format!( "INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ 0x0000000000000000000000000000000000000000000000000000000000000654 were processed in the db" @@ -1187,7 +1300,7 @@ mod tests { ))); let failed_payable_dao = FailedPayableDaoMock::default() .retrieve_txs_params(&retrieve_failed_txs_params_arc) - .retrieve_txs_result(vec![failed_tx_1, failed_tx_2]) + .retrieve_txs_result(btreeset![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); @@ -1195,7 +1308,7 @@ mod tests { 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()]) + .retrieve_txs_result(btreeset![sent_tx.clone()]) .update_statuses_params(&update_statuses_sent_tx_params_arc) .update_statuses_result(Ok(())); let validation_failure_clock = ValidationFailureClockMock::default() @@ -1427,9 +1540,12 @@ mod tests { subject.handle_failed_transactions(detected_failures, &Logger::new("test")); let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); - assert_eq!(*insert_new_records_params, vec![vec![failed_tx_1]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([failed_tx_1])] + ); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![tx_hash_1]]); + assert_eq!(*delete_records_params, vec![btreeset![tx_hash_1]]); let update_statuses_params = update_status_params_arc.lock().unwrap(); assert_eq!( *update_statuses_params, @@ -1617,9 +1733,16 @@ mod tests { ); let replace_records_params = replace_records_params_arc.lock().unwrap(); - assert_eq!(*replace_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + assert_eq!( + *replace_records_params, + vec![btreeset![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]]); + // assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); + assert_eq!( + *delete_records_params, + vec![BTreeSet::from([tx_hash_1, tx_hash_2])] + ); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ @@ -1875,9 +1998,10 @@ mod tests { 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]]); + assert_eq!(*replace_records_params, vec![btreeset![sent_tx_2]]); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); + // assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); + assert_eq!(*delete_records_params, vec![BTreeSet::from([tx_hash_2])]); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "INFO: {test_name}: Reclaimed txs \ @@ -1937,11 +2061,11 @@ mod tests { #[test] #[should_panic( expected = "Unable to complete the tx confirmation by the adjustment of the payable accounts \ - 0x000000000000000000000077616c6c6574343536 due to: \ + 0x0000000000000000000558000000000558000000 due to: \ RusqliteError(\"record change not successful\")" )] fn handle_confirmed_transactions_panics_on_unchecking_payable_table() { - let hash = make_tx_hash(0x315); + let hash = make_tx_hash(315); let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( PayableDaoError::RusqliteError("record change not successful".to_string()), )); 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 index e01425d69..6039cd711 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -93,7 +93,8 @@ impl TxReceiptInterpreter { let replacement_tx = sent_payable_dao .retrieve_txs(Some(RetrieveCondition::ByNonce(vec![failed_tx.nonce]))); let replacement_tx_hash = replacement_tx - .get(0) + .iter() + .next() .unwrap_or_else(|| { panic!( "Attempted to display a replacement tx for {:?} but couldn't find \ @@ -228,15 +229,14 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::{ Detection, RetrieveCondition, SentTx, TxStatus, }; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; 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::accountant::test_utils::{make_transaction_block, SentPayableDaoMock}; use crate::blockchain::errors::internal_errors::InternalErrorKind; use crate::blockchain::errors::rpc_errors::{ AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, @@ -249,6 +249,7 @@ mod tests { 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::collections::BTreeSet; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; @@ -417,7 +418,7 @@ mod tests { 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]); + .retrieve_txs_result(BTreeSet::from([newer_sent_tx_for_older_failed_tx])); let hash = make_tx_hash(0x913); let sent_tx_timestamp = to_unix_timestamp( SystemTime::now() @@ -484,7 +485,7 @@ mod tests { 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]); + .retrieve_txs_result(BTreeSet::from([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; @@ -564,7 +565,7 @@ mod tests { ) { let scan_report = ReceiptScanReport::default(); let still_pending_tx = make_failed_tx(456); - let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(BTreeSet::new()); let _ = TxReceiptInterpreter::handle_still_pending_tx( scan_report, diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index d08808d75..f86984df0 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -3,7 +3,7 @@ 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::accountant::{ResponseSkeleton, TxReceiptResult}; use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::blockchain::errors::validation_status::{ PreviousAttempts, ValidationFailureClock, ValidationStatus, @@ -12,6 +12,7 @@ use crate::blockchain::errors::BlockchainErrorKind; use itertools::Either; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; +use std::cmp::Ordering; use std::collections::HashMap; #[derive(Debug, Default, PartialEq, Eq, Clone)] @@ -203,7 +204,7 @@ impl UpdatableValidationStatus for FailureStatus { FailureStatus::RecheckRequired(ValidationStatus::Reattempting(previous_attempts)) => { Some(FailureStatus::RecheckRequired( ValidationStatus::Reattempting( - previous_attempts.clone().add_attempt(error.into(), clock), + previous_attempts.clone().add_attempt(error, clock), ), )) } @@ -212,11 +213,6 @@ impl UpdatableValidationStatus for FailureStatus { } } -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; @@ -300,7 +296,8 @@ impl RecheckRequiringFailures { #[derive(Debug, PartialEq, Eq)] pub enum PendingPayableScanResult { NoPendingPayablesLeft(Option), - PaymentRetryRequired(Either), + PaymentRetryRequired(Option), + ProcedureShouldBeRepeated(Option), } #[derive(Debug, PartialEq, Eq)] @@ -338,12 +335,37 @@ impl TxByTable { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum TxHashByTable { SentPayable(TxHash), FailedPayable(TxHash), } +impl PartialOrd for TxHashByTable { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for TxHashByTable { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (TxHashByTable::FailedPayable(..), TxHashByTable::SentPayable(..)) => Ordering::Less, + (TxHashByTable::SentPayable(..), TxHashByTable::FailedPayable(..)) => Ordering::Greater, + (TxHashByTable::SentPayable(hash_1), TxHashByTable::SentPayable(hash_2)) => { + hash_1.cmp(hash_2) + } + (TxHashByTable::FailedPayable(hash_1), TxHashByTable::FailedPayable(hash_2)) => { + hash_1.cmp(hash_2) + } + } + } +} + impl TxHashByTable { pub fn hash(&self) -> TxHash { match self { @@ -366,13 +388,13 @@ impl From<&TxByTable> for TxHashByTable { 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::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; 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, @@ -381,6 +403,8 @@ mod tests { 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::cmp::Ordering; + use std::collections::BTreeSet; use std::ops::Sub; use std::time::{Duration, SystemTime}; use std::vec; @@ -720,8 +744,7 @@ mod tests { 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 sent_tx = make_sent_tx(0x567); let records = vec![sent_tx.clone()]; let logger = Logger::new(test_name); subject.load_cache(records); @@ -736,10 +759,10 @@ mod tests { 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) }}}}. \ + {{0x0000000000000000000000000000000000000000000000000000000000000567: SentTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000567, receiver_address: \ + 0x0000000000000000001035000000001035000000, amount_minor: 3658379210721, timestamp: \ + 275427216, gas_price_minor: 2645248887, nonce: 1383, status: Pending(Waiting) }}}}. \ Dumping." )); } @@ -864,8 +887,7 @@ mod tests { 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 failed_tx = make_failed_tx(0x567); let records = vec![failed_tx.clone()]; let logger = Logger::new(test_name); subject.load_cache(records); @@ -880,10 +902,10 @@ mod tests { 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: \ + {{0x0000000000000000000000000000000000000000000000000000000000000567: FailedTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000567, receiver_address: \ + 0x00000000000000000003cc0000000003cc000000, amount_minor: 3658379210721, timestamp: \ + 275427216, gas_price_minor: 2645248887, nonce: 1383, reason: PendingTooLong, status: \ RetryRequired }}}}. Dumping." )); } @@ -1157,4 +1179,26 @@ mod tests { assert_eq!(result_a, TxHashByTable::SentPayable(expected_hash_a)); assert_eq!(result_b, TxHashByTable::FailedPayable(expected_hash_b)); } + + #[test] + fn tx_hash_by_table_ordering_works_correctly() { + let tx_1 = TxHashByTable::SentPayable(make_tx_hash(123)); + let tx_2 = TxHashByTable::FailedPayable(make_tx_hash(333)); + let tx_3 = TxHashByTable::SentPayable(make_tx_hash(654)); + let tx_4 = TxHashByTable::FailedPayable(make_tx_hash(222)); + let tx_1_identical = tx_1; + let tx_2_identical = tx_2; + + let mut set = BTreeSet::new(); + vec![tx_1.clone(), tx_2.clone(), tx_3.clone(), tx_4.clone()] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); + + let expected_order = vec![tx_4, tx_2, tx_1, tx_3]; + assert_eq!(set.into_iter().collect::>(), expected_order); + assert_eq!(tx_1.cmp(&tx_1_identical), Ordering::Equal); + assert_eq!(tx_2.cmp(&tx_2_identical), Ordering::Equal); + } } diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index c459f7226..e69de29bb 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -1,692 +0,0 @@ -// 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, 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::{PendingPayable, SentPayables}; - use crate::sub_lib::accountant::PaymentThresholds; - 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::{Address, H256}; - use masq_lib::ui_gateway::NodeToUiMessage; - 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(HashSet), - } - - #[derive(Debug, PartialEq)] - pub struct PayableScanResult { - pub ui_response_opt: Option, - pub result: OperationOutcome, - } - - #[derive(Debug, PartialEq, Eq)] - pub enum OperationOutcome { - NewPendingPayable, - Failure, - } - - //debugging purposes only - pub fn investigate_debt_extremes( - timestamp: SystemTime, - all_non_pending_payables: &[PayableAccount], - ) -> String { - #[derive(Clone, Copy, Default)] - struct PayableInfo { - balance_wei: u128, - age: u64, - } - fn bigger(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { - match payable_1.balance_wei.cmp(&payable_2.balance_wei) { - Ordering::Greater => payable_1, - Ordering::Less => payable_2, - Ordering::Equal => { - if payable_1.age == payable_2.age { - payable_1 - } else { - older(payable_1, payable_2) - } - } - } - } - fn older(payable_1: PayableInfo, payable_2: PayableInfo) -> PayableInfo { - match payable_1.age.cmp(&payable_2.age) { - Ordering::Greater => payable_1, - Ordering::Less => payable_2, - Ordering::Equal => { - if payable_1.balance_wei == payable_2.balance_wei { - payable_1 - } else { - bigger(payable_1, payable_2) - } - } - } - } - - if all_non_pending_payables.is_empty() { - return "Payable scan found no debts".to_string(); - } - let (biggest, oldest) = all_non_pending_payables - .iter() - .map(|payable| PayableInfo { - balance_wei: payable.balance_wei, - age: timestamp - .duration_since(payable.last_paid_timestamp) - .expect("Payable time is corrupt") - .as_secs(), - }) - .fold( - Default::default(), - |(so_far_biggest, so_far_oldest): (PayableInfo, PayableInfo), payable| { - ( - bigger(so_far_biggest, payable), - older(so_far_oldest, payable), - ) - }, - ); - format!("Payable scan found {} debts; the biggest is {} owed for {}sec, the oldest is {} owed for {}sec", - all_non_pending_payables.len(), biggest.balance_wei, biggest.age, - oldest.balance_wei, oldest.age) - } - - // TODO lifetimes simplification??? - pub fn separate_errors<'a, 'b>( - sent_payables: &'a SentPayables, - logger: &'b Logger, - ) -> (Vec<&'a PendingPayable>, Option) { - match &sent_payables.payment_procedure_result { - Ok(individual_batch_responses) => { - if individual_batch_responses.is_empty() { - panic!("Broken code: An empty vector of processed payments claiming to be an Ok value") - } - - let separated_txs_by_result = - separate_rpc_results(individual_batch_responses, logger); - - 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 the failed process will be deleted. Caused by: {}", - e - ); - - (vec![], Some(LocallyCausedError(e.clone()))) - } - } - } - - fn separate_rpc_results<'a>( - batch_request_responses: &'a [ProcessedPayableFallible], - logger: &Logger, - ) -> SeparatedTxsByResult<'a> { - //TODO maybe we can return not tuple but struct with remote_errors_opt member - let init = SeparatedTxsByResult::default(); - batch_request_responses - .iter() - .fold(init, |acc, rpc_result| { - separate_rpc_results_fold_guts(acc, rpc_result, logger) - }) - } - - #[derive(Default)] - pub struct SeparatedTxsByResult<'a> { - pub ok_results: Vec<&'a PendingPayable>, - pub err_results: HashSet, - } - - fn separate_rpc_results_fold_guts<'a>( - mut acc: SeparatedTxsByResult<'a>, - rpc_result: &'a ProcessedPayableFallible, - logger: &Logger, - ) -> SeparatedTxsByResult<'a> { - match rpc_result { - ProcessedPayableFallible::Correct(pending_payable) => { - acc.ok_results.push(pending_payable); - acc - } - ProcessedPayableFallible::Failed(RpcPayableFailure { - rpc_error, - recipient_wallet, - hash, - }) => { - warning!( - logger, - "Remote sent payable failure '{}' for wallet {} and tx hash {:?}", - rpc_error, - recipient_wallet, - hash - ); - acc.err_results.insert(*hash); - acc - } - } - } - - pub fn payables_debug_summary(qualified_accounts: &[(PayableAccount, u128)], logger: &Logger) { - if qualified_accounts.is_empty() { - return; - } - debug!(logger, "Paying qualified debts:\n{}", { - let now = SystemTime::now(); - qualified_accounts - .iter() - .map(|(payable, threshold_point)| { - let p_age = now - .duration_since(payable.last_paid_timestamp) - .expect("Payable time is corrupt"); - format!( - "{} wei owed for {} sec exceeds the threshold {} wei for creditor {}", - payable.balance_wei.separate_with_commas(), - p_age.as_secs(), - threshold_point.separate_with_commas(), - payable.wallet - ) - }) - .join(".\n") - }) - } - - pub fn debugging_summary_after_error_separation( - oks: &[&PendingPayable], - errs_opt: &Option, - ) -> String { - format!( - "Got {} properly sent payables of {} attempts", - oks.len(), - count_total_errors(errs_opt) - .map(|err_count| (err_count + oks.len()).to_string()) - .unwrap_or_else(|| "an unknown number of".to_string()) - ) - } - - pub(super) fn count_total_errors( - full_set_of_errors: &Option, - ) -> Option { - match full_set_of_errors { - Some(errors) => match errors { - LocallyCausedError(blockchain_error) => match blockchain_error { - PayableTransactionError::Sending { hashes, .. } => Some(hashes.len()), - _ => None, - }, - RemotelyCausedErrors(hashes) => Some(hashes.len()), - }, - None => Some(0), - } - } - - #[derive(Debug, PartialEq, Eq)] - pub struct PendingPayableMissingInDb { - pub recipient: Address, - pub hash: H256, - } - - impl PendingPayableMissingInDb { - pub fn new(recipient: Address, hash: H256) -> PendingPayableMissingInDb { - PendingPayableMissingInDb { recipient, hash } - } - } - - 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 payables {} with missing records. The system has become unreliable", - serialize_hashes(&nonexistent), - )) - } - - pub fn separate_rowids_and_hashes(ids_of_payments: Vec<(u64, H256)>) -> (Vec, Vec) { - ids_of_payments.into_iter().unzip() - } - - pub trait PayableThresholdsGauge { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool; - fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool; - fn calculate_payout_threshold_in_gwei( - &self, - payment_thresholds: &PaymentThresholds, - x: u64, - ) -> u128; - as_any_ref_in_trait!(); - } - - #[derive(Default)] - pub struct PayableThresholdsGaugeReal {} - - impl PayableThresholdsGauge for PayableThresholdsGaugeReal { - fn is_innocent_age(&self, age: u64, limit: u64) -> bool { - age <= limit - } - - fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool { - balance <= limit - } - - fn calculate_payout_threshold_in_gwei( - &self, - payment_thresholds: &PaymentThresholds, - debt_age: u64, - ) -> u128 { - ThresholdUtils::calculate_finite_debt_limit_by_age(payment_thresholds, debt_age) - } - as_any_ref_in_trait_impl!(); - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ - LocallyCausedError, RemotelyCausedErrors, - }; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ - count_total_errors, debugging_summary_after_error_separation, investigate_debt_extremes, - payables_debug_summary, separate_errors, PayableThresholdsGauge, - PayableThresholdsGaugeReal, - }; - 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::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; - use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; - - #[test] - fn investigate_debt_extremes_picks_the_most_relevant_records() { - let now = SystemTime::now(); - let now_t = to_unix_timestamp(now); - let same_amount_significance = 2_000_000; - let same_age_significance = from_unix_timestamp(now_t - 30000); - let payables = &[ - PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: same_amount_significance, - last_paid_timestamp: from_unix_timestamp(now_t - 5000), - pending_payable_opt: None, - }, - //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked - PayableAccount { - wallet: make_wallet("wallet1"), - balance_wei: same_amount_significance, - last_paid_timestamp: from_unix_timestamp(now_t - 10000), - pending_payable_opt: None, - }, - //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen - PayableAccount { - wallet: make_wallet("wallet3"), - balance_wei: 100, - last_paid_timestamp: same_age_significance, - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("wallet2"), - balance_wei: 330, - last_paid_timestamp: same_age_significance, - pending_payable_opt: None, - }, - ]; - - let result = investigate_debt_extremes(now, payables); - - assert_eq!(result, "Payable scan found 4 debts; the biggest is 2000000 owed for 10000sec, the oldest is 330 owed for 30000sec") - } - - #[test] - fn separate_errors_works_for_no_errs_just_oks() { - let correct_payment_1 = PendingPayable { - recipient_wallet: make_wallet("blah"), - hash: make_tx_hash(123), - }; - let correct_payment_2 = PendingPayable { - recipient_wallet: make_wallet("howgh"), - hash: make_tx_hash(456), - }; - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(correct_payment_1.clone()), - ProcessedPayableFallible::Correct(correct_payment_2.clone()), - ]), - response_skeleton_opt: None, - }; - - let (oks, errs) = separate_errors(&sent_payable, &Logger::new("test")); - - assert_eq!(oks, vec![&correct_payment_1, &correct_payment_2]); - assert_eq!(errs, None) - } - - #[test] - fn separate_errors_works_for_local_error() { - init_test_logging(); - let error = PayableTransactionError::Sending { - msg: "Bad luck".to_string(), - hashes: hashset![make_tx_hash(0x7b)], - }; - let sent_payable = SentPayables { - payment_procedure_result: Err(error.clone()), - response_skeleton_opt: None, - }; - - let (oks, errs) = separate_errors(&sent_payable, &Logger::new("test_logger")); - - assert!(oks.is_empty()); - assert_eq!(errs, Some(LocallyCausedError(error))); - TestLogHandler::new().exists_log_containing( - "WARN: test_logger: Any persisted data from \ - the failed process will be deleted. Caused by: Sending phase: \"Bad luck\". Signed and hashed txs: \ - 0x000000000000000000000000000000000000000000000000000000000000007b", - ); - } - - #[test] - fn separate_errors_works_for_their_errors() { - init_test_logging(); - let payable_ok = PendingPayable { - recipient_wallet: make_wallet("blah"), - hash: make_tx_hash(123), - }; - let bad_rpc_call = RpcPayableFailure { - rpc_error: web3::Error::InvalidResponse("That jackass screwed it up".to_string()), - recipient_wallet: make_wallet("whooa"), - hash: make_tx_hash(0x315), - }; - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(payable_ok.clone()), - ProcessedPayableFallible::Failed(bad_rpc_call.clone()), - ]), - response_skeleton_opt: None, - }; - - let (oks, errs) = separate_errors(&sent_payable, &Logger::new("test_logger")); - - assert_eq!(oks, vec![&payable_ok]); - 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] - fn payables_debug_summary_displays_nothing_for_no_qualified_payments() { - init_test_logging(); - let logger = - Logger::new("payables_debug_summary_displays_nothing_for_no_qualified_payments"); - - payables_debug_summary(&vec![], &logger); - - TestLogHandler::new().exists_no_log_containing( - "DEBUG: payables_debug_summary_stays_\ - inert_if_no_qualified_payments: Paying qualified debts:", - ); - } - - #[test] - fn payables_debug_summary_prints_pretty_summary() { - init_test_logging(); - let now = to_unix_timestamp(SystemTime::now()); - let payment_thresholds = PaymentThresholds { - threshold_interval_sec: 2_592_000, - debt_threshold_gwei: 1_000_000_000, - payment_grace_period_sec: 86_400, - maturity_threshold_sec: 86_400, - permanent_debt_allowed_gwei: 10_000_000, - unban_below_gwei: 10_000_000, - }; - let qualified_payables_and_threshold_points = vec![ - ( - PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2000), - last_paid_timestamp: from_unix_timestamp( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec, - ), - ), - pending_payable_opt: None, - }, - 10_000_000_001_152_000_u128, - ), - ( - PayableAccount { - wallet: make_wallet("wallet1"), - balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1), - last_paid_timestamp: from_unix_timestamp( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec + 55, - ), - ), - pending_payable_opt: None, - }, - 999_978_993_055_555_580, - ), - ]; - let logger = Logger::new("test"); - - payables_debug_summary(&qualified_payables_and_threshold_points, &logger); - - TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ - 10,002,000,000,000,000 wei owed for 2678400 sec exceeds 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] - fn payout_sloped_segment_in_payment_thresholds_goes_along_proper_line() { - let payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 333, - payment_grace_period_sec: 444, - permanent_debt_allowed_gwei: 4444, - debt_threshold_gwei: 8888, - threshold_interval_sec: 1111111, - unban_below_gwei: 0, - }; - let higher_corner_timestamp = payment_thresholds.maturity_threshold_sec; - let middle_point_timestamp = payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec / 2; - let lower_corner_timestamp = - payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec; - let tested_fn = |payment_thresholds: &PaymentThresholds, time| { - PayableThresholdsGaugeReal {} - .calculate_payout_threshold_in_gwei(payment_thresholds, time) as i128 - }; - - let higher_corner_point = tested_fn(&payment_thresholds, higher_corner_timestamp); - let middle_point = tested_fn(&payment_thresholds, middle_point_timestamp); - let lower_corner_point = tested_fn(&payment_thresholds, lower_corner_timestamp); - - let allowed_imprecision = WEIS_IN_GWEI; - let ideal_template_higher: i128 = gwei_to_wei(payment_thresholds.debt_threshold_gwei); - let ideal_template_middle: i128 = gwei_to_wei( - (payment_thresholds.debt_threshold_gwei - - payment_thresholds.permanent_debt_allowed_gwei) - / 2 - + payment_thresholds.permanent_debt_allowed_gwei, - ); - let ideal_template_lower: i128 = - gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei); - assert!( - higher_corner_point <= ideal_template_higher + allowed_imprecision - && ideal_template_higher - allowed_imprecision <= higher_corner_point, - "ideal: {}, real: {}", - ideal_template_higher, - higher_corner_point - ); - assert!( - middle_point <= ideal_template_middle + allowed_imprecision - && ideal_template_middle - allowed_imprecision <= middle_point, - "ideal: {}, real: {}", - ideal_template_middle, - middle_point - ); - assert!( - lower_corner_point <= ideal_template_lower + allowed_imprecision - && ideal_template_lower - allowed_imprecision <= lower_corner_point, - "ideal: {}, real: {}", - ideal_template_lower, - lower_corner_point - ) - } - - #[test] - fn is_innocent_age_works_for_age_smaller_than_innocent_age() { - let payable_age = 999; - - let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_age_works_for_age_equal_to_innocent_age() { - let payable_age = 1000; - - let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_age_works_for_excessive_age() { - let payable_age = 1001; - - let result = PayableThresholdsGaugeReal::default().is_innocent_age(payable_age, 1000); - - assert_eq!(result, false) - } - - #[test] - fn is_innocent_balance_works_for_balance_smaller_than_innocent_balance() { - let payable_balance = 999; - - let result = - PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_balance_works_for_balance_equal_to_innocent_balance() { - let payable_balance = 1000; - - let result = - PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); - - assert_eq!(result, true) - } - - #[test] - fn is_innocent_balance_works_for_excessive_balance() { - let payable_balance = 1001; - - let result = - PayableThresholdsGaugeReal::default().is_innocent_balance(payable_balance, 1000); - - assert_eq!(result, false) - } - - #[test] - fn count_total_errors_says_unknown_number_for_early_local_errors() { - let early_local_errors = [ - PayableTransactionError::TransactionID(BlockchainInterfaceError::QueryFailed( - "blah".to_string(), - )), - PayableTransactionError::MissingConsumingWallet, - PayableTransactionError::GasPriceQueryFailed(BlockchainInterfaceError::QueryFailed( - "ouch".to_string(), - )), - PayableTransactionError::UnusableWallet("fooo".to_string()), - PayableTransactionError::Signing("tsss".to_string()), - ]; - - early_local_errors - .into_iter() - .for_each(|err| assert_eq!(count_total_errors(&Some(LocallyCausedError(err))), None)) - } - - #[test] - fn count_total_errors_works_correctly_for_local_error_after_signing() { - let error = PayableTransactionError::Sending { - msg: "Ouuuups".to_string(), - hashes: hashset![make_tx_hash(333), make_tx_hash(666)], - }; - let sent_payable = Some(LocallyCausedError(error)); - - let result = count_total_errors(&sent_payable); - - assert_eq!(result, Some(2)) - } - - #[test] - fn count_total_errors_works_correctly_for_remote_errors() { - let sent_payable = Some(RemotelyCausedErrors(hashset![ - make_tx_hash(123), - make_tx_hash(456), - ])); - - let result = count_total_errors(&sent_payable); - - assert_eq!(result, Some(2)) - } - - #[test] - fn count_total_errors_works_correctly_if_no_errors_found_at_all() { - let sent_payable = None; - - let result = count_total_errors(&sent_payable); - - assert_eq!(result, Some(0)) - } - - #[test] - fn debug_summary_after_error_separation_says_the_count_cannot_be_known() { - let oks = vec![]; - let error = PayableTransactionError::MissingConsumingWallet; - let errs = Some(LocallyCausedError(error)); - - let result = debugging_summary_after_error_separation(&oks, &errs); - - assert_eq!( - result, - "Got 0 properly sent payables of an unknown number of attempts" - ) - } -} diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index ecd0781fe..731fb508d 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -3,23 +3,27 @@ #![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::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, }; -use crate::accountant::scanners::payable_scanner_extension::{ - MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, +use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::{ + PreparedAdjustment, SolvencySensitivePaymentInstructor, }; +use crate::accountant::scanners::payable_scanner::utils::PayableScanResult; +use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PayableScanner}; use crate::accountant::scanners::pending_payable_scanner::utils::{ PendingPayableCache, PendingPayableScanResult, }; +use crate::accountant::scanners::pending_payable_scanner::{ + CachesEmptiableScanner, ExtendedPendingPayablePrivateScanner, +}; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, ScanReschedulingAfterEarlyStop, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::{ - PayableScanner, PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, - Scanner, StartScanError, StartableScanner, + PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, Scanner, + StartScanError, StartableScanner, }; use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, SentPayables, TxReceiptsMessage, @@ -94,7 +98,7 @@ impl MultistageDualPayableScanner for NullScanner {} impl SolvencySensitivePaymentInstructor for NullScanner { fn try_skipping_payment_adjustment( &self, - _msg: BlockchainAgentWithContextMessage, + _msg: PricedTemplatesMessage, _logger: &Logger, ) -> Result, String> { intentionally_blank!() @@ -109,6 +113,14 @@ impl SolvencySensitivePaymentInstructor for NullScanner { } } +impl ExtendedPendingPayablePrivateScanner for NullScanner {} + +impl CachesEmptiableScanner for NullScanner { + fn empty_caches(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + impl Default for NullScanner { fn default() -> Self { Self::new() @@ -273,16 +285,16 @@ impl ScannerMock + for ScannerMock { } impl SolvencySensitivePaymentInstructor - for ScannerMock + for ScannerMock { fn try_skipping_payment_adjustment( &self, - msg: BlockchainAgentWithContextMessage, + msg: PricedTemplatesMessage, _logger: &Logger, ) -> Result, String> { // Always passes... @@ -290,7 +302,7 @@ impl SolvencySensitivePaymentInstructor // mock, plus this functionality can be tested better with the other components mocked, // not the scanner itself. Ok(Either::Left(OutboundPaymentsInstructions { - affordable_accounts: msg.qualified_payables, + priced_templates: msg.priced_templates, agent: msg.agent, response_skeleton_opt: msg.response_skeleton_opt, })) @@ -305,6 +317,19 @@ impl SolvencySensitivePaymentInstructor } } +impl ExtendedPendingPayablePrivateScanner + for ScannerMock +{ +} + +impl CachesEmptiableScanner + for ScannerMock +{ + fn empty_caches(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + pub trait ScannerMockMarker {} impl ScannerMockMarker for ScannerMock {} @@ -364,7 +389,7 @@ pub enum ScannerReplacement { Payable( ReplacementType< PayableScanner, - ScannerMock, + ScannerMock, >, ), PendingPayable( @@ -403,7 +428,7 @@ pub fn parse_system_time_from_str(examined_str: &str) -> Vec { .collect() } -fn trim_expected_timestamp_to_three_digits_nanos(value: SystemTime) -> SystemTime { +pub fn trim_expected_timestamp_to_three_digits_nanos(value: SystemTime) -> SystemTime { let duration = value.duration_since(UNIX_EPOCH).unwrap(); let full_nanos = duration.subsec_nanos(); let diffuser = 10_u32.pow(6); diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 2f777e57b..8d6fb49ed 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -4,41 +4,36 @@ use crate::accountant::db_access_objects::banned_dao::{BannedDao, BannedDaoFactory}; use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureReason, + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::payable_dao::{ MarkPendingPayableID, PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, + PayableRetrieveCondition, }; + 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, + RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoFactory, SentTx, TxStatus, }; use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, TxHash, TxIdentifiers, }; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayableWithGasPrice, - QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, -}; -use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; +use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::utils::PayableThresholdsGauge; 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}; 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::blockchain::errors::validation_status::ValidationFailureClock; +use crate::blockchain::test_utils::make_block_hash; use crate::bootstrapper::BootstrapperConfig; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; @@ -56,13 +51,12 @@ use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; use std::fmt::Debug; 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()); @@ -100,36 +94,6 @@ 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), @@ -137,28 +101,6 @@ pub fn make_transaction_block(num: u64) -> TxBlock { } } -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, @@ -318,9 +260,10 @@ const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::PendingPayableScanner, ]; -//TODO Utkarsh should also update this -const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 1] = - [DestinationMarker::PendingPayableScanner]; +const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 2] = [ + DestinationMarker::PayableScanner, + DestinationMarker::PendingPayableScanner, +]; const SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, @@ -383,7 +326,7 @@ impl AccountantBuilder { ) -> Self { specially_configured_daos.iter_mut().for_each(|dao| { if let DaoWithDestination::ForPendingPayableScanner(dao) = dao { - let mut extended_queue = vec![vec![]]; + let mut extended_queue = vec![BTreeSet::new()]; extended_queue.append(&mut dao.retrieve_txs_results.borrow_mut()); dao.retrieve_txs_results.replace(extended_queue); } @@ -412,6 +355,39 @@ impl AccountantBuilder { ) } + // pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { + // // TODO: GH-605: Bert Merge Cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 + // match self.sent_payable_dao_factory_opt { + // None => { + // self.sent_payable_dao_factory_opt = + // Some(SentPayableDaoFactoryMock::new().make_result(sent_payable_dao)) + // } + // Some(sent_payable_dao_factory) => { + // self.sent_payable_dao_factory_opt = + // Some(sent_payable_dao_factory.make_result(sent_payable_dao)) + // } + // } + // + // self + // } + // + // pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { + // // TODO: GH-605: Bert Merge cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 + // + // match self.failed_payable_dao_factory_opt { + // None => { + // self.failed_payable_dao_factory_opt = + // Some(FailedPayableDaoFactoryMock::new().make_result(failed_payable_dao)) + // } + // Some(failed_payable_dao_factory) => { + // self.failed_payable_dao_factory_opt = + // Some(failed_payable_dao_factory.make_result(failed_payable_dao)) + // } + // } + // + // self + // } + //TODO this method seems to be never used? pub fn banned_dao(mut self, banned_dao: BannedDaoMock) -> Self { match self.banned_dao_factory_opt { @@ -452,9 +428,12 @@ impl AccountantBuilder { .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 failed_payable_dao_factory = self.failed_payable_dao_factory_opt.unwrap_or( + FailedPayableDaoFactoryMock::new() + .make_result(FailedPayableDaoMock::new()) + .make_result(FailedPayableDaoMock::new()) + .make_result(FailedPayableDaoMock::new()), + ); let banned_dao_factory = self .banned_dao_factory_opt .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); @@ -626,8 +605,8 @@ impl ConfigDaoFactoryMock { pub struct PayableDaoMock { more_money_payable_parameters: Arc>>, more_money_payable_results: RefCell>>, - non_pending_payables_params: Arc>>, - non_pending_payables_results: RefCell>>, + retrieve_payables_params: Arc>>>, + retrieve_payables_results: RefCell>>, mark_pending_payables_rowids_params: Arc>>>, mark_pending_payables_rowids_results: RefCell>>, transactions_confirmed_params: Arc>>>, @@ -679,9 +658,15 @@ impl PayableDao for PayableDaoMock { self.transactions_confirmed_results.borrow_mut().remove(0) } - fn non_pending_payables(&self) -> Vec { - self.non_pending_payables_params.lock().unwrap().push(()); - self.non_pending_payables_results.borrow_mut().remove(0) + fn retrieve_payables( + &self, + condition_opt: Option, + ) -> Vec { + self.retrieve_payables_params + .lock() + .unwrap() + .push(condition_opt); + self.retrieve_payables_results.borrow_mut().remove(0) } fn custom_query(&self, custom_query: CustomQuery) -> Option> { @@ -717,13 +702,16 @@ impl PayableDaoMock { self } - pub fn non_pending_payables_params(mut self, params: &Arc>>) -> Self { - self.non_pending_payables_params = params.clone(); + pub fn retrieve_payables_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.retrieve_payables_params = params.clone(); self } - pub fn non_pending_payables_result(self, result: Vec) -> Self { - self.non_pending_payables_results.borrow_mut().push(result); + pub fn retrieve_payables_result(self, result: Vec) -> Self { + self.retrieve_payables_results.borrow_mut().push(result); self } @@ -984,38 +972,38 @@ pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> Boot #[derive(Default)] pub struct SentPayableDaoMock { - get_tx_identifiers_params: Arc>>>, + get_tx_identifiers_params: Arc>>>, get_tx_identifiers_results: RefCell>, - insert_new_records_params: Arc>>>, + insert_new_records_params: Arc>>>, insert_new_records_results: RefCell>>, retrieve_txs_params: Arc>>>, - retrieve_txs_results: RefCell>>, + 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_params: Arc>>>, replace_records_results: RefCell>>, - delete_records_params: Arc>>>, + delete_records_params: Arc>>>, delete_records_results: RefCell>>, } impl SentPayableDao for SentPayableDaoMock { - fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers { self.get_tx_identifiers_params .lock() .unwrap() .push(hashes.clone()); self.get_tx_identifiers_results.borrow_mut().remove(0) } - fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError> { self.insert_new_records_params .lock() .unwrap() - .push(txs.to_vec()); + .push(txs.clone()); self.insert_new_records_results.borrow_mut().remove(0) } - fn retrieve_txs(&self, condition: Option) -> Vec { + fn retrieve_txs(&self, condition: Option) -> BTreeSet { self.retrieve_txs_params.lock().unwrap().push(condition); self.retrieve_txs_results.borrow_mut().remove(0) } @@ -1026,11 +1014,11 @@ impl SentPayableDao for SentPayableDaoMock { .push(hash_map.clone()); self.confirm_tx_results.borrow_mut().remove(0) } - fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError> { self.replace_records_params .lock() .unwrap() - .push(new_txs.to_vec()); + .push(new_txs.clone()); self.replace_records_results.borrow_mut().remove(0) } @@ -1045,7 +1033,7 @@ impl SentPayableDao for SentPayableDaoMock { self.update_statuses_results.borrow_mut().remove(0) } - fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError> { self.delete_records_params .lock() .unwrap() @@ -1059,7 +1047,7 @@ impl SentPayableDaoMock { SentPayableDaoMock::default() } - pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { self.get_tx_identifiers_params = params.clone(); self } @@ -1069,7 +1057,7 @@ impl SentPayableDaoMock { self } - pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { self.insert_new_records_params = params.clone(); self } @@ -1087,7 +1075,7 @@ impl SentPayableDaoMock { self } - pub fn retrieve_txs_result(self, result: Vec) -> Self { + pub fn retrieve_txs_result(self, result: BTreeSet) -> Self { self.retrieve_txs_results.borrow_mut().push(result); self } @@ -1102,7 +1090,7 @@ impl SentPayableDaoMock { self } - pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { + pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { self.replace_records_params = params.clone(); self } @@ -1125,7 +1113,7 @@ impl SentPayableDaoMock { self } - pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { self.delete_records_params = params.clone(); self } @@ -1136,58 +1124,22 @@ impl SentPayableDaoMock { } } -pub struct SentPayableDaoFactoryMock { - make_params: Arc>>, - make_results: RefCell>>, -} - -impl SentPayableDaoFactory for SentPayableDaoFactoryMock { - fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!( - "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(()); - self.make_results.borrow_mut().remove(0) - } -} - -impl SentPayableDaoFactoryMock { - pub fn new() -> Self { - Self { - make_params: Arc::new(Mutex::new(vec![])), - make_results: RefCell::new(vec![]), - } - } - - pub fn make_params(mut self, params: &Arc>>) -> Self { - self.make_params = params.clone(); - self - } - - pub fn make_result(self, result: SentPayableDaoMock) -> Self { - self.make_results.borrow_mut().push(Box::new(result)); - self - } -} - #[derive(Default)] pub struct FailedPayableDaoMock { - get_tx_identifiers_params: Arc>>>, + get_tx_identifiers_params: Arc>>>, get_tx_identifiers_results: RefCell>, - insert_new_records_params: Arc>>>, + insert_new_records_params: Arc>>>, insert_new_records_results: RefCell>>, retrieve_txs_params: Arc>>>, - retrieve_txs_results: RefCell>>, + retrieve_txs_results: RefCell>>, update_statuses_params: Arc>>>, update_statuses_results: RefCell>>, - delete_records_params: Arc>>>, + delete_records_params: Arc>>>, delete_records_results: RefCell>>, } impl FailedPayableDao for FailedPayableDaoMock { - fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers { self.get_tx_identifiers_params .lock() .unwrap() @@ -1195,15 +1147,15 @@ impl FailedPayableDao for FailedPayableDaoMock { self.get_tx_identifiers_results.borrow_mut().remove(0) } - fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError> { + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), FailedPayableDaoError> { self.insert_new_records_params .lock() .unwrap() - .push(txs.to_vec()); + .push(txs.clone()); self.insert_new_records_results.borrow_mut().remove(0) } - fn retrieve_txs(&self, condition: Option) -> Vec { + fn retrieve_txs(&self, condition: Option) -> BTreeSet { self.retrieve_txs_params.lock().unwrap().push(condition); self.retrieve_txs_results.borrow_mut().remove(0) } @@ -1219,7 +1171,7 @@ impl FailedPayableDao for FailedPayableDaoMock { self.update_statuses_results.borrow_mut().remove(0) } - fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError> { + fn delete_records(&self, hashes: &BTreeSet) -> Result<(), FailedPayableDaoError> { self.delete_records_params .lock() .unwrap() @@ -1233,7 +1185,7 @@ impl FailedPayableDaoMock { Self::default() } - pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { self.get_tx_identifiers_params = params.clone(); self } @@ -1243,7 +1195,10 @@ impl FailedPayableDaoMock { self } - pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + pub fn insert_new_records_params( + mut self, + params: &Arc>>>, + ) -> Self { self.insert_new_records_params = params.clone(); self } @@ -1261,7 +1216,7 @@ impl FailedPayableDaoMock { self } - pub fn retrieve_txs_result(self, result: Vec) -> Self { + pub fn retrieve_txs_result(self, result: BTreeSet) -> Self { self.retrieve_txs_results.borrow_mut().push(result); self } @@ -1279,7 +1234,7 @@ impl FailedPayableDaoMock { self } - pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { self.delete_records_params = params.clone(); self } @@ -1321,57 +1276,35 @@ impl FailedPayableDaoFactoryMock { } } -pub struct PayableScannerBuilder { - payable_dao: PayableDaoMock, - sent_payable_dao: SentPayableDaoMock, - payment_thresholds: PaymentThresholds, - payment_adjuster: PaymentAdjusterMock, +pub struct SentPayableDaoFactoryMock { + make_params: Arc>>, + make_results: RefCell>>, } -impl PayableScannerBuilder { +impl SentPayableDaoFactory for SentPayableDaoFactoryMock { + fn make(&self) -> Box { + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) + } +} + +impl SentPayableDaoFactoryMock { pub fn new() -> Self { Self { - payable_dao: PayableDaoMock::new(), - sent_payable_dao: SentPayableDaoMock::new(), - payment_thresholds: PaymentThresholds::default(), - payment_adjuster: PaymentAdjusterMock::default(), + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), } } - pub fn payable_dao(mut self, payable_dao: PayableDaoMock) -> PayableScannerBuilder { - self.payable_dao = payable_dao; - self - } - - pub fn payment_adjuster( - mut self, - payment_adjuster: PaymentAdjusterMock, - ) -> PayableScannerBuilder { - self.payment_adjuster = payment_adjuster; - self - } - - pub fn payment_thresholds(mut self, payment_thresholds: PaymentThresholds) -> Self { - self.payment_thresholds = payment_thresholds; + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); self } - pub fn sent_payable_dao( - mut self, - sent_payable_dao: SentPayableDaoMock, - ) -> PayableScannerBuilder { - self.sent_payable_dao = sent_payable_dao; + pub fn make_result(self, result: SentPayableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); self } - - pub fn build(self) -> PayableScanner { - PayableScanner::new( - Box::new(self.payable_dao), - Box::new(self.sent_payable_dao), - Rc::new(self.payment_thresholds), - Box::new(self.payment_adjuster), - ) - } } pub struct PendingPayableScannerBuilder { @@ -1550,14 +1483,14 @@ pub fn make_qualified_and_unqualified_payables( }, ]; - let mut all_non_pending_payables = Vec::new(); - all_non_pending_payables.extend(qualified_payable_accounts.clone()); - all_non_pending_payables.extend(unqualified_payable_accounts.clone()); + let mut retrieved_payables = Vec::new(); + retrieved_payables.extend(qualified_payable_accounts.clone()); + retrieved_payables.extend(unqualified_payable_accounts.clone()); ( qualified_payable_accounts, unqualified_payable_accounts, - all_non_pending_payables, + retrieved_payables, ) } @@ -1692,8 +1625,7 @@ pub fn trick_rusqlite_with_read_only_conn( #[derive(Default)] pub struct PaymentAdjusterMock { - search_for_indispensable_adjustment_params: - Arc>>, + search_for_indispensable_adjustment_params: Arc>>, search_for_indispensable_adjustment_results: RefCell, AnalysisError>>>, adjust_payments_params: Arc>>, @@ -1703,7 +1635,7 @@ pub struct PaymentAdjusterMock { impl PaymentAdjuster for PaymentAdjusterMock { fn search_for_indispensable_adjustment( &self, - msg: &BlockchainAgentWithContextMessage, + msg: &PricedTemplatesMessage, logger: &Logger, ) -> Result, AnalysisError> { self.search_for_indispensable_adjustment_params @@ -1732,7 +1664,7 @@ impl PaymentAdjuster for PaymentAdjusterMock { impl PaymentAdjusterMock { pub fn is_adjustment_required_params( mut self, - params: &Arc>>, + params: &Arc>>, ) -> Self { self.search_for_indispensable_adjustment_params = params.clone(); self @@ -1761,33 +1693,3 @@ impl PaymentAdjusterMock { self } } - -pub fn make_priced_qualified_payables( - inputs: Vec<(PayableAccount, u128)>, -) -> PricedQualifiedPayables { - PricedQualifiedPayables { - payables: inputs - .into_iter() - .map(|(payable, gas_price_minor)| QualifiedPayableWithGasPrice { - payable, - gas_price_minor, - }) - .collect(), - } -} - -pub fn make_unpriced_qualified_payables_for_retry_mode( - inputs: Vec<(PayableAccount, u128)>, -) -> UnpricedQualifiedPayables { - UnpricedQualifiedPayables { - payables: inputs - .into_iter() - .map(|(payable, previous_attempt_gas_price_minor)| { - QualifiedPayablesBeforeGasPriceSelection { - payable, - previous_attempt_gas_price_minor_opt: Some(previous_attempt_gas_price_minor), - } - }) - .collect(), - } -} diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 61c5ff9c0..79cfa03f8 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -473,8 +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 sent_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let failed_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let sent_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)); diff --git a/node/src/blockchain/blockchain_agent/agent_web3.rs b/node/src/blockchain/blockchain_agent/agent_web3.rs index 8899a0743..66df08d57 100644 --- a/node/src/blockchain/blockchain_agent/agent_web3.rs +++ b/node/src/blockchain/blockchain_agent/agent_web3.rs @@ -1,19 +1,15 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::comma_joined_stringifiable; -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - PricedQualifiedPayables, QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, -}; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; -use itertools::{Either, Itertools}; +use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use masq_lib::utils::ExpectValue; -use thousands::Separable; -use web3::types::Address; #[derive(Debug, Clone)] pub struct BlockchainAgentWeb3 { @@ -28,79 +24,40 @@ pub struct BlockchainAgentWeb3 { impl BlockchainAgent for BlockchainAgentWeb3 { fn price_qualified_payables( &self, - qualified_payables: UnpricedQualifiedPayables, - ) -> PricedQualifiedPayables { - let warning_data_collector_opt = - self.set_up_warning_data_collector_opt(&qualified_payables); - - let init: ( - Vec, - Option, - ) = (vec![], warning_data_collector_opt); - let (priced_qualified_payables, warning_data_collector_opt) = - qualified_payables.payables.into_iter().fold( - init, - |(mut priced_payables, mut warning_data_collector_opt), unpriced_payable| { - let selected_gas_price_wei = - match unpriced_payable.previous_attempt_gas_price_minor_opt { - None => self.latest_gas_price_wei, - Some(previous_price) if self.latest_gas_price_wei < previous_price => { - previous_price - } - Some(_) => self.latest_gas_price_wei, - }; - - let gas_price_increased_by_margin_wei = - increase_gas_price_by_margin(selected_gas_price_wei); - - let price_ceiling_wei = self.chain.rec().gas_price_safe_ceiling_minor; - let checked_gas_price_wei = - if gas_price_increased_by_margin_wei > price_ceiling_wei { - warning_data_collector_opt.as_mut().map(|collector| { - match collector.data.as_mut() { - Either::Left(new_payable_data) => { - new_payable_data - .addresses - .push(unpriced_payable.payable.wallet.address()); - new_payable_data.gas_price_above_limit_wei = - gas_price_increased_by_margin_wei - } - Either::Right(retry_payable_data) => retry_payable_data - .addresses_and_gas_price_value_above_limit_wei - .push(( - unpriced_payable.payable.wallet.address(), - gas_price_increased_by_margin_wei, - )), - } - }); - price_ceiling_wei - } else { - gas_price_increased_by_margin_wei - }; - - priced_payables.push(QualifiedPayableWithGasPrice::new( - unpriced_payable.payable, - checked_gas_price_wei, - )); - - (priced_payables, warning_data_collector_opt) - }, - ); - - warning_data_collector_opt - .map(|collector| collector.log_warning_if_some_reason(&self.logger, self.chain)); - - PricedQualifiedPayables { - payables: priced_qualified_payables, + unpriced_tx_templates: Either, + ) -> Either { + match unpriced_tx_templates { + Either::Left(new_tx_templates) => { + let priced_new_templates = PricedNewTxTemplates::from_initial_with_logging( + new_tx_templates, + self.latest_gas_price_wei, + self.chain.rec().gas_price_safe_ceiling_minor, + &self.logger, + ); + + Either::Left(priced_new_templates) + } + Either::Right(retry_tx_templates) => { + let priced_retry_templates = PricedRetryTxTemplates::from_initial_with_logging( + retry_tx_templates, + self.latest_gas_price_wei, + self.chain.rec().gas_price_safe_ceiling_minor, + &self.logger, + ); + + Either::Right(priced_retry_templates) + } } } - fn estimate_transaction_fee_total(&self, qualified_payables: &PricedQualifiedPayables) -> u128 { - let prices_sum: u128 = qualified_payables - .payables - .iter() - .map(|priced_payable| priced_payable.gas_price_minor) - .sum(); + fn estimate_transaction_fee_total( + &self, + priced_tx_templates: &Either, + ) -> u128 { + let prices_sum = match priced_tx_templates { + Either::Left(new_tx_templates) => new_tx_templates.total_gas_price(), + Either::Right(retry_tx_templates) => retry_tx_templates.total_gas_price(), + }; (self.gas_limit_const_part + WEB3_MAXIMAL_GAS_LIMIT_MARGIN) * prices_sum } @@ -117,89 +74,6 @@ impl BlockchainAgent for BlockchainAgentWeb3 { } } -struct GasPriceAboveLimitWarningReporter { - data: Either, -} - -impl GasPriceAboveLimitWarningReporter { - fn log_warning_if_some_reason(self, logger: &Logger, chain: Chain) { - let ceiling_value_wei = chain.rec().gas_price_safe_ceiling_minor; - match self.data { - Either::Left(new_payable_data) => { - if !new_payable_data.addresses.is_empty() { - warning!( - logger, - "{}", - Self::new_payables_warning_msg(new_payable_data, ceiling_value_wei) - ) - } - } - Either::Right(retry_payable_data) => { - if !retry_payable_data - .addresses_and_gas_price_value_above_limit_wei - .is_empty() - { - warning!( - logger, - "{}", - Self::retry_payable_warning_msg(retry_payable_data, ceiling_value_wei) - ) - } - } - } - } - - fn new_payables_warning_msg( - new_payable_warning_data: NewPayableWarningData, - ceiling_value_wei: u128, - ) -> String { - let accounts = comma_joined_stringifiable(&new_payable_warning_data.addresses, |address| { - format!("{:?}", address) - }); - format!( - "Calculated gas price {} wei for txs to {} is over the spend limit {} wei.", - new_payable_warning_data - .gas_price_above_limit_wei - .separate_with_commas(), - accounts, - ceiling_value_wei.separate_with_commas() - ) - } - - fn retry_payable_warning_msg( - retry_payable_warning_data: RetryPayableWarningData, - ceiling_value_wei: u128, - ) -> String { - let accounts = retry_payable_warning_data - .addresses_and_gas_price_value_above_limit_wei - .into_iter() - .map(|(address, calculated_price_wei)| { - format!( - "{} wei for tx to {:?}", - calculated_price_wei.separate_with_commas(), - address - ) - }) - .join(", "); - format!( - "Calculated gas price {} surplussed the spend limit {} wei.", - accounts, - ceiling_value_wei.separate_with_commas() - ) - } -} - -#[derive(Default)] -struct NewPayableWarningData { - addresses: Vec
, - gas_price_above_limit_wei: u128, -} - -#[derive(Default)] -struct RetryPayableWarningData { - addresses_and_gas_price_value_above_limit_wei: Vec<(Address, u128)>, -} - // 64 * (64 - 12) ... std transaction has data of 64 bytes and 12 bytes are never used with us; // each non-zero byte costs 64 units of gas pub const WEB3_MAXIMAL_GAS_LIMIT_MARGIN: u128 = 3328; @@ -221,51 +95,32 @@ impl BlockchainAgentWeb3 { chain, } } - - fn set_up_warning_data_collector_opt( - &self, - qualified_payables: &UnpricedQualifiedPayables, - ) -> Option { - self.logger.warning_enabled().then(|| { - let is_retry = Self::is_retry(qualified_payables); - GasPriceAboveLimitWarningReporter { - data: if !is_retry { - Either::Left(NewPayableWarningData::default()) - } else { - Either::Right(RetryPayableWarningData::default()) - }, - } - }) - } - - fn is_retry(qualified_payables: &UnpricedQualifiedPayables) -> bool { - qualified_payables - .payables - .first() - .expectv("payable") - .previous_attempt_gas_price_minor_opt - .is_some() - } } #[cfg(test)] mod tests { - use crate::accountant::scanners::payable_scanner_extension::msgs::{ - PricedQualifiedPayables, QualifiedPayableWithGasPrice, - QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, + use crate::accountant::join_with_separator; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::{ + RetryTxTemplate, RetryTxTemplates, }; - use crate::accountant::scanners::test_utils::make_zeroed_consuming_wallet_balances; - use crate::accountant::test_utils::{ - make_payable_account, make_unpriced_qualified_payables_for_retry_mode, + use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ + PricedNewTxTemplate, PricedNewTxTemplates, + }; + use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::{ + PricedRetryTxTemplate, PricedRetryTxTemplates, }; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::RetryTxTemplateBuilder; + use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; + use crate::accountant::scanners::test_utils::make_zeroed_consuming_wallet_balances; + use crate::accountant::test_utils::make_payable_account; use crate::blockchain::blockchain_agent::agent_web3::{ - BlockchainAgentWeb3, GasPriceAboveLimitWarningReporter, NewPayableWarningData, - RetryPayableWarningData, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, + BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, }; use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::test_utils::make_wallet; - use itertools::Itertools; + use itertools::{Either, Itertools}; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; @@ -286,10 +141,7 @@ mod tests { let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); - let address_1 = account_1.wallet.address(); - let address_2 = account_2.wallet.address(); - let unpriced_qualified_payables = - UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let new_tx_templates = NewTxTemplates::from(&vec![account_1.clone(), account_2.clone()]); let rpc_gas_price_wei = 555_666_777; let chain = TEST_DEFAULT_CHAIN; let mut subject = BlockchainAgentWeb3::new( @@ -301,28 +153,15 @@ mod tests { ); subject.logger = Logger::new(test_name); - let priced_qualified_payables = - subject.price_qualified_payables(unpriced_qualified_payables); + let result = subject.price_qualified_payables(Either::Left(new_tx_templates.clone())); let gas_price_with_margin_wei = increase_gas_price_by_margin(rpc_gas_price_wei); - let expected_result = PricedQualifiedPayables { - payables: vec![ - QualifiedPayableWithGasPrice::new(account_1, gas_price_with_margin_wei), - QualifiedPayableWithGasPrice::new(account_2, gas_price_with_margin_wei), - ], - }; - assert_eq!(priced_qualified_payables, expected_result); - let msg_that_should_not_occur = { - let mut new_payable_data = NewPayableWarningData::default(); - new_payable_data.addresses = vec![address_1, address_2]; - - GasPriceAboveLimitWarningReporter::new_payables_warning_msg( - new_payable_data, - chain.rec().gas_price_safe_ceiling_minor, - ) - }; - TestLogHandler::new() - .exists_no_log_containing(&format!("WARN: {test_name}: {msg_that_should_not_occur}")); + let expected_result = Either::Left(PricedNewTxTemplates::new( + new_tx_templates, + gas_price_with_margin_wei, + )); + assert_eq!(result, expected_result); + TestLogHandler::new().exists_no_log_containing(test_name); } #[test] @@ -333,8 +172,8 @@ mod tests { let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); let rpc_gas_price_wei = 444_555_666; let chain = TEST_DEFAULT_CHAIN; - let unpriced_qualified_payables = { - let payables = vec![ + let retry_tx_templates: Vec = { + vec![ rpc_gas_price_wei - 1, rpc_gas_price_wei, rpc_gas_price_wei + 1, @@ -343,21 +182,16 @@ mod tests { ] .into_iter() .enumerate() - .map(|(idx, previous_attempt_gas_price_wei)| { + .map(|(idx, prev_gas_price_wei)| { let account = make_payable_account((idx as u64 + 1) * 3_000); - QualifiedPayablesBeforeGasPriceSelection::new( - account, - Some(previous_attempt_gas_price_wei), - ) + RetryTxTemplate { + base: BaseTxTemplate::from(&account), + prev_gas_price_wei, + prev_nonce: idx as u64, + } }) - .collect_vec(); - UnpricedQualifiedPayables { payables } + .collect_vec() }; - let accounts_from_1_to_5 = unpriced_qualified_payables - .payables - .iter() - .map(|unpriced_payable| unpriced_payable.payable.clone()) - .collect_vec(); let mut subject = BlockchainAgentWeb3::new( rpc_gas_price_wei, 77_777, @@ -367,8 +201,8 @@ mod tests { ); subject.logger = Logger::new(test_name); - let priced_qualified_payables = - subject.price_qualified_payables(unpriced_qualified_payables); + let result = subject + .price_qualified_payables(Either::Right(RetryTxTemplates(retry_tx_templates.clone()))); let expected_result = { let price_wei_for_accounts_from_1_to_5 = vec![ @@ -378,39 +212,22 @@ mod tests { increase_gas_price_by_margin(rpc_gas_price_wei), increase_gas_price_by_margin(rpc_gas_price_wei + 456_789), ]; - if price_wei_for_accounts_from_1_to_5.len() != accounts_from_1_to_5.len() { + if price_wei_for_accounts_from_1_to_5.len() != retry_tx_templates.len() { panic!("Corrupted test") } - PricedQualifiedPayables { - payables: accounts_from_1_to_5 - .into_iter() + + Either::Right(PricedRetryTxTemplates( + retry_tx_templates + .iter() .zip(price_wei_for_accounts_from_1_to_5.into_iter()) - .map(|(account, previous_attempt_price_wei)| { - QualifiedPayableWithGasPrice::new(account, previous_attempt_price_wei) + .map(|(retry_tx_template, increased_gas_price)| { + PricedRetryTxTemplate::new(retry_tx_template.clone(), increased_gas_price) }) .collect_vec(), - } + )) }; - assert_eq!(priced_qualified_payables, expected_result); - let msg_that_should_not_occur = { - let mut retry_payable_data = RetryPayableWarningData::default(); - retry_payable_data.addresses_and_gas_price_value_above_limit_wei = expected_result - .payables - .into_iter() - .map(|payable_with_gas_price| { - ( - payable_with_gas_price.payable.wallet.address(), - payable_with_gas_price.gas_price_minor, - ) - }) - .collect(); - GasPriceAboveLimitWarningReporter::retry_payable_warning_msg( - retry_payable_data, - chain.rec().gas_price_safe_ceiling_minor, - ) - }; - TestLogHandler::new() - .exists_no_log_containing(&format!("WARN: {test_name}: {}", msg_that_should_not_occur)); + assert_eq!(result, expected_result); + TestLogHandler::new().exists_no_log_containing(test_name); } #[test] @@ -480,8 +297,7 @@ mod tests { let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); - let qualified_payables = - UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let tx_templates = NewTxTemplates::from(&vec![account_1.clone(), account_2.clone()]); let mut subject = BlockchainAgentWeb3::new( rpc_gas_price_wei, 77_777, @@ -491,25 +307,25 @@ mod tests { ); subject.logger = Logger::new(test_name); - let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + let result = subject.price_qualified_payables(Either::Left(tx_templates.clone())); - let expected_result = PricedQualifiedPayables { - payables: vec![ - QualifiedPayableWithGasPrice::new(account_1.clone(), ceiling_gas_price_wei), - QualifiedPayableWithGasPrice::new(account_2.clone(), ceiling_gas_price_wei), - ], - }; - assert_eq!(priced_qualified_payables, expected_result); + let expected_result = Either::Left(PricedNewTxTemplates::new( + tx_templates, + ceiling_gas_price_wei, + )); + assert_eq!(result, expected_result); + let addresses_str = join_with_separator( + &vec![account_1.wallet, account_2.wallet], + |wallet| format!("{}", wallet), + "\n", + ); TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Calculated gas price {} wei for txs to {}, {} is over the spend \ - limit {} wei.", + "WARN: {test_name}: The computed gas price {} wei is above the ceil value of {} wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + {}", expected_calculated_surplus_value_wei.separate_with_commas(), - account_1.wallet, - account_2.wallet, - chain - .rec() - .gas_price_safe_ceiling_minor - .separate_with_commas() + ceiling_gas_price_wei.separate_with_commas(), + addresses_str )); } @@ -526,20 +342,28 @@ mod tests { let rpc_gas_price_wei = (ceiling_gas_price_wei * 100) / (DEFAULT_GAS_PRICE_MARGIN as u128 + 100) + 2; let check_value_wei = increase_gas_price_by_margin(rpc_gas_price_wei); - let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ - (account_1.clone(), rpc_gas_price_wei - 1), - (account_2.clone(), rpc_gas_price_wei - 2), - ]); - let expected_surpluses_wallet_and_wei_as_text = "\ - 50,000,000,001 wei for tx to 0x00000000000000000000000077616c6c65743132, 50,000,000,001 \ - wei for tx to 0x00000000000000000000000077616c6c65743334"; + let template_1 = RetryTxTemplateBuilder::new() + .payable_account(&account_1) + .prev_gas_price_wei(rpc_gas_price_wei - 1) + .build(); + let template_2 = RetryTxTemplateBuilder::new() + .payable_account(&account_2) + .prev_gas_price_wei(rpc_gas_price_wei - 2) + .build(); + let retry_tx_templates = vec![template_1, template_2]; + let expected_log_msg = format!( + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + 0x00000000000000000000000077616c6c65743132 with gas price 50,000,000,001\n\ + 0x00000000000000000000000077616c6c65743334 with gas price 50,000,000,001" + ); test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( test_name, chain, rpc_gas_price_wei, - unpriced_qualified_payables, - expected_surpluses_wallet_and_wei_as_text, + Either::Right(RetryTxTemplates(retry_tx_templates)), + &expected_log_msg, ); assert!( @@ -563,20 +387,28 @@ mod tests { (ceiling_gas_price_wei * 100) / (DEFAULT_GAS_PRICE_MARGIN as u128 + 100) + 2; let rpc_gas_price_wei = border_gas_price_wei - 1; let check_value_wei = increase_gas_price_by_margin(border_gas_price_wei); - let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ - (account_1.clone(), border_gas_price_wei), - (account_2.clone(), border_gas_price_wei), - ]); - let expected_surpluses_wallet_and_wei_as_text = "50,000,000,001 wei for tx to \ - 0x00000000000000000000000077616c6c65743132, 50,000,000,001 wei for tx to \ - 0x00000000000000000000000077616c6c65743334"; + let template_1 = RetryTxTemplateBuilder::new() + .payable_account(&account_1) + .prev_gas_price_wei(border_gas_price_wei) + .build(); + let template_2 = RetryTxTemplateBuilder::new() + .payable_account(&account_2) + .prev_gas_price_wei(border_gas_price_wei) + .build(); + let retry_tx_templates = vec![template_1, template_2]; + let expected_log_msg = format!( + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + 0x00000000000000000000000077616c6c65743132 with gas price 50,000,000,001\n\ + 0x00000000000000000000000077616c6c65743334 with gas price 50,000,000,001" + ); test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( test_name, chain, rpc_gas_price_wei, - unpriced_qualified_payables, - expected_surpluses_wallet_and_wei_as_text, + Either::Right(RetryTxTemplates(retry_tx_templates)), + &expected_log_msg, ); assert!(check_value_wei > ceiling_gas_price_wei); } @@ -590,20 +422,28 @@ mod tests { let fetched_gas_price_wei = ceiling_gas_price_wei - 1; let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); - let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ - (account_1.clone(), fetched_gas_price_wei - 2), - (account_2.clone(), fetched_gas_price_wei - 3), - ]); - let expected_surpluses_wallet_and_wei_as_text = "64,999,999,998 wei for tx to \ - 0x00000000000000000000000077616c6c65743132, 64,999,999,998 wei for tx to \ - 0x00000000000000000000000077616c6c65743334"; + let template_1 = RetryTxTemplateBuilder::new() + .payable_account(&account_1) + .prev_gas_price_wei(fetched_gas_price_wei - 2) + .build(); + let template_2 = RetryTxTemplateBuilder::new() + .payable_account(&account_2) + .prev_gas_price_wei(fetched_gas_price_wei - 3) + .build(); + let retry_tx_templates = vec![template_1, template_2]; + let expected_log_msg = format!( + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + 0x00000000000000000000000077616c6c65743132 with gas price 64,999,999,998\n\ + 0x00000000000000000000000077616c6c65743334 with gas price 64,999,999,998" + ); test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( test_name, chain, fetched_gas_price_wei, - unpriced_qualified_payables, - expected_surpluses_wallet_and_wei_as_text, + Either::Right(RetryTxTemplates(retry_tx_templates)), + &expected_log_msg, ); } @@ -614,20 +454,28 @@ mod tests { let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); - let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ - (account_1.clone(), ceiling_gas_price_wei - 1), - (account_2.clone(), ceiling_gas_price_wei - 2), - ]); - let expected_surpluses_wallet_and_wei_as_text = "64,999,999,998 wei for tx to \ - 0x00000000000000000000000077616c6c65743132, 64,999,999,997 wei for tx to \ - 0x00000000000000000000000077616c6c65743334"; + let template_1 = RetryTxTemplateBuilder::new() + .payable_account(&account_1) + .prev_gas_price_wei(ceiling_gas_price_wei - 1) + .build(); + let template_2 = RetryTxTemplateBuilder::new() + .payable_account(&account_2) + .prev_gas_price_wei(ceiling_gas_price_wei - 2) + .build(); + let retry_tx_templates = vec![template_1, template_2]; + let expected_log_msg = format!( + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + 0x00000000000000000000000077616c6c65743132 with gas price 64,999,999,998\n\ + 0x00000000000000000000000077616c6c65743334 with gas price 64,999,999,997" + ); test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( test_name, chain, ceiling_gas_price_wei - 3, - unpriced_qualified_payables, - expected_surpluses_wallet_and_wei_as_text, + Either::Right(RetryTxTemplates(retry_tx_templates)), + &expected_log_msg, ); } @@ -641,20 +489,28 @@ mod tests { let account_2 = make_payable_account(34); // The values can never go above the ceiling, therefore, we can assume only values even or // smaller than that in the previous attempts - let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ - (account_1.clone(), ceiling_gas_price_wei), - (account_2.clone(), ceiling_gas_price_wei), - ]); - let expected_surpluses_wallet_and_wei_as_text = - "650,000,000,000 wei for tx to 0x00000000000000000000\ - 000077616c6c65743132, 650,000,000,000 wei for tx to 0x00000000000000000000000077616c6c65743334"; + let template_1 = RetryTxTemplateBuilder::new() + .payable_account(&account_1) + .prev_gas_price_wei(ceiling_gas_price_wei) + .build(); + let template_2 = RetryTxTemplateBuilder::new() + .payable_account(&account_2) + .prev_gas_price_wei(ceiling_gas_price_wei) + .build(); + let retry_tx_templates = vec![template_1, template_2]; + let expected_log_msg = format!( + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ + Transaction(s) to following receivers are affected:\n\ + 0x00000000000000000000000077616c6c65743132 with gas price 650,000,000,000\n\ + 0x00000000000000000000000077616c6c65743334 with gas price 650,000,000,000" + ); test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( test_name, chain, fetched_gas_price_wei, - unpriced_qualified_payables, - expected_surpluses_wallet_and_wei_as_text, + Either::Right(RetryTxTemplates(retry_tx_templates)), + &expected_log_msg, ); } @@ -662,22 +518,30 @@ mod tests { test_name: &str, chain: Chain, rpc_gas_price_wei: u128, - qualified_payables: UnpricedQualifiedPayables, - expected_surpluses_wallet_and_wei_as_text: &str, + tx_templates: Either, + expected_log_msg: &str, ) { init_test_logging(); let consuming_wallet = make_wallet("efg"); let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; - let expected_priced_payables = PricedQualifiedPayables { - payables: qualified_payables - .payables - .clone() - .into_iter() - .map(|payable| { - QualifiedPayableWithGasPrice::new(payable.payable, ceiling_gas_price_wei) - }) - .collect(), + let expected_result = match &tx_templates { + Either::Left(new_tx_templates) => Either::Left(PricedNewTxTemplates( + new_tx_templates + .iter() + .map(|tx_template| { + PricedNewTxTemplate::new(tx_template.clone(), ceiling_gas_price_wei) + }) + .collect(), + )), + Either::Right(retry_tx_templates) => Either::Right(PricedRetryTxTemplates( + retry_tx_templates + .iter() + .map(|tx_template| { + PricedRetryTxTemplate::new(tx_template.clone(), ceiling_gas_price_wei) + }) + .collect(), + )), }; let mut subject = BlockchainAgentWeb3::new( rpc_gas_price_wei, @@ -688,14 +552,11 @@ mod tests { ); subject.logger = Logger::new(test_name); - let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + let result = subject.price_qualified_payables(tx_templates); - assert_eq!(priced_qualified_payables, expected_priced_payables); - TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Calculated gas price {expected_surpluses_wallet_and_wei_as_text} \ - surplussed the spend limit {} wei.", - ceiling_gas_price_wei.separate_with_commas() - )); + assert_eq!(result, expected_result); + TestLogHandler::new() + .exists_log_containing(&format!("WARN: {test_name}: {expected_log_msg}")); } #[test] @@ -727,7 +588,7 @@ mod tests { let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); let chain = TEST_DEFAULT_CHAIN; - let qualified_payables = UnpricedQualifiedPayables::from(vec![account_1, account_2]); + let tx_templates = NewTxTemplates::from(&vec![account_1, account_2]); let subject = BlockchainAgentWeb3::new( 444_555_666, 77_777, @@ -735,9 +596,9 @@ mod tests { consuming_wallet_balances, chain, ); - let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + let new_tx_templates = subject.price_qualified_payables(Either::Left(tx_templates)); - let result = subject.estimate_transaction_fee_total(&priced_qualified_payables); + let result = subject.estimate_transaction_fee_total(&new_tx_templates); assert_eq!( result, @@ -747,13 +608,13 @@ mod tests { } #[test] - fn estimate_transaction_fee_total_works_for_retry_payable() { + fn estimate_transaction_fee_total_works_for_retry_txs() { let consuming_wallet = make_wallet("efg"); let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); let rpc_gas_price_wei = 444_555_666; let chain = TEST_DEFAULT_CHAIN; - let unpriced_qualified_payables = { - let payables = vec![ + let retry_tx_templates: Vec = { + vec![ rpc_gas_price_wei - 1, rpc_gas_price_wei, rpc_gas_price_wei + 1, @@ -762,15 +623,15 @@ mod tests { ] .into_iter() .enumerate() - .map(|(idx, previous_attempt_gas_price_wei)| { + .map(|(idx, prev_gas_price_wei)| { let account = make_payable_account((idx as u64 + 1) * 3_000); - QualifiedPayablesBeforeGasPriceSelection::new( - account, - Some(previous_attempt_gas_price_wei), - ) + RetryTxTemplate { + base: BaseTxTemplate::from(&account), + prev_gas_price_wei, + prev_nonce: idx as u64, + } }) - .collect_vec(); - UnpricedQualifiedPayables { payables } + .collect() }; let subject = BlockchainAgentWeb3::new( rpc_gas_price_wei, @@ -780,7 +641,7 @@ mod tests { chain, ); let priced_qualified_payables = - subject.price_qualified_payables(unpriced_qualified_payables); + subject.price_qualified_payables(Either::Right(RetryTxTemplates(retry_tx_templates))); let result = subject.estimate_transaction_fee_total(&priced_qualified_payables); diff --git a/node/src/blockchain/blockchain_agent/mod.rs b/node/src/blockchain/blockchain_agent/mod.rs index fb8030a09..2775bddd6 100644 --- a/node/src/blockchain/blockchain_agent/mod.rs +++ b/node/src/blockchain/blockchain_agent/mod.rs @@ -1,13 +1,16 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod agent_web3; +pub mod test_utils; -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - PricedQualifiedPayables, UnpricedQualifiedPayables, -}; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; use crate::arbitrary_id_stamp_in_trait; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; +use itertools::Either; use masq_lib::blockchains::chains::Chain; // Table of chains by // @@ -28,9 +31,12 @@ use masq_lib::blockchains::chains::Chain; pub trait BlockchainAgent: Send { fn price_qualified_payables( &self, - qualified_payables: UnpricedQualifiedPayables, - ) -> PricedQualifiedPayables; - fn estimate_transaction_fee_total(&self, qualified_payables: &PricedQualifiedPayables) -> u128; + unpriced_tx_templates: Either, + ) -> Either; + fn estimate_transaction_fee_total( + &self, + priced_tx_templates: &Either, + ) -> u128; fn consuming_wallet_balances(&self) -> ConsumingWalletBalances; fn consuming_wallet(&self) -> &Wallet; fn get_chain(&self) -> Chain; diff --git a/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs b/node/src/blockchain/blockchain_agent/test_utils.rs similarity index 80% rename from node/src/accountant/scanners/payable_scanner_extension/test_utils.rs rename to node/src/blockchain/blockchain_agent/test_utils.rs index b8e83b78d..76e4ff17a 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs +++ b/node/src/blockchain/blockchain_agent/test_utils.rs @@ -1,15 +1,15 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - #![cfg(test)] -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - PricedQualifiedPayables, UnpricedQualifiedPayables, -}; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::{arbitrary_id_stamp_in_trait_impl, set_arbitrary_id_stamp_in_mock_impl}; +use itertools::Either; use masq_lib::blockchains::chains::Chain; use std::cell::RefCell; @@ -36,14 +36,14 @@ impl Default for BlockchainAgentMock { impl BlockchainAgent for BlockchainAgentMock { fn price_qualified_payables( &self, - _qualified_payables: UnpricedQualifiedPayables, - ) -> PricedQualifiedPayables { + _tx_templates: Either, + ) -> Either { unimplemented!("not needed yet") } fn estimate_transaction_fee_total( &self, - _qualified_payables: &PricedQualifiedPayables, + _priced_tx_templates: &Either, ) -> u128 { todo!("to be implemented by GH-711") } diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 3458a4140..119acaee9 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,20 +1,23 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::sent_payable_dao::SentTx; -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayablesMessage, +use crate::accountant::scanners::payable_scanner::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, }; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; +use crate::accountant::scanners::payable_scanner::utils::initial_templates_msg_stats; use crate::accountant::{ - ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, TxReceiptResult, + ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, }; -use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; +use crate::accountant::{RequestTransactionReceipts, TxReceiptResult, TxReceiptsMessage}; use crate::actor_system_factory::SubsFactory; use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{ - BlockchainInterfaceError, PayableTransactionError, + BlockchainInterfaceError, LocalPayableError, }; use crate::blockchain::blockchain_interface::data_structures::{ - ProcessedPayableFallible, StatusReadFromReceiptCheck, + BatchResults, StatusReadFromReceiptCheck, }; use crate::blockchain::blockchain_interface::BlockchainInterface; use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; @@ -34,7 +37,7 @@ use actix::Handler; use actix::Message; use actix::{Addr, Recipient}; use futures::Future; -use itertools::Itertools; +use itertools::{Either, Itertools}; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; @@ -54,7 +57,7 @@ pub struct BlockchainBridge { logger: Logger, persistent_config_arc: Arc>, sent_payable_subs_opt: Option>, - payable_payments_setup_subs_opt: Option>, + payable_payments_setup_subs_opt: Option>, received_payments_subs_opt: Option>, scan_error_subs_opt: Option>, crashable: bool, @@ -141,15 +144,17 @@ impl Handler for BlockchainBridge { } } -impl Handler for BlockchainBridge { +pub trait MsgInterpretableAsDetailedScanType { + fn detailed_scan_type(&self) -> DetailedScanType; +} + +impl Handler for BlockchainBridge { type Result = (); - fn handle(&mut self, msg: QualifiedPayablesMessage, _ctx: &mut Self::Context) { + fn handle(&mut self, msg: InitialTemplatesMessage, _ctx: &mut Self::Context) { self.handle_scan_future( - Self::handle_qualified_payable_msg, - todo!( - "This needs to be decided on GH-605. Look what mode you run and set it accordingly" - ), + Self::handle_initial_templates_msg, + msg.detailed_scan_type(), msg, ); } @@ -161,9 +166,7 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: OutboundPaymentsInstructions, _ctx: &mut Self::Context) { self.handle_scan_future( Self::handle_outbound_payments_instructions, - todo!( - "This needs to be decided on GH-605. Look what mode you run and set it accordingly" - ), + msg.detailed_scan_type(), msg, ) } @@ -245,17 +248,22 @@ impl BlockchainBridge { BlockchainBridgeSubs { bind: recipient!(addr, BindMessage), outbound_payments_instructions: recipient!(addr, OutboundPaymentsInstructions), - qualified_payables: recipient!(addr, QualifiedPayablesMessage), + qualified_payables: recipient!(addr, InitialTemplatesMessage), retrieve_transactions: recipient!(addr, RetrieveTransactions), ui_sub: recipient!(addr, NodeFromUiMessage), request_transaction_receipts: recipient!(addr, RequestTransactionReceipts), } } - fn handle_qualified_payable_msg( + fn handle_initial_templates_msg( &mut self, - incoming_message: QualifiedPayablesMessage, + incoming_message: InitialTemplatesMessage, ) -> Box> { + debug!( + &self.logger, + "{}", + initial_templates_msg_stats(&incoming_message) + ); // TODO rewrite this into a batch call as soon as GH-629 gets into master let accountant_recipient = self.payable_payments_setup_subs_opt.clone(); Box::new( @@ -263,13 +271,13 @@ impl BlockchainBridge { .introduce_blockchain_agent(incoming_message.consuming_wallet) .map_err(|e| format!("Blockchain agent build error: {:?}", e)) .and_then(move |agent| { - let priced_qualified_payables = - agent.price_qualified_payables(incoming_message.qualified_payables); - let outgoing_message = BlockchainAgentWithContextMessage::new( - priced_qualified_payables, + let priced_templates = + agent.price_qualified_payables(incoming_message.initial_templates); + let outgoing_message = PricedTemplatesMessage { + priced_templates, agent, - incoming_message.response_skeleton_opt, - ); + response_skeleton_opt: incoming_message.response_skeleton_opt, + }; accountant_recipient .expect("Accountant is unbound") .try_send(outgoing_message) @@ -279,36 +287,53 @@ impl BlockchainBridge { ) } + fn payment_procedure_result_from_error(e: LocalPayableError) -> Result { + match e { + LocalPayableError::Sending { failed_txs, .. } => Ok(BatchResults { + sent_txs: vec![], + failed_txs, + }), + _ => Err(e.to_string()), + } + } + fn handle_outbound_payments_instructions( &mut self, msg: OutboundPaymentsInstructions, ) -> Box> { let skeleton_opt = msg.response_skeleton_opt; - let sent_payable_subs = self + let sent_payable_subs_success = self .sent_payable_subs_opt .as_ref() .expect("Accountant is unbound") .clone(); - - let send_message_if_failure = move |msg: SentPayables| { - sent_payable_subs.try_send(msg).expect("Accountant is dead"); - }; - let send_message_if_successful = send_message_if_failure.clone(); + let sent_payable_subs_err = sent_payable_subs_success.clone(); + let payable_scan_type = msg.scan_type(); Box::new( - self.process_payments(msg.agent, msg.affordable_accounts) - .map_err(move |e: PayableTransactionError| { - send_message_if_failure(SentPayables { - payment_procedure_result: Err(e.clone()), - response_skeleton_opt: skeleton_opt, - }); + self.process_payments(msg.agent, msg.priced_templates) + .map_err(move |e: LocalPayableError| { + sent_payable_subs_err + .try_send(SentPayables { + payment_procedure_result: Self::payment_procedure_result_from_error( + e.clone(), + ), + payable_scan_type, + response_skeleton_opt: skeleton_opt, + }) + .expect("Accountant is dead"); + format!("ReportAccountsPayable: {}", e) }) - .and_then(move |payment_result| { - send_message_if_successful(SentPayables { - payment_procedure_result: Ok(payment_result), - response_skeleton_opt: skeleton_opt, - }); + .and_then(move |batch_results| { + sent_payable_subs_success + .try_send(SentPayables { + payment_procedure_result: Ok(batch_results), + payable_scan_type, + response_skeleton_opt: skeleton_opt, + }) + .expect("Accountant is dead"); + Ok(()) }), ) @@ -475,24 +500,11 @@ impl BlockchainBridge { fn process_payments( &self, agent: Box, - affordable_accounts: PricedQualifiedPayables, - ) -> Box, Error = PayableTransactionError>> - { - let recipient = self.new_pending_payables_recipient(); + priced_templates: Either, + ) -> Box> { let logger = self.logger.clone(); - self.blockchain_interface.submit_payables_in_batch( - logger, - agent, - recipient, - affordable_accounts, - ) - } - - fn new_pending_payables_recipient(&self) -> Recipient { - self.pending_payable_confirmation - .register_new_pending_payables_sub_opt - .clone() - .expect("Accountant unbound") + self.blockchain_interface + .submit_payables_in_batch(logger, agent, priced_templates) } pub fn extract_max_block_count(error: BlockchainInterfaceError) -> Option { @@ -541,27 +553,31 @@ impl SubsFactory for BlockchainBridgeSub #[cfg(test)] mod tests { use super::*; + use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; + use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::Submission; + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; use crate::accountant::db_access_objects::payable_dao::PayableAccount; - 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::db_access_objects::sent_payable_dao::TxStatus::Pending; + use crate::accountant::db_access_objects::test_utils::{ + assert_on_failed_txs, assert_on_sent_txs, }; - use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplate; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::make_priced_new_tx_templates; 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_agent::test_utils::BlockchainAgentMock; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainAgentBuildError; + use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError::TransactionID; use crate::blockchain::blockchain_interface::data_structures::{ BlockchainTransaction, RetrievedBlockchainTransactions, TxBlock, }; - use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalErrorKind, RemoteError, + }; use crate::blockchain::errors::validation_status::ValidationStatus; + use crate::blockchain::errors::validation_status::ValidationStatus::Waiting; use crate::blockchain::test_utils::{ make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; @@ -689,9 +705,10 @@ mod tests { } #[test] - fn handles_qualified_payables_msg_in_new_payables_mode_and_sends_response_back_to_accountant() { - let system = System::new( - "handles_qualified_payables_msg_in_new_payables_mode_and_sends_response_back_to_accountant"); + fn handles_initial_templates_msg_in_new_payables_mode_and_sends_response_back_to_accountant() { + init_test_logging(); + let test_name = "handles_initial_templates_msg_in_new_payables_mode_and_sends_response_back_to_accountant"; + let system = System::new(test_name); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) // Fetching a recommended gas price @@ -732,11 +749,11 @@ mod tests { Arc::new(Mutex::new(persistent_configuration)), false, ); + subject.logger = Logger::new(test_name); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let unpriced_qualified_payables = - UnpricedQualifiedPayables::from(qualified_payables.clone()); - let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: unpriced_qualified_payables.clone(), + let tx_templates = NewTxTemplates::from(&qualified_payables); + let qualified_payables_msg = InitialTemplatesMessage { + initial_templates: Either::Left(tx_templates.clone()), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -745,27 +762,27 @@ mod tests { }; subject - .handle_qualified_payable_msg(qualified_payables_msg) + .handle_initial_templates_msg(qualified_payables_msg) .wait() .unwrap(); System::current().stop(); system.run(); let accountant_received_payment = accountant_recording_arc.lock().unwrap(); - let blockchain_agent_with_context_msg_actual: &BlockchainAgentWithContextMessage = + let blockchain_agent_with_context_msg_actual: &PricedTemplatesMessage = accountant_received_payment.get_record(0); - let expected_priced_qualified_payables = PricedQualifiedPayables { - payables: qualified_payables - .into_iter() - .map(|payable| QualifiedPayableWithGasPrice { - payable, - gas_price_minor: increase_gas_price_by_margin(0x230000000), - }) - .collect(), - }; + let computed_gas_price_wei = increase_gas_price_by_margin(0x230000000); + let expected_tx_templates = tx_templates + .iter() + .map(|tx_template| PricedNewTxTemplate { + base: tx_template.base, + computed_gas_price_wei, + }) + .collect::(); + assert_eq!( - blockchain_agent_with_context_msg_actual.qualified_payables, - expected_priced_qualified_payables + blockchain_agent_with_context_msg_actual.priced_templates, + Either::Left(expected_tx_templates) ); let actual_agent = blockchain_agent_with_context_msg_actual.agent.as_ref(); assert_eq!(actual_agent.consuming_wallet(), &consuming_wallet); @@ -775,7 +792,7 @@ mod tests { ); assert_eq!( actual_agent.estimate_transaction_fee_total( - &actual_agent.price_qualified_payables(unpriced_qualified_payables) + &actual_agent.price_qualified_payables(Either::Left(tx_templates)) ), 1_791_228_995_698_688 ); @@ -787,6 +804,8 @@ mod tests { }) ); assert_eq!(accountant_received_payment.len(), 1); + TestLogHandler::new() + .exists_log_containing(&format!("DEBUG: {test_name}: Found 2 new txs to process")); } #[test] @@ -810,9 +829,9 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = UnpricedQualifiedPayables::from(vec![make_payable_account(123)]); - let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables, + let new_tx_templates = NewTxTemplates::from(&vec![make_payable_account(123)]); + let qualified_payables_msg = InitialTemplatesMessage { + initial_templates: Either::Left(new_tx_templates), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -821,7 +840,7 @@ mod tests { }; let error_msg = subject - .handle_qualified_payable_msg(qualified_payables_msg) + .handle_initial_templates_msg(qualified_payables_msg) .wait() .unwrap_err(); @@ -865,6 +884,10 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let blockchain_interface = make_blockchain_interface_web3(port); let persistent_configuration_mock = PersistentConfigurationMock::default(); + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }; let subject = BlockchainBridge::new( Box::new(blockchain_interface), Arc::new(Mutex::new(persistent_configuration_mock)), @@ -890,63 +913,55 @@ mod tests { let _ = addr .try_send(OutboundPaymentsInstructions { - affordable_accounts: make_priced_qualified_payables(vec![( + priced_templates: Either::Left(make_priced_new_tx_templates(vec![( account.clone(), 111_222_333, - )]), + )])), agent: Box::new(agent), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), + response_skeleton_opt: Some(response_skeleton), }) .unwrap(); - let time_before = SystemTime::now(); system.run(); - let time_after = SystemTime::now(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - 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.clone(), - hash: expected_hash - })]), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321 - }) - } - ); - let first_actual_sent_tx = ®ister_new_pending_payables_msg.new_sent_txs[0]; - assert_eq!( - first_actual_sent_tx.receiver_address, - account.wallet.address() + // TODO: GH-701: This card is related to the commented out code in this test + // let pending_payable_fingerprint_seeds_msg = + // accountant_recording.get_record::(0); + let sent_payables_msg = accountant_recording.get_record::(0); + let batch_results = sent_payables_msg.clone().payment_procedure_result.unwrap(); + assert!(batch_results.failed_txs.is_empty()); + assert_on_sent_txs( + batch_results.sent_txs, + vec![SentTx { + hash: H256::from_str( + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c", + ) + .unwrap(), + receiver_address: account.wallet.address(), + amount_minor: account.balance_wei, + timestamp: to_unix_timestamp(SystemTime::now()), + gas_price_minor: 111_222_333, + nonce: 32, + status: Pending(Waiting), + }], ); - 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); + sent_payables_msg.response_skeleton_opt, + Some(response_skeleton) + ); + // assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp >= time_before); + // assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp <= time_after); + // assert_eq!( + // pending_payable_fingerprint_seeds_msg.hashes_and_balances, + // vec![HashAndAmount { + // hash: H256::from_str( + // "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" + // ) + // .unwrap(), + // amount: account.balance_wei + // }] + // ); + assert_eq!(accountant_recording.len(), 1); } #[test] @@ -987,13 +1002,12 @@ mod tests { .gas_price_result(123) .get_chain_result(Chain::PolyMainnet); send_bind_message!(subject_subs, peer_actors); + let priced_new_tx_templates = + make_priced_new_tx_templates(vec![(account.clone(), 111_222_333)]); let _ = addr .try_send(OutboundPaymentsInstructions { - affordable_accounts: make_priced_qualified_payables(vec![( - account.clone(), - 111_222_333, - )]), + priced_templates: Either::Left(priced_new_tx_templates), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1004,51 +1018,52 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - 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( - sent_payables_msg - .payment_procedure_result - .as_ref() - .unwrap_err(), - "Transport error: Error(IncompleteMessage)", - ); - assert_eq!( - 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 - ); + // let pending_payable_fingerprint_seeds_msg = + // accountant_recording.get_record::(0); + let sent_payables_msg = accountant_recording.get_record::(0); + let scan_error_msg = accountant_recording.get_record::(1); + let batch_results = sent_payables_msg.clone().payment_procedure_result.unwrap(); + let failed_tx = FailedTx { + hash: H256::from_str( + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c", + ) + .unwrap(), + receiver_address: account.wallet.address(), + amount_minor: account.balance_wei, + timestamp: to_unix_timestamp(SystemTime::now()), + gas_price_minor: 111222333, + nonce: 32, + reason: Submission(AppRpcErrorKind::Local(LocalErrorKind::Transport)), + status: RetryRequired, + }; + assert_on_failed_txs(batch_results.failed_txs, vec![failed_tx]); + // TODO: GH-701: This card is related to the commented out code in this test + // assert_eq!( + // pending_payable_fingerprint_seeds_msg.hashes_and_balances, + // vec![HashAndAmount { + // hash: H256::from_str( + // "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" + // ) + // .unwrap(), + // amount: account.balance_wei + // }] + // ); + assert_eq!(scan_error_msg.scan_type, DetailedScanType::NewPayables); assert_eq!( - *scan_error_msg, - ScanError { - scan_type: DetailedScanType::NewPayables, - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321 - }), - msg: format!( - "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". \ - Signed and hashed txs: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - } + scan_error_msg.response_skeleton_opt, + Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321 + }) ); - assert_eq!(accountant_recording.len(), 3); + assert!(scan_error_msg + .msg + .contains("ReportAccountsPayable: Sending error: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions:"), "This string didn't contain the expected: {}", scan_error_msg.msg); + assert!(scan_error_msg.msg.contains( + "FailedTx { hash: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c," + )); + assert!(scan_error_msg.msg.contains("FailedTx { hash: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c, receiver_address: 0x00000000000000000000000000000000626c6168, amount_minor: 111420204, timestamp:"), "This string didn't contain the expected: {}", scan_error_msg.msg); + assert_eq!(accountant_recording.len(), 2); } #[test] @@ -1064,19 +1079,22 @@ mod tests { .start(); let blockchain_interface_web3 = make_blockchain_interface_web3(port); let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let accounts_1 = make_payable_account(1); - let accounts_2 = make_payable_account(2); - let affordable_qualified_payables = make_priced_qualified_payables(vec![ - (accounts_1.clone(), 777_777_777), - (accounts_2.clone(), 999_999_999), + let account_1 = make_payable_account(1); + let account_2 = make_payable_account(2); + let priced_new_tx_templates = make_priced_new_tx_templates(vec![ + (account_1.clone(), 777_777_777), + (account_2.clone(), 999_999_999), ]); let system = System::new(test_name); let agent = BlockchainAgentMock::default() .consuming_wallet_result(consuming_wallet) .gas_price_result(1) .get_chain_result(Chain::PolyMainnet); - let msg = - OutboundPaymentsInstructions::new(affordable_qualified_payables, Box::new(agent), None); + let msg = OutboundPaymentsInstructions::new( + Either::Left(priced_new_tx_templates), + Box::new(agent), + None, + ); let persistent_config = PersistentConfigurationMock::new(); let mut subject = BlockchainBridge::new( Box::new(blockchain_interface_web3), @@ -1089,34 +1107,44 @@ mod tests { .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject - .process_payments(msg.agent, msg.affordable_accounts) + .process_payments(msg.agent, msg.priced_templates) .wait(); System::current().stop(); system.run(); - let processed_payments = result.unwrap(); - assert_eq!( - processed_payments[0], - Correct(PendingPayable { - recipient_wallet: accounts_1.wallet, - hash: H256::from_str( - "c0756e8da662cee896ed979456c77931668b7f8456b9f978fc3305671f8f82ad" - ) - .unwrap() - }) - ); - assert_eq!( - processed_payments[1], - Correct(PendingPayable { - recipient_wallet: accounts_2.wallet, - hash: H256::from_str( - "9ba19f88ce43297d700b1f57ed8bc6274d01a5c366b78dd05167f9874c867ba0" - ) - .unwrap() - }) + let batch_results = result.unwrap(); + assert_on_sent_txs( + batch_results.sent_txs, + vec![ + SentTx { + hash: H256::from_str( + "c0756e8da662cee896ed979456c77931668b7f8456b9f978fc3305671f8f82ad", + ) + .unwrap(), + receiver_address: account_1.wallet.address(), + amount_minor: account_1.balance_wei, + timestamp: to_unix_timestamp(SystemTime::now()), + gas_price_minor: 777_777_777, + nonce: 1, + status: Pending(ValidationStatus::Waiting), + }, + SentTx { + hash: H256::from_str( + "9ba19f88ce43297d700b1f57ed8bc6274d01a5c366b78dd05167f9874c867ba0", + ) + .unwrap(), + receiver_address: account_2.wallet.address(), + amount_minor: account_2.balance_wei, + timestamp: to_unix_timestamp(SystemTime::now()), + gas_price_minor: 999_999_999, + nonce: 2, + status: Pending(ValidationStatus::Waiting), + }, + ], ); + assert!(batch_results.failed_txs.is_empty()); let recording = accountant_recording.lock().unwrap(); - assert_eq!(recording.len(), 1); + assert_eq!(recording.len(), 0); } #[test] @@ -1133,8 +1161,10 @@ mod tests { .get_chain_result(TEST_DEFAULT_CHAIN) .consuming_wallet_result(consuming_wallet) .gas_price_result(123); + let priced_new_tx_templates = + make_priced_new_tx_templates(vec![(make_payable_account(111), 111_000_000)]); let msg = OutboundPaymentsInstructions::new( - make_priced_qualified_payables(vec![(make_payable_account(111), 111_000_000)]), + Either::Left(priced_new_tx_templates), Box::new(agent), None, ); @@ -1150,7 +1180,7 @@ mod tests { .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject - .process_payments(msg.agent, msg.affordable_accounts) + .process_payments(msg.agent, msg.priced_templates) .wait(); System::current().stop(); @@ -1166,19 +1196,6 @@ mod tests { assert_eq!(recording.len(), 0); } - fn assert_sending_error(error: &PayableTransactionError, error_msg: &str) { - if let PayableTransactionError::Sending { msg, .. } = error { - assert!( - msg.contains(error_msg), - "Actual Error message: {} does not contain this fragment {}", - msg, - error_msg - ); - } else { - panic!("Received wrong error: {:?}", error); - } - } - #[test] fn blockchain_bridge_processes_requests_for_a_complete_and_null_transaction_receipt() { let (accountant, _, accountant_recording_arc) = make_recorder(); @@ -1232,7 +1249,7 @@ mod tests { assert_eq!( tx_receipts_message, &TxReceiptsMessage { - results: hashmap![ + results: btreemap![ TxHashByTable::SentPayable(tx_hash_1) => Ok( expected_receipt.into() ), @@ -1369,7 +1386,7 @@ mod tests { assert_eq!( *report_receipts_msg, TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + results: btreemap![TxHashByTable::SentPayable(tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), TxHashByTable::SentPayable(tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { block_hash: Default::default(), block_number, 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 7178d9d90..9249c6ee0 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,9 +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, StatusReadFromReceiptCheck}; +use std::collections::{BTreeMap}; +use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, LocalPayableError}; +use crate::blockchain::blockchain_interface::data_structures::{BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainInterface}; @@ -17,16 +17,19 @@ use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; use std::convert::{From, TryInto}; use std::fmt::Debug; -use actix::Recipient; use ethereum_types::U64; +use itertools::Either; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplates; 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_bridge::{BlockMarker, BlockScanRange}; 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}; @@ -220,7 +223,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { tx_hashes: Vec, ) -> Box< dyn Future< - Item = HashMap, + Item = BTreeMap, Error = BlockchainInterfaceError, >, > { @@ -254,7 +257,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { } Err(e) => (tx_hash, Err(AppRpcError::from(e))), }) - .collect::>()) + .collect::>()) }), ) } @@ -263,10 +266,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &self, logger: Logger, agent: Box, - new_pending_payables_recipient: Recipient, - affordable_accounts: PricedQualifiedPayables, - ) -> Box, Error = PayableTransactionError>> - { + priced_templates: Either, + ) -> Box> { let consuming_wallet = agent.consuming_wallet().clone(); let web3_batch = self.lower_interface().get_web3_batch(); let get_transaction_id = self @@ -276,16 +277,17 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { Box::new( get_transaction_id - .map_err(PayableTransactionError::TransactionID) - .and_then(move |pending_nonce| { + .map_err(LocalPayableError::TransactionID) + .and_then(move |latest_nonce| { + let templates = + SignableTxTemplates::new(priced_templates, latest_nonce.as_u64()); + send_payables_within_batch( &logger, chain, &web3_batch, + templates, consuming_wallet, - pending_nonce, - new_pending_payables_recipient, - affordable_accounts, ) }), ) @@ -298,6 +300,15 @@ pub struct HashAndAmount { pub amount_minor: u128, } +impl From<&SentTx> for HashAndAmount { + fn from(tx: &SentTx) -> Self { + HashAndAmount { + hash: tx.hash, + amount_minor: tx.amount_minor, + } + } +} + impl BlockchainInterfaceWeb3 { pub fn new(transport: Http, event_loop_handle: EventLoopHandle, chain: Chain) -> Self { let gas_limit_const_part = Self::web3_gas_limit_const_part(chain); @@ -456,10 +467,6 @@ impl BlockchainInterfaceWeb3 { #[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; @@ -491,9 +498,13 @@ mod tests { use masq_lib::utils::find_free_port; use std::net::Ipv4Addr; use std::str::FromStr; + use itertools::Either; use web3::transports::Http; use web3::types::{H256, U256}; - use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplate; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::RetryTxTemplateBuilder; #[test] fn constants_are_correct() { @@ -868,87 +879,68 @@ mod tests { fn blockchain_interface_web3_can_introduce_blockchain_agent_in_the_new_payables_mode() { let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); - let unpriced_qualified_payables = - UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let tx_templates = NewTxTemplates::from(&vec![account_1.clone(), account_2.clone()]); let gas_price_wei_from_rpc_hex = "0x3B9ACA00"; // 1000000000 let gas_price_wei_from_rpc_u128_wei = u128::from_str_radix(&gas_price_wei_from_rpc_hex[2..], 16).unwrap(); let gas_price_wei_from_rpc_u128_wei_with_margin = increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei); - let expected_priced_qualified_payables = PricedQualifiedPayables { - payables: vec![ - QualifiedPayableWithGasPrice::new( - account_1, - gas_price_wei_from_rpc_u128_wei_with_margin, - ), - QualifiedPayableWithGasPrice::new( - account_2, - gas_price_wei_from_rpc_u128_wei_with_margin, - ), - ], - }; + let expected_result = Either::Left(PricedNewTxTemplates::new( + tx_templates.clone(), + gas_price_wei_from_rpc_u128_wei_with_margin, + )); let expected_estimated_transaction_fee_total = 190_652_800_000_000; test_blockchain_interface_web3_can_introduce_blockchain_agent( - unpriced_qualified_payables, + Either::Left(tx_templates), gas_price_wei_from_rpc_hex, - expected_priced_qualified_payables, + expected_result, expected_estimated_transaction_fee_total, ); } #[test] fn blockchain_interface_web3_can_introduce_blockchain_agent_in_the_retry_payables_mode() { - let gas_price_wei_from_rpc_hex = "0x3B9ACA00"; // 1000000000 - let gas_price_wei_from_rpc_u128_wei = - u128::from_str_radix(&gas_price_wei_from_rpc_hex[2..], 16).unwrap(); - let account_1 = make_payable_account(12); - let account_2 = make_payable_account(34); - let account_3 = make_payable_account(56); - let unpriced_qualified_payables = UnpricedQualifiedPayables { - payables: vec![ - QualifiedPayablesBeforeGasPriceSelection::new( - account_1.clone(), - Some(gas_price_wei_from_rpc_u128_wei - 1), - ), - QualifiedPayablesBeforeGasPriceSelection::new( - account_2.clone(), - Some(gas_price_wei_from_rpc_u128_wei), - ), - QualifiedPayablesBeforeGasPriceSelection::new( - account_3.clone(), - Some(gas_price_wei_from_rpc_u128_wei + 1), - ), - ], - }; + let gas_price_wei = "0x3B9ACA00"; // 1000000000 + let gas_price_from_rpc = u128::from_str_radix(&gas_price_wei[2..], 16).unwrap(); + let retry_1 = RetryTxTemplateBuilder::default() + .payable_account(&make_payable_account(12)) + .prev_gas_price_wei(gas_price_from_rpc - 1) + .build(); + let retry_2 = RetryTxTemplateBuilder::default() + .payable_account(&make_payable_account(34)) + .prev_gas_price_wei(gas_price_from_rpc) + .build(); + let retry_3 = RetryTxTemplateBuilder::default() + .payable_account(&make_payable_account(56)) + .prev_gas_price_wei(gas_price_from_rpc + 1) + .build(); + + let retry_tx_templates = + RetryTxTemplates(vec![retry_1.clone(), retry_2.clone(), retry_3.clone()]); + let expected_retry_tx_templates = PricedRetryTxTemplates(vec![ + PricedRetryTxTemplate::new(retry_1, increase_gas_price_by_margin(gas_price_from_rpc)), + PricedRetryTxTemplate::new(retry_2, increase_gas_price_by_margin(gas_price_from_rpc)), + PricedRetryTxTemplate::new( + retry_3, + increase_gas_price_by_margin(gas_price_from_rpc + 1), + ), + ]); - let expected_priced_qualified_payables = { - let gas_price_account_1 = increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei); - let gas_price_account_2 = increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei); - let gas_price_account_3 = - increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei + 1); - PricedQualifiedPayables { - payables: vec![ - QualifiedPayableWithGasPrice::new(account_1, gas_price_account_1), - QualifiedPayableWithGasPrice::new(account_2, gas_price_account_2), - QualifiedPayableWithGasPrice::new(account_3, gas_price_account_3), - ], - } - }; let expected_estimated_transaction_fee_total = 285_979_200_073_328; test_blockchain_interface_web3_can_introduce_blockchain_agent( - unpriced_qualified_payables, - gas_price_wei_from_rpc_hex, - expected_priced_qualified_payables, + Either::Right(retry_tx_templates), + gas_price_wei, + Either::Right(expected_retry_tx_templates), expected_estimated_transaction_fee_total, ); } fn test_blockchain_interface_web3_can_introduce_blockchain_agent( - unpriced_qualified_payables: UnpricedQualifiedPayables, + tx_templates: Either, gas_price_wei_from_rpc_hex: &str, - expected_priced_qualified_payables: PricedQualifiedPayables, + expected_tx_templates: Either, expected_estimated_transaction_fee_total: u128, ) { let port = find_free_port(); @@ -981,14 +973,10 @@ mod tests { masq_token_balance_in_minor_units: expected_masq_balance } ); - let priced_qualified_payables = - result.price_qualified_payables(unpriced_qualified_payables); - assert_eq!( - priced_qualified_payables, - expected_priced_qualified_payables - ); + let computed_tx_templates = result.price_qualified_payables(tx_templates); + assert_eq!(computed_tx_templates, expected_tx_templates); assert_eq!( - result.estimate_transaction_fee_total(&priced_qualified_payables), + result.estimate_transaction_fee_total(&computed_tx_templates), expected_estimated_transaction_fee_total ) } 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 d8e1729f9..a4c771fb1 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -1,36 +1,34 @@ // 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::failed_payable_dao::FailedTx; 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::accountant::db_access_objects::utils::to_unix_timestamp; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::{ + SignableTxTemplate, SignableTxTemplates, +}; use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ 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::blockchain_interface::data_structures::errors::LocalPayableError; +use crate::blockchain::blockchain_interface::data_structures::BatchResults; use crate::blockchain::errors::validation_status::ValidationStatus; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; -use actix::Recipient; +use ethabi::Address; use futures::Future; use masq_lib::blockchains::chains::Chain; 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)] @@ -40,52 +38,48 @@ pub struct BlockchainAgentFutureResult { pub masq_token_balance: U256, } -// 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( +fn return_sending_error(sent_txs: &[SentTx], error: &Web3Error) -> LocalPayableError { + LocalPayableError::Sending { + error: format!("{}", error), + failed_txs: sent_txs + .iter() + .map(|sent_tx| FailedTx::from((sent_tx, error))) + .collect(), + } +} + +pub fn return_batch_results( + txs: Vec, responses: Vec>, - sent_tx_hashes: Vec, - accounts: Vec, -) -> Vec { - let iterator_with_all_data = responses - .into_iter() - .zip(sent_tx_hashes.into_iter()) - .zip(accounts.iter()); - iterator_with_all_data - .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, - }) +) -> BatchResults { + txs.into_iter().zip(responses).fold( + BatchResults::default(), + |mut batch_results, (sent_tx, response)| { + match response { + Ok(_) => batch_results.sent_txs.push(sent_tx), // TODO: GH-547: Validate the JSON output + Err(rpc_error) => batch_results + .failed_txs + .push(FailedTx::from((&sent_tx, &rpc_error))), } - Err(rpc_error) => ProcessedPayableFallible::Failed(RpcPayableFailure { - rpc_error, - recipient_wallet: account.wallet.clone(), - hash, - }), - }) - .collect() + batch_results + }, + ) } -pub fn transmission_log( - chain: Chain, - qualified_payables: &PricedQualifiedPayables, - lowest_nonce_used: U256, -) -> String { +fn calculate_payments_column_width(signable_tx_templates: &SignableTxTemplates) -> usize { + let label_length = "[payment wei]".len(); + let largest_amount_length = signable_tx_templates + .largest_amount() + .separate_with_commas() + .len(); + + label_length.max(largest_amount_length) +} + +pub fn transmission_log(chain: Chain, signable_tx_templates: &SignableTxTemplates) -> String { let chain_name = chain.rec().literal_identifier; - let account_count = qualified_payables.payables.len(); - let last_nonce_used = lowest_nonce_used + U256::from(account_count - 1); - let biggest_payable = qualified_payables - .payables - .iter() - .map(|payable_with_gas_price| payable_with_gas_price.payable.balance_wei) - .max() - .unwrap(); - let max_length_as_str = biggest_payable.separate_with_commas().len(); - let payment_wei_label = "[payment wei]"; - let payment_column_width = payment_wei_label.len().max(max_length_as_str); + let (first_nonce, last_nonce) = signable_tx_templates.nonce_range(); + let payment_column_width = calculate_payments_column_width(signable_tx_templates); let introduction = once(format!( "\n\ @@ -99,8 +93,8 @@ pub fn transmission_log( "chain:", chain_name, "nonces:", - lowest_nonce_used.separate_with_commas(), - last_nonce_used.separate_with_commas(), + first_nonce.separate_with_commas(), + last_nonce.separate_with_commas(), "[wallet address]", "[payment wei]", "[gas price wei]", @@ -108,29 +102,23 @@ pub fn transmission_log( payment_column_width = payment_column_width, )); - let body = qualified_payables - .payables - .iter() - .map(|payable_with_gas_price| { - let payable = &payable_with_gas_price.payable; - format!( - "{:wallet_address_length$} {: [u8; 68] { +pub fn sign_transaction_data(amount_minor: u128, receiver_address: Address) -> [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[..]); + data[16..36].copy_from_slice(&receiver_address.0[..]); U256::from(amount_minor).to_big_endian(&mut data[36..68]); data } @@ -146,25 +134,30 @@ pub fn gas_limit(data: [u8; 68], chain: Chain) -> U256 { pub fn sign_transaction( chain: Chain, web3_batch: &Web3>, - recipient_wallet: Wallet, - consuming_wallet: Wallet, - amount_minor: u128, - nonce: U256, - gas_price_in_wei: u128, + signable_tx_template: &SignableTxTemplate, + consuming_wallet: &Wallet, ) -> SignedTransaction { - let data = sign_transaction_data(amount_minor, recipient_wallet); + let &SignableTxTemplate { + receiver_address, + amount_in_wei, + gas_price_wei, + nonce, + } = signable_tx_template; + + let data = sign_transaction_data(amount_in_wei, receiver_address); 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). let transaction_parameters = TransactionParameters { - nonce: Some(nonce), + nonce: Some(U256::from(nonce)), to: Some(chain.rec().contract), gas: gas_limit, - gas_price: Some(U256::from(gas_price_in_wei)), + gas_price: Some(U256::from(gas_price_wei)), value: ethereum_types::U256::zero(), data: Bytes(data.to_vec()), chain_id: Some(chain.rec().num_chain_id), }; + let key = consuming_wallet .prepare_secp256k1_secret() .expect("Consuming wallet doesn't contain a secret key"); @@ -195,23 +188,41 @@ pub fn sign_transaction_locally( pub fn sign_and_append_payment( chain: Chain, web3_batch: &Web3>, - recipient: &PayableAccount, - consuming_wallet: Wallet, - nonce: U256, - gas_price_in_wei: u128, -) -> TxHash { - let signed_tx = sign_transaction( - chain, - web3_batch, - recipient.wallet.clone(), - consuming_wallet, - recipient.balance_wei, + signable_tx_template: &SignableTxTemplate, + consuming_wallet: &Wallet, + logger: &Logger, +) -> SentTx { + let &SignableTxTemplate { + receiver_address, + amount_in_wei, + gas_price_wei, nonce, - gas_price_in_wei, - ); + } = signable_tx_template; + + let signed_tx = sign_transaction(chain, web3_batch, signable_tx_template, consuming_wallet); + append_signed_transaction_to_batch(web3_batch, signed_tx.raw_transaction); - signed_tx.transaction_hash + let hash = signed_tx.transaction_hash; + debug!( + logger, + "Appending transaction with hash {:?}, amount: {} wei, to {:?}, nonce: {}, gas price: {} wei", + hash, + amount_in_wei.separate_with_commas(), + receiver_address, + nonce, + gas_price_wei.separate_with_commas() + ); + + SentTx { + hash, + receiver_address, + amount_minor: amount_in_wei, + timestamp: to_unix_timestamp(SystemTime::now()), + gas_price_minor: gas_price_wei, + nonce, + status: TxStatus::Pending(ValidationStatus::Waiting), + } } pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_transaction: Bytes) { @@ -220,64 +231,33 @@ 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>, + signable_tx_templates: &SignableTxTemplates, consuming_wallet: Wallet, - initial_pending_nonce: U256, - accounts: &PricedQualifiedPayables, ) -> Vec { - let unix_mow = to_unix_timestamp(now); - accounts - .payables + signable_tx_templates .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( + .map(|signable_tx_template| { + 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), - } + signable_tx_template, + &consuming_wallet, + logger, + ) }) .collect() } -#[allow(clippy::too_many_arguments)] pub fn send_payables_within_batch( logger: &Logger, chain: Chain, web3_batch: &Web3>, + signable_tx_templates: SignableTxTemplates, consuming_wallet: Wallet, - pending_nonce: U256, - new_pending_payables_recipient: Recipient, - accounts: PricedQualifiedPayables, -) -> Box, Error = PayableTransactionError> + 'static> -{ +) -> Box + 'static> { debug!( logger, "Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", @@ -286,51 +266,28 @@ pub fn send_payables_within_batch( chain.rec().num_chain_id, ); - let common_timestamp = SystemTime::now(); - - let prepared_sent_txs_records = sign_and_append_multiple_payments( - common_timestamp, + let sent_txs = sign_and_append_multiple_payments( logger, chain, web3_batch, + &signable_tx_templates, consuming_wallet, - pending_nonce, - &accounts, ); - - 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()); - - 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"); + let sent_txs_for_err = sent_txs.clone(); + // TODO: GH-701: We were sending a message here to register txs at an initial stage (refer commit - 2fd4bcc72) info!( logger, "{}", - transmission_log(chain, &accounts, pending_nonce) + transmission_log(chain, &signable_tx_templates) ); Box::new( web3_batch .transport() .submit_batch() - .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, - sent_txs_hashes, - accounts.into(), - )) - }), + .map_err(move |e| return_sending_error(&sent_txs_for_err, &e)) + .and_then(move |batch_responses| Ok(return_batch_results(sent_txs, batch_responses))), ) } @@ -359,34 +316,34 @@ pub fn create_blockchain_agent_web3( #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; + use crate::accountant::db_access_objects::failed_payable_dao::{FailureReason, FailureStatus}; + use crate::accountant::db_access_objects::test_utils::{ + assert_on_failed_txs, assert_on_sent_txs, FailedTxBuilder, TxBuilder, + }; use crate::accountant::gwei_to_wei; - use crate::accountant::test_utils::{ - make_payable_account, make_payable_account_with_wallet_and_balance_and_timestamp_opt, - make_priced_qualified_payables, + use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ + PricedNewTxTemplate, PricedNewTxTemplates, }; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::make_signable_tx_template; + use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; use crate::blockchain::bip32::Bip32EncryptionKeyProvider; use crate::blockchain::blockchain_agent::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; - use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::Sending; - use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible::{ - Correct, Failed, - }; + use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError::Sending; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::test_utils::{ - make_tx_hash, transport_error_code, transport_error_message, + make_address, transport_error_code, transport_error_message, }; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; use crate::test_utils::make_wallet; - use crate::test_utils::recorder::make_recorder; use crate::test_utils::unshared_test_utils::decode_hex; - use actix::{Actor, System}; + use actix::System; use ethabi::Address; use ethereum_types::H256; - use jsonrpc_core::ErrorCode::ServerError; - use jsonrpc_core::{Error, ErrorCode}; + use itertools::Either; use masq_lib::constants::{DEFAULT_CHAIN, DEFAULT_GAS_PRICE}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; @@ -396,10 +353,11 @@ mod tests { use std::str::FromStr; use std::time::SystemTime; use web3::api::Namespace; - use web3::Error::Rpc; #[test] fn sign_and_append_payment_works() { + init_test_logging(); + let test_name = "sign_and_append_payment_works"; let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() @@ -414,39 +372,54 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let pending_nonce = 1; let chain = DEFAULT_CHAIN; let gas_price_in_gwei = DEFAULT_GAS_PRICE; let consuming_wallet = make_paying_wallet(b"paying_wallet"); - let account = make_payable_account(1); let web3_batch = Web3::new(Batch::new(transport)); + let signable_tx_template = SignableTxTemplate { + receiver_address: make_wallet("wallet1").address(), + amount_in_wei: 1_000_000_000, + gas_price_wei: gwei_to_wei(gas_price_in_gwei), + nonce: 1, + }; let result = sign_and_append_payment( chain, &web3_batch, - &account, - consuming_wallet, - pending_nonce.into(), - gwei_to_wei(gas_price_in_gwei), + &signable_tx_template, + &consuming_wallet, + &Logger::new(test_name), ); let mut batch_result = web3_batch.eth().transport().submit_batch().wait().unwrap(); - assert_eq!( - result, + let hash = H256::from_str("94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2") - .unwrap() - ); + .unwrap(); + let expected_tx = TxBuilder::default() + .hash(hash) + .template(signable_tx_template) + .timestamp(to_unix_timestamp(SystemTime::now())) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); + assert_on_sent_txs(vec![result], vec![expected_tx]); assert_eq!( batch_result.pop().unwrap().unwrap(), Value::String( "0x94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2".to_string() ) ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Appending transaction with hash \ + 0x94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2, \ + amount: 1,000,000,000 wei, \ + to 0x0000000000000000000000000077616c6c657431, \ + nonce: 1, \ + gas price: 1,000,000,000 wei" + )); } #[test] fn sign_and_append_multiple_payments_works() { - let now = SystemTime::now(); let port = find_free_port(); let logger = Logger::new("sign_and_append_multiple_payments_works"); let (_event_loop_handle, transport) = Http::with_max_parallel( @@ -455,67 +428,54 @@ mod tests { ) .unwrap(); let web3_batch = Web3::new(Batch::new(transport)); - let chain = DEFAULT_CHAIN; - let pending_nonce = 1; - let consuming_wallet = make_paying_wallet(b"paying_wallet"); - let account_1 = make_payable_account(1); - let account_2 = make_payable_account(2); - let accounts = make_priced_qualified_payables(vec![ - (account_1.clone(), 111_234_111), - (account_2.clone(), 222_432_222), + let signable_tx_templates = SignableTxTemplates(vec![ + make_signable_tx_template(1), + make_signable_tx_template(2), + make_signable_tx_template(3), + make_signable_tx_template(4), + make_signable_tx_template(5), ]); - let mut result = sign_and_append_multiple_payments( - now, + let result = sign_and_append_multiple_payments( &logger, - chain, + DEFAULT_CHAIN, &web3_batch, - consuming_wallet, - pending_nonce.into(), - &accounts, + &signable_tx_templates, + make_paying_wallet(b"paying_wallet"), ); - 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!( - 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)); + result + .iter() + .zip(signable_tx_templates.iter()) + .enumerate() + .for_each(|(index, (sent_tx, template))| { + assert_eq!( + sent_tx.receiver_address, template.receiver_address, + "Transaction {} receiver_address mismatch", + index + ); + assert_eq!( + sent_tx.amount_minor, template.amount_in_wei, + "Transaction {} amount mismatch", + index + ); + assert_eq!( + sent_tx.gas_price_minor, template.gas_price_wei, + "Transaction {} gas_price_wei mismatch", + index + ); + assert_eq!( + sent_tx.nonce, template.nonce, + "Transaction {} nonce mismatch", + index + ); + assert_eq!( + sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting), + "Transaction {} status mismatch", + index + ) + }); } #[test] @@ -529,7 +489,7 @@ mod tests { 123_456_789_u128, gwei_to_wei(33_355_666_u64), ]; - let pending_nonce = 123456789.into(); + let latest_nonce = 123456789; let expected_format = "\n\ Paying creditors\n\ Transactions:\n\ @@ -546,7 +506,7 @@ mod tests { 1, payments, Chain::BaseSepolia, - pending_nonce, + latest_nonce, expected_format, ); @@ -556,7 +516,7 @@ mod tests { gwei_to_wei(10_000_u64), 44_444_555_u128, ]; - let pending_nonce = 100.into(); + let latest_nonce = 100; let expected_format = "\n\ Paying creditors\n\ Transactions:\n\ @@ -573,13 +533,13 @@ mod tests { 2, payments, Chain::EthMainnet, - pending_nonce, + latest_nonce, expected_format, ); // Case 3 let payments = [45_000_888, 1_999_999, 444_444_555]; - let pending_nonce = 1.into(); + let latest_nonce = 1; let expected_format = "\n\ Paying creditors\n\ Transactions:\n\ @@ -596,7 +556,7 @@ mod tests { 3, payments, Chain::PolyMainnet, - pending_nonce, + latest_nonce, expected_format, ); } @@ -605,24 +565,28 @@ mod tests { case: usize, payments: [u128; 3], chain: Chain, - pending_nonce: U256, + latest_nonce: u64, expected_result: &str, ) { - let accounts_to_process_seeds = payments + let priced_new_tx_templates = payments .iter() .enumerate() - .map(|(i, payment)| { + .map(|(i, amount_in_wei)| { let wallet = make_wallet(&format!("wallet{}", i)); - let gas_price = (i as u128 + 1) * 2 * 123_456_789; - let account = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - wallet, *payment, None, - ); - (account, gas_price) + let computed_gas_price_wei = (i as u128 + 1) * 2 * 123_456_789; + PricedNewTxTemplate { + base: BaseTxTemplate { + receiver_address: wallet.address(), + amount_in_wei: *amount_in_wei, + }, + computed_gas_price_wei, + } }) - .collect(); - let accounts_to_process = make_priced_qualified_payables(accounts_to_process_seeds); + .collect::(); + let signable_tx_templates = + SignableTxTemplates::new(Either::Left(priced_new_tx_templates), latest_nonce); - let result = transmission_log(chain, &accounts_to_process, pending_nonce); + let result = transmission_log(chain, &signable_tx_templates); assert_eq!( result, expected_result, @@ -631,114 +595,37 @@ mod tests { ); } - #[test] - fn output_by_joining_sources_works() { - let accounts = vec![ - PayableAccount { - wallet: make_wallet("4567"), - balance_wei: 2_345_678, - last_paid_timestamp: from_unix_timestamp(4500000), - pending_payable_opt: None, - }, - PayableAccount { - wallet: make_wallet("5656"), - balance_wei: 6_543_210, - last_paid_timestamp: from_unix_timestamp(333000), - pending_payable_opt: None, - }, - ]; - 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 { - code: ErrorCode::ParseError, - message: "I guess we've got a problem".to_string(), - data: None, - })), - ]; - - let result = merged_output_data(responses, tx_hashes, accounts.to_vec()); - - assert_eq!( - result, - vec![ - Correct(PendingPayable { - recipient_wallet: make_wallet("4567"), - hash: make_tx_hash(444) - }), - Failed(RpcPayableFailure { - rpc_error: web3::Error::Rpc(Error { - code: ErrorCode::ParseError, - message: "I guess we've got a problem".to_string(), - data: None, - }), - recipient_wallet: make_wallet("5656"), - hash: make_tx_hash(333) - }) - ] - ) - } - fn test_send_payables_within_batch( test_name: &str, - accounts: PricedQualifiedPayables, - expected_result: Result, PayableTransactionError>, + signable_tx_templates: SignableTxTemplates, + expected_result: Result, port: u16, ) { + // TODO: GH-701: Add assertions for the new_fingerprints_message here, since it existed earlier init_test_logging(); let (_event_loop_handle, transport) = Http::with_max_parallel( &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), REQUESTS_IN_PARALLEL, ) .unwrap(); - 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_pending_payables_recipient = accountant.start().recipient(); let system = System::new(test_name); - let timestamp_before = SystemTime::now(); + let expected_transmission_log = transmission_log(chain, &signable_tx_templates); let result = send_payables_within_batch( &logger, chain, &web3_batch, + signable_tx_templates, consuming_wallet.clone(), - pending_nonce, - new_pending_payables_recipient, - accounts.clone(), ) .wait(); System::current().stop(); system.run(); - let timestamp_after = SystemTime::now(); - assert_eq!(result, expected_result); - let accountant_recording_result = accountant_recording.lock().unwrap(); - let rnpp_message = accountant_recording_result.get_record::(0); - assert_eq!(accountant_recording_result.len(), 1); - 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: {}", @@ -747,17 +634,60 @@ mod tests { chain.rec().num_chain_id, ) ); - tlh.exists_log_containing(&format!( - "INFO: {test_name}: {}", - transmission_log(chain, &accounts, pending_nonce) - )); + tlh.exists_log_containing(&format!("INFO: {test_name}: {expected_transmission_log}")); + match result { + Ok(resulted_batch) => { + let expected_batch = expected_result.unwrap(); + assert_on_failed_txs(resulted_batch.failed_txs, expected_batch.failed_txs); + assert_on_sent_txs(resulted_batch.sent_txs, expected_batch.sent_txs); + } + Err(resulted_err) => match resulted_err { + LocalPayableError::Sending { error, failed_txs } => { + if let Err(LocalPayableError::Sending { + error: expected_error, + failed_txs: expected_failed_txs, + }) = expected_result + { + assert_on_failed_txs(failed_txs, expected_failed_txs); + assert_eq!(error, expected_error) + } else { + panic!( + "Expected different error but received {}", + expected_result.unwrap_err(), + ) + } + } + other_err => { + panic!("Only LocalPayableError::Sending is returned by send_payables_within_batch but received something else: {} ", other_err) + } + }, + } } #[test] fn send_payables_within_batch_works() { - let account_1 = make_payable_account(1); - let account_2 = make_payable_account(2); let port = find_free_port(); + let (_event_loop_handle, transport) = Http::with_max_parallel( + &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), + REQUESTS_IN_PARALLEL, + ) + .unwrap(); + let web3_batch = Web3::new(Batch::new(transport)); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); + let template_1 = SignableTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 111_222, + gas_price_wei: 123, + nonce: 1, + }; + let template_2 = SignableTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 222_333, + gas_price_wei: 234, + nonce: 2, + }; + let signable_tx_templates = + SignableTxTemplates(vec![template_1.clone(), template_2.clone()]); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() // TODO: GH-547: This rpc_result should be validated in production code. @@ -765,54 +695,93 @@ mod tests { .ok_response("irrelevant_ok_rpc_response_2".to_string(), 8) .end_batch() .start(); - let expected_result = Ok(vec![ - Correct(PendingPayable { - recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str( - "0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3", - ) - .unwrap(), - }), - Correct(PendingPayable { - recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str( - "6b485dbd4d769b5a19fa57058d612fad99cdd78769db6b3be129f981c42657ac", - ) - .unwrap(), - }), - ]); + let batch_results = { + let signed_tx_1 = + sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_1, &consuming_wallet); + let sent_tx_1 = TxBuilder::default() + .hash(signed_tx_1.transaction_hash) + .template(template_1) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); + let signed_tx_2 = + sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_2, &consuming_wallet); + let sent_tx_2 = TxBuilder::default() + .hash(signed_tx_2.transaction_hash) + .template(template_2) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); + + BatchResults { + sent_txs: vec![sent_tx_1, sent_tx_2], + failed_txs: vec![], + } + }; test_send_payables_within_batch( "send_payables_within_batch_works", - make_priced_qualified_payables(vec![ - (account_1, 111_111_111), - (account_2, 222_222_222), - ]), - expected_result, + signable_tx_templates, + Ok(batch_results), port, ); } #[test] fn send_payables_within_batch_fails_on_submit_batch_call() { - let accounts = make_priced_qualified_payables(vec![ - (make_payable_account(1), 111_222_333), - (make_payable_account(2), 222_333_444), - ]); - let os_code = transport_error_code(); - let os_msg = transport_error_message(); let port = find_free_port(); + let (_event_loop_handle, transport) = Http::with_max_parallel( + &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), + REQUESTS_IN_PARALLEL, + ) + .unwrap(); + let web3_batch = Web3::new(Batch::new(transport)); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); + let signable_tx_templates = SignableTxTemplates(vec![ + SignableTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 12345, + gas_price_wei: 99, + nonce: 5, + }, + SignableTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 22345, + gas_price_wei: 100, + nonce: 6, + }, + ]); + let os_specific_code = transport_error_code(); + let os_specific_msg = transport_error_message(); + let err_msg = format!( + "Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", + os_specific_code, os_specific_msg + ); + let failed_txs = signable_tx_templates + .iter() + .map(|template| { + let signed_tx = + sign_transaction(DEFAULT_CHAIN, &web3_batch, template, &consuming_wallet); + FailedTxBuilder::default() + .hash(signed_tx.transaction_hash) + .receiver_address(template.receiver_address) + .amount(template.amount_in_wei) + .timestamp(to_unix_timestamp(SystemTime::now()) - 5) + .gas_price_wei(template.gas_price_wei) + .nonce(template.nonce) + .reason(FailureReason::Submission(AppRpcErrorKind::Local( + LocalErrorKind::Transport, + ))) + .status(FailureStatus::RetryRequired) + .build() + }) + .collect(); let expected_result = Err(Sending { - msg: format!("Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", os_code, os_msg).to_string(), - hashes: hashset![ - H256::from_str("5bbe90ad19d86b69ee49879cec4b3f8b769223e6a872aae0be88773de2fc3beb").unwrap(), - H256::from_str("a1b609dbe9cc77ad586dbe4e5c1079d6ad76020a353c960928d6daeafd43f366").unwrap() - ], + error: err_msg, + failed_txs, }); test_send_payables_within_batch( "send_payables_within_batch_fails_on_submit_batch_call", - accounts, + signable_tx_templates, expected_result, port, ); @@ -820,9 +789,28 @@ mod tests { #[test] fn send_payables_within_batch_all_payments_fail() { - let account_1 = make_payable_account(1); - let account_2 = make_payable_account(2); let port = find_free_port(); + let (_event_loop_handle, transport) = Http::with_max_parallel( + &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), + REQUESTS_IN_PARALLEL, + ) + .unwrap(); + let web3_batch = Web3::new(Batch::new(transport)); + let signable_tx_templates = SignableTxTemplates(vec![ + SignableTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 111_222, + gas_price_wei: 123, + nonce: 1, + }, + SignableTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 222_333, + gas_price_wei: 234, + nonce: 2, + }, + ]); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() .err_response( @@ -839,43 +827,61 @@ mod tests { ) .end_batch() .start(); - let expected_result = Ok(vec![ - Failed(RpcPayableFailure { - rpc_error: Rpc(Error { - code: ServerError(429), - message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), - data: None, - }), - recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), - }), - Failed(RpcPayableFailure { - rpc_error: Rpc(Error { - code: ServerError(429), - message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), - data: None, - }), - recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), - }), - ]); + let failed_txs = signable_tx_templates + .iter() + .map(|template| { + let signed_tx = + sign_transaction(DEFAULT_CHAIN, &web3_batch, template, &consuming_wallet); + FailedTxBuilder::default() + .hash(signed_tx.transaction_hash) + .receiver_address(template.receiver_address) + .amount(template.amount_in_wei) + .timestamp(to_unix_timestamp(SystemTime::now()) - 5) + .gas_price_wei(template.gas_price_wei) + .nonce(template.nonce) + .reason(FailureReason::Submission(AppRpcErrorKind::Remote( + RemoteErrorKind::Web3RpcError(429), + ))) + .status(FailureStatus::RetryRequired) + .build() + }) + .collect(); test_send_payables_within_batch( "send_payables_within_batch_all_payments_fail", - make_priced_qualified_payables(vec![ - (account_1, 111_111_111), - (account_2, 111_111_111), - ]), - expected_result, + signable_tx_templates, + Ok(BatchResults { + sent_txs: vec![], + failed_txs, + }), port, ); } #[test] fn send_payables_within_batch_one_payment_works_the_other_fails() { - let account_1 = make_payable_account(1); - let account_2 = make_payable_account(2); let port = find_free_port(); + let (_event_loop_handle, transport) = Http::with_max_parallel( + &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), + REQUESTS_IN_PARALLEL, + ) + .unwrap(); + let web3_batch = Web3::new(Batch::new(transport)); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); + let template_1 = SignableTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 111_222, + gas_price_wei: 123, + nonce: 1, + }; + let template_2 = SignableTxTemplate { + receiver_address: make_address(2), + amount_in_wei: 222_333, + gas_price_wei: 234, + nonce: 2, + }; + let signable_tx_templates = + SignableTxTemplates(vec![template_1.clone(), template_2.clone()]); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() .ok_response("rpc_result".to_string(), 7) @@ -887,29 +893,37 @@ mod tests { ) .end_batch() .start(); - let expected_result = Ok(vec![ - Correct(PendingPayable { - recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), - }), - Failed(RpcPayableFailure { - rpc_error: Rpc(Error { - code: ServerError(429), - message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), - data: None, - }), - recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), - }), - ]); + let batch_results = { + let signed_tx_1 = + sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_1, &consuming_wallet); + let sent_tx = TxBuilder::default() + .hash(signed_tx_1.transaction_hash) + .template(template_1) + .timestamp(to_unix_timestamp(SystemTime::now())) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); + let signed_tx_2 = + sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_2, &consuming_wallet); + let failed_tx = FailedTxBuilder::default() + .hash(signed_tx_2.transaction_hash) + .template(template_2) + .timestamp(to_unix_timestamp(SystemTime::now())) + .reason(FailureReason::Submission(AppRpcErrorKind::Remote( + RemoteErrorKind::Web3RpcError(429), + ))) + .status(FailureStatus::RetryRequired) + .build(); + + BatchResults { + sent_txs: vec![sent_tx], + failed_txs: vec![failed_tx], + } + }; test_send_payables_within_batch( "send_payables_within_batch_one_payment_works_the_other_fails", - make_priced_qualified_payables(vec![ - (account_1, 111_111_111), - (account_2, 111_111_111), - ]), - expected_result, + signable_tx_templates, + Ok(batch_results), port, ); } @@ -925,19 +939,20 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let recipient_wallet = make_wallet("unlucky man"); let consuming_wallet = make_wallet("bad_wallet"); let gas_price = 123_000_000_000; - let nonce = U256::from(1); + let signable_tx_template = SignableTxTemplate { + receiver_address: make_address(1), + amount_in_wei: 1223, + gas_price_wei: gas_price, + nonce: 1, + }; sign_transaction( Chain::PolyAmoy, &Web3::new(Batch::new(transport)), - recipient_wallet, - consuming_wallet, - 444444, - nonce, - gas_price, + &signable_tx_template, + &consuming_wallet, ); } @@ -952,14 +967,14 @@ mod tests { let web3 = Web3::new(transport.clone()); let chain = DEFAULT_CHAIN; let amount = 11_222_333_444; - let gas_price_in_wei = 123 * 10_u128.pow(18); - let nonce = U256::from(5); + let gas_price_in_wei = 123 * 10_u128.pow(9); + let nonce = 5; let recipient_wallet = make_wallet("recipient_wallet"); let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let consuming_wallet_secret_key = consuming_wallet.prepare_secp256k1_secret().unwrap(); - let data = sign_transaction_data(amount, recipient_wallet.clone()); + let data = sign_transaction_data(amount, recipient_wallet.address()); let tx_parameters = TransactionParameters { - nonce: Some(nonce), + nonce: Some(U256::from(nonce)), to: Some(chain.rec().contract), gas: gas_limit(data, chain), gas_price: Some(U256::from(gas_price_in_wei)), @@ -967,14 +982,17 @@ mod tests { data: Bytes(data.to_vec()), chain_id: Some(chain.rec().num_chain_id), }; + let signable_tx_template = SignableTxTemplate { + receiver_address: recipient_wallet.address(), + amount_in_wei: amount, + gas_price_wei: gas_price_in_wei, + nonce, + }; let result = sign_transaction( chain, &Web3::new(Batch::new(transport)), - recipient_wallet, - consuming_wallet, - amount, - nonce, - gas_price_in_wei, + &signable_tx_template, + &consuming_wallet, ); let expected_tx_result = web3 @@ -1001,7 +1019,7 @@ mod tests { let gas_price = U256::from(5); let recipient_wallet = make_wallet("recipient_wallet"); let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let data = sign_transaction_data(amount, recipient_wallet); + let data = sign_transaction_data(amount, recipient_wallet.address()); // sign_transaction makes a blockchain call because nonce is set to None let transaction_parameters = TransactionParameters { nonce: None, @@ -1137,7 +1155,6 @@ mod tests { let address = Address::from_slice(&recipient_address_bytes); Wallet::from(address) }; - let nonce_correct_type = U256::from(nonce); let gas_price_in_gwei = match chain { Chain::EthMainnet => TEST_GAS_PRICE_ETH, Chain::EthRopsten => TEST_GAS_PRICE_ETH, @@ -1145,20 +1162,18 @@ mod tests { Chain::PolyAmoy => TEST_GAS_PRICE_POLYGON, _ => panic!("isn't our interest in this test"), }; - let payable_account = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - recipient_wallet, - TEST_PAYMENT_AMOUNT, - None, - ); + let signable_tx_template = SignableTxTemplate { + receiver_address: recipient_wallet.address(), + amount_in_wei: TEST_PAYMENT_AMOUNT, + gas_price_wei: gwei_to_wei(gas_price_in_gwei), + nonce, + }; let signed_transaction = sign_transaction( chain, &Web3::new(Batch::new(transport)), - payable_account.wallet, - consuming_wallet, - payable_account.balance_wei, - nonce_correct_type, - gwei_to_wei(gas_price_in_gwei), + &signable_tx_template, + &consuming_wallet, ); let byte_set_to_compare = signed_transaction.raw_transaction.0; @@ -1168,7 +1183,7 @@ mod tests { fn test_gas_limit_is_between_limits(chain: Chain) { let not_under_this_value = BlockchainInterfaceWeb3::web3_gas_limit_const_part(chain); let not_above_this_value = not_under_this_value + WEB3_MAXIMAL_GAS_LIMIT_MARGIN; - let data = sign_transaction_data(1_000_000_000, make_wallet("wallet1")); + let data = sign_transaction_data(1_000_000_000, make_wallet("wallet1").address()); let gas_limit = gas_limit(data, chain); diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index 1d01532ec..03899343e 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -1,9 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::comma_joined_stringifiable; -use crate::accountant::db_access_objects::utils::TxHash; -use itertools::{Either, Itertools}; -use std::collections::HashSet; +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; +use crate::accountant::join_with_separator; +use itertools::Either; use std::fmt; use std::fmt::{Display, Formatter}; use variant_count::VariantCount; @@ -35,20 +34,18 @@ impl Display for BlockchainInterfaceError { } #[derive(Clone, Debug, PartialEq, Eq, VariantCount)] -pub enum PayableTransactionError { +pub enum LocalPayableError { MissingConsumingWallet, GasPriceQueryFailed(BlockchainInterfaceError), TransactionID(BlockchainInterfaceError), - UnusableWallet(String), - Signing(String), Sending { - msg: String, - hashes: HashSet, + error: String, + failed_txs: Vec, }, UninitializedInterface, } -impl Display for PayableTransactionError { +impl Display for LocalPayableError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::MissingConsumingWallet => { @@ -60,21 +57,12 @@ impl Display for PayableTransactionError { Self::TransactionID(blockchain_err) => { write!(f, "Transaction id fetching failed: {}", blockchain_err) } - Self::UnusableWallet(msg) => write!( + Self::Sending { error, failed_txs } => write!( f, - "Unusable wallet for signing payable transactions: \"{}\"", - msg + "Sending error: \"{}\". Signed and hashed transactions: \"{}\"", + error, + join_with_separator(failed_txs, |failed_tx| format!("{:?}", failed_tx), ",") ), - Self::Signing(msg) => write!(f, "Signing phase: \"{}\"", msg), - 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) } @@ -122,13 +110,13 @@ impl Display for BlockchainAgentBuildError { #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::test_utils::make_failed_tx; use crate::blockchain::blockchain_interface::data_structures::errors::{ - PayableTransactionError, BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED, + LocalPayableError, BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED, }; use crate::blockchain::blockchain_interface::{ BlockchainAgentBuildError, BlockchainInterfaceError, }; - use crate::blockchain::test_utils::make_tx_hash; use crate::test_utils::make_wallet; use masq_lib::utils::{slice_of_strs_to_vec_of_strings, to_string}; @@ -175,29 +163,23 @@ mod tests { #[test] fn payable_payment_error_implements_display() { let original_errors = [ - PayableTransactionError::MissingConsumingWallet, - PayableTransactionError::GasPriceQueryFailed(BlockchainInterfaceError::QueryFailed( + LocalPayableError::MissingConsumingWallet, + LocalPayableError::GasPriceQueryFailed(BlockchainInterfaceError::QueryFailed( "Gas halves shut, no drop left".to_string(), )), - PayableTransactionError::TransactionID(BlockchainInterfaceError::InvalidResponse), - PayableTransactionError::UnusableWallet( - "This is a LEATHER wallet, not LEDGER wallet, stupid.".to_string(), - ), - PayableTransactionError::Signing( - "You cannot sign with just three crosses here, clever boy".to_string(), - ), - PayableTransactionError::Sending { - msg: "Sending to cosmos belongs elsewhere".to_string(), - hashes: hashset![make_tx_hash(0x6f), make_tx_hash(0xde)], + LocalPayableError::TransactionID(BlockchainInterfaceError::InvalidResponse), + LocalPayableError::Sending { + error: "Terrible error!!".to_string(), + failed_txs: vec![make_failed_tx(456)], }, - PayableTransactionError::UninitializedInterface, + LocalPayableError::UninitializedInterface, ]; let actual_error_msgs = original_errors.iter().map(to_string).collect::>(); assert_eq!( original_errors.len(), - PayableTransactionError::VARIANT_COUNT, + LocalPayableError::VARIANT_COUNT, "you forgot to add all variants in this test" ); assert_eq!( @@ -206,12 +188,10 @@ mod tests { "Missing consuming wallet to pay payable from", "Unsuccessful gas price query: \"Blockchain error: Query failed: Gas halves shut, no drop left\"", "Transaction id fetching failed: Blockchain error: Invalid response", - "Unusable wallet for signing payable transactions: \"This is a LEATHER wallet, not \ - 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 \ - txs: 0x000000000000000000000000000000000000000000000000000000000000006f, \ - 0x00000000000000000000000000000000000000000000000000000000000000de", + "Sending error: \"Terrible error!!\". Signed and hashed transactions: \"FailedTx { hash: 0x00000000000000\ + 000000000000000000000000000000000000000000000001c8, receiver_address: 0x00000000000\ + 00000002556000000002556000000, amount_minor: 43237380096, timestamp: 29942784, gas_\ + price_minor: 94818816, nonce: 456, reason: PendingTooLong, status: RetryRequired }\"", 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 1e8c918de..f79f12345 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -2,9 +2,9 @@ pub mod errors; -use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; 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; @@ -12,7 +12,6 @@ use serde_derive::{Deserialize, Serialize}; use std::fmt; use std::fmt::{Display, Formatter}; use web3::types::{TransactionReceipt, H256}; -use web3::Error; #[derive(Clone, Debug, Eq, PartialEq)] pub struct BlockchainTransaction { @@ -37,17 +36,10 @@ pub struct RetrievedBlockchainTransactions { pub transactions: Vec, } -#[derive(Debug, PartialEq, Clone)] -pub struct RpcPayableFailure { - pub rpc_error: Error, - pub recipient_wallet: Wallet, - pub hash: TxHash, -} - -#[derive(Debug, PartialEq, Clone)] -pub enum ProcessedPayableFallible { - Correct(PendingPayable), - Failed(RpcPayableFailure), +#[derive(Default, Debug, PartialEq, Eq, Clone)] +pub struct BatchResults { + pub sent_txs: Vec, + pub failed_txs: Vec, } #[derive(Debug, PartialEq, Eq, Clone)] diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index 09961776e..3db1bbeab 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -4,26 +4,25 @@ pub mod blockchain_interface_web3; pub mod data_structures; pub mod lower_level_interface; -use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; 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_bridge::{BlockMarker, BlockScanRange}; use crate::blockchain::blockchain_interface::data_structures::errors::{ - BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError, + BlockchainAgentBuildError, BlockchainInterfaceError, LocalPayableError, }; use crate::blockchain::blockchain_interface::data_structures::{ - ProcessedPayableFallible, RetrievedBlockchainTransactions, + BatchResults, RetrievedBlockchainTransactions, }; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::sub_lib::wallet::Wallet; -use actix::Recipient; use futures::Future; +use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use std::collections::HashMap; +use std::collections::BTreeMap; use web3::types::Address; pub trait BlockchainInterface { @@ -50,7 +49,7 @@ pub trait BlockchainInterface { tx_hashes: Vec, ) -> Box< dyn Future< - Item = HashMap, + Item = BTreeMap, Error = BlockchainInterfaceError, >, >; @@ -59,9 +58,8 @@ pub trait BlockchainInterface { &self, logger: Logger, agent: Box, - new_pending_payables_recipient: Recipient, - affordable_accounts: PricedQualifiedPayables, - ) -> Box, Error = PayableTransactionError>>; + priced_templates: Either, + ) -> Box>; as_any_ref_in_trait!(); } diff --git a/node/src/blockchain/blockchain_interface_initializer.rs b/node/src/blockchain/blockchain_interface_initializer.rs index d7f452311..9f36ca84f 100644 --- a/node/src/blockchain/blockchain_interface_initializer.rs +++ b/node/src/blockchain/blockchain_interface_initializer.rs @@ -44,14 +44,14 @@ impl BlockchainInterfaceInitializer { #[cfg(test)] mod tests { - use crate::accountant::scanners::payable_scanner_extension::msgs::{ - PricedQualifiedPayables, QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, - }; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; use crate::accountant::test_utils::make_payable_account; use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; use crate::test_utils::make_wallet; use futures::Future; + use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::DEFAULT_CHAIN; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; @@ -80,29 +80,22 @@ mod tests { let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); - let unpriced_qualified_payables = - UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let tx_templates = NewTxTemplates::from(&vec![account_1.clone(), account_2.clone()]); let payable_wallet = make_wallet("payable"); let blockchain_agent = result .introduce_blockchain_agent(payable_wallet.clone()) .wait() .unwrap(); assert_eq!(blockchain_agent.consuming_wallet(), &payable_wallet); - let priced_qualified_payables = - blockchain_agent.price_qualified_payables(unpriced_qualified_payables); + let result = blockchain_agent.price_qualified_payables(Either::Left(tx_templates.clone())); let gas_price_with_margin = increase_gas_price_by_margin(1_000_000_000); - let expected_priced_qualified_payables = PricedQualifiedPayables { - payables: vec![ - QualifiedPayableWithGasPrice::new(account_1, gas_price_with_margin), - QualifiedPayableWithGasPrice::new(account_2, gas_price_with_margin), - ], - }; + let expected_result = Either::Left(PricedNewTxTemplates::new( + tx_templates, + gas_price_with_margin, + )); + assert_eq!(result, expected_result); assert_eq!( - priced_qualified_payables, - expected_priced_qualified_payables - ); - assert_eq!( - blockchain_agent.estimate_transaction_fee_total(&priced_qualified_payables), + blockchain_agent.estimate_transaction_fee_total(&result), 190_652_800_000_000 ); } diff --git a/node/src/blockchain/errors/internal_errors.rs b/node/src/blockchain/errors/internal_errors.rs index 9982d0667..537519480 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, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum InternalErrorKind { PendingTooLongNotReplaced, } diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index e406a96b1..b6d1af111 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, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, 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 bf78fa53b..41d9d3863 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -4,13 +4,13 @@ use serde_derive::{Deserialize, Serialize}; use web3::error::Error as Web3Error; // Prefixed with App to clearly distinguish app-specific errors from library errors. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum AppRpcError { Local(LocalError), Remote(RemoteError), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum LocalError { Decoder(String), Internal, @@ -19,7 +19,7 @@ pub enum LocalError { Transport(String), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum RemoteError { InvalidResponse(String), Unreachable, @@ -53,22 +53,22 @@ impl From for AppRpcError { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum AppRpcErrorKind { Local(LocalErrorKind), Remote(RemoteErrorKind), } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum LocalErrorKind { Decoder, Internal, - IO, + Io, Signing, Transport, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum RemoteErrorKind { InvalidResponse, Unreachable, @@ -81,7 +81,7 @@ impl From<&AppRpcError> for AppRpcErrorKind { AppRpcError::Local(local) => match local { LocalError::Decoder(_) => Self::Local(LocalErrorKind::Decoder), LocalError::Internal => Self::Local(LocalErrorKind::Internal), - LocalError::IO(_) => Self::Local(LocalErrorKind::IO), + LocalError::IO(_) => Self::Local(LocalErrorKind::Io), LocalError::Signing(_) => Self::Local(LocalErrorKind::Signing), LocalError::Transport(_) => Self::Local(LocalErrorKind::Transport), }, @@ -162,7 +162,7 @@ mod tests { ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::IO("IO error".to_string()))), - AppRpcErrorKind::Local(LocalErrorKind::IO) + AppRpcErrorKind::Local(LocalErrorKind::Io) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Signing( @@ -200,7 +200,7 @@ mod tests { let errors = vec![ AppRpcErrorKind::Local(LocalErrorKind::Decoder), AppRpcErrorKind::Local(LocalErrorKind::Internal), - AppRpcErrorKind::Local(LocalErrorKind::IO), + AppRpcErrorKind::Local(LocalErrorKind::Io), AppRpcErrorKind::Local(LocalErrorKind::Signing), AppRpcErrorKind::Local(LocalErrorKind::Transport), AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse), diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 34cb2c5e3..a3e8ada27 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -7,8 +7,10 @@ use serde::{ Deserialize as ManualDeserialize, Deserializer, Serialize as ManualSerialize, Serializer, }; use serde_derive::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::cmp::Ordering; +use std::collections::BTreeMap; use std::fmt::Formatter; +use std::hash::Hash; use std::time::SystemTime; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -17,9 +19,32 @@ pub enum ValidationStatus { Reattempting(PreviousAttempts), } -#[derive(Debug, Clone, PartialEq, Eq)] +impl PartialOrd for ValidationStatus { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for ValidationStatus { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (ValidationStatus::Reattempting(..), ValidationStatus::Waiting) => Ordering::Less, + (ValidationStatus::Waiting, ValidationStatus::Reattempting(..)) => Ordering::Greater, + (ValidationStatus::Waiting, ValidationStatus::Waiting) => Ordering::Equal, + (ValidationStatus::Reattempting(prev1), ValidationStatus::Reattempting(prev2)) => { + prev1.cmp(prev2) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PreviousAttempts { - inner: HashMap, + inner: BTreeMap, } // had to implement it manually in an array JSON layout, as the original, default HashMap @@ -75,7 +100,7 @@ impl<'de> Visitor<'de> for PreviousAttemptsVisitor { stats: ErrorStats, } - let mut error_stats_map: HashMap = hashmap!(); + let mut error_stats_map: BTreeMap = btreemap!(); while let Some(entry) = seq.next_element::()? { error_stats_map.insert(entry.error_kind, entry.stats); } @@ -88,7 +113,7 @@ impl<'de> Visitor<'de> for PreviousAttemptsVisitor { impl PreviousAttempts { pub fn new(error: BlockchainErrorKind, clock: &dyn ValidationFailureClock) -> Self { Self { - inner: hashmap!(error => ErrorStats::now(clock)), + inner: btreemap!(error => ErrorStats::now(clock)), } } @@ -105,7 +130,7 @@ impl PreviousAttempts { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct ErrorStats { #[serde(rename = "firstSeen")] pub first_seen: SystemTime, @@ -141,12 +166,14 @@ impl ValidationFailureClock for ValidationFailureClockReal { #[cfg(test)] mod tests { use super::*; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::blockchain::errors::internal_errors::InternalErrorKind; use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; - use crate::blockchain::test_utils::ValidationFailureClockMock; use crate::test_utils::serde_serializer_mock::{SerdeSerializerMock, SerializeSeqMock}; use serde::ser::Error as SerdeError; - use std::time::{Duration, UNIX_EPOCH}; + use std::collections::BTreeSet; + use std::time::Duration; + use std::time::UNIX_EPOCH; #[test] fn previous_attempts_and_validation_failure_clock_work_together_fine() { @@ -165,7 +192,7 @@ mod tests { ); let timestamp_c = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &validation_failure_clock, ); let timestamp_d = SystemTime::now(); @@ -174,7 +201,7 @@ mod tests { &validation_failure_clock, ); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &validation_failure_clock, ); @@ -211,7 +238,7 @@ mod tests { let io_error_stats = subject .inner .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( - LocalErrorKind::IO, + LocalErrorKind::Io, ))) .unwrap(); assert!( @@ -231,6 +258,73 @@ mod tests { assert_eq!(other_error_stats, None); } + // #[test] + // fn previous_attempts_hash_works_correctly() { + // let now = SystemTime::now(); + // let clock = ValidationFailureClockMock::default() + // .now_result(now) + // .now_result(now) + // .now_result(now + Duration::from_secs(2)); + // let attempts1 = PreviousAttempts::new( + // BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + // &clock, + // ); + // let attempts2 = PreviousAttempts::new( + // BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + // &clock, + // ); + // let attempts3 = PreviousAttempts::new( + // BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), + // &clock, + // ); + // let hash1 = { + // let mut hasher = DefaultHasher::new(); + // attempts1.hash(&mut hasher); + // hasher.finish() + // }; + // let hash2 = { + // let mut hasher = DefaultHasher::new(); + // attempts2.hash(&mut hasher); + // hasher.finish() + // }; + // let hash3 = { + // let mut hasher = DefaultHasher::new(); + // attempts3.hash(&mut hasher); + // hasher.finish() + // }; + // + // assert_eq!(hash1, hash2); + // assert_ne!(hash1, hash3); + // } + + #[test] + fn previous_attempts_ordering_works_correctly_with_mock() { + let now = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(now) + .now_result(now + Duration::from_secs(1)) + .now_result(now + Duration::from_secs(2)) + .now_result(now + Duration::from_secs(3)); + let mut attempts1 = PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &clock, + ); + attempts1 = attempts1.add_attempt( + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + &clock, + ); + let mut attempts2 = PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), + &clock, + ); + attempts2 = attempts2.add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Signing)), + &clock, + ); + + assert_eq!(attempts2.partial_cmp(&attempts1), Some(Ordering::Greater)); + } + #[test] fn previous_attempts_custom_serialize_seq_happy_path() { let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); @@ -308,7 +402,7 @@ mod tests { let clock = ValidationFailureClockMock::default().now_result(timestamp); assert_eq!( result.unwrap().inner, - hashmap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) + btreemap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) ); } @@ -324,4 +418,49 @@ mod tests { "invalid type: string \"Yesterday\", expected struct SystemTime at line 1 column 79" ); } + + #[test] + fn validation_status_ordering_works_correctly() { + let now = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(now) + .now_result(now + Duration::from_secs(1)); + + let waiting = ValidationStatus::Waiting; + let reattempting_early = ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &clock, + )); + let reattempting_late = ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), + &clock, + )); + let waiting_identical = waiting.clone(); + let reattempting_early_identical = reattempting_early.clone(); + + let mut set = BTreeSet::new(); + vec![ + reattempting_early.clone(), + waiting.clone(), + reattempting_late.clone(), + waiting_identical.clone(), + reattempting_early_identical.clone(), + ] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); + + let expected_order = vec![ + reattempting_early.clone(), + reattempting_late, + waiting.clone(), + ]; + assert_eq!(set.into_iter().collect::>(), expected_order); + assert_eq!(waiting.cmp(&waiting_identical), Ordering::Equal); + assert_eq!( + reattempting_early.cmp(&reattempting_early_identical), + Ordering::Equal + ); + } } diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 2ce57f261..238703d98 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -5,7 +5,6 @@ use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; -use crate::blockchain::errors::validation_status::ValidationFailureClock; use bip39::{Language, Mnemonic, Seed}; use ethabi::Hash; use ethereum_types::{BigEndianHash, H160, H256, U64}; @@ -14,12 +13,10 @@ use masq_lib::blockchains::chains::Chain; use masq_lib::utils::to_string; use serde::Serialize; use serde_derive::Deserialize; -use std::cell::RefCell; use std::fmt::Debug; use std::net::Ipv4Addr; -use std::time::SystemTime; use web3::transports::{EventLoopHandle, Http}; -use web3::types::{Index, Log, SignedTransaction, TransactionReceipt, H2048, U256}; +use web3::types::{Address, Index, Log, SignedTransaction, TransactionReceipt, H2048, U256}; lazy_static! { static ref BIG_MEANINGLESS_PHRASE: Vec<&'static str> = vec![ @@ -188,7 +185,7 @@ pub fn make_default_signed_transaction() -> SignedTransaction { } } -pub fn make_hash(base: u32) -> Hash { +fn make_hash(base: u32) -> H256 { H256::from_uint(&U256::from(base)) } @@ -200,6 +197,19 @@ pub fn make_block_hash(base: u32) -> H256 { make_hash(base + 1000000000) } +pub fn make_address(base: u32) -> Address { + let base = base % 0xfff; + let value = U256::from(base * 3); + let shifted = value << 72; + let value = U256::from(value) << 24; + let value = value | shifted; + let mut full_bytes = [0u8; 32]; + value.to_big_endian(&mut full_bytes); + let mut bytes = [0u8; 20]; + bytes.copy_from_slice(&full_bytes[12..]); + H160(bytes) +} + pub fn all_chains() -> [Chain; 4] { [ Chain::EthMainnet, @@ -277,21 +287,3 @@ impl TransactionReceiptBuilder { } } } - -#[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/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 317070c09..039b1fe4f 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -4,7 +4,7 @@ use crate::accountant::db_access_objects::failed_payable_dao::FailedPayableDaoFa use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; 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::scanners::payable_scanner::msgs::PricedTemplatesMessage; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ScanError, SentPayables, TxReceiptsMessage, }; @@ -101,7 +101,7 @@ pub struct AccountantSubs { pub report_routing_service_provided: Recipient, pub report_exit_service_provided: Recipient, pub report_services_consumed: Recipient, - pub report_payable_payments_setup: Recipient, + pub report_payable_payments_setup: Recipient, pub report_inbound_payments: Recipient, pub register_new_pending_payables: Recipient, pub report_transaction_status: Recipient, diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 669e37042..25834d5f6 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -1,14 +1,20 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - PricedQualifiedPayables, QualifiedPayablesMessage, +use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; +use crate::accountant::{ + PayableScanType, RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder, }; -use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use crate::blockchain::blockchain_bridge::{ + MsgInterpretableAsDetailedScanType, RetrieveTransactions, +}; +use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::peer_actors::BindMessage; use actix::Message; use actix::Recipient; +use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::ui_gateway::NodeFromUiMessage; use std::fmt; @@ -28,7 +34,7 @@ pub struct BlockchainBridgeConfig { pub struct BlockchainBridgeSubs { pub bind: Recipient, pub outbound_payments_instructions: Recipient, - pub qualified_payables: Recipient, + pub qualified_payables: Recipient, pub retrieve_transactions: Recipient, pub ui_sub: Recipient, pub request_transaction_receipts: Recipient, @@ -42,23 +48,39 @@ impl Debug for BlockchainBridgeSubs { #[derive(Message)] pub struct OutboundPaymentsInstructions { - pub affordable_accounts: PricedQualifiedPayables, + pub priced_templates: Either, pub agent: Box, pub response_skeleton_opt: Option, } +impl MsgInterpretableAsDetailedScanType for OutboundPaymentsInstructions { + fn detailed_scan_type(&self) -> DetailedScanType { + match self.priced_templates { + Either::Left(_) => DetailedScanType::NewPayables, + Either::Right(_) => DetailedScanType::RetryPayables, + } + } +} + impl OutboundPaymentsInstructions { pub fn new( - affordable_accounts: PricedQualifiedPayables, + priced_templates: Either, agent: Box, response_skeleton_opt: Option, ) -> Self { Self { - affordable_accounts, + priced_templates, agent, response_skeleton_opt, } } + + pub fn scan_type(&self) -> PayableScanType { + match &self.priced_templates { + Either::Left(_new_templates) => PayableScanType::New, + Either::Right(_retry_templates) => PayableScanType::Retry, + } + } } impl SkeletonOptHolder for OutboundPaymentsInstructions { @@ -84,12 +106,21 @@ impl ConsumingWalletBalances { #[cfg(test)] mod tests { + use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::make_priced_new_tx_templates; + use crate::accountant::test_utils::make_payable_account; use crate::actor_system_factory::SubsFactory; - use crate::blockchain::blockchain_bridge::{BlockchainBridge, BlockchainBridgeSubsFactoryReal}; + use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; + use crate::blockchain::blockchain_bridge::{ + BlockchainBridge, BlockchainBridgeSubsFactoryReal, MsgInterpretableAsDetailedScanType, + }; use crate::blockchain::test_utils::make_blockchain_interface_web3; + use crate::sub_lib::accountant::DetailedScanType; + use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{make_blockchain_bridge_subs_from_recorder, Recorder}; use actix::{Actor, System}; + use itertools::Either; use masq_lib::utils::find_free_port; use std::sync::{Arc, Mutex}; @@ -121,4 +152,24 @@ mod tests { system.run(); assert_eq!(subs, BlockchainBridge::make_subs_from(&addr)) } + + #[test] + fn detailed_scan_type_is_implemented_for_outbound_payments_instructions() { + let msg_a = OutboundPaymentsInstructions { + priced_templates: Either::Left(make_priced_new_tx_templates(vec![( + make_payable_account(123), + 123, + )])), + agent: Box::new(BlockchainAgentMock::default()), + response_skeleton_opt: None, + }; + let msg_b = OutboundPaymentsInstructions { + priced_templates: Either::Right(PricedRetryTxTemplates(vec![])), + agent: Box::new(BlockchainAgentMock::default()), + response_skeleton_opt: None, + }; + + assert_eq!(msg_a.detailed_scan_type(), DetailedScanType::NewPayables); + assert_eq!(msg_b.detailed_scan_type(), DetailedScanType::RetryPayables) + } } diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index ed35378c2..f52b1a0c8 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -1,8 +1,9 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, +}; use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForReceivables, SentPayables, @@ -20,20 +21,19 @@ use crate::sub_lib::accountant::ReportRoutingServiceProvidedMessage; use crate::sub_lib::accountant::ReportServicesConsumedMessage; use crate::sub_lib::blockchain_bridge::BlockchainBridgeSubs; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use crate::sub_lib::configurator::ConfiguratorSubs; use crate::sub_lib::dispatcher::InboundClientData; use crate::sub_lib::dispatcher::{DispatcherSubs, StreamShutdownMsg}; use crate::sub_lib::hopper::IncipientCoresPackage; use crate::sub_lib::hopper::{ExpiredCoresPackage, NoLookupIncipientCoresPackage}; use crate::sub_lib::hopper::{HopperSubs, MessageType}; use crate::sub_lib::neighborhood::NeighborhoodSubs; -use crate::sub_lib::neighborhood::{ConfigChangeMsg, ConnectionProgressMessage}; - -use crate::sub_lib::configurator::ConfiguratorSubs; use crate::sub_lib::neighborhood::NodeQueryResponseMetadata; use crate::sub_lib::neighborhood::RemoveNeighborMessage; use crate::sub_lib::neighborhood::RouteQueryMessage; use crate::sub_lib::neighborhood::RouteQueryResponse; use crate::sub_lib::neighborhood::UpdateNodeRecordMetadataMessage; +use crate::sub_lib::neighborhood::{ConfigChangeMsg, ConnectionProgressMessage}; use crate::sub_lib::neighborhood::{DispatcherNodeQueryMessage, GossipFailure_0v1}; use crate::sub_lib::peer_actors::PeerActors; use crate::sub_lib::peer_actors::{BindMessage, NewPublicIp, StartMessage}; @@ -131,7 +131,7 @@ recorder_message_handler_t_m_p!(AddReturnRouteMessage); recorder_message_handler_t_m_p!(AddRouteResultMessage); recorder_message_handler_t_p!(AddStreamMsg); recorder_message_handler_t_m_p!(BindMessage); -recorder_message_handler_t_p!(BlockchainAgentWithContextMessage); +recorder_message_handler_t_p!(PricedTemplatesMessage); recorder_message_handler_t_m_p!(ConfigChangeMsg); recorder_message_handler_t_m_p!(ConnectionProgressMessage); recorder_message_handler_t_m_p!(CrashNotification); @@ -155,7 +155,7 @@ recorder_message_handler_t_m_p!(NoLookupIncipientCoresPackage); recorder_message_handler_t_p!(OutboundPaymentsInstructions); 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!(InitialTemplatesMessage); recorder_message_handler_t_m_p!(ReceivedPayments); recorder_message_handler_t_m_p!(RemoveNeighborMessage); recorder_message_handler_t_m_p!(RemoveStreamMsg); @@ -527,7 +527,7 @@ pub fn make_accountant_subs_from_recorder(addr: &Addr) -> AccountantSu report_routing_service_provided: recipient!(addr, ReportRoutingServiceProvidedMessage), report_exit_service_provided: recipient!(addr, ReportExitServiceProvidedMessage), report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), - report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), + report_payable_payments_setup: recipient!(addr, PricedTemplatesMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), report_transaction_status: recipient!(addr, TxReceiptsMessage), @@ -549,7 +549,7 @@ pub fn make_blockchain_bridge_subs_from_recorder(addr: &Addr) -> Block BlockchainBridgeSubs { bind: recipient!(addr, BindMessage), outbound_payments_instructions: recipient!(addr, OutboundPaymentsInstructions), - qualified_payables: recipient!(addr, QualifiedPayablesMessage), + qualified_payables: recipient!(addr, InitialTemplatesMessage), retrieve_transactions: recipient!(addr, RetrieveTransactions), ui_sub: recipient!(addr, NodeFromUiMessage), request_transaction_receipts: recipient!(addr, RequestTransactionReceipts),