From 15347614df1cd2ade1fa8fe22f200fd79ee8de23 Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta <32920299+utkarshg6@users.noreply.github.com> Date: Fri, 4 Apr 2025 17:11:46 +0530 Subject: [PATCH 01/37] GH-601: Introduce SentPayable Table (#611) * GH-601: introduce sent_payable table * GH-601: test that table with certain columns were created during migration * GH-601: resuse code to test fields of table * GH-601: migrate utils code * GH-601: trigger actions * GH-601: fix test constants_have_correct_values * GH-601: review changes * GH-601: move constant to database/test_utils.rs --- masq_lib/src/constants.rs | 2 +- node/src/database/db_initializer.rs | 57 ++++++++++++- .../src/database/db_migrations/db_migrator.rs | 2 + .../migrations/migration_10_to_11.rs | 85 +++++++++++++++++++ .../migrations/migration_8_to_9.rs | 27 +++--- .../migrations/migration_9_to_10.rs | 27 +++--- .../database/db_migrations/migrations/mod.rs | 2 + node/src/database/test_utils/mod.rs | 37 ++++++++ node/src/test_utils/database_utils.rs | 5 ++ 9 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 node/src/database/db_migrations/migrations/migration_10_to_11.rs diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index 155bff5cc..bcd1b94c7 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -5,7 +5,7 @@ use crate::data_version::DataVersion; use const_format::concatcp; pub const DEFAULT_CHAIN: Chain = Chain::PolyMainnet; -pub const CURRENT_SCHEMA_VERSION: usize = 10; +pub const CURRENT_SCHEMA_VERSION: usize = 11; pub const HIGHEST_RANDOM_CLANDESTINE_PORT: u16 = 9999; pub const HTTP_PORT: u16 = 80; diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index be5547576..0f9685c2b 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -135,6 +135,7 @@ impl DbInitializerReal { Self::create_config_table(conn); Self::initialize_config(conn, external_params); Self::create_payable_table(conn); + Self::create_sent_payable_table(conn); Self::create_pending_payable_table(conn); Self::create_receivable_table(conn); Self::create_banned_table(conn); @@ -258,6 +259,31 @@ impl DbInitializerReal { Self::set_config_value(conn, "max_block_count", None, false, "maximum block count"); } + pub fn create_sent_payable_table(conn: &Connection) { + conn.execute( + "create table if not exists sent_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei integer not null, + nonce integer not null, + status text not null, + retried integer not null + )", + [], + ) + .expect("Can't create sent_payable table"); + + conn.execute( + "CREATE UNIQUE INDEX sent_payable_tx_hash_idx ON sent_payable (tx_hash)", + [], + ) + .expect("Can't create transaction hash index in sent payments"); + } + pub fn create_pending_payable_table(conn: &Connection) { conn.execute( "create table if not exists pending_payable ( @@ -621,6 +647,7 @@ impl Debug for DbInitializationConfig { mod tests { use super::*; use crate::database::db_initializer::InitializationError::SqliteError; + use crate::database::test_utils::SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE; use crate::db_config::config_dao::{ConfigDao, ConfigDaoReal}; use crate::test_utils::database_utils::{ assert_create_table_stm_contains_all_parts, @@ -652,7 +679,7 @@ mod tests { #[test] fn constants_have_correct_values() { assert_eq!(DATABASE_FILE, "node-data.db"); - assert_eq!(CURRENT_SCHEMA_VERSION, 10); + assert_eq!(CURRENT_SCHEMA_VERSION, 11); } #[test] @@ -713,6 +740,34 @@ mod tests { ) } + #[test] + fn db_initialize_creates_sent_payable_table() { + let home_dir = ensure_node_home_directory_does_not_exist( + "db_initializer", + "db_initialize_creates_sent_payable_table", + ); + let subject = DbInitializerReal::default(); + + let conn = subject + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + + let mut stmt = conn.prepare("select rowid, tx_hash, receiver_address, amount_high_b, amount_low_b, timestamp, gas_price_wei, nonce, status, retried from sent_payable").unwrap(); + let mut sent_payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); + assert!(sent_payable_contents.next().is_none()); + assert_create_table_stm_contains_all_parts( + &*conn, + "sent_payable", + SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + ); + let expected_key_words: &[&[&str]] = &[&["tx_hash"]]; + assert_index_stm_is_coupled_with_right_parameter( + conn.as_ref(), + "sent_payable_tx_hash_idx", + expected_key_words, + ) + } + #[test] fn db_initialize_creates_payable_table() { let home_dir = ensure_node_home_directory_does_not_exist( diff --git a/node/src/database/db_migrations/db_migrator.rs b/node/src/database/db_migrations/db_migrator.rs index 7d1ec4f8c..369a78788 100644 --- a/node/src/database/db_migrations/db_migrator.rs +++ b/node/src/database/db_migrations/db_migrator.rs @@ -2,6 +2,7 @@ use crate::database::db_initializer::ExternalData; use crate::database::db_migrations::migrations::migration_0_to_1::Migrate_0_to_1; +use crate::database::db_migrations::migrations::migration_10_to_11::Migrate_10_to_11; use crate::database::db_migrations::migrations::migration_1_to_2::Migrate_1_to_2; use crate::database::db_migrations::migrations::migration_2_to_3::Migrate_2_to_3; use crate::database::db_migrations::migrations::migration_3_to_4::Migrate_3_to_4; @@ -80,6 +81,7 @@ impl DbMigratorReal { &Migrate_7_to_8, &Migrate_8_to_9, &Migrate_9_to_10, + &Migrate_10_to_11, ] } diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs new file mode 100644 index 000000000..4b683208d --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -0,0 +1,85 @@ +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; + +#[allow(non_camel_case_types)] +pub struct Migrate_10_to_11; + +impl DatabaseMigration for Migrate_10_to_11 { + fn migrate<'a>( + &self, + declaration_utils: Box, + ) -> rusqlite::Result<()> { + let sql_statement = "create table if not exists sent_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei integer not null, + nonce integer not null, + status text not null, + retried integer not null + )"; + + declaration_utils.execute_upon_transaction(&[&sql_statement]) + } + + fn old_version(&self) -> usize { + 10 + } +} + +#[cfg(test)] +mod tests { + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::database::test_utils::SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE; + use crate::test_utils::database_utils::{ + assert_create_table_stm_contains_all_parts, assert_table_exists, + bring_db_0_back_to_life_and_return_connection, make_external_data, + }; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use std::fs::create_dir_all; + + #[test] + fn migration_from_10_to_11_is_applied_correctly() { + init_test_logging(); + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_10_to_11_is_properly_set", + ); + create_dir_all(&dir_path).unwrap(); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + + let _prev_connection = subject + .initialize_to_version( + &dir_path, + 10, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + + let connection = subject + .initialize_to_version( + &dir_path, + 11, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + + assert_table_exists(connection.as_ref(), "sent_payable"); + assert_create_table_stm_contains_all_parts( + &*connection, + "sent_payable", + SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + ); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + "DbMigrator: Database successfully migrated from version 10 to 11", + ]); + } +} diff --git a/node/src/database/db_migrations/migrations/migration_8_to_9.rs b/node/src/database/db_migrations/migrations/migration_8_to_9.rs index 4bf95e955..eb89ac002 100644 --- a/node/src/database/db_migrations/migrations/migration_8_to_9.rs +++ b/node/src/database/db_migrations/migrations/migration_8_to_9.rs @@ -43,21 +43,22 @@ mod tests { let _ = bring_db_0_back_to_life_and_return_connection(&db_path); let subject = DbInitializerReal::default(); - let result = subject.initialize_to_version( - &dir_path, - 8, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); - - assert!(result.is_ok()); + let _prev_connection = subject + .initialize_to_version( + &dir_path, + 8, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let result = subject.initialize_to_version( - &dir_path, - 9, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); + let connection = subject + .initialize_to_version( + &dir_path, + 9, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let connection = result.unwrap(); let (mp_value, mp_encrypted) = retrieve_config_row(connection.as_ref(), "max_block_count"); let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); assert_eq!(mp_value, None); diff --git a/node/src/database/db_migrations/migrations/migration_9_to_10.rs b/node/src/database/db_migrations/migrations/migration_9_to_10.rs index 7622ef01f..be240429a 100644 --- a/node/src/database/db_migrations/migrations/migration_9_to_10.rs +++ b/node/src/database/db_migrations/migrations/migration_9_to_10.rs @@ -43,21 +43,22 @@ mod tests { let _ = bring_db_0_back_to_life_and_return_connection(&db_path); let subject = DbInitializerReal::default(); - let result = subject.initialize_to_version( - &dir_path, - 9, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); - - assert!(result.is_ok()); + let _prev_connection = subject + .initialize_to_version( + &dir_path, + 9, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let result = subject.initialize_to_version( - &dir_path, - 10, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); + let connection = subject + .initialize_to_version( + &dir_path, + 10, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); - let connection = result.unwrap(); let (mp_value, mp_encrypted) = retrieve_config_row(connection.as_ref(), "max_block_count"); let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); assert_eq!(mp_value, Some(100_000u64.to_string())); diff --git a/node/src/database/db_migrations/migrations/mod.rs b/node/src/database/db_migrations/migrations/mod.rs index bcdb14176..e093df006 100644 --- a/node/src/database/db_migrations/migrations/mod.rs +++ b/node/src/database/db_migrations/migrations/mod.rs @@ -10,3 +10,5 @@ pub mod migration_6_to_7; pub mod migration_7_to_8; pub mod migration_8_to_9; pub mod migration_9_to_10; +#[rustfmt::skip] +pub mod migration_10_to_11; diff --git a/node/src/database/test_utils/mod.rs b/node/src/database/test_utils/mod.rs index e8b9060f1..a9c5fac77 100644 --- a/node/src/database/test_utils/mod.rs +++ b/node/src/database/test_utils/mod.rs @@ -12,6 +12,19 @@ use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +pub const SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE: &[&[&str]] = &[ + &["rowid", "integer", "primary", "key"], + &["tx_hash", "text", "not", "null"], + &["receiver_address", "text", "not", "null"], + &["amount_high_b", "integer", "not", "null"], + &["amount_low_b", "integer", "not", "null"], + &["timestamp", "integer", "not", "null"], + &["gas_price_wei", "integer", "not", "null"], + &["nonce", "integer", "not", "null"], + &["status", "text", "not", "null"], + &["retried", "integer", "not", "null"], +]; + #[derive(Debug, Default)] pub struct ConnectionWrapperMock<'conn> { prepare_params: Arc>>, @@ -114,3 +127,27 @@ impl DbInitializerMock { self } } + +#[cfg(test)] +mod tests { + use crate::database::test_utils::SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE; + + #[test] + fn constants_have_correct_values() { + assert_eq!( + SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + &[ + &["rowid", "integer", "primary", "key"], + &["tx_hash", "text", "not", "null"], + &["receiver_address", "text", "not", "null"], + &["amount_high_b", "integer", "not", "null"], + &["amount_low_b", "integer", "not", "null"], + &["timestamp", "integer", "not", "null"], + &["gas_price_wei", "integer", "not", "null"], + &["nonce", "integer", "not", "null"], + &["status", "text", "not", "null"], + &["retried", "integer", "not", "null"] + ] + ); + } +} diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index 02ba441a4..fb8ba3a83 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -103,6 +103,11 @@ pub fn retrieve_config_row(conn: &dyn ConnectionWrapper, name: &str) -> (Option< }) } +pub fn assert_table_exists(conn: &dyn ConnectionWrapper, table_name: &str) { + let result = conn.prepare(&format!("select * from {}", table_name)); + assert!(result.is_ok(), "Table {} should exist", table_name); +} + pub fn assert_table_does_not_exist(conn: &dyn ConnectionWrapper, table_name: &str) { let error_stm = conn .prepare(&format!("select * from {}", table_name)) From 24eb855e6751f2d6114df0cca76c339ec0485526 Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta <32920299+utkarshg6@users.noreply.github.com> Date: Thu, 15 May 2025 15:21:25 +0530 Subject: [PATCH 02/37] GH-608: Redesign the PendingPayableScanner for `sent_payable table` (manily its DAO) (#617) * GH-608: change the trait object to support SentPayables * Revert "GH-608: change the trait object to support SentPayables" This reverts commit df415c0bfd0448db7b7caebd0ed4ea77058bd305. * GH-608: introduce sent payable dao * GH-608: add boilerplate code * GH-608: implement insert_new_records * GH-608: add test cases for the panic and error clause for the insert method * GH-608: introduce builder pattern for the Tx * GH-608: implement get_tx_identifiers * GH-608: implement retrieve_pending_txs() * GH-608: add more assertions for the retrieve_pending_txs() and implement Display for TxStatus * GH-608: rename time_t in fn names to unix_timestamp * GH-608: refactor sent_payable_dao.rs * GH-608: add stronger tests for insert_new_records() * GH-608: retrieve txs conditionally * GH-608: change SQL to uppercase wherever necessary * GH-608: add RetrieveCondtion::ToRetry * GH-608: add constant RETRY_THRESHOLD_SECS * GH-608: implement display and from_str() for TxStatus * GH-608: implement retrieve_condition_display_works * GH-608: update the query for txs_to_retry and reorder trait functions * GH-608: change SQL query for txs to retry * GH-608: remove RETRY_THRESHOLD_SECS * GH-608: remove retried column from the sent_payable table * GH-608: add ability to retrieve tx by hash * GH-608: add the ability to update statuses * GH-608: add types for TxHash and RowID * GH-608: add test for deleting records * GH-608: add fn for deleting records * GH-608: add more TODOs * GH-608: error testing * GH-608: write tests for error handling while deleting records * GH-608: add more TODOs * GH-608: return SqlExecutionFailed in insert, change_status and delete operations * GH-608: add better error handling for delete records * GH-608: change the signature of delete_records() to accept Hashset * GH-608: remove unused SentPayableDaoError * GH-608: use the variant EmptyInput * GH-608: add more validations for insert_new_records * GH-608: test all errors * GH-608: remove unnecessary TODOs * GH-608: perform some cleanup * GH-608: eliminate clippy warnings * GH-608: review 1 changes * GH-608: remove clippy warnings * GH-608: review 2 changes --- node/src/accountant/db_access_objects/mod.rs | 2 + .../db_access_objects/payable_dao.rs | 77 +- .../db_access_objects/pending_payable_dao.rs | 26 +- .../db_access_objects/receivable_dao.rs | 141 +-- .../db_access_objects/sent_payable_dao.rs | 908 ++++++++++++++++++ .../db_access_objects/test_utils.rs | 51 + .../src/accountant/db_access_objects/utils.rs | 28 +- node/src/accountant/mod.rs | 31 +- node/src/accountant/scanners/mod.rs | 30 +- .../src/accountant/scanners/scanners_utils.rs | 18 +- node/src/accountant/test_utils.rs | 26 +- node/src/blockchain/blockchain_bridge.rs | 8 +- .../lower_level_interface_web3.rs | 116 ++- .../blockchain_interface_web3/utils.rs | 6 +- node/src/database/db_initializer.rs | 5 +- .../src/database/db_migrations/db_migrator.rs | 2 +- .../migrations/migration_10_to_11.rs | 3 +- .../migrations/migration_4_to_5.rs | 8 +- node/src/database/test_utils/mod.rs | 2 - node/tests/financials_test.rs | 8 +- 20 files changed, 1288 insertions(+), 208 deletions(-) create mode 100644 node/src/accountant/db_access_objects/sent_payable_dao.rs create mode 100644 node/src/accountant/db_access_objects/test_utils.rs diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index a350148ab..cf1ca4611 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -4,4 +4,6 @@ pub mod banned_dao; pub mod payable_dao; pub mod pending_payable_dao; pub mod receivable_dao; +pub mod sent_payable_dao; +mod test_utils; pub mod utils; diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 88897281b..c7d438a41 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -7,7 +7,7 @@ use crate::accountant::db_big_integer::big_int_db_processor::{BigIntDbProcessor, use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::db_access_objects::utils; use crate::accountant::db_access_objects::utils::{ - sum_i128_values_from_table, to_time_t, AssemblerFeeder, CustomQuery, DaoFactoryReal, + sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten, }; use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::{ @@ -100,7 +100,7 @@ impl PayableDao for PayableDaoReal { let update_clause_with_compensated_overflow = "update payable set \ balance_high_b = :balance_high_b, balance_low_b = :balance_low_b where wallet_address = :wallet"; - let last_paid_timestamp = to_time_t(timestamp); + let last_paid_timestamp = to_unix_timestamp(timestamp); let params = SQLParamsBuilder::default() .key(WalletAddress(wallet)) .wei_change(WeiChange::new( @@ -158,7 +158,7 @@ impl PayableDao for PayableDaoReal { pending_payable_rowid = null where pending_payable_rowid = :rowid"; let i64_rowid = checked_conversion::(pending_payable_fingerprint.rowid); - let last_paid = to_time_t(pending_payable_fingerprint.timestamp); + let last_paid = to_unix_timestamp(pending_payable_fingerprint.timestamp); let params = SQLParamsBuilder::default() .key( PendingPayableRowid(&i64_rowid)) .wei_change(WeiChange::new( "balance", pending_payable_fingerprint.amount, WeiChangeDirection::Subtraction)) @@ -196,7 +196,7 @@ impl PayableDao for PayableDaoReal { balance_wei: checked_conversion::(BigIntDivider::reconstitute( high_b, low_b, )), - last_paid_timestamp: utils::from_time_t(last_paid_timestamp), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), pending_payable_opt: None, }) } @@ -282,7 +282,7 @@ impl PayableDao for PayableDaoReal { balance_wei: checked_conversion::(BigIntDivider::reconstitute( high_bytes, low_bytes, )), - last_paid_timestamp: utils::from_time_t(last_paid_timestamp), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), pending_payable_opt: match rowid { Some(rowid) => Some(PendingPayableId::new( u64::try_from(rowid).unwrap(), @@ -338,7 +338,7 @@ impl PayableDaoReal { balance_wei: checked_conversion::(BigIntDivider::reconstitute( high_bytes, low_bytes, )), - last_paid_timestamp: utils::from_time_t(last_paid_timestamp), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), pending_payable_opt: rowid_opt.map(|rowid| { let hash_str = hash_opt.expect("database corrupt; missing hash but existing rowid"); @@ -541,7 +541,7 @@ mod mark_pending_payable_associated_functions { #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::{from_time_t, now_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, current_unix_timestamp, to_unix_timestamp}; use crate::accountant::gwei_to_wei; use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::explanatory_extension; use crate::accountant::test_utils::{assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_pending_payable_fingerprint, trick_rusqlite_with_read_only_conn}; @@ -577,7 +577,10 @@ mod tests { let status = subject.account_status(&wallet).unwrap(); assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, 1234); - assert_eq!(to_time_t(status.last_paid_timestamp), to_time_t(now)); + assert_eq!( + to_unix_timestamp(status.last_paid_timestamp), + to_unix_timestamp(now) + ); } #[test] @@ -616,8 +619,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, expected_balance); assert_eq!( - to_time_t(status.last_paid_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_paid_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); }; assert_account(wallet, initial_value + balance_change); @@ -653,8 +656,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, initial_value + balance_change); assert_eq!( - to_time_t(status.last_paid_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_paid_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); } @@ -746,13 +749,13 @@ mod tests { PayableAccount { wallet: wallet_0, balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), - last_paid_timestamp: from_time_t(45678), + last_paid_timestamp: from_unix_timestamp(45678), pending_payable_opt: None, }, PayableAccount { wallet: wallet_1, balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), - last_paid_timestamp: from_time_t(150_000_000), + last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: Some(PendingPayableId::new( pending_payable_rowid_1, make_tx_hash(0) @@ -762,7 +765,7 @@ mod tests { PayableAccount { wallet: wallet_2, balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), - last_paid_timestamp: from_time_t(151_000_000), + last_paid_timestamp: from_unix_timestamp(151_000_000), pending_payable_opt: Some(PendingPayableId::new( pending_payable_rowid_2, make_tx_hash(0) @@ -907,12 +910,12 @@ mod tests { let hash_1 = make_tx_hash(12345); let rowid_1 = 789; let previous_timestamp_1_s = 190_000_000; - let new_payable_timestamp_1 = from_time_t(199_000_000); + let new_payable_timestamp_1 = from_unix_timestamp(199_000_000); let wallet_1 = make_wallet("bobble"); let hash_2 = make_tx_hash(54321); let rowid_2 = 792; let previous_timestamp_2_s = 187_100_000; - let new_payable_timestamp_2 = from_time_t(191_333_000); + let new_payable_timestamp_2 = from_unix_timestamp(191_333_000); let wallet_2 = make_wallet("booble bobble"); { insert_payable_record_fn( @@ -946,8 +949,8 @@ mod tests { amount: balance_change_2, process_error: None, }; - let previous_timestamp_1 = from_time_t(previous_timestamp_1_s); - let previous_timestamp_2 = from_time_t(previous_timestamp_2_s); + let previous_timestamp_1 = from_unix_timestamp(previous_timestamp_1_s); + let previous_timestamp_2 = from_unix_timestamp(previous_timestamp_2_s); TestSetupValuesHolder { fingerprint_1, fingerprint_2, @@ -1203,13 +1206,13 @@ mod tests { PayableAccount { wallet: make_wallet("foobar"), balance_wei: 1234567890123456 as u128, - last_paid_timestamp: from_time_t(111_111_111), + last_paid_timestamp: from_unix_timestamp(111_111_111), pending_payable_opt: None }, PayableAccount { wallet: make_wallet("barfoo"), balance_wei: 1234567890123456 as u128, - last_paid_timestamp: from_time_t(111_111_111), + last_paid_timestamp: from_unix_timestamp(111_111_111), pending_payable_opt: None }, ] @@ -1304,7 +1307,7 @@ mod tests { //Accounts of balances smaller than one gwei don't qualify. //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, //here by balance and then by age. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( "custom_query_in_top_records_mode_with_default_ordering", @@ -1324,13 +1327,13 @@ mod tests { PayableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 7_562_000_300_000, - last_paid_timestamp: from_time_t(now - 86_001), + last_paid_timestamp: from_unix_timestamp(now - 86_001), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_401), + last_paid_timestamp: from_unix_timestamp(now - 86_401), pending_payable_opt: Some(PendingPayableId::new( 1, H256::from_str( @@ -1342,7 +1345,7 @@ mod tests { PayableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_300), + last_paid_timestamp: from_unix_timestamp(now - 86_300), pending_payable_opt: None }, ] @@ -1354,7 +1357,7 @@ mod tests { //Accounts of balances smaller than one gwei don't qualify. //Two accounts differ only in balance but not in the debt's age which allows to check doubled ordering, //here by age and then by balance. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( "custom_query_in_top_records_mode_ordered_by_age", @@ -1374,7 +1377,7 @@ mod tests { PayableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_401), + last_paid_timestamp: from_unix_timestamp(now - 86_401), pending_payable_opt: Some(PendingPayableId::new( 1, H256::from_str( @@ -1386,13 +1389,13 @@ mod tests { PayableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), balance_wei: 1_000_000_002, - last_paid_timestamp: from_time_t(now - 86_401), + last_paid_timestamp: from_unix_timestamp(now - 86_401), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_300), + last_paid_timestamp: from_unix_timestamp(now - 86_300), pending_payable_opt: None }, ] @@ -1422,7 +1425,7 @@ mod tests { fn custom_query_in_range_mode() { //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, //by balance and then by age. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { insert( conn, @@ -1482,7 +1485,7 @@ mod tests { max_age_s: 200000, min_amount_gwei: 500_000_000, max_amount_gwei: 35_000_000_000, - timestamp: from_time_t(now), + timestamp: from_unix_timestamp(now), }) .unwrap(); @@ -1492,19 +1495,19 @@ mod tests { PayableAccount { wallet: Wallet::new("0x7777777777777777777777777777777777777777"), balance_wei: gwei_to_wei(2_500_647_000_u32), - last_paid_timestamp: from_time_t(now - 80_333), + last_paid_timestamp: from_unix_timestamp(now - 80_333), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x6666666666666666666666666666666666666666"), balance_wei: gwei_to_wei(1_800_456_000_u32), - last_paid_timestamp: from_time_t(now - 100_401), + last_paid_timestamp: from_unix_timestamp(now - 100_401), pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: gwei_to_wei(1_800_456_000_u32), - last_paid_timestamp: from_time_t(now - 55_120), + last_paid_timestamp: from_unix_timestamp(now - 55_120), pending_payable_opt: Some(PendingPayableId::new( 1, H256::from_str( @@ -1519,7 +1522,7 @@ mod tests { #[test] fn range_query_does_not_display_values_from_below_1_gwei() { - let now = now_time_t(); + let now = current_unix_timestamp(); let timestamp_1 = now - 11_001; let timestamp_2 = now - 5000; let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { @@ -1558,7 +1561,7 @@ mod tests { vec![PayableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 30_000_300_000, - last_paid_timestamp: from_time_t(timestamp_2), + last_paid_timestamp: from_unix_timestamp(timestamp_2), pending_payable_opt: None },] ) @@ -1570,7 +1573,7 @@ mod tests { let conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let timestamp = utils::now_time_t(); + let timestamp = utils::current_unix_timestamp(); insert_payable_record_fn( &*conn, "0x1111111111111111111111111111111111111111", diff --git a/node/src/accountant/db_access_objects/pending_payable_dao.rs b/node/src/accountant/db_access_objects/pending_payable_dao.rs index 67c779ce0..e555fcc9a 100644 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ b/node/src/accountant/db_access_objects/pending_payable_dao.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::utils::{ - from_time_t, to_time_t, DaoFactoryReal, VigilantRusqliteFlatten, + from_unix_timestamp, to_unix_timestamp, DaoFactoryReal, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; @@ -104,7 +104,7 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { let attempt: u16 = Self::get_with_expect(row, 5); Ok(PendingPayableFingerprint { rowid, - timestamp: from_time_t(timestamp), + timestamp: from_unix_timestamp(timestamp), hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { panic!( "Invalid hash format (\"{}\": {:?}) - database corrupt", @@ -133,7 +133,7 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { hashes_and_amounts: &[HashAndAmount], batch_wide_timestamp: SystemTime, ) -> String { - let time_t = to_time_t(batch_wide_timestamp); + let time_t = to_unix_timestamp(batch_wide_timestamp); comma_joined_stringifiable(hashes_and_amounts, |hash_and_amount| { let amount_checked = checked_conversion::(hash_and_amount.amount); let (high_bytes, low_bytes) = BigIntDivider::deconstruct(amount_checked); @@ -275,7 +275,7 @@ mod tests { use crate::accountant::db_access_objects::pending_payable_dao::{ PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, }; - use crate::accountant::db_access_objects::utils::from_time_t; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; @@ -304,7 +304,7 @@ mod tests { let amount_1 = 55556; let hash_2 = make_tx_hash(6789); let amount_2 = 44445; - let batch_wide_timestamp = from_time_t(200_000_000); + let batch_wide_timestamp = from_unix_timestamp(200_000_000); let subject = PendingPayableDaoReal::new(wrapped_conn); let hash_and_amount_1 = HashAndAmount { hash: hash_1, @@ -365,7 +365,7 @@ mod tests { let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); let hash = make_tx_hash(45466); let amount = 55556; - let timestamp = from_time_t(200_000_000); + let timestamp = from_unix_timestamp(200_000_000); let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); let hash_and_amount = HashAndAmount { hash, amount }; @@ -394,7 +394,7 @@ mod tests { let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); let hash_1 = make_tx_hash(4546); let amount_1 = 55556; - let batch_wide_timestamp = from_time_t(200_000_000); + let batch_wide_timestamp = from_unix_timestamp(200_000_000); let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); let hash_and_amount = HashAndAmount { hash: hash_1, @@ -414,7 +414,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_time_t(195_000_000); + let timestamp = from_unix_timestamp(195_000_000); // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, // then complain about its excessive size if supplied in unquoted strings let hash_1 = @@ -510,7 +510,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = PendingPayableDaoReal::new(wrapped_conn); - let batch_wide_timestamp = from_time_t(195_000_000); + let batch_wide_timestamp = from_unix_timestamp(195_000_000); let hash_1 = make_tx_hash(11119); let amount_1 = 787; let hash_2 = make_tx_hash(10000); @@ -568,7 +568,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_time_t(198_000_000); + let timestamp = from_unix_timestamp(198_000_000); let hash = make_tx_hash(10000); let amount = 333; let hash_and_amount_1 = HashAndAmount { @@ -750,7 +750,7 @@ mod tests { hash: hash_3, amount: 3344, }; - let timestamp = from_time_t(190_000_000); + let timestamp = from_unix_timestamp(190_000_000); let subject = PendingPayableDaoReal::new(conn); { subject @@ -842,7 +842,7 @@ mod tests { hash: hash_2, amount: amount_2, }; - let timestamp = from_time_t(190_000_000); + let timestamp = from_unix_timestamp(190_000_000); let subject = PendingPayableDaoReal::new(conn); { subject @@ -868,7 +868,7 @@ mod tests { let process_error: Option = row.get(6).unwrap(); Ok(PendingPayableFingerprint { rowid, - timestamp: from_time_t(timestamp), + timestamp: from_unix_timestamp(timestamp), hash: H256::from_str(&transaction_hash[2..]).unwrap(), attempt, amount: checked_conversion::(BigIntDivider::reconstitute( diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index 9b71a3939..ad8f52462 100644 --- a/node/src/accountant/db_access_objects/receivable_dao.rs +++ b/node/src/accountant/db_access_objects/receivable_dao.rs @@ -4,7 +4,7 @@ use crate::accountant::checked_conversion; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoError::RusqliteError; use crate::accountant::db_access_objects::utils; use crate::accountant::db_access_objects::utils::{ - sum_i128_values_from_table, to_time_t, AssemblerFeeder, CustomQuery, DaoFactoryReal, + sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, RangeStmConfig, ThresholdUtils, TopStmConfig, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; @@ -120,7 +120,7 @@ impl ReceivableDao for ReceivableDaoReal { let update_clause_with_compensated_overflow = "update receivable set balance_high_b = :balance_high_b, balance_low_b = :balance_low_b \ where wallet_address = :wallet"; - let last_received_timestamp = to_time_t(timestamp); + let last_received_timestamp = to_unix_timestamp(timestamp); let params = SQLParamsBuilder::default() .key(WalletAddress(wallet)) .wei_change(WeiChange::new( @@ -216,7 +216,7 @@ impl ReceivableDao for ReceivableDaoReal { named_params! { ":debt_threshold": checked_conversion::(payment_thresholds.debt_threshold_gwei), ":slope": slope, - ":sugg_and_grace": payment_thresholds.sugg_and_grace(to_time_t(now)), + ":sugg_and_grace": payment_thresholds.sugg_and_grace(to_unix_timestamp(now)), ":permanent_debt_allowed_high_b": permanent_debt_allowed_high_b, ":permanent_debt_allowed_low_b": permanent_debt_allowed_low_b }, @@ -337,7 +337,7 @@ impl ReceivableDaoReal { where wallet_address = :wallet"; match received_payments.iter().try_for_each(|received_payment| { - let last_received_timestamp = to_time_t(timestamp); + let last_received_timestamp = to_unix_timestamp(timestamp); let params = SQLParamsBuilder::default() .key(WalletAddress(&received_payment.from)) .wei_change(WeiChange::new( @@ -414,7 +414,7 @@ impl ReceivableDaoReal { Ok(ReceivableAccount { wallet, balance_wei: BigIntDivider::reconstitute(high_bytes, low_bytes), - last_received_timestamp: utils::from_time_t(last_received_timestamp), + last_received_timestamp: utils::from_unix_timestamp(last_received_timestamp), }) } e => panic!( @@ -493,7 +493,7 @@ impl TableNameDAO for ReceivableDaoReal { mod tests { use super::*; use crate::accountant::db_access_objects::utils::{ - from_time_t, now_time_t, to_time_t, CustomQuery, + current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, CustomQuery, }; use crate::accountant::gwei_to_wei; use crate::accountant::test_utils::{ @@ -609,8 +609,8 @@ mod tests { "receivable_dao", "more_money_receivable_works_for_new_address", ); - let payment_time_t = to_time_t(SystemTime::now()) - 1111; - let payment_time = from_time_t(payment_time_t); + let payment_time_t = to_unix_timestamp(SystemTime::now()) - 1111; + let payment_time = from_unix_timestamp(payment_time_t); let wallet = make_wallet("booga"); let subject = ReceivableDaoReal::new( DbInitializerReal::default() @@ -625,7 +625,10 @@ mod tests { let status = subject.account_status(&wallet).unwrap(); assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, 1234); - assert_eq!(to_time_t(status.last_received_timestamp), payment_time_t); + assert_eq!( + to_unix_timestamp(status.last_received_timestamp), + payment_time_t + ); } #[test] @@ -661,8 +664,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, expected_balance); assert_eq!( - to_time_t(status.last_received_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_received_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); }; assert_account(wallet, 1234 + 2345); @@ -695,8 +698,8 @@ mod tests { assert_eq!(status.wallet, wallet); assert_eq!(status.balance_wei, 1234 + i64::MAX as i128); assert_eq!( - to_time_t(status.last_received_timestamp), - to_time_t(SystemTime::UNIX_EPOCH) + to_unix_timestamp(status.last_received_timestamp), + to_unix_timestamp(SystemTime::UNIX_EPOCH) ); } @@ -812,15 +815,15 @@ mod tests { assert_eq!(status1.wallet, debtor1); assert_eq!(status1.balance_wei, first_expected_result); assert_eq!( - to_time_t(status1.last_received_timestamp), - to_time_t(payment_time) + to_unix_timestamp(status1.last_received_timestamp), + to_unix_timestamp(payment_time) ); let status2 = subject.account_status(&debtor2).unwrap(); assert_eq!(status2.wallet, debtor2); assert_eq!(status2.balance_wei, second_expected_result); assert_eq!( - to_time_t(status2.last_received_timestamp), - to_time_t(payment_time) + to_unix_timestamp(status2.last_received_timestamp), + to_unix_timestamp(payment_time) ); } @@ -887,8 +890,8 @@ mod tests { first_initial_balance as i128 - 1111 ); assert_eq!( - to_time_t(actual_record_1.last_received_timestamp), - to_time_t(time_of_change) + to_unix_timestamp(actual_record_1.last_received_timestamp), + to_unix_timestamp(time_of_change) ); let actual_record_2 = subject.account_status(&unknown_wallet); assert!(actual_record_2.is_none()); @@ -899,8 +902,8 @@ mod tests { second_initial_balance as i128 - 9999 ); assert_eq!( - to_time_t(actual_record_3.last_received_timestamp), - to_time_t(time_of_change) + to_unix_timestamp(actual_record_3.last_received_timestamp), + to_unix_timestamp(time_of_change) ); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( @@ -1202,37 +1205,37 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut not_delinquent_inside_grace_period = make_receivable_account(1234, false); not_delinquent_inside_grace_period.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei + 1); not_delinquent_inside_grace_period.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) + 2); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) + 2); let mut not_delinquent_after_grace_below_slope = make_receivable_account(2345, false); not_delinquent_after_grace_below_slope.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 2); not_delinquent_after_grace_below_slope.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 1); let mut delinquent_above_slope_after_grace = make_receivable_account(3456, true); delinquent_above_slope_after_grace.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); delinquent_above_slope_after_grace.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 2); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 2); let mut not_delinquent_below_slope_before_stop = make_receivable_account(4567, false); not_delinquent_below_slope_before_stop.balance_wei = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); not_delinquent_below_slope_before_stop.last_received_timestamp = - from_time_t(payment_thresholds.sugg_thru_decreasing(now) + 2); + from_unix_timestamp(payment_thresholds.sugg_thru_decreasing(now) + 2); let mut delinquent_above_slope_before_stop = make_receivable_account(5678, true); delinquent_above_slope_before_stop.balance_wei = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2); delinquent_above_slope_before_stop.last_received_timestamp = - from_time_t(payment_thresholds.sugg_thru_decreasing(now) + 1); + from_unix_timestamp(payment_thresholds.sugg_thru_decreasing(now) + 1); let mut not_delinquent_above_slope_after_stop = make_receivable_account(6789, false); not_delinquent_above_slope_after_stop.balance_wei = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei - 1); not_delinquent_above_slope_after_stop.last_received_timestamp = - from_time_t(payment_thresholds.sugg_thru_decreasing(now) - 2); + from_unix_timestamp(payment_thresholds.sugg_thru_decreasing(now) - 2); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); add_receivable_account(&conn, ¬_delinquent_inside_grace_period); @@ -1243,7 +1246,7 @@ mod tests { add_receivable_account(&conn, ¬_delinquent_above_slope_after_stop); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &delinquent_above_slope_after_grace); assert_contains(&result, &delinquent_above_slope_before_stop); @@ -1260,15 +1263,15 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut not_delinquent = make_receivable_account(1234, false); not_delinquent.balance_wei = gwei_to_wei(105); not_delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 25); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 25); let mut delinquent = make_receivable_account(2345, true); delinquent.balance_wei = gwei_to_wei(105); delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 75); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 75); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies_shallow_slope"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); @@ -1276,7 +1279,7 @@ mod tests { add_receivable_account(&conn, &delinquent); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &delinquent); assert_eq!(result.len(), 1); @@ -1292,15 +1295,15 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut not_delinquent = make_receivable_account(1234, false); not_delinquent.balance_wei = gwei_to_wei(600); not_delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 25); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 25); let mut delinquent = make_receivable_account(2345, true); delinquent.balance_wei = gwei_to_wei(600); delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 75); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 75); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies_steep_slope"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); @@ -1308,7 +1311,7 @@ mod tests { add_receivable_account(&conn, &delinquent); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &delinquent); assert_eq!(result.len(), 1); @@ -1324,15 +1327,15 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let mut existing_delinquency = make_receivable_account(1234, true); existing_delinquency.balance_wei = gwei_to_wei(250); existing_delinquency.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 1); let mut new_delinquency = make_receivable_account(2345, true); new_delinquency.balance_wei = gwei_to_wei(250); new_delinquency.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_unix_timestamp(payment_thresholds.sugg_and_grace(now) - 1); let home_dir = ensure_node_home_directory_exists( "receivable_dao", "new_delinquencies_does_not_find_existing_delinquencies", @@ -1343,7 +1346,7 @@ mod tests { add_banned_account(&conn, &existing_delinquency); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_contains(&result, &new_delinquency); assert_eq!(result.len(), 1); @@ -1359,7 +1362,7 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = now_time_t(); + let now = current_unix_timestamp(); let home_dir = ensure_node_home_directory_exists( "receivable_dao", "new_delinquencies_work_for_still_empty_tables", @@ -1367,7 +1370,7 @@ mod tests { let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert!(result.is_empty()) } @@ -1387,24 +1390,24 @@ mod tests { threshold_interval_sec: 100, unban_below_gwei: 0, }; - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let sugg_and_grace = payment_thresholds.sugg_and_grace(now); let too_young_new_delinquency = ReceivableAccount { wallet: make_wallet("abc123"), balance_wei: 123_456_789_101_112, - last_received_timestamp: from_time_t(sugg_and_grace + 1), + last_received_timestamp: from_unix_timestamp(sugg_and_grace + 1), }; let ok_new_delinquency = ReceivableAccount { wallet: make_wallet("aaa999"), balance_wei: 123_456_789_101_112, - last_received_timestamp: from_time_t(sugg_and_grace - 1), + last_received_timestamp: from_unix_timestamp(sugg_and_grace - 1), }; let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); add_receivable_account(&conn, &too_young_new_delinquency); add_receivable_account(&conn, &ok_new_delinquency.clone()); let subject = ReceivableDaoReal::new(conn); - let result = subject.new_delinquencies(from_time_t(now), &payment_thresholds); + let result = subject.new_delinquencies(from_unix_timestamp(now), &payment_thresholds); assert_eq!(result, vec![ok_new_delinquency]) } @@ -1535,7 +1538,7 @@ mod tests { #[test] fn custom_query_in_top_records_mode_default_ordering() { - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = common_setup_of_accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_receivable( "custom_query_in_top_records_mode_default_ordering", @@ -1555,17 +1558,17 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 32_000_000_200, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ReceivableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 222_000), + last_received_timestamp: from_unix_timestamp(now - 222_000), }, ReceivableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ] ); @@ -1573,7 +1576,7 @@ mod tests { #[test] fn custom_query_in_top_records_mode_ordered_by_age() { - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = common_setup_of_accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_receivable( "custom_query_in_top_records_mode_ordered_by_age", @@ -1593,17 +1596,17 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 222_000), + last_received_timestamp: from_unix_timestamp(now - 222_000), }, ReceivableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 32_000_000_200, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ReceivableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), balance_wei: 1_000_000_001, - last_received_timestamp: from_time_t(now - 86_480), + last_received_timestamp: from_unix_timestamp(now - 86_480), }, ] ); @@ -1632,7 +1635,7 @@ mod tests { fn custom_query_in_range_mode() { //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, //by balance and then by age. - let now = now_time_t(); + let now = current_unix_timestamp(); let main_test_setup = |conn: &dyn ConnectionWrapper, insert: InsertReceivableHelperFn| { insert( conn, @@ -1692,7 +1695,7 @@ mod tests { max_age_s: 99000, min_amount_gwei: -560000, max_amount_gwei: 1_100_000_000, - timestamp: from_time_t(now), + timestamp: from_unix_timestamp(now), }) .unwrap(); @@ -1702,22 +1705,22 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x6666666666666666666666666666666666666666"), balance_wei: gwei_to_wei(1_050_444_230), - last_received_timestamp: from_time_t(now - 66_244), + last_received_timestamp: from_unix_timestamp(now - 66_244), }, ReceivableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: gwei_to_wei(1_000_000_230), - last_received_timestamp: from_time_t(now - 86_000), + last_received_timestamp: from_unix_timestamp(now - 86_000), }, ReceivableAccount { wallet: Wallet::new("0x3333333333333333333333333333333333333333"), balance_wei: gwei_to_wei(1_000_000_230), - last_received_timestamp: from_time_t(now - 70_000), + last_received_timestamp: from_unix_timestamp(now - 70_000), }, ReceivableAccount { wallet: Wallet::new("0x8888888888888888888888888888888888888888"), balance_wei: gwei_to_wei(-90), - last_received_timestamp: from_time_t(now - 66_000), + last_received_timestamp: from_unix_timestamp(now - 66_000), } ] ); @@ -1725,20 +1728,20 @@ mod tests { #[test] fn range_query_does_not_display_values_from_below_1_gwei() { - let timestamp1 = now_time_t() - 5000; - let timestamp2 = now_time_t() - 3232; + let timestamp1 = current_unix_timestamp() - 5000; + let timestamp2 = current_unix_timestamp() - 3232; let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertReceivableHelperFn| { insert( conn, "0x1111111111111111111111111111111111111111", 999_999_999, //smaller than 1 gwei - now_time_t() - 11_001, + current_unix_timestamp() - 11_001, ); insert( conn, "0x2222222222222222222222222222222222222222", -999_999_999, //smaller than -1 gwei - now_time_t() - 5_606, + current_unix_timestamp() - 5_606, ); insert( conn, @@ -1774,12 +1777,12 @@ mod tests { ReceivableAccount { wallet: Wallet::new("0x3333333333333333333333333333333333333333"), balance_wei: 30_000_300_000, - last_received_timestamp: from_time_t(timestamp1), + last_received_timestamp: from_unix_timestamp(timestamp1), }, ReceivableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), balance_wei: -2_000_300_000, - last_received_timestamp: from_time_t(timestamp2), + last_received_timestamp: from_unix_timestamp(timestamp2), } ] ) @@ -1793,7 +1796,7 @@ mod tests { .unwrap(); let insert = insert_account_by_separate_values; - let timestamp = utils::now_time_t(); + let timestamp = utils::current_unix_timestamp(); insert( &*conn, "0x1111111111111111111111111111111111111111", @@ -1855,7 +1858,7 @@ mod tests { &account.wallet, &high_bytes, &low_bytes, - &to_time_t(account.last_received_timestamp), + &to_unix_timestamp(account.last_received_timestamp), ]; stmt.execute(params).unwrap(); } diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs new file mode 100644 index 000000000..1ef307224 --- /dev/null +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -0,0 +1,908 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use ethereum_types::H256; +use web3::types::Address; +use masq_lib::utils::ExpectValue; +use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TxStatus; +use crate::database::rusqlite_wrappers::ConnectionWrapper; + +#[derive(Debug, PartialEq, Eq)] +pub enum SentPayableDaoError { + EmptyInput, + NoChange, + InvalidInput(String), + PartialExecution(String), + SqlExecutionFailed(String), +} + +type TxHash = H256; +type RowId = u64; + +type TxIdentifiers = HashMap; +type TxUpdates = HashMap; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Tx { + pub hash: TxHash, + pub receiver_address: Address, + pub amount: u128, + pub timestamp: i64, + pub gas_price_wei: u64, + pub nonce: u32, + pub status: TxStatus, +} + +pub enum RetrieveCondition { + IsPending, + ToRetry, + ByHash(Vec), +} + +impl Display for RetrieveCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RetrieveCondition::IsPending => { + write!(f, "WHERE status = 'Pending'") + } + RetrieveCondition::ToRetry => { + write!(f, "WHERE status = 'Failed'") + } + RetrieveCondition::ByHash(tx_hashes) => { + write!( + f, + "WHERE tx_hash IN ({})", + comma_joined_stringifiable(tx_hashes, |hash| format!("'{:?}'", hash)) + ) + } + } + } +} + +pub trait SentPayableDao { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> Vec; + fn change_statuses(&self, hash_map: &TxUpdates) -> Result<(), SentPayableDaoError>; + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; +} + +#[derive(Debug)] +pub struct SentPayableDaoReal<'a> { + conn: Box, +} + +impl<'a> SentPayableDaoReal<'a> { + pub fn new(conn: Box) -> Self { + Self { conn } + } +} + +impl SentPayableDao for SentPayableDaoReal<'_> { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + let hashes_vec: Vec = hashes.iter().copied().collect(); + let sql = format!( + "SELECT tx_hash, rowid FROM sent_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)) + ); + + let mut stmt = self + .conn + .prepare(&sql) + .expect("Failed to prepare SQL statement"); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let tx_hash = H256::from_str(&tx_hash_str[2..]).expect("Failed to parse H256"); + let row_id: u64 = row.get(1).expectv("rowid"); + + Ok((tx_hash, row_id)) + }) + .expect("Failed to execute query") + .filter_map(Result::ok) + .collect() + } + + fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError> { + if txs.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); + if unique_hashes.len() != txs.len() { + return Err(SentPayableDaoError::InvalidInput( + "Duplicate hashes found in the input".to_string(), + )); + } + + if !self.get_tx_identifiers(&unique_hashes).is_empty() { + return Err(SentPayableDaoError::InvalidInput( + "Input hash is already present in the database".to_string(), + )); + } + + let sql = format!( + "INSERT INTO sent_payable (\ + tx_hash, receiver_address, amount_high_b, amount_low_b, \ + timestamp, gas_price_wei, nonce, status + ) VALUES {}", + comma_joined_stringifiable(txs, |tx| { + let amount_checked = checked_conversion::(tx.amount); + let (high_bytes, low_bytes) = BigIntDivider::deconstruct(amount_checked); + format!( + "('{:?}', '{:?}', {}, {}, {}, {}, {}, '{}')", + tx.hash, + tx.receiver_address, + high_bytes, + low_bytes, + tx.timestamp, + tx.gas_price_wei, + tx.nonce, + tx.status + ) + }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(inserted_rows) => { + if inserted_rows == txs.len() { + Ok(()) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} out of {} records inserted", + inserted_rows, + txs.len() + ))) + } + } + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn retrieve_txs(&self, condition_opt: Option) -> Vec { + let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ + timestamp, gas_price_wei, nonce, status FROM sent_payable" + .to_string(); + let sql = match condition_opt { + None => raw_sql, + Some(condition) => format!("{} {}", raw_sql, condition), + }; + + let mut stmt = self + .conn + .prepare(&sql) + .expect("Failed to prepare SQL statement"); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let hash = H256::from_str(&tx_hash_str[2..]).expect("Failed to parse H256"); + let receiver_address_str: String = row.get(1).expectv("receivable_address"); + let receiver_address = + Address::from_str(&receiver_address_str[2..]).expect("Failed to parse H160"); + let amount_high_b = row.get(2).expectv("amount_high_b"); + let amount_low_b = row.get(3).expectv("amount_low_b"); + let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let timestamp = row.get(4).expectv("timestamp"); + let gas_price_wei = row.get(5).expectv("gas_price_wei"); + let nonce = row.get(6).expectv("nonce"); + let status_str: String = row.get(7).expectv("status"); + let status = TxStatus::from_str(&status_str).expect("Failed to parse TxStatus"); + + Ok(Tx { + hash, + receiver_address, + amount, + timestamp, + gas_price_wei, + nonce, + status, + }) + }) + .expect("Failed to execute query") + .filter_map(Result::ok) + .collect() + } + + fn change_statuses(&self, hash_map: &TxUpdates) -> Result<(), SentPayableDaoError> { + if hash_map.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + for (hash, status) in hash_map { + let sql = format!( + "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", + status, hash + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(updated_rows) => { + if updated_rows == 1 { + continue; + } else { + return Err(SentPayableDaoError::PartialExecution(format!( + "Failed to update status for hash {:?}", + hash + ))); + } + } + Err(e) => { + return Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())); + } + } + } + + Ok(()) + } + + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + if hashes.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let hashes_vec: Vec = hashes.iter().cloned().collect(); + let sql = format!( + "DELETE FROM sent_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| { format!("'{:?}'", hash) }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(deleted_rows) => { + if deleted_rows == hashes.len() { + Ok(()) + } else if deleted_rows == 0 { + Err(SentPayableDaoError::NoChange) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} of the {} hashes has been deleted.", + deleted_rows, + hashes.len(), + ))) + } + } + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + use crate::accountant::db_access_objects::sent_payable_dao::{RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal}; + use crate::accountant::db_access_objects::utils::current_unix_timestamp; + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::database::rusqlite_wrappers::ConnectionWrapperReal; + use crate::database::test_utils::ConnectionWrapperMock; + use ethereum_types::{ H256, U64}; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::{Connection, OpenFlags}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending, ToRetry}; + use crate::accountant::db_access_objects::test_utils::TxBuilder; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxStatus}; + + #[test] + fn insert_new_records_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "insert_new_records_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let tx1 = TxBuilder::default() + .hash(H256::from_low_u64_le(1)) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(H256::from_low_u64_le(2)) + .status(TxStatus::Failed) + .build(); + let tx3 = TxBuilder::default() + .hash(H256::from_low_u64_le(3)) + .status(TxStatus::Succeeded(TransactionBlock { + block_hash: Default::default(), + block_number: Default::default(), + })) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let txs = vec![tx1, tx2, tx3]; + + let result = subject.insert_new_records(&txs); + + let retrieved_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(retrieved_txs.len(), 3); + assert_eq!(retrieved_txs, txs); + } + + #[test] + fn insert_new_records_throws_err_for_empty_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_err_for_empty_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let empty_input = vec![]; + + let result = subject.insert_new_records(&empty_input); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = H256::from_low_u64_be(1234567890); + let tx1 = TxBuilder::default() + .hash(hash) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(hash) + .status(TxStatus::Failed) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.insert_new_records(&vec![tx1, tx2]); + + assert_eq!( + result, + Err(SentPayableDaoError::InvalidInput( + "Duplicate hashes found in the input".to_string() + )) + ); + } + + #[test] + fn insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = H256::from_low_u64_be(1234567890); + let tx1 = TxBuilder::default() + .hash(hash) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(hash) + .status(TxStatus::Failed) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let initial_insertion_result = subject.insert_new_records(&vec![tx1]); + + let result = subject.insert_new_records(&vec![tx2]); + + assert_eq!(initial_insertion_result, Ok(())); + assert_eq!( + result, + Err(SentPayableDaoError::InvalidInput( + "Input hash is already present in the database".to_string() + )) + ); + } + + #[test] + fn insert_new_records_returns_err_if_partially_executed() { + let setup_conn = Connection::open_in_memory().unwrap(); + setup_conn + .execute("CREATE TABLE example (id integer)", []) + .unwrap(); + let get_tx_identifiers_stmt = setup_conn.prepare("SELECT id FROM example").unwrap(); + let faulty_insert_stmt = { setup_conn.prepare("SELECT id FROM example").unwrap() }; + let wrapped_conn = ConnectionWrapperMock::default() + .prepare_result(Ok(get_tx_identifiers_stmt)) + .prepare_result(Ok(faulty_insert_stmt)); + let tx = TxBuilder::default().build(); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution( + "Only 0 out of 1 records inserted".to_string() + )) + ); + } + + #[test] + fn insert_new_records_can_throw_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_can_throw_error", + ); + { + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + } + let read_only_conn = Connection::open_with_flags( + home_dir.join(DATABASE_FILE), + OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .unwrap(); + let wrapped_conn = ConnectionWrapperReal::new(read_only_conn); + let tx = TxBuilder::default().build(); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn get_tx_identifiers_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "get_tx_identifiers_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let present_hash = H256::from_low_u64_le(1); + let absent_hash = H256::from_low_u64_le(2); + let another_present_hash = H256::from_low_u64_le(3); + let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); + let present_tx = TxBuilder::default().hash(present_hash).build(); + let another_present_tx = TxBuilder::default().hash(another_present_hash).build(); + subject + .insert_new_records(&vec![present_tx, another_present_tx]) + .unwrap(); + + let result = subject.get_tx_identifiers(&hashset); + + assert_eq!(result.get(&present_hash), Some(&1u64)); + assert_eq!(result.get(&absent_hash), None); + assert_eq!(result.get(&another_present_hash), Some(&2u64)); + } + + #[test] + fn retrieve_condition_display_works() { + assert_eq!(IsPending.to_string(), "WHERE status = 'Pending'"); + assert_eq!(ToRetry.to_string(), "WHERE status = 'Failed'"); + assert_eq!( + ByHash(vec![ + H256::from_low_u64_be(0x123456789), + H256::from_low_u64_be(0x987654321), + ]) + .to_string(), + "WHERE tx_hash IN (\ + '0x0000000000000000000000000000000000000000000000000000000123456789', \ + '0x0000000000000000000000000000000000000000000000000000000987654321'\ + )" + .to_string() + ); + } + + #[test] + fn can_retrieve_all_txs() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_all_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(H256::from_low_u64_le(1)) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(H256::from_low_u64_le(2)) + .status(TxStatus::Failed) + .build(); + let tx3 = TxBuilder::default() + .hash(H256::from_low_u64_le(3)) + .status(TxStatus::Succeeded(TransactionBlock { + block_hash: Default::default(), + block_number: Default::default(), + })) + .build(); + let tx4 = TxBuilder::default() + .hash(H256::from_low_u64_le(4)) + .status(TxStatus::Pending) + .build(); + let tx5 = TxBuilder::default() + .hash(H256::from_low_u64_le(5)) + .status(TxStatus::Failed) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone()]) + .unwrap(); + subject + .insert_new_records(&vec![tx4.clone(), tx5.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(None); + + assert_eq!(result, vec![tx1, tx2, tx3, tx4, tx5]); + } + + #[test] + fn can_retrieve_pending_txs() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_pending_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(H256::from_low_u64_le(1)) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(H256::from_low_u64_le(2)) + .status(TxStatus::Pending) + .build(); + let tx3 = TxBuilder::default() + .hash(H256::from_low_u64_le(3)) + .status(TxStatus::Failed) + .build(); + let tx4 = TxBuilder::default() + .hash(H256::from_low_u64_le(4)) + .status(TxStatus::Succeeded(TransactionBlock { + block_hash: Default::default(), + block_number: Default::default(), + })) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3, tx4]) + .unwrap(); + + let result = subject.retrieve_txs(Some(RetrieveCondition::IsPending)); + + assert_eq!(result, vec![tx1, tx2]); + } + + #[test] + fn can_retrieve_txs_to_retry() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_txs_to_retry"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let old_timestamp = current_unix_timestamp() - 60; // 1 minute old + let tx1 = TxBuilder::default() + .hash(H256::from_low_u64_le(3)) + .timestamp(old_timestamp) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(H256::from_low_u64_le(4)) + .timestamp(old_timestamp) + .status(TxStatus::Succeeded(TransactionBlock { + block_hash: Default::default(), + block_number: Default::default(), + })) + .build(); + // TODO: GH-631: Instead of fetching it from SentPayables, fetch it from the FailedPayables table + let tx3 = TxBuilder::default() // this should be picked for retry + .hash(H256::from_low_u64_le(5)) + .timestamp(old_timestamp) + .status(TxStatus::Failed) + .build(); + let tx4 = TxBuilder::default() // this should be picked for retry + .hash(H256::from_low_u64_le(6)) + .status(TxStatus::Failed) + .build(); + let tx5 = TxBuilder::default() + .hash(H256::from_low_u64_le(7)) + .timestamp(old_timestamp) + .status(TxStatus::Pending) + .build(); + subject + .insert_new_records(&vec![tx1, tx2, tx3.clone(), tx4.clone(), tx5]) + .unwrap(); + + let result = subject.retrieve_txs(Some(RetrieveCondition::ToRetry)); + + assert_eq!(result, vec![tx3, tx4]); + } + + #[test] + fn tx_can_be_retrieved_by_hash() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_hash"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(H256::from_low_u64_le(1)) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(H256::from_low_u64_le(2)) + .status(TxStatus::Failed) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(ByHash(vec![tx1.hash]))); + + assert_eq!(result, vec![tx1]); + } + + #[test] + fn change_statuses_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "change_statuses_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(H256::from_low_u64_le(1)) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(H256::from_low_u64_le(2)) + .status(TxStatus::Pending) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + let hash_map = HashMap::from([ + (tx1.hash, TxStatus::Failed), + ( + tx2.hash, + TxStatus::Succeeded(TransactionBlock { + block_hash: H256::from_low_u64_le(3), + block_number: U64::from(1), + }), + ), + ]); + + let result = subject.change_statuses(&hash_map); + + let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); + assert_eq!(result, Ok(())); + assert_eq!(updated_txs[0].status, TxStatus::Failed); + assert_eq!( + updated_txs[1].status, + TxStatus::Succeeded(TransactionBlock { + block_hash: H256::from_low_u64_le(3), + block_number: U64::from(1), + }) + ) + } + + #[test] + fn change_statuses_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "change_statuses_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let existent_hash = H256::from_low_u64_le(1); + let tx = TxBuilder::default() + .hash(existent_hash) + .status(TxStatus::Pending) + .build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hash_map = HashMap::new(); + + let result = subject.change_statuses(&hash_map); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn change_statuses_returns_error_during_partial_execution() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "change_statuses_returns_error_during_partial_execution", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let existent_hash = H256::from_low_u64_le(1); + let non_existent_hash = H256::from_low_u64_le(999); + let tx = TxBuilder::default() + .hash(existent_hash) + .status(TxStatus::Pending) + .build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hash_map = HashMap::from([ + (existent_hash, TxStatus::Failed), + (non_existent_hash, TxStatus::Failed), + ]); + + let result = subject.change_statuses(&hash_map); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution(format!( + "Failed to update status for hash {:?}", + non_existent_hash + ))) + ); + } + + #[test] + fn change_statuses_returns_error_when_an_error_occurs_while_executing_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "change_statuses_returns_error_when_an_error_occurs_while_executing_sql", + ); + { + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + } + let read_only_conn = Connection::open_with_flags( + home_dir.join(DATABASE_FILE), + OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .unwrap(); + let wrapped_conn = ConnectionWrapperReal::new(read_only_conn); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let hash = H256::from_low_u64_le(1); + let hash_map = HashMap::from([(hash, TxStatus::Failed)]); + + let result = subject.change_statuses(&hash_map); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn txs_can_be_deleted() { + let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "txs_can_be_deleted"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(H256::from_low_u64_le(1)) + .status(TxStatus::Pending) + .build(); + let tx2 = TxBuilder::default() + .hash(H256::from_low_u64_le(2)) + .status(TxStatus::Pending) + .build(); + let tx3 = TxBuilder::default() + .hash(H256::from_low_u64_le(3)) + .status(TxStatus::Failed) + .build(); + let tx4 = TxBuilder::default() + .hash(H256::from_low_u64_le(4)) + .status(TxStatus::Succeeded(TransactionBlock { + block_hash: Default::default(), + block_number: Default::default(), + })) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .unwrap(); + let hashset = HashSet::from([tx1.hash, tx3.hash]); + + let result = subject.delete_records(&hashset); + + let remaining_records = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(remaining_records, vec![tx2, tx4]); + } + + #[test] + fn delete_records_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.delete_records(&HashSet::new()); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn delete_records_returns_error_when_no_records_are_deleted() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_no_records_are_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let non_existent_hash = H256::from_low_u64_le(999); + let hashset = HashSet::from([non_existent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!(result, Err(SentPayableDaoError::NoChange)); + } + + #[test] + fn delete_records_returns_error_when_not_all_input_records_were_deleted() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_not_all_input_records_were_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let present_hash = H256::from_low_u64_le(1); + let absent_hash = H256::from_low_u64_le(2); + let tx = TxBuilder::default() + .hash(present_hash) + .status(TxStatus::Failed) + .build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hashset = HashSet::from([present_hash, absent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution( + "Only 1 of the 2 hashes has been deleted.".to_string() + )) + ); + } + + #[test] + fn delete_records_returns_a_general_error_from_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_a_general_error_from_sql", + ); + { + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + } + let read_only_conn = Connection::open_with_flags( + home_dir.join(DATABASE_FILE), + OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .unwrap(); + let wrapped_conn = ConnectionWrapperReal::new(read_only_conn); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let hashes = HashSet::from([H256::from_low_u64_le(1)]); + + let result = subject.delete_records(&hashes); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } +} diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs new file mode 100644 index 000000000..3a571ff6a --- /dev/null +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +#![cfg(test)] + +use web3::types::{Address, H256}; +use crate::accountant::db_access_objects::sent_payable_dao::Tx; +use crate::accountant::db_access_objects::utils::current_unix_timestamp; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TxStatus; + +#[derive(Default)] +pub struct TxBuilder { + hash_opt: Option, + receiver_address_opt: Option
, + amount_opt: Option, + timestamp_opt: Option, + gas_price_wei_opt: Option, + nonce_opt: Option, + status_opt: Option, +} + +impl TxBuilder { + pub fn default() -> Self { + Default::default() + } + + pub fn hash(mut self, hash: H256) -> Self { + self.hash_opt = Some(hash); + self + } + + pub fn timestamp(mut self, timestamp: i64) -> Self { + self.timestamp_opt = Some(timestamp); + self + } + + pub fn status(mut self, status: TxStatus) -> Self { + self.status_opt = Some(status); + self + } + + pub fn build(self) -> Tx { + Tx { + hash: self.hash_opt.unwrap_or_default(), + receiver_address: self.receiver_address_opt.unwrap_or_default(), + amount: self.amount_opt.unwrap_or_default(), + timestamp: self.timestamp_opt.unwrap_or_else(current_unix_timestamp), + gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + nonce: self.nonce_opt.unwrap_or_default(), + status: self.status_opt.unwrap_or(TxStatus::Pending), + } + } +} diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 8b78bb5f4..fe84712d7 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -21,7 +21,7 @@ use std::string::ToString; use std::time::Duration; use std::time::SystemTime; -pub fn to_time_t(system_time: SystemTime) -> i64 { +pub fn to_unix_timestamp(system_time: SystemTime) -> i64 { match system_time.duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => sign_conversion::(d.as_secs()).expect("MASQNode has expired"), Err(e) => panic!( @@ -31,12 +31,12 @@ pub fn to_time_t(system_time: SystemTime) -> i64 { } } -pub fn now_time_t() -> i64 { - to_time_t(SystemTime::now()) +pub fn current_unix_timestamp() -> i64 { + to_unix_timestamp(SystemTime::now()) } -pub fn from_time_t(time_t: i64) -> SystemTime { - let interval = Duration::from_secs(time_t as u64); +pub fn from_unix_timestamp(unix_timestamp: i64) -> SystemTime { + let interval = Duration::from_secs(unix_timestamp as u64); SystemTime::UNIX_EPOCH + interval } @@ -193,11 +193,11 @@ impl CustomQuery { max_age: u64, timestamp: SystemTime, ) -> RusqliteParamsWithOwnedToSql { - let now = to_time_t(timestamp); - let age_to_time_t = |age_limit| now - checked_conversion::(age_limit); + let now = to_unix_timestamp(timestamp); + let age_to_unix_timestamp = |age_limit| now - checked_conversion::(age_limit); vec![ - (":min_timestamp", Box::new(age_to_time_t(max_age))), - (":max_timestamp", Box::new(age_to_time_t(min_age))), + (":min_timestamp", Box::new(age_to_unix_timestamp(max_age))), + (":max_timestamp", Box::new(age_to_unix_timestamp(min_age))), ] } @@ -299,7 +299,7 @@ pub fn remap_receivable_accounts(accounts: Vec) -> Vec u64 { - (to_time_t(SystemTime::now()) - to_time_t(timestamp)) as u64 + (to_unix_timestamp(SystemTime::now()) - to_unix_timestamp(timestamp)) as u64 } #[allow(clippy::type_complexity)] @@ -466,8 +466,8 @@ mod tests { }; let assigned_value_1 = get_assigned_value(param_pair_1.1.to_sql().unwrap()); let assigned_value_2 = get_assigned_value(param_pair_2.1.to_sql().unwrap()); - assert_eq!(assigned_value_1, to_time_t(now) - 10000); - assert_eq!(assigned_value_2, to_time_t(now) - 5555) + assert_eq!(assigned_value_1, to_unix_timestamp(now) - 10000); + assert_eq!(assigned_value_2, to_unix_timestamp(now) - 5555) } #[test] @@ -608,10 +608,10 @@ mod tests { #[test] #[should_panic(expected = "Must be wrong, moment way far in the past")] - fn to_time_t_does_not_like_time_traveling() { + fn to_unix_timestamp_does_not_like_time_traveling() { let far_far_before = UNIX_EPOCH.checked_sub(Duration::from_secs(1)).unwrap(); - let _ = to_time_t(far_far_before); + let _ = to_unix_timestamp(far_far_before); } #[test] diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 0a74d076c..750c4c1a7 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1043,7 +1043,7 @@ mod tests { PendingPayable, PendingPayableDaoError, TransactionHashes, }; use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::protect_payables_in_test; @@ -2126,7 +2126,8 @@ mod tests { assert!(new_delinquencies_params.is_empty()); assert!( captured_timestamp < SystemTime::now() - && captured_timestamp >= from_time_t(to_time_t(SystemTime::now()) - 5) + && captured_timestamp + >= from_unix_timestamp(to_unix_timestamp(SystemTime::now()) - 5) ); assert_eq!(captured_curves, PaymentThresholds::default()); assert_eq!(paid_delinquencies_params.len(), 1); @@ -2517,13 +2518,13 @@ mod tests { unban_below_gwei: 10_000_000, }; let config = bc_from_earning_wallet(make_wallet("mine")); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let accounts = vec![ // below minimum balance, to the right of time intersection (inside buffer zone) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei - 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( payment_thresholds.threshold_interval_sec + 10, ), @@ -2534,7 +2535,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( payment_thresholds.maturity_threshold_sec - 10, ), @@ -2545,7 +2546,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet2"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 55), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( payment_thresholds.maturity_threshold_sec + 15, ), @@ -2592,7 +2593,7 @@ mod tests { payable_scan_interval: Duration::from_secs(50_000), receivable_scan_interval: Duration::from_secs(50_000), }); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let qualified_payables = vec![ // slightly above minimum balance, to the right of the curve (time intersection) PayableAccount { @@ -2600,7 +2601,7 @@ mod tests { balance_wei: gwei_to_wei( DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei + 1, ), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( DEFAULT_PAYMENT_THRESHOLDS.threshold_interval_sec + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec @@ -2613,7 +2614,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 10, ), @@ -2670,13 +2671,13 @@ mod tests { )) .start(); let pps_for_blockchain_bridge_sub = blockchain_bridge_addr.clone().recipient(); - let last_paid_timestamp = to_time_t(SystemTime::now()) + let last_paid_timestamp = to_unix_timestamp(SystemTime::now()) - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec as i64 - 1; let payable_account = PayableAccount { wallet: make_wallet("scan_for_payables"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t(last_paid_timestamp), + last_paid_timestamp: from_unix_timestamp(last_paid_timestamp), pending_payable_opt: None, }; let payable_dao = payable_dao @@ -2753,7 +2754,7 @@ mod tests { .start(); let payable_fingerprint_1 = PendingPayableFingerprint { rowid: 555, - timestamp: from_time_t(210_000_000), + timestamp: from_unix_timestamp(210_000_000), hash: make_tx_hash(45678), attempt: 1, amount: 4444, @@ -2761,7 +2762,7 @@ mod tests { }; let payable_fingerprint_2 = PendingPayableFingerprint { rowid: 550, - timestamp: from_time_t(210_000_100), + timestamp: from_unix_timestamp(210_000_100), hash: make_tx_hash(112233), attempt: 2, amount: 7999, @@ -3806,7 +3807,7 @@ mod tests { }; let fingerprint_1 = PendingPayableFingerprint { rowid: 5, - timestamp: from_time_t(200_000_000), + timestamp: from_unix_timestamp(200_000_000), hash: transaction_hash_1, attempt: 2, amount: 444, @@ -3822,7 +3823,7 @@ mod tests { }; let fingerprint_2 = PendingPayableFingerprint { rowid: 10, - timestamp: from_time_t(199_780_000), + timestamp: from_unix_timestamp(199_780_000), hash: Default::default(), attempt: 15, amount: 1212, diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 1307cb006..e8ce95812 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1068,7 +1068,7 @@ mod tests { use crate::accountant::db_access_objects::pending_payable_dao::{ PendingPayable, PendingPayableDaoError, TransactionHashes, }; - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; @@ -2103,11 +2103,11 @@ mod tests { let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); let debt = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); - let time = to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; + let time = to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; let unqualified_payable_account = vec![PayableAccount { wallet: make_wallet("wallet0"), balance_wei: debt, - last_paid_timestamp: from_time_t(time), + last_paid_timestamp: from_unix_timestamp(time), pending_payable_opt: None, }]; let subject = PayableScannerBuilder::new() @@ -2136,7 +2136,7 @@ mod tests { let qualified_payable = PayableAccount { wallet: make_wallet("wallet0"), balance_wei: debt, - last_paid_timestamp: from_time_t(time), + last_paid_timestamp: from_unix_timestamp(time), pending_payable_opt: None, }; let subject = PayableScannerBuilder::new() @@ -2168,8 +2168,8 @@ mod tests { let unqualified_payable_account = vec![PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, ), pending_payable_opt: None, }]; @@ -2194,7 +2194,7 @@ mod tests { let now = SystemTime::now(); let payable_fingerprint_1 = PendingPayableFingerprint { rowid: 555, - timestamp: from_time_t(210_000_000), + timestamp: from_unix_timestamp(210_000_000), hash: make_tx_hash(45678), attempt: 1, amount: 4444, @@ -2202,7 +2202,7 @@ mod tests { }; let payable_fingerprint_2 = PendingPayableFingerprint { rowid: 550, - timestamp: from_time_t(210_000_100), + timestamp: from_unix_timestamp(210_000_100), hash: make_tx_hash(112233), attempt: 1, amount: 7999, @@ -2682,7 +2682,7 @@ mod tests { let rowid_2 = 5; let pending_payable_fingerprint_1 = PendingPayableFingerprint { rowid: rowid_1, - timestamp: from_time_t(199_000_000), + timestamp: from_unix_timestamp(199_000_000), hash: make_tx_hash(0x123), attempt: 1, amount: 4567, @@ -2690,7 +2690,7 @@ mod tests { }; let pending_payable_fingerprint_2 = PendingPayableFingerprint { rowid: rowid_2, - timestamp: from_time_t(200_000_000), + timestamp: from_unix_timestamp(200_000_000), hash: make_tx_hash(0x567), attempt: 1, amount: 5555, @@ -2759,7 +2759,7 @@ mod tests { let test_name = "total_paid_payable_rises_with_each_bill_paid"; let fingerprint_1 = PendingPayableFingerprint { rowid: 5, - timestamp: from_time_t(189_999_888), + timestamp: from_unix_timestamp(189_999_888), hash: make_tx_hash(56789), attempt: 1, amount: 5478, @@ -2767,7 +2767,7 @@ mod tests { }; let fingerprint_2 = PendingPayableFingerprint { rowid: 6, - timestamp: from_time_t(200_000_011), + timestamp: from_unix_timestamp(200_000_011), hash: make_tx_hash(33333), attempt: 1, amount: 6543, @@ -2816,7 +2816,7 @@ mod tests { }; let fingerprint_1 = PendingPayableFingerprint { rowid: 5, - timestamp: from_time_t(200_000_000), + timestamp: from_unix_timestamp(200_000_000), hash: transaction_hash_1, attempt: 2, amount: 444, @@ -2832,7 +2832,7 @@ mod tests { }; let fingerprint_2 = PendingPayableFingerprint { rowid: 10, - timestamp: from_time_t(199_780_000), + timestamp: from_unix_timestamp(199_780_000), hash: transaction_hash_2, attempt: 15, amount: 1212, @@ -3277,7 +3277,7 @@ mod tests { let test_name = "signal_scanner_completion_and_log_if_timestamp_is_correct"; let logger = Logger::new(test_name); let mut subject = ScannerCommon::new(Rc::new(make_custom_payment_thresholds())); - let start = from_time_t(1_000_000_000); + let start = from_unix_timestamp(1_000_000_000); let end = start.checked_add(Duration::from_millis(145)).unwrap(); subject.initiated_at_opt = Some(start); diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 30b3a3d2d..ce2851e28 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -440,7 +440,7 @@ pub mod receivable_scanner_utils { #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ @@ -467,21 +467,21 @@ mod tests { #[test] fn investigate_debt_extremes_picks_the_most_relevant_records() { let now = SystemTime::now(); - let now_t = to_time_t(now); + let now_t = to_unix_timestamp(now); let same_amount_significance = 2_000_000; - let same_age_significance = from_time_t(now_t - 30000); + let same_age_significance = from_unix_timestamp(now_t - 30000); let payables = &[ PayableAccount { wallet: make_wallet("wallet0"), balance_wei: same_amount_significance, - last_paid_timestamp: from_time_t(now_t - 5000), + last_paid_timestamp: from_unix_timestamp(now_t - 5000), pending_payable_opt: None, }, //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked PayableAccount { wallet: make_wallet("wallet1"), balance_wei: same_amount_significance, - last_paid_timestamp: from_time_t(now_t - 10000), + last_paid_timestamp: from_unix_timestamp(now_t - 10000), pending_payable_opt: None, }, //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen @@ -511,7 +511,7 @@ mod tests { let receivable_account = ReceivableAccount { wallet: make_wallet("wallet0"), balance_wei: 10_000_000_000, - last_received_timestamp: from_time_t(to_time_t(now) - offset), + last_received_timestamp: from_unix_timestamp(to_unix_timestamp(now) - offset), }; let (balance, age) = balance_and_age(now, &receivable_account); @@ -614,7 +614,7 @@ mod tests { #[test] fn payables_debug_summary_prints_pretty_summary() { init_test_logging(); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let payment_thresholds = PaymentThresholds { threshold_interval_sec: 2_592_000, debt_threshold_gwei: 1_000_000_000, @@ -628,7 +628,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2000), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec, @@ -642,7 +642,7 @@ mod tests { PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( payment_thresholds.maturity_threshold_sec + 55, ), diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 5c9d6f14a..e7186d2cd 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -12,7 +12,9 @@ use crate::accountant::db_access_objects::pending_payable_dao::{ use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; -use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; +use crate::accountant::db_access_objects::utils::{ + from_unix_timestamp, to_unix_timestamp, CustomQuery, +}; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, @@ -60,7 +62,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); ReceivableAccount { wallet: make_wallet(&format!( "wallet{}{}", @@ -68,13 +70,13 @@ pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableA if expected_delinquent { "d" } else { "n" } )), balance_wei: gwei_to_wei(n), - last_received_timestamp: from_time_t(now - (n as i64)), + last_received_timestamp: from_unix_timestamp(now - (n as i64)), } } pub fn make_payable_account(n: u64) -> PayableAccount { - let now = to_time_t(SystemTime::now()); - let timestamp = from_time_t(now - (n as i64)); + let now = to_unix_timestamp(SystemTime::now()); + let timestamp = from_unix_timestamp(now - (n as i64)); make_payable_account_with_wallet_and_balance_and_timestamp_opt( make_wallet(&format!("wallet{}", n)), gwei_to_wei(n), @@ -1250,7 +1252,7 @@ pub fn make_custom_payment_thresholds() -> PaymentThresholds { pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { PendingPayableFingerprint { rowid: 33, - timestamp: from_time_t(222_222_222), + timestamp: from_unix_timestamp(222_222_222), hash: make_tx_hash(456), attempt: 1, amount: 12345, @@ -1269,8 +1271,8 @@ pub fn make_payables( let unqualified_payable_accounts = vec![PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, ), pending_payable_opt: None, }]; @@ -1280,8 +1282,8 @@ pub fn make_payables( balance_wei: gwei_to_wei( payment_thresholds.permanent_debt_allowed_gwei + 1_000_000_000, ), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 1, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1, ), pending_payable_opt: None, }, @@ -1290,8 +1292,8 @@ pub fn make_payables( balance_wei: gwei_to_wei( payment_thresholds.permanent_debt_allowed_gwei + 1_200_000_000, ), - last_paid_timestamp: from_time_t( - to_time_t(now) - payment_thresholds.maturity_threshold_sec as i64 - 100, + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 100, ), pending_payable_opt: None, }, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index e4275b036..1a01087e1 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -548,7 +548,7 @@ mod tests { use super::*; use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; - use crate::accountant::db_access_objects::utils::from_time_t; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::protect_payables_in_test; @@ -876,7 +876,7 @@ mod tests { let accounts = vec![PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, - last_paid_timestamp: from_time_t(150_000_000), + last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, }]; let agent_id_stamp = ArbitraryIdStamp::new(); @@ -966,7 +966,7 @@ mod tests { let accounts = vec![PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, - last_paid_timestamp: from_time_t(150_000_000), + last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, }]; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); @@ -1337,7 +1337,7 @@ mod tests { }; let fingerprint_4 = PendingPayableFingerprint { rowid: 450, - timestamp: from_time_t(230_000_000), + timestamp: from_unix_timestamp(230_000_000), hash: hash_4, attempt: 1, amount: 7879, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index 5879a47a3..25a747907 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -7,6 +7,8 @@ use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchai use ethereum_types::{H256, U256, U64}; use futures::Future; use serde_json::Value; +use std::fmt::Display; +use std::str::FromStr; use web3::contract::{Contract, Options}; use web3::transports::{Batch, Http}; use web3::types::{Address, BlockNumber, Filter, Log, TransactionReceipt}; @@ -25,6 +27,50 @@ pub enum TxStatus { Succeeded(TransactionBlock), } +impl FromStr for TxStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "Pending" => Ok(TxStatus::Pending), + "Failed" => Ok(TxStatus::Failed), // TODO: GH-631: This should be removed + s if s.starts_with("Succeeded") => { + // The format is "Succeeded(block_number, block_hash)" + let parts: Vec<&str> = s[10..s.len() - 1].split(',').collect(); + if parts.len() != 2 { + return Err("Invalid Succeeded format".to_string()); + } + let block_number: u64 = parts[0] + .parse() + .map_err(|_| "Invalid block number".to_string())?; + let block_hash = + H256::from_str(&parts[1][2..]).map_err(|_| "Invalid block hash".to_string())?; + Ok(TxStatus::Succeeded(TransactionBlock { + block_hash, + block_number: U64::from(block_number), + })) + } + _ => Err(format!("Unknown status: {}", s)), + } + } +} + +impl Display for TxStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TxStatus::Failed => write!(f, "Failed"), + TxStatus::Pending => write!(f, "Pending"), + TxStatus::Succeeded(block) => { + write!( + f, + "Succeeded({},{:?})", + block.block_number, block.block_hash + ) + } + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct TxReceipt { pub transaction_hash: H256, @@ -184,7 +230,7 @@ mod tests { use masq_lib::utils::find_free_port; use std::str::FromStr; use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, TransactionReceipt, U256}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TxReceipt, TxStatus}; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; #[test] fn get_transaction_fee_balance_works() { @@ -612,6 +658,74 @@ mod tests { assert_eq!(tx_receipt.status, TxStatus::Pending); } + #[test] + fn tx_status_display_works() { + // Test Failed + assert_eq!(TxStatus::Failed.to_string(), "Failed"); + + // Test Pending + assert_eq!(TxStatus::Pending.to_string(), "Pending"); + + // Test Succeeded + let block_number = U64::from(12345); + let block_hash = H256::from_low_u64_be(0xabcdef); + let succeeded = TxStatus::Succeeded(TransactionBlock { + block_hash, + block_number, + }); + assert_eq!( + succeeded.to_string(), + format!("Succeeded({},0x{:x})", block_number, block_hash) + ); + } + + #[test] + fn tx_status_from_str_works() { + // Test Pending + assert_eq!(TxStatus::from_str("Pending"), Ok(TxStatus::Pending)); + + // Test Failed + assert_eq!(TxStatus::from_str("Failed"), Ok(TxStatus::Failed)); + + // Test Succeeded with valid input + let block_number = 123456789; + let block_hash = H256::from_low_u64_be(0xabcdef); + let input = format!("Succeeded({},0x{:x})", block_number, block_hash); + assert_eq!( + TxStatus::from_str(&input), + Ok(TxStatus::Succeeded(TransactionBlock { + block_hash, + block_number: U64::from(block_number), + })) + ); + + // Test Succeeded with invalid format + assert_eq!( + TxStatus::from_str("Succeeded(123)"), + Err("Invalid Succeeded format".to_string()) + ); + + // Test Succeeded with invalid block number + assert_eq!( + TxStatus::from_str( + "Succeeded(abc,0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef)" + ), + Err("Invalid block number".to_string()) + ); + + // Test Succeeded with invalid block hash + assert_eq!( + TxStatus::from_str("Succeeded(123,0xinvalidhash)"), + Err("Invalid block hash".to_string()) + ); + + // Test unknown status + assert_eq!( + TxStatus::from_str("InProgress"), + Err("Unknown status: InProgress".to_string()) + ); + } + fn create_tx_receipt( status: Option, block_hash: Option, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index 03ed4150b..ee2e49786 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -330,7 +330,7 @@ pub fn create_blockchain_agent_web3( #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::from_time_t; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::gwei_to_wei; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::test_utils::{ @@ -522,13 +522,13 @@ mod tests { PayableAccount { wallet: make_wallet("4567"), balance_wei: 2_345_678, - last_paid_timestamp: from_time_t(4500000), + last_paid_timestamp: from_unix_timestamp(4500000), pending_payable_opt: None, }, PayableAccount { wallet: make_wallet("5656"), balance_wei: 6_543_210, - last_paid_timestamp: from_time_t(333000), + last_paid_timestamp: from_unix_timestamp(333000), pending_payable_opt: None, }, ]; diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index 0f9685c2b..6e0090f0d 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -270,8 +270,7 @@ impl DbInitializerReal { timestamp integer not null, gas_price_wei integer not null, nonce integer not null, - status text not null, - retried integer not null + status text not null )", [], ) @@ -752,7 +751,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let mut stmt = conn.prepare("select rowid, tx_hash, receiver_address, amount_high_b, amount_low_b, timestamp, gas_price_wei, nonce, status, retried from sent_payable").unwrap(); + let mut stmt = conn.prepare("select rowid, tx_hash, receiver_address, amount_high_b, amount_low_b, timestamp, gas_price_wei, nonce, status from sent_payable").unwrap(); let mut sent_payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); assert!(sent_payable_contents.next().is_none()); assert_create_table_stm_contains_all_parts( diff --git a/node/src/database/db_migrations/db_migrator.rs b/node/src/database/db_migrations/db_migrator.rs index 369a78788..6cae9599a 100644 --- a/node/src/database/db_migrations/db_migrator.rs +++ b/node/src/database/db_migrations/db_migrator.rs @@ -81,7 +81,7 @@ impl DbMigratorReal { &Migrate_7_to_8, &Migrate_8_to_9, &Migrate_9_to_10, - &Migrate_10_to_11, + &Migrate_10_to_11, // TODO: GH-598: Make this one as null migration and yours as 12 ] } diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs index 4b683208d..8b7984673 100644 --- a/node/src/database/db_migrations/migrations/migration_10_to_11.rs +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -18,8 +18,7 @@ impl DatabaseMigration for Migrate_10_to_11 { timestamp integer not null, gas_price_wei integer not null, nonce integer not null, - status text not null, - retried integer not null + status text not null )"; declaration_utils.execute_upon_transaction(&[&sql_statement]) diff --git a/node/src/database/db_migrations/migrations/migration_4_to_5.rs b/node/src/database/db_migrations/migrations/migration_4_to_5.rs index 06deb809f..4b5bbb50a 100644 --- a/node/src/database/db_migrations/migrations/migration_4_to_5.rs +++ b/node/src/database/db_migrations/migrations/migration_4_to_5.rs @@ -76,7 +76,7 @@ impl DatabaseMigration for Migrate_4_to_5 { #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, DATABASE_FILE, }; @@ -124,7 +124,7 @@ mod tests { None, &wallet_1, 113344, - from_time_t(250_000_000), + from_unix_timestamp(250_000_000), ); let config_table_before = fetch_all_from_config_table(conn.as_ref()); @@ -160,7 +160,7 @@ mod tests { let params: &[&dyn ToSql] = &[ &wallet, &amount, - &to_time_t(timestamp), + &to_unix_timestamp(timestamp), if !hash_str.is_empty() { &hash_str } else { @@ -208,7 +208,7 @@ mod tests { Some(transaction_hash_2), &wallet_2, 1111111, - from_time_t(200_000_000), + from_unix_timestamp(200_000_000), ); let config_table_before = fetch_all_from_config_table(&conn); diff --git a/node/src/database/test_utils/mod.rs b/node/src/database/test_utils/mod.rs index a9c5fac77..4251d1588 100644 --- a/node/src/database/test_utils/mod.rs +++ b/node/src/database/test_utils/mod.rs @@ -22,7 +22,6 @@ pub const SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE: &[&[&str]] = &[ &["gas_price_wei", "integer", "not", "null"], &["nonce", "integer", "not", "null"], &["status", "text", "not", "null"], - &["retried", "integer", "not", "null"], ]; #[derive(Debug, Default)] @@ -146,7 +145,6 @@ mod tests { &["gas_price_wei", "integer", "not", "null"], &["nonce", "integer", "not", "null"], &["status", "text", "not", "null"], - &["retried", "integer", "not", "null"] ] ); } diff --git a/node/tests/financials_test.rs b/node/tests/financials_test.rs index 9847efa38..7aff319d2 100644 --- a/node/tests/financials_test.rs +++ b/node/tests/financials_test.rs @@ -13,7 +13,7 @@ use masq_lib::test_utils::utils::{ensure_node_home_directory_exists, open_all_fi use masq_lib::utils::find_free_port; use node_lib::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoReal}; use node_lib::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoReal}; -use node_lib::accountant::db_access_objects::utils::{from_time_t, to_time_t}; +use node_lib::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use node_lib::accountant::gwei_to_wei; use node_lib::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, @@ -30,9 +30,9 @@ fn financials_command_retrieves_payable_and_receivable_records_integration() { let port = find_free_port(); let home_dir = ensure_node_home_directory_exists("integration", test_name); let now = SystemTime::now(); - let timestamp_payable = from_time_t(to_time_t(now) - 678); - let timestamp_receivable_1 = from_time_t(to_time_t(now) - 10000); - let timestamp_receivable_2 = from_time_t(to_time_t(now) - 1111); + let timestamp_payable = from_unix_timestamp(to_unix_timestamp(now) - 678); + let timestamp_receivable_1 = from_unix_timestamp(to_unix_timestamp(now) - 10000); + let timestamp_receivable_2 = from_unix_timestamp(to_unix_timestamp(now) - 1111); let wallet_payable = make_wallet("efef"); let wallet_receivable_1 = make_wallet("abcde"); let wallet_receivable_2 = make_wallet("ccccc"); From 009e9a2f98960cbef83df85dd7df89b724e5f909 Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:19:28 +0200 Subject: [PATCH 03/37] GH-602: Scanner system revamp (#640) * GH-602: big bunch of changes * GH-602: interim commit * GH-602: four scans layout installed * GH-602: mostly done but still some crumbs of work left * GH-602: interim commit * GH-602: Recorder enriched by counter messages * GH-602: removed obfuscated and made right the formerly developed StopConditions * GH-602: removed obfuscated and made right the formerly developed StopConditions * GH-602: main part probably done * GH-602: more todos knocked off in accountant/mod.rs * GH-602: deep dive into schedulers and the awarness of the schedule state * GH-602: save point before going into fully private scanner * GH-602: another save point, this time before turning the scanner into fully private object * GH-602: finally fully scanners as flexible parametrized generic objects * GH-602: todos placed (many) as understood as the whole design * GH-602: more cases covered...knocking off todos!() continues * GH-602: made a markup for upcoming steps * GH-602: made a markup for upcoming steps * GH-602: Manual scanners prevented by the automation. Tested * GH-602: More tests written; mainly constraining the manual scanning * GH-602: before trying to implement the ScanScheduleHintErrorResolver * GH-602: tests for pending payable scanner and its start scan error resolver are compiling * GH-602: all tests in scan_schedulers passing now * GH-602: all tests in scan_schedulers passing now * GH-602: slowly fixing test in accountant/mod.rs * GH-602: fixed another couple of failing tests in accountant/mod.rs and scanners/mod.rs * GH-602: adding another little pile * GH-602: closing the gap... probably only one test to be written yet * GH-602: one more test written...ext tr. pen payable * GH-602: eliminated last todos in scanners/mod.rs * GH-602: first rough clean-up * GH-602: some comments fixed * GH-602: first piece of the review finished * GH-602: addressing review continues... * GH-602: addressing review continues on and on... * GH-602: more things for the review... * GH-602: last 6 comments to address * GH-602: finished the big two tests covering the very start of the automatic process with pp scanner playing the first violin * GH-602: huge tests refactored * GH-602: small functionalities made right and huge tests refactored * GH-602: prepared some fixes for early rescheduling * GH-602: moved the scanner test utils * GH-602: counter message summary comment added * GH-602: probably finished the minimm of what can be submitted seriously * GH-602: removed commented out impl * GH-602: vast majority of the review is covered * GH-602: refactoring in the biggeste tests - a la utkarsh * GH-602: review two ends * GH-602: improved comments --------- Co-authored-by: Bert --- masq_lib/src/lib.rs | 1 - masq_lib/src/messages.rs | 16 +- masq_lib/src/type_obfuscation.rs | 83 - node/Cargo.toml | 2 +- node/src/accountant/mod.rs | 3776 +++++++++++------ node/src/accountant/payment_adjuster.rs | 11 +- .../scanners/mid_scan_msg_handling/mod.rs | 3 - node/src/accountant/scanners/mod.rs | 1635 +++++-- .../agent_null.rs | 6 +- .../agent_web3.rs | 6 +- .../blockchain_agent.rs | 0 .../mod.rs | 23 +- .../msgs.rs | 26 +- .../test_utils.rs | 2 +- .../accountant/scanners/scan_schedulers.rs | 941 ++++ .../src/accountant/scanners/scanners_utils.rs | 86 + node/src/accountant/scanners/test_utils.rs | 488 ++- node/src/accountant/test_utils.rs | 232 +- node/src/actor_system_factory.rs | 8 +- node/src/blockchain/blockchain_bridge.rs | 63 +- .../blockchain_interface_web3/mod.rs | 4 +- .../blockchain_interface_web3/utils.rs | 6 +- .../blockchain/blockchain_interface/mod.rs | 2 +- node/src/bootstrapper.rs | 10 +- node/src/daemon/setup_reporter.rs | 7 +- .../src/db_config/persistent_configuration.rs | 4 +- node/src/node_configurator/configurator.rs | 12 +- .../unprivileged_parse_args_configuration.rs | 41 +- node/src/proxy_server/mod.rs | 12 +- node/src/sub_lib/accountant.rs | 6 +- node/src/sub_lib/blockchain_bridge.rs | 4 +- node/src/sub_lib/combined_parameters.rs | 18 +- node/src/sub_lib/peer_actors.rs | 2 + node/src/sub_lib/utils.rs | 1 + node/src/test_utils/mod.rs | 91 +- node/src/test_utils/recorder.rs | 493 ++- node/src/test_utils/recorder_counter_msgs.rs | 172 + .../test_utils/recorder_stop_conditions.rs | 291 +- 38 files changed, 6450 insertions(+), 2134 deletions(-) delete mode 100644 masq_lib/src/type_obfuscation.rs delete mode 100644 node/src/accountant/scanners/mid_scan_msg_handling/mod.rs rename node/src/accountant/scanners/{mid_scan_msg_handling/payable_scanner => payable_scanner_extension}/agent_null.rs (94%) rename node/src/accountant/scanners/{mid_scan_msg_handling/payable_scanner => payable_scanner_extension}/agent_web3.rs (93%) rename node/src/accountant/scanners/{mid_scan_msg_handling/payable_scanner => payable_scanner_extension}/blockchain_agent.rs (100%) rename node/src/accountant/scanners/{mid_scan_msg_handling/payable_scanner => payable_scanner_extension}/mod.rs (62%) rename node/src/accountant/scanners/{mid_scan_msg_handling/payable_scanner => payable_scanner_extension}/msgs.rs (66%) rename node/src/accountant/scanners/{mid_scan_msg_handling/payable_scanner => payable_scanner_extension}/test_utils.rs (96%) create mode 100644 node/src/accountant/scanners/scan_schedulers.rs create mode 100644 node/src/test_utils/recorder_counter_msgs.rs diff --git a/masq_lib/src/lib.rs b/masq_lib/src/lib.rs index e5232b221..db70ff4cf 100644 --- a/masq_lib/src/lib.rs +++ b/masq_lib/src/lib.rs @@ -22,6 +22,5 @@ pub mod crash_point; pub mod data_version; pub mod shared_schema; pub mod test_utils; -pub mod type_obfuscation; pub mod ui_gateway; pub mod ui_traffic_converter; diff --git a/masq_lib/src/messages.rs b/masq_lib/src/messages.rs index 45842e419..19fa128b2 100644 --- a/masq_lib/src/messages.rs +++ b/masq_lib/src/messages.rs @@ -525,10 +525,10 @@ pub struct UiRatePack { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct UiScanIntervals { - #[serde(rename = "pendingPayableSec")] - pub pending_payable_sec: u64, #[serde(rename = "payableSec")] pub payable_sec: u64, + #[serde(rename = "pendingPayableSec")] + pub pending_payable_sec: u64, #[serde(rename = "receivableSec")] pub receivable_sec: u64, } @@ -781,8 +781,8 @@ conversation_message!(UiRecoverWalletsResponse, "recoverWallets"); #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum ScanType { Payables, - Receivables, PendingPayables, + Receivables, } impl FromStr for ScanType { @@ -791,8 +791,8 @@ impl FromStr for ScanType { fn from_str(s: &str) -> Result { match s { s if &s.to_lowercase() == "payables" => Ok(ScanType::Payables), - s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), s if &s.to_lowercase() == "pendingpayables" => Ok(ScanType::PendingPayables), + s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), s => Err(format!("Unrecognized ScanType: '{}'", s)), } } @@ -1160,10 +1160,10 @@ mod tests { let result: Vec = vec![ "Payables", "pAYABLES", - "Receivables", - "rECEIVABLES", "PendingPayables", "pENDINGpAYABLES", + "Receivables", + "rECEIVABLES", ] .into_iter() .map(|s| ScanType::from_str(s).unwrap()) @@ -1174,10 +1174,10 @@ mod tests { vec![ ScanType::Payables, ScanType::Payables, - ScanType::Receivables, - ScanType::Receivables, ScanType::PendingPayables, ScanType::PendingPayables, + ScanType::Receivables, + ScanType::Receivables, ] ) } diff --git a/masq_lib/src/type_obfuscation.rs b/masq_lib/src/type_obfuscation.rs deleted file mode 100644 index 1f3c79258..000000000 --- a/masq_lib/src/type_obfuscation.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use std::any::TypeId; -use std::mem::transmute; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Obfuscated { - type_id: TypeId, - bytes: Vec, -} - -impl Obfuscated { - // Although we're asking the compiler for a cast between two types - // where one is generic and both could possibly be of a different - // size, which almost applies to an unsupported kind of operation, - // the compiler stays calm here. The use of vectors at the input as - // well as output lets us avoid the above depicted situation. - // - // If you wish to write an implementation allowing more arbitrary - // types on your own, instead of helping yourself by a library like - // 'bytemuck', consider these functions from the std library, - // 'mem::transmute_copy' or 'mem::forget()', which will renew - // the compiler's trust for you. However, the true adventure will - // begin when you are supposed to write code to realign the plain - // bytes backwards to your desired type... - - pub fn obfuscate_vector(data: Vec) -> Obfuscated { - let bytes = unsafe { transmute::, Vec>(data) }; - - Obfuscated { - type_id: TypeId::of::(), - bytes, - } - } - - pub fn expose_vector(self) -> Vec { - if self.type_id != TypeId::of::() { - panic!("Forbidden! You're trying to interpret obfuscated data as the wrong type.") - } - - unsafe { transmute::, Vec>(self.bytes) } - } - - // Proper casting from a non vec structure into a vector of bytes - // is difficult and ideally requires an involvement of a library - // like bytemuck. - // If you think we do need such cast, place other methods in here - // and don't remove the ones above because: - // a) bytemuck will force you to implement its 'Pod' trait which - // might imply an (at minimum) ugly implementation for a std - // type like a Vec because both the trait and the type have - // their definitions situated externally to our project, - // therefore you might need to solve it by introducing - // a super-trait from our code - // b) using our simple 'obfuscate_vector' function will always - // be fairly more efficient than if done with help of - // the other library -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn obfuscation_works() { - let data = vec!["I'm fearing of losing my entire identity".to_string()]; - - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let fenix_like_data: Vec = obfuscated_data.expose_vector(); - - assert_eq!(data, fenix_like_data) - } - - #[test] - #[should_panic( - expected = "Forbidden! You're trying to interpret obfuscated data as the wrong type." - )] - fn obfuscation_attempt_to_reinterpret_to_wrong_type() { - let data = vec![0_u64]; - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let _: Vec = obfuscated_data.expose_vector(); - } -} diff --git a/node/Cargo.toml b/node/Cargo.toml index 93f2dcde1..b6d91b0cd 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -15,7 +15,7 @@ automap = { path = "../automap"} backtrace = "0.3.57" base64 = "0.13.0" bytes = "0.4.12" -time = {version = "0.3.11", features = [ "macros" ]} +time = {version = "0.3.11", features = [ "macros", "parsing" ]} clap = "2.33.3" crossbeam-channel = "0.5.1" dirs = "4.0.0" diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 750c4c1a7..79f7c52f5 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -22,10 +22,10 @@ use crate::accountant::db_access_objects::utils::{ use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, }; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, ScanSchedulers, Scanners}; +use crate::accountant::scanners::{StartScanError, Scanners}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -57,14 +57,14 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::UiFinancialsResponse; +use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, ScanType, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, + QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; use masq_lib::ui_gateway::MessageTarget::ClientId; -use masq_lib::ui_gateway::{MessageBody, MessagePath}; +use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; @@ -76,13 +76,15 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; +use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours pub struct Accountant { - suppress_initial_scans: bool, consuming_wallet_opt: Option, earning_wallet: Wallet, payable_dao: Box, @@ -95,7 +97,7 @@ pub struct Accountant { outbound_payments_instructions_sub_opt: Option>, qualified_payables_sub_opt: Option>, retrieve_transactions_sub_opt: Option>, - request_transaction_receipts_subs_opt: Option>, + request_transaction_receipts_sub_opt: Option>, report_inbound_payments_sub_opt: Option>, report_sent_payables_sub_opt: Option>, ui_message_sub_opt: Option>, @@ -134,24 +136,35 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } -#[derive(Debug, Message, PartialEq)] +#[derive(Debug, PartialEq, Eq, Message, Clone)] +pub struct ReportTransactionReceipts { + pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, + pub response_skeleton_opt: Option, +} + +#[derive(Debug, Message, PartialEq, Clone)] pub struct SentPayables { pub payment_procedure_result: Result, PayableTransactionError>, pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForPayables { +pub struct ScanForPendingPayables { pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForReceivables { +pub struct ScanForNewPayables { pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForPendingPayables { +pub struct ScanForRetryPayables { + pub response_skeleton_opt: Option, +} + +#[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] +pub struct ScanForReceivables { pub response_skeleton_opt: Option, } @@ -183,94 +196,202 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, _msg: StartMessage, ctx: &mut Self::Context) -> Self::Result { - if self.suppress_initial_scans { - info!( - &self.logger, - "Started with --scans off; declining to begin database and blockchain scans" - ); - } else { + if self.scan_schedulers.automatic_scans_enabled { debug!( &self.logger, "Started with --scans on; starting database and blockchain scans" ); - ctx.notify(ScanForPendingPayables { response_skeleton_opt: None, }); - ctx.notify(ScanForPayables { - response_skeleton_opt: None, - }); ctx.notify(ScanForReceivables { response_skeleton_opt: None, }); + } else { + info!( + &self.logger, + "Started with --scans off; declining to begin database and blockchain scans" + ); } } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self.scanners.receivable.finish_scan(msg, &self.logger) { - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); + fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { + // By now we know this is an automatic scan process. The scan may be or may not be + // rescheduled. It depends on the findings. Any failed transaction will lead to the launch + // of the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled + // to run again. However, not from here. + let response_skeleton_opt = msg.response_skeleton_opt; + + let scheduling_hint = + self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); + + match scheduling_hint { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + ScanRescheduleAfterEarlyStop::Schedule(scan_type) => unreachable!( + "Early stopped pending payable scan was suggested to be followed up \ + by the scan for {:?}, which is not supported though", + scan_type + ), + ScanRescheduleAfterEarlyStop::DoNotSchedule => { + trace!( + self.logger, + "No early rescheduling, as the pending payable scan did find results" + ); + } } } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle( - &mut self, - msg: BlockchainAgentWithContextMessage, - _ctx: &mut Self::Context, - ) -> Self::Result { - self.handle_payable_payment_setup(msg) + fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { + // We know this must be a scheduled scan, but are yet clueless where it's going to be + // rescheduled. If no payable qualifies for a payment, we do it here right away. If some + // transactions made it out, the next scheduling of this scanner is going to be decided by + // the PendingPayableScanner whose job is to evaluate if it has seen every pending payable + // complete. That's the moment when another run of the NewPayableScanner makes sense again. + let response_skeleton = msg.response_skeleton_opt; + + let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); + + match scheduling_hint { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + ScanRescheduleAfterEarlyStop::Schedule(other_scan_type) => unreachable!( + "Early stopped new payable scan was suggested to be followed up by the scan \ + for {:?}, which is not supported though", + other_scan_type + ), + ScanRescheduleAfterEarlyStop::DoNotSchedule => { + trace!( + self.logger, + "No early rescheduling, as the new payable scan did find results" + ) + } + } } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: SentPayables, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self.scanners.payable.finish_scan(msg, &self.logger) { - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); - } + fn handle(&mut self, msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { + // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes discovering + // that there have been some failed payables. No place for that here. + let response_skeleton = msg.response_skeleton_opt; + self.handle_request_of_scan_for_retry_payable(response_skeleton); } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_payable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::Payables, ctx); + fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { + // By now we know it is an automatic scan. The ReceivableScanner is independent of other + // scanners and rescheduled regularly, just here. + self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); + self.scan_schedulers.receivable.schedule(ctx, &self.logger); } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_pending_payable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::PendingPayables, ctx); + fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { + let response_skeleton_opt = msg.response_skeleton_opt; + match self.scanners.finish_pending_payable_scan(msg, &self.logger) { + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) => { + if let Some(node_to_ui_msg) = ui_msg_opt { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + // Externally triggered scan should never be allowed to spark a procedure that + // would bring over payables with fresh nonces. The job's done. + } else { + self.scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger) + } + } + PendingPayableScanResult::PaymentRetryRequired => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + }; } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::Receivables, ctx); + fn handle( + &mut self, + msg: BlockchainAgentWithContextMessage, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_payable_payment_setup(msg) + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { + let scan_result = self.scanners.finish_payable_scan(msg, &self.logger); + + match scan_result.ui_response_opt { + None => match scan_result.result { + OperationOutcome::NewPendingPayable => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + OperationOutcome::Failure => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + }, + Some(node_to_ui_msg) => { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + + // Externally triggered scans are not allowed to provoke an unwinding scan sequence + // with intervals. The only exception is the PendingPayableScanner and retry- + // payable scanner, which are ever meant to run in a tight tandem. + } + } + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { + if let Some(node_to_ui_msg) = self.scanners.finish_receivable_scan(msg, &self.logger) { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } } } @@ -279,17 +400,8 @@ impl Handler for Accountant { fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); - match scan_error.scan_type { - ScanType::Payables => { - self.scanners.payable.mark_as_ended(&self.logger); - } - ScanType::PendingPayables => { - self.scanners.pending_payable.mark_as_ended(&self.logger); - } - ScanType::Receivables => { - self.scanners.receivable.mark_as_ended(&self.logger); - } - }; + self.scanners + .acknowledge_scan_error(&scan_error, &self.logger); if let Some(response_skeleton) = scan_error.response_skeleton_opt { let error_msg = NodeToUiMessage { target: ClientId(response_skeleton.client_id), @@ -357,7 +469,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable: Vec, + pub pending_payable_fingerprints: Vec, pub response_skeleton_opt: Option, } @@ -367,26 +479,6 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -#[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, - pub response_skeleton_opt: Option, -} - -impl Handler for Accountant { - type Result = (); - - fn handle(&mut self, msg: ReportTransactionReceipts, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self.scanners.pending_payable.finish_scan(msg, &self.logger) { - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); - } - } -} - impl Handler for Accountant { type Result = (); fn handle( @@ -429,6 +521,7 @@ impl Accountant { let payable_dao = dao_factories.payable_dao_factory.make(); let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); + let scan_schedulers = ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), @@ -437,7 +530,6 @@ impl Accountant { ); Accountant { - suppress_initial_scans: config.suppress_initial_scans, consuming_wallet_opt: config.consuming_wallet_opt.clone(), earning_wallet, payable_dao, @@ -445,14 +537,14 @@ impl Accountant { pending_payable_dao, scanners, crashable: config.crash_point == CrashPoint::Message, - scan_schedulers: ScanSchedulers::new(scan_intervals), + scan_schedulers, financial_statistics: Rc::clone(&financial_statistics), outbound_payments_instructions_sub_opt: None, qualified_payables_sub_opt: None, report_sent_payables_sub_opt: None, retrieve_transactions_sub_opt: None, report_inbound_payments_sub_opt: None, - request_transaction_receipts_subs_opt: None, + request_transaction_receipts_sub_opt: None, ui_message_sub_opt: None, message_id_generator: Box::new(MessageIdGeneratorReal::default()), logger: Logger::new("Accountant"), @@ -571,7 +663,7 @@ impl Accountant { Some(msg.peer_actors.blockchain_bridge.qualified_payables); self.report_sent_payables_sub_opt = Some(msg.peer_actors.accountant.report_sent_payments); self.ui_message_sub_opt = Some(msg.peer_actors.ui_gateway.node_to_ui_message_sub); - self.request_transaction_receipts_subs_opt = Some( + self.request_transaction_receipts_sub_opt = Some( msg.peer_actors .blockchain_bridge .request_transaction_receipts, @@ -600,14 +692,6 @@ impl Accountant { } } - fn schedule_next_scan(&self, scan_type: ScanType, ctx: &mut Context) { - self.scan_schedulers - .schedulers - .get(&scan_type) - .unwrap_or_else(|| panic!("Scan Scheduler {:?} not properly prepared", scan_type)) - .schedule(ctx) - } - fn handle_report_routing_service_provided_message( &mut self, msg: ReportRoutingServiceProvidedMessage, @@ -691,15 +775,13 @@ impl Accountant { fn handle_payable_payment_setup(&mut self, msg: BlockchainAgentWithContextMessage) { let blockchain_bridge_instructions = match self .scanners - .payable - .try_skipping_payment_adjustment(msg, &self.logger) + .try_skipping_payable_adjustment(msg, &self.logger) { Ok(Either::Left(finalized_msg)) => finalized_msg, Ok(Either::Right(unaccepted_msg)) => { //TODO we will eventually query info from Neighborhood before the adjustment, according to GH-699 self.scanners - .payable - .perform_payment_adjustment(unaccepted_msg, &self.logger) + .perform_payable_adjustment(unaccepted_msg, &self.logger) } Err(_e) => todo!("be completed by GH-711"), }; @@ -839,19 +921,53 @@ impl Accountant { } } - fn handle_request_of_scan_for_payable( + fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) { - let result = match self.consuming_wallet_opt.clone() { - Some(consuming_wallet) => self.scanners.payable.begin_scan( - consuming_wallet, - SystemTime::now(), + ) -> ScanRescheduleAfterEarlyStop { + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( + consuming_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + self.scan_schedulers.automatic_scans_enabled, + ), + None => Err(StartScanError::NoConsumingWalletFound), + }; + + match result { + Ok(scan_message) => { + self.qualified_payables_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"); + ScanRescheduleAfterEarlyStop::DoNotSchedule + } + Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::NewPayables, + e, response_skeleton_opt, - &self.logger, ), - None => Err(BeginScanError::NoConsumingWalletFound), - }; + } + } + + fn handle_request_of_scan_for_retry_payable( + &mut self, + response_skeleton_opt: Option, + ) { + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( + consuming_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ), + None => Err(StartScanError::NoConsumingWalletFound), + }; match result { Ok(scan_message) => { @@ -861,65 +977,125 @@ impl Accountant { .try_send(scan_message) .expect("BlockchainBridge is dead"); } - Err(e) => e.handle_error( - &self.logger, - ScanType::Payables, - response_skeleton_opt.is_some(), - ), + Err(e) => { + let _ = self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::RetryPayables, + e, + response_skeleton_opt, + ); + } } } fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) { - let result = match self.consuming_wallet_opt.clone() { - Some(consuming_wallet) => self.scanners.pending_payable.begin_scan( - consuming_wallet, // This argument is not used and is therefore irrelevant - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ), - None => Err(BeginScanError::NoConsumingWalletFound), + ) -> ScanRescheduleAfterEarlyStop { + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( + consuming_wallet, // This argument is not used and is therefore irrelevant + SystemTime::now(), + response_skeleton_opt, + &self.logger, + self.scan_schedulers.automatic_scans_enabled, + ), + None => Err(StartScanError::NoConsumingWalletFound), + }; + + let hint: ScanRescheduleAfterEarlyStop = match result { + Ok(scan_message) => { + self.request_transaction_receipts_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"); + ScanRescheduleAfterEarlyStop::DoNotSchedule + } + Err(e) => { + let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); + self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan, + }, + e, + response_skeleton_opt, + ) + } }; - match result { - Ok(scan_message) => self - .request_transaction_receipts_subs_opt - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(scan_message) - .expect("BlockchainBridge is dead"), - Err(e) => e.handle_error( - &self.logger, - ScanType::PendingPayables, - response_skeleton_opt.is_some(), - ), + if self.scanners.initial_pending_payable_scan() { + self.scanners.unset_initial_pending_payable_scan() } + + hint + } + + fn handle_start_scan_error_and_prevent_scan_stall_point( + &self, + scanner: PayableSequenceScanner, + e: StartScanError, + response_skeleton_opt: Option, + ) -> ScanRescheduleAfterEarlyStop { + let is_externally_triggered = response_skeleton_opt.is_some(); + + e.log_error(&self.logger, scanner.into(), is_externally_triggered); + + response_skeleton_opt.map(|skeleton| { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }); + + self.scan_schedulers + .reschedule_on_error_resolver + .resolve_rescheduling_on_error(scanner, &e, is_externally_triggered, &self.logger) } fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, ) { - match self.scanners.receivable.begin_scan( - self.earning_wallet.clone(), - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ) { + let result: Result = + self.scanners.start_receivable_scan_guarded( + &self.earning_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + self.scan_schedulers.automatic_scans_enabled, + ); + + match result { Ok(scan_message) => self .retrieve_transactions_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"), - Err(e) => e.handle_error( - &self.logger, - ScanType::Receivables, - response_skeleton_opt.is_some(), - ), - }; + Err(e) => { + e.log_error( + &self.logger, + ScanType::Receivables, + response_skeleton_opt.is_some(), + ); + + response_skeleton_opt.map(|skeleton| { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }); + } + } } fn handle_externally_triggered_scan( @@ -928,13 +1104,17 @@ impl Accountant { scan_type: ScanType, response_skeleton: ResponseSkeleton, ) { + // Each of these scans runs only once per request, they do not go on into a sequence under + // any circumstances match scan_type { - ScanType::Payables => self.handle_request_of_scan_for_payable(Some(response_skeleton)), + ScanType::Payables => { + self.handle_request_of_scan_for_new_payable(Some(response_skeleton)); + } ScanType::PendingPayables => { self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)); } ScanType::Receivables => { - self.handle_request_of_scan_for_receivable(Some(response_skeleton)) + self.handle_request_of_scan_for_receivable(Some(response_skeleton)); } } } @@ -1045,31 +1225,24 @@ mod tests { use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::BeginScanError; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement}; + use crate::accountant::scanners::{StartScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_payables, - BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, NullScanner, - PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, - PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, - ReceivableDaoMock, ScannerMock, - }; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; - use crate::blockchain::blockchain_bridge::BlockchainBridge; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::test_utils::{ - make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, + make_tx_hash }; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::config_dao::ConfigDaoRecord; use crate::db_config::mocks::ConfigDaoMock; - use crate::match_every_type_id; + use crate::{match_lazily_every_type_id, setup_for_counter_msg_triggered_via_type_id}; use crate::sub_lib::accountant::{ ExitServiceConsumed, PaymentThresholds, RoutingServiceConsumed, ScanIntervals, DEFAULT_EARNING_WALLET, DEFAULT_PAYMENT_THRESHOLDS, @@ -1077,47 +1250,46 @@ mod tests { use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::neighborhood::ConfigChange; use crate::sub_lib::neighborhood::{Hops, WalletPair}; - use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; - use crate::test_utils::recorder::make_recorder; + use crate::test_utils::recorder::{make_recorder, PeerActorsBuilder, SetUpCounterMsgs}; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use crate::test_utils::unshared_test_utils::notify_handlers::NotifyLaterHandleMock; + use crate::test_utils::unshared_test_utils::notify_handlers::{NotifyHandleMock, NotifyLaterHandleMock}; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; use crate::test_utils::unshared_test_utils::{ assert_on_initialization_with_panic_on_migration, make_bc_with_defaults, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; use crate::test_utils::{make_paying_wallet, make_wallet}; - use actix::{Arbiter, System}; + use actix::{System}; use ethereum_types::U64; use ethsign_crypto::Keccak256; - use log::Level; + use log::{Level}; use masq_lib::constants::{ REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, VALUE_EXCEEDS_ALLOWED_LIMIT, }; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; use masq_lib::messages::{ - CustomQueries, RangeQuery, ScanType, TopRecordsConfig, UiFinancialStatistics, + CustomQueries, RangeQuery, TopRecordsConfig, UiFinancialStatistics, UiMessageError, UiPayableAccount, UiReceivableAccount, UiScanRequest, UiScanResponse, }; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; - use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; - use masq_lib::utils::find_free_port; - use std::any::TypeId; - use std::ops::{Add, Sub}; - use std::str::FromStr; + use std::any::{TypeId}; + use std::ops::{Sub}; use std::sync::Arc; use std::sync::Mutex; - use std::time::Duration; + use std::time::{Duration, UNIX_EPOCH}; use std::vec; + use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::test_utils::recorder_counter_msgs::SingleTypeCounterMsgSetup; impl Handler> for Accountant { type Result = (); @@ -1230,33 +1402,26 @@ mod tests { ); let financial_statistics = result.financial_statistics().clone(); - let assert_scan_scheduler = |scan_type: ScanType, expected_scan_interval: Duration| { - assert_eq!( - result - .scan_schedulers - .schedulers - .get(&scan_type) - .unwrap() - .interval(), - expected_scan_interval - ) - }; let default_scan_intervals = ScanIntervals::default(); - assert_scan_scheduler( - ScanType::Payables, - default_scan_intervals.payable_scan_interval, + assert_eq!( + result.scan_schedulers.payable.new_payable_interval, + default_scan_intervals.payable_scan_interval ); - assert_scan_scheduler( - ScanType::PendingPayables, + assert_eq!( + result.scan_schedulers.pending_payable.interval, default_scan_intervals.pending_payable_scan_interval, ); - assert_scan_scheduler( - ScanType::Receivables, + assert_eq!( + result.scan_schedulers.receivable.interval, default_scan_intervals.receivable_scan_interval, ); + assert_eq!(result.scan_schedulers.automatic_scans_enabled, true); + assert_eq!( + result.scanners.aware_of_unresolved_pending_payables(), + false + ); assert_eq!(result.consuming_wallet_opt, None); assert_eq!(result.earning_wallet, *DEFAULT_EARNING_WALLET); - assert_eq!(result.suppress_initial_scans, false); result .message_id_generator .as_any() @@ -1330,100 +1495,7 @@ mod tests { } #[test] - fn scan_receivables_request() { - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }); - let receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_result(vec![]) - .paid_delinquencies_result(vec![]); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) - .build(); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let ui_message = NodeFromUiMessage { - client_id: 1234, - body: UiScanRequest { - scan_type: ScanType::Receivables, - } - .tmb(4321), - }; - - subject_addr.try_send(ui_message).unwrap(); - - System::current().stop(); - system.run(); - let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!( - blockchain_bridge_recording.get_record::(0), - &RetrieveTransactions { - recipient: make_wallet("earning_wallet"), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - } - ); - } - - #[test] - fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }); - config.suppress_initial_scans = true; - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .config_dao( - ConfigDaoMock::new() - .get_result(Ok(ConfigDaoRecord::new("start_block", None, false))) - .set_result(Ok(())), - ) - .build(); - let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let received_payments = ReceivedPayments { - timestamp: SystemTime::now(), - new_start_block: BlockMarker::Value(0), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - transactions: vec![], - }; - - subject_addr.try_send(received_payments).unwrap(); - - System::current().stop(); - system.run(); - let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); - assert_eq!( - ui_gateway_recording.get_record::(0), - &NodeToUiMessage { - target: ClientId(1234), - body: UiScanResponse {}.tmb(4321), - } - ); - } - - #[test] - fn scan_payables_request() { + fn externally_triggered_scan_payables_request() { let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); let consuming_wallet = make_paying_wallet(b"consuming"); let payable_account = PayableAccount { @@ -1436,18 +1508,24 @@ mod tests { }; let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(QualifiedPayablesMessage)); + let blockchain_bridge_addr = blockchain_bridge.start(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); let subject_addr = subject.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { @@ -1458,13 +1536,12 @@ mod tests { subject_addr.try_send(ui_message).unwrap(); - System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( blockchain_bridge_recording.get_record::(0), &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable_account]), + qualified_payables: vec![payable_account], consuming_wallet, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1483,17 +1560,18 @@ mod tests { no_rowid_results: vec![], }); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let sent_payable = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: make_wallet("blah"), @@ -1504,6 +1582,7 @@ mod tests { context_id: 4321, }), }; + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); subject_addr.try_send(sent_payable).unwrap(); @@ -1520,17 +1599,16 @@ mod tests { } #[test] - fn received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge( - ) { - // the numbers for balances don't do real math, they need not to match either the condition for + fn qualified_payables_under_our_money_limit_are_forwarded_to_blockchain_bridge_right_away() { + // The numbers in balances don't do real math, they don't need to match either the condition for // the payment adjustment or the actual values that come from the payable size reducing algorithm; // all that is mocked in this test init_test_logging(); - let test_name = "received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge"; + let test_name = "qualified_payables_under_our_money_limit_are_forwarded_to_blockchain_bridge_right_away"; let is_adjustment_required_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let instructions_recipient = blockchain_bridge - .system_stop_conditions(match_every_type_id!(OutboundPaymentsInstructions)) + .system_stop_conditions(match_lazily_every_type_id!(OutboundPaymentsInstructions)) .start() .recipient(); let mut subject = AccountantBuilder::default().build(); @@ -1540,7 +1618,11 @@ mod tests { let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); - subject.scanners.payable = Box::new(payable_scanner); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); subject.outbound_payments_instructions_sub_opt = Some(instructions_recipient); subject.logger = Logger::new(test_name); let subject_addr = subject.start(); @@ -1551,7 +1633,7 @@ mod tests { let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp); let accounts = vec![account_1, account_2]; let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_payables_in_test(accounts.clone()), + qualified_payables: accounts.clone(), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1566,8 +1648,8 @@ mod tests { let (blockchain_agent_with_context_msg_actual, logger_clone) = is_adjustment_required_params.remove(0); assert_eq!( - blockchain_agent_with_context_msg_actual.protected_qualified_payables, - protect_payables_in_test(accounts.clone()) + blockchain_agent_with_context_msg_actual.qualified_payables, + accounts.clone() ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -1599,31 +1681,37 @@ mod tests { agent_id_stamp ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) - // adjust_payments() did not need a prepared result which means it wasn't reached + assert_using_the_same_logger(&logger_clone, test_name, None) + // adjust_payments() did not need a prepared result, which means it wasn't reached // because otherwise this test would've panicked } - fn test_use_of_the_same_logger(logger_clone: &Logger, test_name: &str) { - let experiment_msg = format!("DEBUG: {test_name}: hello world"); + fn assert_using_the_same_logger( + logger_clone: &Logger, + test_name: &str, + differentiation_opt: Option<&str>, + ) { let log_handler = TestLogHandler::default(); + let experiment_msg = format!("DEBUG: {test_name}: hello world: {:?}", differentiation_opt); log_handler.exists_no_log_containing(&experiment_msg); - debug!(logger_clone, "hello world"); + + debug!(logger_clone, "hello world: {:?}", differentiation_opt); + log_handler.exists_log_containing(&experiment_msg); } #[test] - fn received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge( - ) { - // the numbers for balances don't do real math, they need not to match either the condition for + fn qualified_payables_over_masq_balance_are_adjusted_before_sending_to_blockchain_bridge() { + // The numbers in balances don't do real math, they don't need to match either the condition for // the payment adjustment or the actual values that come from the payable size reducing algorithm; // all that is mocked in this test init_test_logging(); - let test_name = "received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge"; + let test_name = + "qualified_payables_over_masq_balance_are_adjusted_before_sending_to_blockchain_bridge"; let adjust_payments_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let report_recipient = blockchain_bridge - .system_stop_conditions(match_every_type_id!(OutboundPaymentsInstructions)) + .system_stop_conditions(match_lazily_every_type_id!(OutboundPaymentsInstructions)) .start() .recipient(); let mut subject = AccountantBuilder::default().build(); @@ -1644,12 +1732,10 @@ mod tests { let agent_id_stamp_first_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_first_phase); - let initial_unadjusted_accounts = protect_payables_in_test(vec![ - unadjusted_account_1.clone(), - unadjusted_account_2.clone(), - ]); + let initial_unadjusted_accounts = + vec![unadjusted_account_1.clone(), unadjusted_account_2.clone()]; let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: initial_unadjusted_accounts.clone(), + qualified_payables: initial_unadjusted_accounts.clone(), agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; @@ -1671,7 +1757,11 @@ mod tests { let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); - subject.scanners.payable = Box::new(payable_scanner); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); subject.outbound_payments_instructions_sub_opt = Some(report_recipient); subject.logger = Logger::new(test_name); let subject_addr = subject.start(); @@ -1689,7 +1779,7 @@ mod tests { assert_eq!( actual_prepared_adjustment .original_setup_msg - .protected_qualified_payables, + .qualified_payables, initial_unadjusted_accounts ); assert_eq!( @@ -1723,13 +1813,12 @@ mod tests { Some(response_skeleton) ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) + assert_using_the_same_logger(&logger_clone, test_name, None) } #[test] - fn scan_pending_payables_request() { + fn externally_triggered_scan_pending_payables_request() { let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), receivable_scan_interval: Duration::from_millis(10_000), @@ -1745,18 +1834,24 @@ mod tests { }; let pending_payable_dao = PendingPayableDaoMock::default() .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let subject_addr = subject.start(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)); + let blockchain_bridge_addr = blockchain_bridge.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); + let subject_addr = subject.start(); let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { @@ -1767,13 +1862,12 @@ mod tests { subject_addr.try_send(ui_message).unwrap(); - System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable: vec![fingerprint], + pending_payable_fingerprints: vec![fingerprint], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1783,46 +1877,106 @@ mod tests { } #[test] - fn scan_request_from_ui_is_handled_in_case_the_scan_is_already_running() { - init_test_logging(); - let test_name = "scan_request_from_ui_is_handled_in_case_the_scan_is_already_running"; - let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), + fn externally_triggered_scan_identifies_all_pending_payables_as_complete() { + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 565, + context_id: 112233, }); - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transaction_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let ui_gateway_addr = ui_gateway.start(); + let system = System::new("test"); + subject.scan_schedulers.automatic_scans_enabled = false; + // Making sure we would kill the test if any sort of scan was scheduled + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); + let subject_addr = subject.start(); + let tx_fingerprint = make_pending_payable_fingerprint(); + let report_tx_receipts = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(777), + status: TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(456), + block_number: 78901234.into(), + }), + }), + tx_fingerprint.clone(), + )], + response_skeleton_opt, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint]); + + subject_addr.try_send(report_tx_receipts).unwrap(); + + system.run(); + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transaction_confirmed_params, vec![vec![tx_fingerprint]]); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + assert_eq!( + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton_opt.unwrap().client_id), + body: UiScanResponse {}.tmb(response_skeleton_opt.unwrap().context_id), + } + ); + assert_eq!(ui_gateway_recording.len(), 1); + } + + #[test] + fn externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running() { + init_test_logging(); + let test_name = + "externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running"; + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.automatic_scans_enabled = false; + let now_unix = to_unix_timestamp(SystemTime::now()); + let payment_thresholds = PaymentThresholds::default(); + let past_timestamp_unix = now_unix + - (payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec) as i64; + let mut payable_account = make_payable_account(123); + payable_account.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei); + payable_account.last_paid_timestamp = from_unix_timestamp(past_timestamp_unix); + let payable_dao = + PayableDaoMock::default().non_pending_payables_result(vec![payable_account]); let subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(make_paying_wallet(b"consuming")) .logger(Logger::new(test_name)) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); - let system = System::new("test"); + let system = System::new(test_name); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .ui_gateway(ui_gateway) + .build(); let first_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: ScanType::PendingPayables, + scan_type: ScanType::Payables, } .tmb(4321), }; let second_message = first_message.clone(); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); subject_addr.try_send(BindMessage { peer_actors }).unwrap(); subject_addr.try_send(first_message).unwrap(); @@ -1832,117 +1986,222 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {}: PendingPayables scan was already initiated", + "INFO: {}: Payables scan was already initiated", test_name )); assert_eq!(blockchain_bridge_recording.len(), 1); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let msg = ui_gateway_recording.get_record::(0); + assert_eq!(msg.body, UiScanResponse {}.tmb(4321)); + } + + fn test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled( + test_name: &str, + scan_type: ScanType, + ) { + let expected_log_msg = format!( + "WARN: {test_name}: User requested {:?} scan was denied. Automatic mode \ + prevents manual triggers.", + scan_type + ); + + test_externally_triggered_scan_is_prevented_if( + true, + true, + test_name, + scan_type, + &expected_log_msg, + ) + } + + fn test_externally_triggered_scan_is_prevented_if( + automatic_scans_enabled: bool, + aware_of_unresolved_pending_payables: bool, + test_name: &str, + scan_type: ScanType, + expected_log_message: &str, + ) { + init_test_logging(); + let (blockchain_bridge, _, blockchain_bridge_recorder_arc) = make_recorder(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .consuming_wallet(make_wallet("abc")) + .build(); + subject.scan_schedulers.automatic_scans_enabled = automatic_scans_enabled; + subject + .scanners + .set_aware_of_unresolved_pending_payables(aware_of_unresolved_pending_payables); + subject.scanners.unset_initial_pending_payable_scan(); + let subject_addr = subject.start(); + let system = System::new(test_name); + let peer_actors = PeerActorsBuilder::default() + .ui_gateway(ui_gateway) + .blockchain_bridge(blockchain_bridge) + .build(); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { scan_type }.tmb(6789), + }; + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + + subject_addr.try_send(ui_message).unwrap(); + + assert_eq!(system.run(), 0); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let msg = ui_gateway_recording.get_record::(0); + assert_eq!(msg.body, UiScanResponse {}.tmb(6789)); + assert_eq!(ui_gateway_recording.len(), 1); + let blockchain_bridge_recorder = blockchain_bridge_recorder_arc.lock().unwrap(); + assert_eq!(blockchain_bridge_recorder.len(), 0); + TestLogHandler::new().exists_log_containing(expected_log_message); } #[test] - fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), + fn externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled() { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled("externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled", ScanType::Payables) + } + + #[test] + fn externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled() + { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled("externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled", ScanType::PendingPayables) + } + + #[test] + fn externally_triggered_scan_for_receivables_is_prevented_if_automatic_scans_are_enabled() { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled( + "externally_triggered_scan_for_receivables_is_prevented_if_automatic_scans_are_enabled", + ScanType::Receivables, + ) + } + + #[test] + fn externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete( + ) { + let test_name = "externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete"; + let expected_log_msg = format!( + "INFO: {test_name}: User requested PendingPayables scan was denied expecting zero \ + findings. Run the Payable scanner first." + ); + + test_externally_triggered_scan_is_prevented_if( + false, + false, + test_name, + ScanType::PendingPayables, + &expected_log_msg, + ) + } + + #[test] + fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( + ) { + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 4555, + context_id: 5566, }); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) + // TODO when we have more logic in place with the other cards taken in, we'll need to configure these + // accordingly + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]) + .mark_failures_result(Ok(())); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable)]) .build(); + subject.scan_schedulers.automatic_scans_enabled = false; + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let (peer_actors, peer_addresses) = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .ui_gateway(ui_gateway) + .build_and_provide_addresses(); let subject_addr = subject.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(234), + status: TxStatus::Failed + }), + make_pending_payable_fingerprint() + )], + response_skeleton_opt + }, + &subject_addr + ); + let second_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable { + recipient_wallet: make_wallet("abc"), + hash: make_tx_hash(789) + } + )]), + response_skeleton_opt + }, + &subject_addr + ); + peer_addresses + .blockchain_bridge_addr + .try_send(SetUpCounterMsgs::new(vec![ + first_counter_msg_setup, + second_counter_msg_setup, + ])) + .unwrap(); subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), + let pending_payable_request = ScanForPendingPayables { + response_skeleton_opt, }; - subject_addr.try_send(report_transaction_receipts).unwrap(); + subject_addr.try_send(pending_payable_request).unwrap(); - System::current().stop(); system.run(); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), &NodeToUiMessage { - target: ClientId(1234), - body: UiScanResponse {}.tmb(4321), + target: ClientId(4555), + body: UiScanResponse {}.tmb(5566), } ); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!(blockchain_bridge_recording.len(), 2); } #[test] - fn accountant_calls_payable_dao_to_mark_pending_payable() { - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let expected_wallet = make_wallet("paying_you"); - let expected_hash = H256::from("transaction_hash".keccak256()); - let expected_rowid = 45623; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(expected_rowid, expected_hash)], - no_rowid_results: vec![], - }); - let payable_dao = PayableDaoMock::new() - .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) - .mark_pending_payables_rowids_result(Ok(())); - let system = System::new("accountant_calls_payable_dao_to_mark_pending_payable"); - let accountant = AccountantBuilder::default() - .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) - .payable_daos(vec![ForPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) - .build(); - let expected_payable = PendingPayable::new(expected_wallet.clone(), expected_hash.clone()); - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( - expected_payable.clone(), - )]), - response_skeleton_opt: None, - }; - let subject = accountant.start(); - - subject - .try_send(sent_payable) - .expect("unexpected actix error"); - - System::current().stop(); - system.run(); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); - assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); - let mark_pending_payables_rowids_params = - mark_pending_payables_rowids_params_arc.lock().unwrap(); - assert_eq!( - *mark_pending_payables_rowids_params, - vec![vec![(expected_wallet, expected_rowid)]] - ); - } - - #[test] - fn accountant_sends_initial_payable_payments_msg_when_qualified_payable_found() { + fn accountant_sends_qualified_payable_msg_when_qualified_payable_found() { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); let (qualified_payables, _, all_non_pending_payables) = - make_payables(now, &payment_thresholds); + make_qualified_and_unqualified_payables(now, &payment_thresholds); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let system = System::new( - "accountant_sends_initial_payable_payments_msg_when_qualified_payable_found", - ); + let system = + System::new("accountant_sends_qualified_payable_msg_when_qualified_payable_found"); let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.receivable = Box::new(NullScanner::new()); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -1950,7 +2209,11 @@ mod tests { .build(); send_bind_message!(accountant_subs, peer_actors); - send_start_message!(accountant_subs); + accountant_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); System::current().stop(); system.run(); @@ -1960,17 +2223,188 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + qualified_payables, consuming_wallet, response_skeleton_opt: None, } ); } + #[test] + fn automatic_scan_for_new_payables_schedules_another_one_immediately_if_no_qualified_payables_found( + ) { + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let system = + System::new("automatic_scan_for_new_payables_schedules_another_one_immediately_if_no_qualified_payables_found"); + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = AccountantBuilder::default() + .consuming_wallet(consuming_wallet) + .build(); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + subject.scan_schedulers.payable.dyn_interval_computer = Box::new( + NewPayableScanDynIntervalComputerMock::default() + .compute_interval_result(Some(Duration::from_secs(500))), + ); + let payable_scanner = ScannerMock::default() + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_result(Err(StartScanError::NothingToProcess)); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + let accountant_addr = subject.start(); + + accountant_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + System::current().stop(); + assert_eq!(system.run(), 0); + let mut notify_later_params = notify_later_params_arc.lock().unwrap(); + let (msg, interval) = notify_later_params.remove(0); + assert_eq!( + msg, + ScanForNewPayables { + response_skeleton_opt: None + } + ); + assert_eq!(interval, Duration::from_secs(500)); + assert_eq!(notify_later_params.len(), 0); + // Accountant is unbound; therefore, it is guaranteed that sending a message to + // the BlockchainBridge wasn't attempted. It would've panicked otherwise. + } + + #[test] + fn accountant_handles_scan_for_retry_payables() { + init_test_logging(); + let test_name = "accountant_handles_scan_for_retry_payables"; + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let consuming_wallet = make_wallet("abc"); + subject.consuming_wallet_opt = Some(consuming_wallet.clone()); + let qualified_payables_msg = QualifiedPayablesMessage { + qualified_payables: vec![make_payable_account(789)], + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + }; + let payable_scanner_mock = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner_mock, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + let accountant_addr = subject.start(); + let accountant_subs = Accountant::make_subs_from(&accountant_addr); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); + send_bind_message!(accountant_subs, peer_actors); + + accountant_addr + .try_send(ScanForRetryPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + System::current().stop(); + let before = SystemTime::now(); + system.run(); + let after = SystemTime::now(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let (actual_wallet, actual_now, actual_response_skeleton_opt, actual_logger, _) = + start_scan_params.remove(0); + assert_eq!(actual_wallet, consuming_wallet); + assert_eq!(actual_response_skeleton_opt, None); + assert!(before <= actual_now && actual_now <= after); + assert!( + start_scan_params.is_empty(), + "should be empty but was {:?}", + start_scan_params + ); + let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); + let message = blockchain_bridge_recorder.get_record::(0); + assert_eq!(message, &qualified_payables_msg); + assert_eq!(blockchain_bridge_recorder.len(), 1); + assert_using_the_same_logger(&actual_logger, test_name, None) + } + + #[test] + fn scan_for_retry_payables_if_consuming_wallet_is_not_present() { + init_test_logging(); + let test_name = "scan_for_retry_payables_if_consuming_wallet_is_not_present"; + let system = System::new(test_name); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let ui_gateway_addr = ui_gateway.start(); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let payable_scanner_mock = ScannerMock::new(); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner_mock, + ))); + subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); + // It must be populated because no errors are tolerated at the RetryPayableScanner + // if automatic scans are on + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 789, + context_id: 111, + }); + let accountant_addr = subject.start(); + + accountant_addr + .try_send(ScanForRetryPayables { + response_skeleton_opt, + }) + .unwrap(); + + system.run(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let message = ui_gateway_recording.get_record::(0); + assert_eq!( + message, + &NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton_opt.unwrap().client_id), + body: UiScanResponse {}.tmb(response_skeleton_opt.unwrap().context_id) + } + ); + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found")); + } + #[test] fn accountant_requests_blockchain_bridge_to_scan_for_received_payments() { init_test_logging(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RetrieveTransactions)); let earning_wallet = make_wallet("someearningwallet"); let system = System::new("accountant_requests_blockchain_bridge_to_scan_for_received_payments"); @@ -1981,8 +2415,12 @@ mod tests { .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.payable = Box::new(NullScanner::new()); + // Important. Preventing the possibly endless sequence of + // PendingPayableScanner -> NewPayableScanner -> NewPayableScanner... + subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default()); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -1992,10 +2430,8 @@ mod tests { send_start_message!(accountant_subs); - System::current().stop(); system.run(); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recorder.len(), 1); let retrieve_transactions_msg = blockchain_bridge_recorder.get_record::(0); assert_eq!( @@ -2005,6 +2441,101 @@ mod tests { response_skeleton_opt: None, } ); + assert_eq!(blockchain_bridge_recorder.len(), 1); + } + + #[test] + fn externally_triggered_scan_receivables_request() { + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_millis(2_000), + receivable_scan_interval: Duration::from_millis(10_000), + }); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_result(vec![]) + .paid_delinquencies_result(vec![]); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(config) + .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) + .build(); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RetrieveTransactions)); + let blockchain_bridge_addr = blockchain_bridge.start(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.retrieve_transactions_sub_opt = Some(blockchain_bridge_addr.recipient()); + let subject_addr = subject.start(); + let system = System::new("test"); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: ScanType::Receivables, + } + .tmb(4321), + }; + + subject_addr.try_send(ui_message).unwrap(); + + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!( + blockchain_bridge_recording.get_record::(0), + &RetrieveTransactions { + recipient: make_wallet("earning_wallet"), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + } + ); + } + + #[test] + fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_millis(2_000), + receivable_scan_interval: Duration::from_millis(10_000), + }); + config.automatic_scans_enabled = false; + let subject = AccountantBuilder::default() + .bootstrapper_config(config) + .config_dao( + ConfigDaoMock::new() + .get_result(Ok(ConfigDaoRecord::new("start_block", None, false))) + .set_result(Ok(())), + ) + .build(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + let received_payments = ReceivedPayments { + timestamp: SystemTime::now(), + new_start_block: BlockMarker::Value(0), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + transactions: vec![], + }; + + subject_addr.try_send(received_payments).unwrap(); + + System::current().stop(); + system.run(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + assert_eq!( + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + target: ClientId(1234), + body: UiScanResponse {}.tmb(4321), + } + ); } #[test] @@ -2077,83 +2608,747 @@ mod tests { } #[test] - fn accountant_scans_after_startup() { + fn accountant_scans_after_startup_and_does_not_detect_any_pending_payables() { + // We will want to prove that the PendingPayableScanner runs before the NewPayableScanner. + // Their relationship towards the ReceivableScanner isn't important. init_test_logging(); - let pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let payable_params_arc = Arc::new(Mutex::new(vec![])); - let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); - let paid_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); - let (blockchain_bridge, _, _) = make_recorder(); + let test_name = "accountant_scans_after_startup_and_does_not_detect_any_pending_payables"; + let scan_params = ScanParams::default(); + let notify_and_notify_later_params = NotifyAndNotifyLaterParams::default(); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let earning_wallet = make_wallet("earning"); - let system = System::new("accountant_scans_after_startup"); - let config = bc_from_wallets(make_wallet("buy"), earning_wallet.clone()); - let payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&payable_params_arc) - .non_pending_payables_result(vec![]); - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_params(&pending_payable_params_arc) - .return_all_errorless_fingerprints_result(vec![]); - let receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_parameters(&new_delinquencies_params_arc) - .new_delinquencies_result(vec![]) - .paid_delinquencies_parameters(&paid_delinquencies_params_arc) - .paid_delinquencies_result(vec![]); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .payable_daos(vec![ForPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) - .build(); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); + let consuming_wallet = make_wallet("consuming"); + let system = System::new(test_name); + let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); + let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.payable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.pending_payable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let receivable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.receivable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let (subject, new_payable_expected_computed_interval, receivable_scan_interval) = + set_up_subject_for_no_pending_payables_found_startup_test( + test_name, + ¬ify_and_notify_later_params, + &compute_interval_params_arc, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let peer_actors = peer_actors_builder().build(); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); - System::current().stop(); + // The system is stopped by the NotifyLaterHandleMock for the Receivable scanner + let before = SystemTime::now(); system.run(); - let payable_params = payable_params_arc.lock().unwrap(); - let pending_payable_params = pending_payable_params_arc.lock().unwrap(); - //proof of calling pieces of scan_for_delinquencies() - let mut new_delinquencies_params = new_delinquencies_params_arc.lock().unwrap(); - let (captured_timestamp, captured_curves) = new_delinquencies_params.remove(0); - let paid_delinquencies_params = paid_delinquencies_params_arc.lock().unwrap(); - assert_eq!(*payable_params, vec![()]); - assert_eq!(*pending_payable_params, vec![()]); - assert!(new_delinquencies_params.is_empty()); + let after = SystemTime::now(); + assert_pending_payable_scanner_for_no_pending_payable_found( + test_name, + consuming_wallet, + &scan_params.pending_payable_start_scan, + ¬ify_and_notify_later_params.pending_payables_notify_later, + before, + after, + ); + assert_payable_scanner_for_no_pending_payable_found( + ¬ify_and_notify_later_params, + compute_interval_params_arc, + new_payable_expected_computed_interval, + before, + after, + ); + assert_receivable_scanner( + test_name, + earning_wallet, + &scan_params.receivable_start_scan, + ¬ify_and_notify_later_params.receivables_notify_later, + receivable_scan_interval, + ); + // The test lays down evidences that the NewPayableScanner couldn't run before + // the PendingPayableScanner, which is an intention. + // To interpret the evidence, we have to notice that the PendingPayableScanner ran + // certainly, while it wasn't attempted to schedule in the whole test. That points out that + // the scanning sequence started spontaneously, not requiring any prior scheduling. Most + // importantly, regarding the payable scanner, it ran not even once. We know, though, + // that its scheduling did take place, specifically an urgent call of the new payable mode. + // That totally corresponds with the expected behavior where the PendingPayableScanner + // should first search for any stray pending payables; if no findings, the NewPayableScanner + // is supposed to go next, and it shouldn't have to undertake the standard new-payable + // interval, but here, at the beginning, it comes immediately. + } + + #[test] + fn accountant_scans_after_startup_and_detects_pending_payable_from_before() { + // We do ensure the PendingPayableScanner runs before the NewPayableScanner. Not interested + // in an exact placing of the ReceivableScanner so much. + init_test_logging(); + let test_name = "accountant_scans_after_startup_and_detects_pending_payable_from_before"; + let scan_params = ScanParams::default(); + let notify_and_notify_later_params = NotifyAndNotifyLaterParams::default(); + let earning_wallet = make_wallet("earning"); + let consuming_wallet = make_wallet("consuming"); + let system = System::new(test_name); + let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); + let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); + let pp_fingerprint = make_pending_payable_fingerprint(); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_params(&scan_params.payable_start_scan) + .start_scan_result(Ok(QualifiedPayablesMessage { + qualified_payables: vec![make_payable_account(123)], + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + })) + .finish_scan_params(&scan_params.payable_finish_scan) + // Important + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.pending_payable_start_scan) + .start_scan_result(Ok(RequestTransactionReceipts { + pending_payable_fingerprints: vec![pp_fingerprint], + response_skeleton_opt: None, + })) + .finish_scan_params(&scan_params.pending_payable_finish_scan) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + let receivable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.receivable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let (subject, pending_payable_expected_notify_later_interval, receivable_scan_interval) = + set_up_subject_for_some_pending_payable_found_startup_test( + test_name, + ¬ify_and_notify_later_params, + config, + payable_scanner, + pending_payable_scanner, + receivable_scanner, + ); + let (peer_actors, addresses) = peer_actors_builder().build_and_provide_addresses(); + let subject_addr: Addr = subject.start(); + let subject_subs = Accountant::make_subs_from(&subject_addr); + let expected_report_transaction_receipts = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(789), + status: TxStatus::Failed, + }), + make_pending_payable_fingerprint(), + )], + response_skeleton_opt: None, + }; + let expected_sent_payables = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { + recipient_wallet: make_wallet("bcd"), + hash: make_tx_hash(890), + })]), + response_skeleton_opt: None, + }; + let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + expected_report_transaction_receipts.clone(), + &subject_addr + ); + let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + expected_sent_payables.clone(), + &subject_addr + ); + send_bind_message!(subject_subs, peer_actors); + addresses + .blockchain_bridge_addr + .try_send(SetUpCounterMsgs::new(vec![ + blockchain_bridge_counter_msg_setup_for_pending_payable_scanner, + blockchain_bridge_counter_msg_setup_for_payable_scanner, + ])) + .unwrap(); + + send_start_message!(subject_subs); + + // The system is stopped by the NotifyHandleLaterMock for the PendingPayable scanner + let before = SystemTime::now(); + system.run(); + let after = SystemTime::now(); + assert_pending_payable_scanner_for_some_pending_payable_found( + test_name, + consuming_wallet.clone(), + &scan_params, + ¬ify_and_notify_later_params.pending_payables_notify_later, + pending_payable_expected_notify_later_interval, + expected_report_transaction_receipts, + before, + after, + ); + assert_payable_scanner_for_some_pending_payable_found( + test_name, + consuming_wallet, + &scan_params, + ¬ify_and_notify_later_params, + expected_sent_payables, + ); + assert_receivable_scanner( + test_name, + earning_wallet, + &scan_params.receivable_start_scan, + ¬ify_and_notify_later_params.receivables_notify_later, + receivable_scan_interval, + ); + // Given the assertions prove that the pending payable scanner has run multiple times + // before the new payable scanner started or was scheduled, the front position belongs to + // the one first mentioned, no doubts. + } + + #[derive(Default)] + struct ScanParams { + payable_start_scan: + Arc, Logger, String)>>>, + payable_finish_scan: Arc>>, + pending_payable_start_scan: + Arc, Logger, String)>>>, + pending_payable_finish_scan: Arc>>, + receivable_start_scan: + Arc, Logger, String)>>>, + // receivable_finish_scan ... not needed + } + + #[derive(Default)] + struct NotifyAndNotifyLaterParams { + new_payables_notify_later: Arc>>, + new_payables_notify: Arc>>, + retry_payables_notify: Arc>>, + pending_payables_notify_later: Arc>>, + receivables_notify_later: Arc>>, + } + + fn set_up_subject_for_no_pending_payables_found_startup_test( + test_name: &str, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + compute_interval_params_arc: &Arc>>, + config: BootstrapperConfig, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + payable_scanner: ScannerMock, + ) -> (Accountant, Duration, Duration) { + let mut subject = make_subject_and_inject_scanners( + test_name, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let new_payable_expected_computed_interval = Duration::from_secs(3600); + // Important that this is made short because the test relies on it with the system stop. + let receivable_scan_interval = Duration::from_millis(50); + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later), + ); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), + ); + subject.scan_schedulers.payable.retry_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.retry_payables_notify), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.new_payables_notify), + ); + let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later) + .stop_system_on_count_received(1); + subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); + subject.scan_schedulers.receivable.interval = receivable_scan_interval; + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_result(Some(new_payable_expected_computed_interval)); + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + ( + subject, + new_payable_expected_computed_interval, + receivable_scan_interval, + ) + } + + fn set_up_subject_for_some_pending_payable_found_startup_test( + test_name: &str, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + config: BootstrapperConfig, + payable_scanner: ScannerMock, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + ) -> (Accountant, Duration, Duration) { + let mut subject = make_subject_and_inject_scanners( + test_name, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let pending_payable_scan_interval = Duration::from_secs(3600); + let receivable_scan_interval = Duration::from_secs(3600); + let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later) + // This should stop the system + .stop_system_on_count_received(1); + subject.scan_schedulers.pending_payable.handle = + Box::new(pending_payable_notify_later_handle_mock); + subject.scan_schedulers.pending_payable.interval = pending_payable_scan_interval; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), + ); + subject.scan_schedulers.payable.retry_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.retry_payables_notify) + .capture_msg_and_let_it_fly_on(), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.new_payables_notify), + ); + let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later); + subject.scan_schedulers.receivable.interval = receivable_scan_interval; + subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); + ( + subject, + pending_payable_scan_interval, + receivable_scan_interval, + ) + } + + fn make_subject_and_inject_scanners( + test_name: &str, + config: BootstrapperConfig, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + payable_scanner: ScannerMock, + ) -> Accountant { + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .bootstrapper_config(config) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + } + + fn assert_pending_payable_scanner_for_no_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + pending_payable_start_scan_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + scan_for_pending_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) { + let pp_logger = pending_payable_common( + consuming_wallet, + pending_payable_start_scan_params_arc, + act_started_at, + act_finished_at, + ); + let scan_for_pending_payables_notify_later_params = + scan_for_pending_payables_notify_later_params_arc + .lock() + .unwrap(); + // PendingPayableScanner can only start after NewPayableScanner finishes and makes at least + // one transaction. The test stops before running NewPayableScanner, missing both + // the second PendingPayableScanner run and its scheduling event. assert!( - captured_timestamp < SystemTime::now() - && captured_timestamp - >= from_unix_timestamp(to_unix_timestamp(SystemTime::now()) - 5) + scan_for_pending_payables_notify_later_params.is_empty(), + "We did not expect to see another schedule for pending payables, but it happened {:?}", + scan_for_pending_payables_notify_later_params ); - assert_eq!(captured_curves, PaymentThresholds::default()); - assert_eq!(paid_delinquencies_params.len(), 1); - assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing("INFO: Accountant: Scanning for payables"); - tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); - tlh.exists_log_containing(&format!( - "INFO: Accountant: Scanning for receivables to {}", - earning_wallet - )); - tlh.exists_log_containing("INFO: Accountant: Scanning for delinquencies"); + assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); + } + + fn assert_pending_payable_scanner_for_some_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + scan_params: &ScanParams, + scan_for_pending_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + pending_payable_expected_notify_later_interval: Duration, + expected_report_tx_receipts_msg: ReportTransactionReceipts, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) { + let pp_start_scan_logger = pending_payable_common( + consuming_wallet, + &scan_params.pending_payable_start_scan, + act_started_at, + act_finished_at, + ); + assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); + let mut pending_payable_finish_scan_params = + scan_params.pending_payable_finish_scan.lock().unwrap(); + let (actual_report_tx_receipts_msg, pp_finish_scan_logger) = + pending_payable_finish_scan_params.remove(0); + assert_eq!( + actual_report_tx_receipts_msg, + expected_report_tx_receipts_msg + ); + assert_using_the_same_logger(&pp_finish_scan_logger, test_name, Some("pp finish scan")); + let scan_for_pending_payables_notify_later_params = + scan_for_pending_payables_notify_later_params_arc + .lock() + .unwrap(); + // This is the moment when the test ends. It says that we went the way of the pending payable + // sequence, instead of calling the NewPayableScan just after the initial pending payable + // scan. + assert_eq!( + *scan_for_pending_payables_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + pending_payable_expected_notify_later_interval + )], + ); + } + + fn pending_payable_common( + consuming_wallet: Wallet, + pending_payable_start_scan_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) -> Logger { + let mut pending_payable_params = pending_payable_start_scan_params_arc.lock().unwrap(); + let ( + pp_wallet, + pp_scan_started_at, + pp_response_skeleton_opt, + pp_logger, + pp_trigger_msg_type_str, + ) = pending_payable_params.remove(0); + assert_eq!(pp_wallet, consuming_wallet); + assert_eq!(pp_response_skeleton_opt, None); + assert!( + pp_trigger_msg_type_str.contains("PendingPayable"), + "Should contain PendingPayable but {}", + pp_trigger_msg_type_str + ); + assert!( + pending_payable_params.is_empty(), + "Should be empty but was {:?}", + pending_payable_params + ); + assert!( + act_started_at <= pp_scan_started_at && pp_scan_started_at <= act_finished_at, + "The scanner was supposed to run between {:?} and {:?} but it was {:?}", + act_started_at, + act_finished_at, + pp_scan_started_at + ); + pp_logger + } + + fn assert_payable_scanner_for_no_pending_payable_found( + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + compute_interval_params_arc: Arc>>, + new_payable_expected_computed_interval: Duration, + act_started_at: SystemTime, + act_finished_at: SystemTime, + ) { + // Note that there is no functionality from the payable scanner actually running. + // We only witness it to be scheduled. + let scan_for_new_payables_notify_later_params = notify_and_notify_later_params + .new_payables_notify_later + .lock() + .unwrap(); + assert_eq!( + *scan_for_new_payables_notify_later_params, + vec![( + ScanForNewPayables { + response_skeleton_opt: None + }, + new_payable_expected_computed_interval + )] + ); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (p_scheduling_now, last_new_payable_scan_timestamp, _) = + compute_interval_params.remove(0); + assert_eq!(last_new_payable_scan_timestamp, UNIX_EPOCH); + let scan_for_new_payables_notify_params = notify_and_notify_later_params + .new_payables_notify + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_params.is_empty(), + "We did not expect any immediate scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_params + ); + let scan_for_retry_payables_notify_params = notify_and_notify_later_params + .retry_payables_notify + .lock() + .unwrap(); + assert!( + scan_for_retry_payables_notify_params.is_empty(), + "We did not expect any scheduling of retry payables, but it happened {:?}", + scan_for_retry_payables_notify_params + ); + assert!( + act_started_at <= p_scheduling_now && p_scheduling_now <= act_finished_at, + "The payable scan scheduling was supposed to take place between {:?} and {:?} \ + but it was {:?}", + act_started_at, + act_finished_at, + p_scheduling_now + ); + } + + fn assert_payable_scanner_for_some_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + scan_params: &ScanParams, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + expected_sent_payables: SentPayables, + ) { + let mut payable_start_scan_params = scan_params.payable_start_scan.lock().unwrap(); + let (p_wallet, _, p_response_skeleton_opt, p_start_scan_logger, p_trigger_msg_type_str) = + payable_start_scan_params.remove(0); + assert_eq!(p_wallet, consuming_wallet); + assert_eq!(p_response_skeleton_opt, None); + // Important: it's the proof that we're dealing with the RetryPayableScanner not NewPayableScanner + assert!( + p_trigger_msg_type_str.contains("RetryPayable"), + "Should contain RetryPayable but {}", + p_trigger_msg_type_str + ); + assert!( + payable_start_scan_params.is_empty(), + "Should be empty but was {:?}", + payable_start_scan_params + ); + assert_using_the_same_logger(&p_start_scan_logger, test_name, Some("retry payable start")); + let mut payable_finish_scan_params = scan_params.payable_finish_scan.lock().unwrap(); + let (actual_sent_payable, p_finish_scan_logger) = payable_finish_scan_params.remove(0); + assert_eq!(actual_sent_payable, expected_sent_payables,); + assert!( + payable_finish_scan_params.is_empty(), + "Should be empty but was {:?}", + payable_finish_scan_params + ); + assert_using_the_same_logger( + &p_finish_scan_logger, + test_name, + Some("retry payable finish"), + ); + let scan_for_new_payables_notify_later_params = notify_and_notify_later_params + .new_payables_notify_later + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_later_params.is_empty(), + "We did not expect any later scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_later_params + ); + let scan_for_new_payables_notify_params = notify_and_notify_later_params + .new_payables_notify + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_params.is_empty(), + "We did not expect any immediate scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_params + ); + let scan_for_retry_payables_notify_params = notify_and_notify_later_params + .retry_payables_notify + .lock() + .unwrap(); + assert_eq!( + *scan_for_retry_payables_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt: None + }], + ); + } + + fn assert_receivable_scanner( + test_name: &str, + earning_wallet: Wallet, + receivable_start_scan_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + scan_for_receivables_notify_later_params_arc: &Arc< + Mutex>, + >, + receivable_scan_interval: Duration, + ) { + let mut receivable_start_scan_params = receivable_start_scan_params_arc.lock().unwrap(); + let (r_wallet, _r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = + receivable_start_scan_params.remove(0); + assert_eq!(r_wallet, earning_wallet); + assert_eq!(r_response_skeleton_opt, None); + assert!( + r_trigger_msg_name_str.contains("Receivable"), + "Should contain Receivable but {}", + r_trigger_msg_name_str + ); + assert!( + receivable_start_scan_params.is_empty(), + "Should be already empty but was {:?}", + receivable_start_scan_params + ); + assert_using_the_same_logger(&r_logger, test_name, Some("r")); + let scan_for_receivables_notify_later_params = + scan_for_receivables_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *scan_for_receivables_notify_later_params, + vec![( + ScanForReceivables { + response_skeleton_opt: None + }, + receivable_scan_interval + )] + ); + } + + #[test] + fn initial_pending_payable_scan_if_some_payables_found() { + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let system = System::new("test"); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_addr = blockchain_bridge.start(); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); + let flag_before = subject.scanners.initial_pending_payable_scan(); + + let hint = subject.handle_request_of_scan_for_pending_payable(None); + + System::current().stop(); + system.run(); + let flag_after = subject.scanners.initial_pending_payable_scan(); + assert_eq!(hint, ScanRescheduleAfterEarlyStop::DoNotSchedule); + assert_eq!(flag_before, true); + assert_eq!(flag_after, false); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let _ = blockchain_bridge_recording.get_record::(0); + } + + #[test] + fn initial_pending_payable_scan_if_no_payables_found() { + let pending_payable_dao = + PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let flag_before = subject.scanners.initial_pending_payable_scan(); + + let hint = subject.handle_request_of_scan_for_pending_payable(None); + + let flag_after = subject.scanners.initial_pending_payable_scan(); + assert_eq!( + hint, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ); + assert_eq!(flag_before, true); + assert_eq!(flag_after, false); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ + cross_scan_cause_opt: None, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ + should be impossible with PendingPayableScanner in automatic mode" + )] + fn initial_pending_payable_scan_hits_unexpected_error() { + init_test_logging(); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("abc")) + .build(); + let pending_payable_scanner = + ScannerMock::default().scan_started_at_result(Some(UNIX_EPOCH)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + + let _ = subject.handle_request_of_scan_for_pending_payable(None); } #[test] fn periodical_scanning_for_receivables_and_delinquencies_works() { init_test_logging(); let test_name = "periodical_scanning_for_receivables_and_delinquencies_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_receivable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions let receivable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(RetrieveTransactions { + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Err(StartScanError::NothingToProcess)) + .start_scan_result(Ok(RetrieveTransactions { recipient: make_wallet("some_recipient"), response_skeleton_opt: None, })) @@ -2162,64 +3357,70 @@ mod tests { let mut config = bc_from_earning_wallet(earning_wallet.clone()); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(100), + pending_payable_scan_interval: Duration::from_secs(10), receivable_scan_interval: Duration::from_millis(99), - pending_payable_scan_interval: Duration::from_secs(100), }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .logger(Logger::new(test_name)) .build(); - subject.scanners.payable = Box::new(NullScanner::new()); // Skipping - subject.scanners.pending_payable = Box::new(NullScanner::new()); // Skipping - subject.scanners.receivable = Box::new(receivable_scanner); - subject.scan_schedulers.update_scheduler( - ScanType::Receivables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_receivable_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject.scan_schedulers.receivable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_later_receivable_params_arc) + .capture_msg_and_let_it_fly_on(), ); let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); - send_start_message!(subject_subs); + subject_addr + .try_send(ScanForReceivables { + response_skeleton_opt: None, + }) + .unwrap(); let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); let notify_later_receivable_params = notify_later_receivable_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Receivables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); + let tlh = TestLogHandler::new(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); let ( first_attempt_wallet, first_attempt_timestamp, first_attempt_response_skeleton_opt, first_attempt_logger, - ) = begin_scan_params.remove(0); + _, + ) = start_scan_params.remove(0); let ( second_attempt_wallet, second_attempt_timestamp, second_attempt_response_skeleton_opt, second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); + _, + ) = start_scan_params.remove(0); + assert_eq!(first_attempt_wallet, earning_wallet); assert_eq!(second_attempt_wallet, earning_wallet); assert!(time_before <= first_attempt_timestamp); assert!(first_attempt_timestamp <= second_attempt_timestamp); assert!(second_attempt_timestamp <= time_after); assert_eq!(first_attempt_response_skeleton_opt, None); assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); + assert!(start_scan_params.is_empty()); + debug!( + first_attempt_logger, + "first attempt verifying receivable scanner" + ); + debug!( + second_attempt_logger, + "second attempt verifying receivable scanner" + ); assert_eq!( *notify_later_receivable_params, vec![ @@ -2236,208 +3437,241 @@ mod tests { Duration::from_millis(99) ), ] - ) + ); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: There was nothing to process during Receivables scan." + )); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: first attempt verifying receivable scanner", + )); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: second attempt verifying receivable scanner", + )); } + // This test begins with the new payable scan, continues over the retry payable scan and ends + // with another attempt for new payables which proves one complete cycle. #[test] - fn periodical_scanning_for_pending_payable_works() { + fn periodical_scanning_for_payables_works() { init_test_logging(); - let test_name = "periodical_scanning_for_pending_payable_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_payables_works"; + let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); + let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); - SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_addr = blockchain_bridge.start(); + let payable_account = make_payable_account(123); + let qualified_payable = vec![payable_account.clone()]; let consuming_wallet = make_paying_wallet(b"consuming"); - let pending_payable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(RequestTransactionReceipts { - pending_payable: vec![], - response_skeleton_opt: None, - })) - .stop_the_system_after_last_msg(); - let mut config = make_bc_with_defaults(); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_secs(100), - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_millis(98), - }); - let mut subject = AccountantBuilder::default() - .consuming_wallet(consuming_wallet.clone()) - .bootstrapper_config(config) - .logger(Logger::new(test_name)) - .build(); - subject.scanners.payable = Box::new(NullScanner::new()); //skipping - subject.scanners.pending_payable = Box::new(pending_payable_scanner); - subject.scanners.receivable = Box::new(NullScanner::new()); //skipping - subject.scan_schedulers.update_scheduler( - ScanType::PendingPayables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_pending_payable_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + let counter_msg_1 = BlockchainAgentWithContextMessage { + qualified_payables: qualified_payable.clone(), + agent: Box::new(BlockchainAgentMock::default()), + response_skeleton_opt: None, + }; + let transaction_hash = make_tx_hash(789); + let creditor_wallet = make_wallet("blah"); + let counter_msg_2 = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable::new(creditor_wallet, transaction_hash), + )]), + response_skeleton_opt: None, + }; + let tx_receipt = TxReceipt { + transaction_hash, + status: TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), + }), + }; + let pending_payable_fingerprint = make_pending_payable_fingerprint(); + let counter_msg_3 = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(tx_receipt), + pending_payable_fingerprint.clone(), + )], + response_skeleton_opt: None, + }; + let request_transaction_receipts_msg = RequestTransactionReceipts { + pending_payable_fingerprints: vec![pending_payable_fingerprint], + response_skeleton_opt: None, + }; + let qualified_payables_msg = QualifiedPayablesMessage { + qualified_payables: qualified_payable.clone(), + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + }; + let subject = set_up_subject_to_prove_periodical_payable_scan( + test_name, + &blockchain_bridge_addr, + &consuming_wallet, + &qualified_payables_msg, + &request_transaction_receipts_msg, + &start_scan_pending_payable_params_arc, + &start_scan_payable_params_arc, + ¬ify_later_pending_payables_params_arc, + ¬ify_payable_params_arc, ); - let subject_addr: Addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); - let peer_actors = peer_actors_builder().build(); - send_bind_message!(subject_subs, peer_actors); + let subject_addr = subject.start(); + let set_up_counter_msgs = SetUpCounterMsgs::new(vec![ + setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + counter_msg_1, + &subject_addr + ), + setup_for_counter_msg_triggered_via_type_id!( + OutboundPaymentsInstructions, + counter_msg_2, + &subject_addr + ), + setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + counter_msg_3, + &subject_addr + ), + ]); + blockchain_bridge_addr + .try_send(set_up_counter_msgs) + .unwrap(); - send_start_message!(subject_subs); + subject_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); - let notify_later_pending_payable_params = - notify_later_pending_payable_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = begin_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, consuming_wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); - assert_eq!( - *notify_later_pending_payable_params, - vec![ - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ), - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ), - ] - ) + let mut start_scan_payable_params = start_scan_payable_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = + start_scan_payable_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(time_before <= timestamp && timestamp <= time_after); + assert_eq!(response_skeleton_opt, None); + assert!(start_scan_payable_params.is_empty()); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); + let mut start_scan_pending_payable_params = + start_scan_pending_payable_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = + start_scan_pending_payable_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(time_before <= timestamp && timestamp <= time_after); + assert_eq!(response_skeleton_opt, None); + assert!(start_scan_pending_payable_params.is_empty()); + assert_using_the_same_logger(&logger, test_name, Some("start scan pending payable")); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let actual_qualified_payables_msg = + blockchain_bridge_recording.get_record::(0); + assert_eq!(actual_qualified_payables_msg, &qualified_payables_msg); + let actual_outbound_payment_instructions_msg = + blockchain_bridge_recording.get_record::(1); + assert_eq!( + actual_outbound_payment_instructions_msg.affordable_accounts, + vec![payable_account] + ); + let actual_requested_receipts_1 = + blockchain_bridge_recording.get_record::(2); + assert_eq!( + actual_requested_receipts_1, + &request_transaction_receipts_msg + ); + let notify_later_pending_payables_params = + notify_later_pending_payables_params_arc.lock().unwrap(); + assert_eq!( + *notify_later_pending_payables_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_millis(50) + ),] + ); + let notify_payables_params = notify_payable_params_arc.lock().unwrap(); + assert_eq!( + *notify_payables_params, + vec![ScanForNewPayables { + response_skeleton_opt: None + },] + ); } - #[test] - fn periodical_scanning_for_payable_works() { - init_test_logging(); - let test_name = "periodical_scanning_for_payable_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_payables_params_arc = Arc::new(Mutex::new(vec![])); - let system = System::new(test_name); - SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions - let consuming_wallet = make_paying_wallet(b"consuming"); + fn set_up_subject_to_prove_periodical_payable_scan( + test_name: &str, + blockchain_bridge_addr: &Addr, + consuming_wallet: &Wallet, + qualified_payables_msg: &QualifiedPayablesMessage, + request_transaction_receipts: &RequestTransactionReceipts, + start_scan_pending_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + start_scan_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + notify_later_pending_payables_params_arc: &Arc< + Mutex>, + >, + notify_payable_params_arc: &Arc>>, + ) -> Accountant { + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_pending_payable_params_arc) + .start_scan_result(Ok(request_transaction_receipts.clone())) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); let payable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( - 123, - )]), - consuming_wallet: consuming_wallet.clone(), - response_skeleton_opt: None, - })) - .stop_the_system_after_last_msg(); + .scan_started_at_result(None) + // Always checking also on the payable scanner when handling ScanForPendingPayable + .scan_started_at_result(None) + .start_scan_params(&start_scan_payable_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(97), + // This simply means that we're gonna surplus this value (it abides by how many pending + // payable cycles have to go in between before the lastly submitted txs are confirmed), + payable_scan_interval: Duration::from_millis(10), + pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - pending_payable_scan_interval: Duration::from_secs(100), // We'll never run this scanner }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(consuming_wallet.clone()) .logger(Logger::new(test_name)) .build(); - subject.scanners.payable = Box::new(payable_scanner); - subject.scanners.pending_payable = Box::new(NullScanner::new()); //skipping - subject.scanners.receivable = Box::new(NullScanner::new()); //skipping - subject.scan_schedulers.update_scheduler( - ScanType::Payables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_payables_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::::default() + .notify_later_params(¬ify_later_pending_payables_params_arc) + .capture_msg_and_let_it_fly_on(), ); - let subject_addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); - let peer_actors = peer_actors_builder().build(); - send_bind_message!(subject_subs, peer_actors); - - send_start_message!(subject_subs); - - let time_before = SystemTime::now(); - system.run(); - let time_after = SystemTime::now(); - //the second attempt is the one where the queue is empty and System::current.stop() ends the cycle - let notify_later_payables_params = notify_later_payables_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Payables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = begin_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, consuming_wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); - assert_eq!( - *notify_later_payables_params, - vec![ - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ] - ) + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::::default() + .notify_params(¬ify_payable_params_arc) + // This should stop the system. If anything goes wrong, the SystemKillerActor will. + .stop_system_on_count_received(1), + ); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); + subject.outbound_payments_instructions_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject.request_transaction_receipts_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject } #[test] @@ -2448,12 +3682,15 @@ mod tests { subject.consuming_wallet_opt = None; subject.logger = Logger::new(test_name); - subject.handle_request_of_scan_for_payable(None); + subject.handle_request_of_scan_for_new_payable(None); - let has_scan_started = subject.scanners.payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::Payables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." )); } @@ -2467,10 +3704,13 @@ mod tests { subject.handle_request_of_scan_for_pending_payable(None); - let has_scan_started = subject.scanners.pending_payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::PendingPayables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." )); } @@ -2482,10 +3722,10 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(100), + pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_millis(100), - pending_payable_scan_interval: Duration::from_millis(100), }); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; let peer_actors = peer_actors_builder().build(); let subject = AccountantBuilder::default() .bootstrapper_config(config) @@ -2499,14 +3739,14 @@ mod tests { System::current().stop(); assert_eq!(system.run(), 0); - // no panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes + // No panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes TestLogHandler::new().exists_log_containing( &format!("{test_name}: Started with --scans off; declining to begin database and blockchain scans"), ); } #[test] - fn scan_for_payables_message_does_not_trigger_payment_for_balances_below_the_curve() { + fn scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve() { init_test_logging(); let consuming_wallet = make_paying_wallet(b"consuming wallet"); let payment_thresholds = PaymentThresholds { @@ -2526,7 +3766,8 @@ mod tests { balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei - 1), last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( - payment_thresholds.threshold_interval_sec + 10, + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec, ), ), pending_payable_opt: None, @@ -2536,20 +3777,20 @@ mod tests { 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 - 10, - ), + now - checked_conversion::(payment_thresholds.maturity_threshold_sec) + + 1, ), pending_payable_opt: None, }, // above minimum balance, to the right of minimum time (not in buffer zone, below the curve) PayableAccount { wallet: make_wallet("wallet2"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 55), + balance_wei: gwei_to_wei::( + payment_thresholds.permanent_debt_allowed_gwei, + ) + 1, last_paid_timestamp: from_unix_timestamp( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec + 15, - ), + now - checked_conversion::(payment_thresholds.threshold_interval_sec) + + 1, ), pending_payable_opt: None, }, @@ -2559,43 +3800,56 @@ mod tests { .non_pending_payables_result(vec![]); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); let system = System::new( - "scan_for_payable_message_does_not_trigger_payment_for_balances_below_the_curve", + "scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve", ); let blockchain_bridge_addr: Addr = blockchain_bridge.start(); - let outbound_payments_instructions_sub = - blockchain_bridge_addr.recipient::(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_daos(vec![ForPayableScanner(payable_dao)]) + .consuming_wallet(consuming_wallet.clone()) + .build(); + let payable_scanner = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .payable_dao(payable_dao) .build(); - subject.outbound_payments_instructions_sub_opt = Some(outbound_payments_instructions_sub); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + bind_ui_gateway_unasserted(&mut subject); - let _result = subject.scanners.payable.begin_scan( - consuming_wallet, - SystemTime::now(), - None, - &subject.logger, - ); + let result = subject.handle_request_of_scan_for_new_payable(None); System::current().stop(); system.run(); + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); } #[test] - fn scan_for_payable_message_triggers_payment_for_balances_over_the_curve() { + fn scan_for_new_payables_triggers_payment_for_balances_over_the_curve() { init_test_logging(); let mut config = bc_from_earning_wallet(make_wallet("mine")); let consuming_wallet = make_paying_wallet(b"consuming"); config.scan_intervals_opt = Some(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(50_000), payable_scan_interval: Duration::from_secs(50_000), + pending_payable_scan_interval: Duration::from_secs(10_000), receivable_scan_interval: Duration::from_secs(50_000), }); let now = to_unix_timestamp(SystemTime::now()); let qualified_payables = vec![ - // slightly above minimum balance, to the right of the curve (time intersection) + // slightly above the minimum balance, to the right of the curve (time intersection) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei( @@ -2625,65 +3879,87 @@ mod tests { let payable_dao = PayableDaoMock::default().non_pending_payables_result(qualified_payables.clone()); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); - let blockchain_bridge = blockchain_bridge - .system_stop_conditions(match_every_type_id!(QualifiedPayablesMessage)); + let blockchain_bridge_addr = blockchain_bridge.start(); let system = System::new("scan_for_payable_message_triggers_payment_for_balances_over_the_curve"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.receivable = Box::new(NullScanner::new()); - let subject_addr = subject.start(); - let accountant_subs = Accountant::make_subs_from(&subject_addr); - send_bind_message!(accountant_subs, peer_actors); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + bind_ui_gateway_unasserted(&mut subject); - send_start_message!(accountant_subs); + subject.handle_request_of_scan_for_new_payable(None); + System::current().stop(); system.run(); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); let message = blockchain_bridge_recordings.get_record::(0); assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + qualified_payables, consuming_wallet, response_skeleton_opt: None, } ); } + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Early stopped new payable scan \ + was suggested to be followed up by the scan for Receivables, which is not supported though" + )] + fn start_scan_error_in_new_payables_and_unexpected_reaction_by_receivable_scan_scheduling() { + let mut subject = AccountantBuilder::default().build(); + let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() + .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + ScanType::Receivables, + )); + subject.scan_schedulers.reschedule_on_error_resolver = + Box::new(reschedule_on_error_resolver); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + } + #[test] fn accountant_does_not_initiate_another_scan_if_one_is_already_running() { init_test_logging(); let test_name = "accountant_does_not_initiate_another_scan_if_one_is_already_running"; - let payable_dao = PayableDaoMock::default(); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( QualifiedPayablesMessage, QualifiedPayablesMessage )) .start(); - let pps_for_blockchain_bridge_sub = blockchain_bridge_addr.clone().recipient(); - let last_paid_timestamp = to_unix_timestamp(SystemTime::now()) - - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec as i64 - - 1; - let payable_account = PayableAccount { - wallet: make_wallet("scan_for_payables"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: from_unix_timestamp(last_paid_timestamp), - pending_payable_opt: None, - }; - let payable_dao = payable_dao - .non_pending_payables_result(vec![payable_account.clone()]) - .non_pending_payables_result(vec![payable_account]); - let config = bc_from_earning_wallet(make_wallet("mine")); + let qualified_payables_sub = blockchain_bridge_addr.clone().recipient(); + let (mut qualified_payables, _, _) = + make_qualified_and_unqualified_payables(now, &payment_thresholds); + let payable_1 = qualified_payables.remove(0); + let payable_2 = qualified_payables.remove(0); + let payable_dao = PayableDaoMock::new() + .non_pending_payables_result(vec![payable_1.clone()]) + .non_pending_payables_result(vec![payable_2.clone()]); + let mut config = bc_from_earning_wallet(make_wallet("mine")); + config.payment_thresholds_opt = Some(payment_thresholds); let system = System::new(test_name); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) @@ -2691,56 +3967,63 @@ mod tests { .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); - let message_before = ScanForPayables { + let message_before = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 111, context_id: 222, }), }; - let message_after = ScanForPayables { + let message_simultaneous = ScanForNewPayables { + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 999, + context_id: 888, + }), + }; + let message_after = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 333, context_id: 444, }), }; - subject.qualified_payables_sub_opt = Some(pps_for_blockchain_bridge_sub); + subject.qualified_payables_sub_opt = Some(qualified_payables_sub); + bind_ui_gateway_unasserted(&mut subject); + // important + subject.scan_schedulers.automatic_scans_enabled = false; let addr = subject.start(); addr.try_send(message_before.clone()).unwrap(); - addr.try_send(ScanForPayables { - response_skeleton_opt: None, - }) - .unwrap(); + addr.try_send(message_simultaneous).unwrap(); - // We ignored the second ScanForPayables message because the first message meant a scan - // was already in progress; now let's make it look like that scan has ended so that we - // can prove the next message will start another one. - addr.try_send(AssertionsMessage { - assertions: Box::new(|accountant: &mut Accountant| { - accountant - .scanners - .payable - .mark_as_ended(&Logger::new("irrelevant")) + // We ignored the second ScanForNewPayables message as there was already in progress from + // the first message. Now we reset the state by ending the first scan by a failure and see + // that the third scan request is going to be accepted willingly again. + addr.try_send(SentPayables { + payment_procedure_result: Err(PayableTransactionError::Signing("bluh".to_string())), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1122, + context_id: 7788, }), }) .unwrap(); addr.try_send(message_after.clone()).unwrap(); system.run(); - let recording = blockchain_bridge_recording.lock().unwrap(); - let messages_received = recording.len(); - assert_eq!(messages_received, 2); - let first_message: &QualifiedPayablesMessage = recording.get_record(0); + let blockchain_bridge_recording = blockchain_bridge_recording.lock().unwrap(); + let first_message_actual: &QualifiedPayablesMessage = + blockchain_bridge_recording.get_record(0); assert_eq!( - first_message.response_skeleton_opt, + first_message_actual.response_skeleton_opt, message_before.response_skeleton_opt ); - let second_message: &QualifiedPayablesMessage = recording.get_record(1); + let second_message_actual: &QualifiedPayablesMessage = + blockchain_bridge_recording.get_record(1); assert_eq!( - second_message.response_skeleton_opt, + second_message_actual.response_skeleton_opt, message_after.response_skeleton_opt ); + let messages_received = blockchain_bridge_recording.len(); + assert_eq!(messages_received, 2); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {}: Payables scan was already initiated", + "INFO: {}: Payables scan was already initiated", test_name )); } @@ -2750,7 +4033,7 @@ mod tests { init_test_logging(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge - .system_stop_conditions(match_every_type_id!(RequestTransactionReceipts)) + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); let payable_fingerprint_1 = PendingPayableFingerprint { rowid: 555, @@ -2781,7 +4064,7 @@ mod tests { .bootstrapper_config(config) .build(); - subject.request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); let _ = account_addr @@ -2792,19 +4075,89 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recording.len(), 1); let received_msg = blockchain_bridge_recording.get_record::(0); assert_eq!( received_msg, &RequestTransactionReceipts { - pending_payable: vec![payable_fingerprint_1, payable_fingerprint_2], + pending_payable_fingerprints: vec![payable_fingerprint_1, payable_fingerprint_2], response_skeleton_opt: None, } ); + assert_eq!(blockchain_bridge_recording.len(), 1); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); } + #[test] + fn start_scan_error_in_pending_payables_if_initial_scan_is_true_and_no_consuming_wallet_found() + { + let pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default().build(); + subject.consuming_wallet_opt = None; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payables_notify_later_params_arc) + .stop_system_on_count_received(1), + ); + subject.scan_schedulers.pending_payable.interval = Duration::from_secs(60); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payables_notify_params_arc)); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForPendingPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + let pending_payables_notify_later_params = + pending_payables_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payables_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_secs(60) + )] + ); + let new_payables_notify_params = new_payables_notify_params_arc.lock().unwrap(); + assert_eq!( + new_payables_notify_params.len(), + 0, + "Did not expect the new payables request" + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Early stopped pending payable scan \ + was suggested to be followed up by the scan for Receivables, which is not supported though" + )] + fn start_scan_error_in_pending_payables_and_unexpected_reaction_by_receivable_scan_scheduling() + { + let mut subject = AccountantBuilder::default().build(); + let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() + .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + ScanType::Receivables, + )); + subject.scan_schedulers.reschedule_on_error_resolver = + Box::new(reschedule_on_error_resolver); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForPendingPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + } + #[test] fn report_routing_service_provided_message_is_received() { init_test_logging(); @@ -3402,459 +4755,462 @@ mod tests { } #[test] - #[should_panic( - expected = "Recording services consumed from 0x000000000000000000000000000000626f6f6761 but \ - has hit fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" - )] - fn record_service_consumed_panics_on_fatal_errors() { + #[should_panic( + expected = "Recording services consumed from 0x000000000000000000000000000000626f6f6761 but \ + has hit fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" + )] + fn record_service_consumed_panics_on_fatal_errors() { + init_test_logging(); + let wallet = make_wallet("booga"); + let payable_dao = PayableDaoMock::new().more_money_payable_result(Err( + PayableDaoError::RusqliteError("we cannot help ourselves; this is baaad".to_string()), + )); + let subject = AccountantBuilder::default() + .payable_daos(vec![ForAccountantBody(payable_dao)]) + .build(); + + let _ = subject.record_service_consumed(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); + } + + #[test] + #[should_panic( + expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" + )] + fn accountant_can_be_crashed_properly_but_not_improperly() { + let mut config = make_bc_with_defaults(); + config.crash_point = CrashPoint::Message; + let accountant = AccountantBuilder::default() + .bootstrapper_config(config) + .build(); + + prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); + } + + #[test] + fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { + let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let expected_wallet = make_wallet("paying_you"); + let expected_hash = H256::from("transaction_hash".keccak256()); + let expected_rowid = 45623; + let pending_payable_dao = PendingPayableDaoMock::default() + .fingerprints_rowids_params(&fingerprints_rowids_params_arc) + .fingerprints_rowids_result(TransactionHashes { + rowid_results: vec![(expected_rowid, expected_hash)], + no_rowid_results: vec![], + }); + let payable_dao = PayableDaoMock::new() + .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) + .mark_pending_payables_rowids_result(Ok(())); + let system = + System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![ForPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .build(); + let pending_payable_interval = Duration::from_millis(55); + subject.scan_schedulers.pending_payable.interval = pending_payable_interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), + ); + let expected_payable = PendingPayable::new(expected_wallet.clone(), expected_hash.clone()); + let sent_payable = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + expected_payable.clone(), + )]), + response_skeleton_opt: None, + }; + let addr = subject.start(); + + addr.try_send(sent_payable).expect("unexpected actix error"); + + System::current().stop(); + system.run(); + let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); + assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); + let mark_pending_payables_rowids_params = + mark_pending_payables_rowids_params_arc.lock().unwrap(); + assert_eq!( + *mark_pending_payables_rowids_params, + vec![vec![(expected_wallet, expected_rowid)]] + ); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payable_notify_later_params, + vec![(ScanForPendingPayables::default(), pending_payable_interval)] + ); + // The accountant is unbound here. We don't use the bind message. It means we can prove + // none of those other scan requests could have been sent (especially ScanForNewPayables, + // ScanForRetryPayables) + } + + #[test] + fn no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted( + ) { init_test_logging(); - let wallet = make_wallet("booga"); - let payable_dao = PayableDaoMock::new().more_money_payable_result(Err( - PayableDaoError::RusqliteError("we cannot help ourselves; this is baaad".to_string()), - )); - let subject = AccountantBuilder::default() - .payable_daos(vec![ForAccountantBody(payable_dao)]) + let test_name = "no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + ScannerMock::default() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::Failure, + }), + ))); + // Important. Otherwise, the scan would've been handled through a different endpoint and + // gone for a very long time + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = SystemTime::now(); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&payable_notify_later_params_arc), + ); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let sent_payable = SentPayables { + payment_procedure_result: Err(PayableTransactionError::Sending { + msg: "booga".to_string(), + hashes: vec![make_tx_hash(456)], + }), + response_skeleton_opt: None, + }; + let addr = subject.start(); - let _ = subject.record_service_consumed(i64::MAX as u64, 1, SystemTime::now(), 2, &wallet); + addr.try_send(sent_payable.clone()) + .expect("unexpected actix error"); + + System::current().stop(); + assert_eq!(system.run(), 0); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (actual_sent_payable, logger) = finish_scan_params.remove(0); + assert_eq!(actual_sent_payable, sent_payable,); + assert_using_the_same_logger(&logger, test_name, None); + let mut payable_notify_later_params = payable_notify_later_params_arc.lock().unwrap(); + let (scheduled_msg, _interval) = payable_notify_later_params.remove(0); + assert_eq!(scheduled_msg, ScanForNewPayables::default()); + assert!( + payable_notify_later_params.is_empty(), + "Should be empty but {:?}", + payable_notify_later_params + ); } #[test] - #[should_panic( - expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" - )] - fn accountant_can_be_crashed_properly_but_not_improperly() { - let mut config = make_bc_with_defaults(); - config.crash_point = CrashPoint::Message; - let accountant = AccountantBuilder::default() - .bootstrapper_config(config) + fn accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed() { + init_test_logging(); + let test_name = + "accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + let system = System::new(test_name); + let (mut msg, _) = + make_report_transaction_receipts_msg(vec![TxStatus::Pending, TxStatus::Failed]); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 45, + context_id: 7, + }); + msg.response_skeleton_opt = response_skeleton_opt; + let subject_addr = subject.start(); - prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); + subject_addr.try_send(msg.clone()).unwrap(); + + System::current().stop(); + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let retry_payable_notify_params = retry_payable_notify_params_arc.lock().unwrap(); + assert_eq!( + *retry_payable_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt + }] + ); + assert_using_the_same_logger(&logger, test_name, None) } #[test] - fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { - init_test_logging(); - let port = find_free_port(); - let pending_tx_hash_1 = - H256::from_str("e66814b2812a80d619813f51aa999c0df84eb79d10f4923b2b7667b30d6b33d3") - .unwrap(); - let pending_tx_hash_2 = - H256::from_str("0288ef000581b3bca8a2017eac9aea696366f8f1b7437f18d1aad57bccb7032c") - .unwrap(); - let _blockchain_client_server = MBCSBuilder::new(port) - // Blockchain Agent Gas Price - .ok_response("0x3B9ACA00".to_string(), 0) // 1000000000 - // Blockchain Agent transaction fee balance - .ok_response("0xFFF0".to_string(), 0) // 65520 - // Blockchain Agent masq balance - .ok_response( - "0x000000000000000000000000000000000000000000000000000000000000FFFF".to_string(), - 0, - ) - // Submit payments to blockchain - .ok_response("0xFFF0".to_string(), 1) - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 1 - handle_request_transaction_receipts - .begin_batch() - .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) // Null response - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 2 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 3 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .status(U64::from(0)) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 4 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .status(U64::from(1)) - .block_number(U64::from(1234)) - .block_hash(Default::default()) - .build(), - ) - .end_batch() - .start(); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let return_all_errorless_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let update_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); + fn accountant_confirms_payable_txs_and_schedules_the_new_payable_scanner_timely() { let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_record_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_scan_for_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_scan_for_pending_payable_arc_cloned = - notify_later_scan_for_pending_payable_params_arc.clone(); // because it moves into a closure - let rowid_for_account_1 = 3; - let rowid_for_account_2 = 5; - let now = SystemTime::now(); - let past_payable_timestamp_1 = now.sub(Duration::from_secs( - (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 555) as u64, - )); - let past_payable_timestamp_2 = now.sub(Duration::from_secs( - (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 50) as u64, - )); - let this_payable_timestamp_1 = now; - let this_payable_timestamp_2 = now.add(Duration::from_millis(50)); - let payable_account_balance_1 = - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 10); - let payable_account_balance_2 = - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 666); - let wallet_account_1 = make_wallet("creditor1"); - let wallet_account_2 = make_wallet("creditor2"); - let blockchain_interface = make_blockchain_interface_web3(port); - let consuming_wallet = make_paying_wallet(b"wallet"); - let system = System::new("pending_transaction"); - let persistent_config_id_stamp = ArbitraryIdStamp::new(); - let persistent_config = PersistentConfigurationMock::default() - .set_arbitrary_id_stamp(persistent_config_id_stamp); - let blockchain_bridge = BlockchainBridge::new( - Box::new(blockchain_interface), - Arc::new(Mutex::new(persistent_config)), - false, - ); - let account_1 = PayableAccount { - wallet: wallet_account_1.clone(), - balance_wei: payable_account_balance_1, - last_paid_timestamp: past_payable_timestamp_1, - pending_payable_opt: None, - }; - let account_2 = PayableAccount { - wallet: wallet_account_2.clone(), - balance_wei: payable_account_balance_2, - last_paid_timestamp: past_payable_timestamp_2, - pending_payable_opt: None, - }; - let pending_payable_scan_interval = 1000; // should be slightly less than 1/5 of the time until shutting the system - let payable_dao_for_payable_scanner = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![account_1, account_2]) - .mark_pending_payables_rowids_params(&mark_pending_payable_params_arc) - .mark_pending_payables_rowids_result(Ok(())); - let payable_dao_for_pending_payable_scanner = PayableDaoMock::new() + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let mut bootstrapper_config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - bootstrapper_config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_secs(1_000_000), // we don't care about this scan - receivable_scan_interval: Duration::from_secs(1_000_000), // we don't care about this scan - pending_payable_scan_interval: Duration::from_millis(pending_payable_scan_interval), - }); - let fingerprint_1_first_round = PendingPayableFingerprint { - rowid: rowid_for_account_1, - timestamp: this_payable_timestamp_1, - hash: pending_tx_hash_1, - attempt: 1, - amount: payable_account_balance_1, - process_error: None, - }; - let fingerprint_2_first_round = PendingPayableFingerprint { - rowid: rowid_for_account_2, - timestamp: this_payable_timestamp_2, - hash: pending_tx_hash_2, - attempt: 1, - amount: payable_account_balance_2, - process_error: None, - }; - let fingerprint_1_second_round = PendingPayableFingerprint { - attempt: 2, - ..fingerprint_1_first_round.clone() - }; - let fingerprint_2_second_round = PendingPayableFingerprint { - attempt: 2, - ..fingerprint_2_first_round.clone() - }; - let fingerprint_1_third_round = PendingPayableFingerprint { - attempt: 3, - ..fingerprint_1_first_round.clone() - }; - let fingerprint_2_third_round = PendingPayableFingerprint { - attempt: 3, - ..fingerprint_2_first_round.clone() - }; - let fingerprint_2_fourth_round = PendingPayableFingerprint { - attempt: 4, - ..fingerprint_2_first_round.clone() - }; - let pending_payable_dao_for_payable_scanner = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }); - let mut pending_payable_dao_for_pending_payable_scanner = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_params(&return_all_errorless_fingerprints_params_arc) - .return_all_errorless_fingerprints_result(vec![]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_first_round, - fingerprint_2_first_round, - ]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_second_round, - fingerprint_2_second_round, - ]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_third_round, - fingerprint_2_third_round, - ]) - .return_all_errorless_fingerprints_result(vec![fingerprint_2_fourth_round.clone()]) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }) - .increment_scan_attempts_params(&update_fingerprint_params_arc) - .increment_scan_attempts_result(Ok(())) - .increment_scan_attempts_result(Ok(())) - .increment_scan_attempts_result(Ok(())) - .mark_failures_params(&mark_failure_params_arc) - // we don't have a better solution yet, so we mark this down - .mark_failures_result(Ok(())) - .delete_fingerprints_params(&delete_record_params_arc) - // this is used during confirmation of the successful one - .delete_fingerprints_result(Ok(())); - pending_payable_dao_for_pending_payable_scanner - .have_return_all_errorless_fingerprints_shut_down_the_system = true; - let pending_payable_dao_for_accountant = - PendingPayableDaoMock::new().insert_fingerprints_result(Ok(())); - let accountant_addr = Arbiter::builder() - .stop_system_on_panic(true) - .start(move |_| { - let mut subject = AccountantBuilder::default() - .consuming_wallet(consuming_wallet) - .bootstrapper_config(bootstrapper_config) - .payable_daos(vec![ - ForPayableScanner(payable_dao_for_payable_scanner), - ForPendingPayableScanner(payable_dao_for_pending_payable_scanner), - ]) - .pending_payable_daos(vec![ - ForAccountantBody(pending_payable_dao_for_accountant), - ForPayableScanner(pending_payable_dao_for_payable_scanner), - ForPendingPayableScanner(pending_payable_dao_for_pending_payable_scanner), - ]) - .build(); - subject.scanners.receivable = Box::new(NullScanner::new()); - let notify_later_half_mock = NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_scan_for_pending_payable_arc_cloned) - .capture_msg_and_let_it_fly_on(); - subject.scan_schedulers.update_scheduler( - ScanType::PendingPayables, - Some(Box::new(notify_later_half_mock)), - None, - ); - subject - }); - let mut peer_actors = peer_actors_builder().build(); - let accountant_subs = Accountant::make_subs_from(&accountant_addr); - peer_actors.accountant = accountant_subs.clone(); - let blockchain_bridge_addr = blockchain_bridge.start(); - let blockchain_bridge_subs = BlockchainBridge::make_subs_from(&blockchain_bridge_addr); - peer_actors.blockchain_bridge = blockchain_bridge_subs.clone(); - send_bind_message!(accountant_subs, peer_actors); - send_bind_message!(blockchain_bridge_subs, peer_actors); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let system = System::new("new_payable_scanner_timely"); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(3)) + .unwrap(); + let nominal_interval = Duration::from_secs(6); + let expected_computed_interval = Duration::from_secs(3); + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_result(Some(expected_computed_interval)); + subject.scan_schedulers.payable.new_payable_interval = nominal_interval; + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let subject_addr = subject.start(); + let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + ]); - send_start_message!(accountant_subs); + subject_addr.try_send(msg).unwrap(); - assert_eq!(system.run(), 0); - let mut mark_pending_payable_params = mark_pending_payable_params_arc.lock().unwrap(); - let mut one_set_of_mark_pending_payable_params = mark_pending_payable_params.remove(0); - assert!(mark_pending_payable_params.is_empty()); - let first_payable = one_set_of_mark_pending_payable_params.remove(0); - assert_eq!(first_payable.0, wallet_account_1); - assert_eq!(first_payable.1, rowid_for_account_1); - let second_payable = one_set_of_mark_pending_payable_params.remove(0); - assert!( - one_set_of_mark_pending_payable_params.is_empty(), - "{:?}", - one_set_of_mark_pending_payable_params - ); - assert_eq!(second_payable.0, wallet_account_2); - assert_eq!(second_payable.1, rowid_for_account_2); - let return_all_errorless_fingerprints_params = - return_all_errorless_fingerprints_params_arc.lock().unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary - assert!(return_all_errorless_fingerprints_params.len() >= 5); - let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); - assert_eq!(*non_pending_payables_params, vec![()]); // because we disabled further scanning for payables - let update_fingerprints_params = update_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *update_fingerprints_params, - vec![ - vec![rowid_for_account_1, rowid_for_account_2], - vec![rowid_for_account_1, rowid_for_account_2], - vec![rowid_for_account_2], - ] - ); - let mark_failure_params = mark_failure_params_arc.lock().unwrap(); - assert_eq!(*mark_failure_params, vec![vec![rowid_for_account_1]]); - let delete_record_params = delete_record_params_arc.lock().unwrap(); - assert_eq!(*delete_record_params, vec![vec![rowid_for_account_2]]); - let transaction_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + System::current().stop(); + system.run(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (_, last_new_payable_timestamp_actual, scan_interval_actual) = + compute_interval_params.remove(0); assert_eq!( - *transaction_confirmed_params, - vec![vec![fingerprint_2_fourth_round.clone()]] - ); - let expected_scan_pending_payable_msg_and_interval = ( - ScanForPendingPayables { - response_skeleton_opt: None, - }, - Duration::from_millis(pending_payable_scan_interval), + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp ); - let mut notify_later_check_for_confirmation = - notify_later_scan_for_pending_payable_params_arc - .lock() - .unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary - let vector_of_first_five_cycles = notify_later_check_for_confirmation - .drain(0..=4) - .collect_vec(); + assert_eq!(scan_interval_actual, nominal_interval); + assert!(compute_interval_params.is_empty()); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); assert_eq!( - vector_of_first_five_cycles, - vec![ - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval, - ] - ); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "WARN: Accountant: Broken transactions 0xe66814b2812a80d619813f51aa999c0df84eb79d10f\ - 4923b2b7667b30d6b33d3 marked as an error. You should take over the care of those to make sure \ - your debts are going to be settled properly. At the moment, there is no automated process \ - fixing that without your assistance"); - log_handler.exists_log_matching("INFO: Accountant: Transaction 0x0288ef000581b3bca8a2017eac9\ - aea696366f8f1b7437f18d1aad57bccb7032c has been added to the blockchain; detected locally at \ - attempt 4 at \\d{2,}ms after its sending"); - log_handler.exists_log_containing( - "INFO: Accountant: Transactions 0x0288ef000581b3bca8a2017eac9aea696366f8f1b7437f18d1aad5\ - 7bccb7032c completed their confirmation process succeeding", + *new_payable_notify_later, + vec![(ScanForNewPayables::default(), expected_computed_interval)] ); + let new_payable_notify = new_payable_notify_arc.lock().unwrap(); + assert!( + new_payable_notify.is_empty(), + "should be empty but was: {:?}", + new_payable_notify + ) } #[test] - fn accountant_receives_reported_transaction_receipts_and_processes_them_all() { + fn accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(8)) + .unwrap(); + let nominal_interval = Duration::from_secs(6); + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_result(None); + subject.scan_schedulers.payable.new_payable_interval = nominal_interval; + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); let subject_addr = subject.start(); - let transaction_hash_1 = make_tx_hash(4545); - let transaction_receipt_1 = TxReceipt { - transaction_hash: transaction_hash_1, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), + let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), block_number: U64::from(100), }), - }; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, - }; - let transaction_hash_2 = make_tx_hash(3333333); - let transaction_receipt_2 = TxReceipt { - transaction_hash: transaction_hash_2, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), block_number: U64::from(200), }), - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 10, - timestamp: from_unix_timestamp(199_780_000), - hash: Default::default(), - attempt: 15, - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_1), - fingerprint_1.clone(), - ), - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_2), - fingerprint_2.clone(), - ), - ], - response_skeleton_opt: None, - }; + ]); subject_addr.try_send(msg).unwrap(); - let system = System::new("processing reported receipts"); + let system = System::new("new_payable_scanner_asap"); System::current().stop(); system.run(); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (_, last_new_payable_timestamp_actual, scan_interval_actual) = + compute_interval_params.remove(0); assert_eq!( - *transactions_confirmed_params, - vec![vec![fingerprint_1, fingerprint_2]] + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp + ); + assert_eq!(scan_interval_actual, nominal_interval); + assert!(compute_interval_params.is_empty()); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); + assert!( + new_payable_notify_later.is_empty(), + "should be empty but was: {:?}", + new_payable_notify_later + ); + let new_payable_notify = new_payable_notify_arc.lock().unwrap(); + assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]) + } + + #[test] + fn scheduler_for_new_payables_operates_with_proper_now_timestamp() { + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let system = System::new("scheduler_for_new_payables_operates_with_proper_now_timestamp"); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_millis(3500)) + .unwrap(); + let new_payable_interval = Duration::from_secs(6); + subject.scan_schedulers.payable.new_payable_interval = new_payable_interval; + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); + let subject_addr = subject.start(); + let (msg, _) = make_report_transaction_receipts_msg(vec![ + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + ]); + + subject_addr.try_send(msg).unwrap(); + + let before = SystemTime::now(); + System::current().stop(); + system.run(); + let after = SystemTime::now(); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); + let (_, actual_interval) = new_payable_notify_later[0]; + let interval_computer = NewPayableScanDynIntervalComputerReal::default(); + let left_side_bound = interval_computer + .compute_interval( + before, + last_new_payable_scan_timestamp, + new_payable_interval, + ) + .unwrap(); + let right_side_bound = interval_computer + .compute_interval(after, last_new_payable_scan_timestamp, new_payable_interval) + .unwrap(); + assert!( + left_side_bound >= actual_interval && actual_interval >= right_side_bound, + "expected actual {:?} to be between {:?} and {:?}", + actual_interval, + left_side_bound, + right_side_bound ); } + fn make_report_transaction_receipts_msg( + status_txs: Vec, + ) -> (ReportTransactionReceipts, Vec) { + let (receipt_result_fingerprint_pairs, fingerprints): (Vec<_>, Vec<_>) = status_txs + .into_iter() + .enumerate() + .map(|(idx, status)| { + let transaction_hash = make_tx_hash(idx as u32); + let transaction_receipt_result = TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash, + status, + }); + let fingerprint = PendingPayableFingerprint { + rowid: idx as u64, + timestamp: from_unix_timestamp(1_000_000_000 * idx as i64), + hash: transaction_hash, + attempt: 2, + amount: 1_000_000 * idx as u128 * idx as u128, + process_error: None, + }; + ( + (transaction_receipt_result, fingerprint.clone()), + fingerprint, + ) + }) + .unzip(); + + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: receipt_result_fingerprint_pairs, + response_skeleton_opt: None, + }; + + (msg, fingerprints) + } + #[test] fn accountant_handles_inserting_new_fingerprints() { init_test_logging(); @@ -4772,17 +6128,9 @@ mod tests { let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); - match message.scan_type { - ScanType::Payables => subject.scanners.payable.mark_as_started(SystemTime::now()), - ScanType::PendingPayables => subject - .scanners - .pending_payable - .mark_as_started(SystemTime::now()), - ScanType::Receivables => subject - .scanners - .receivable - .mark_as_started(SystemTime::now()), - } + subject + .scanners + .reset_scan_started(message.scan_type, MarkScanner::Started(SystemTime::now())); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -4793,19 +6141,13 @@ mod tests { subject_addr .try_send(AssertionsMessage { assertions: Box::new(move |actor: &mut Accountant| { - let scan_started_at_opt = match message.scan_type { - ScanType::Payables => actor.scanners.payable.scan_started_at(), - ScanType::PendingPayables => { - actor.scanners.pending_payable.scan_started_at() - } - ScanType::Receivables => actor.scanners.receivable.scan_started_at(), - }; + let scan_started_at_opt = actor.scanners.scan_started_at(message.scan_type); assert_eq!(scan_started_at_opt, None); }), }) .unwrap(); System::current().stop(); - system.run(); + assert_eq!(system.run(), 0); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); match message.response_skeleton_opt { Some(response_skeleton) => { @@ -4861,6 +6203,10 @@ mod tests { assert_on_initialization_with_panic_on_migration(&data_dir, &act); } + + fn bind_ui_gateway_unasserted(accountant: &mut Accountant) { + accountant.ui_message_sub_opt = Some(make_recorder().0.start().recipient()); + } } #[cfg(test)] diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index 88ee13e74..74c88690b 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use masq_lib::logger::Logger; use std::time::SystemTime; @@ -71,9 +71,8 @@ pub enum AnalysisError {} #[cfg(test)] mod tests { use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; + use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::test_utils::make_payable_account; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; @@ -86,7 +85,7 @@ mod tests { payable.balance_wei = 100_000_000; let agent = BlockchainAgentMock::default(); let setup_msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable]), + qualified_payables: vec![payable], agent: Box::new(agent), response_skeleton_opt: None, }; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs b/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs deleted file mode 100644 index 16331e4bf..000000000 --- a/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -pub mod payable_scanner; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index e8ce95812..ca1810290 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod mid_scan_msg_handling; +pub mod payable_scanner_extension; +pub mod scan_schedulers; pub mod scanners_utils; pub mod test_utils; @@ -11,31 +12,23 @@ use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ - debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, - investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, - separate_errors, separate_rowids_and_hashes, PayableThresholdsGauge, - PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata, -}; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::PendingPayableId; +use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ - comma_joined_stringifiable, gwei_to_wei, Accountant, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, - ScanForPendingPayables, ScanForReceivables, SentPayables, + comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, + ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, + ScanForReceivables, SentPayables, }; use crate::accountant::db_access_objects::banned_dao::BannedDao; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, -}; -use crate::sub_lib::blockchain_bridge::{ - OutboundPaymentsInstructions, + DaoFactories, FinancialStatistics, PaymentThresholds, }; -use crate::sub_lib::utils::{NotifyLaterHandle, NotifyLaterHandleReal}; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; -use actix::{Context, Message}; +use actix::{Message}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; @@ -44,22 +37,40 @@ use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; use std::rc::Rc; -use std::time::{Duration, SystemTime}; +use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; +use variant_count::VariantCount; use web3::types::H256; -use masq_lib::type_obfuscation::Obfuscated; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{PreparedAdjustment, MultistagePayableScanner, SolvencySensitivePaymentInstructor}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; +use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; +// Leave the individual scanner objects private! pub struct Scanners { - pub payable: Box>, - pub pending_payable: Box>, - pub receivable: Box>, + payable: Box, + aware_of_unresolved_pending_payable: bool, + initial_pending_payable_scan: bool, + pending_payable: Box< + dyn PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + >, + receivable: Box< + dyn PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + >, + >, } impl Scanners { @@ -86,6 +97,7 @@ impl Scanners { let persistent_configuration = PersistentConfigurationReal::from(dao_factories.config_dao_factory.make()); + let receivable = Box::new(ReceivableScanner::new( dao_factories.receivable_dao_factory.make(), dao_factories.banned_dao_factory.make(), @@ -96,25 +108,290 @@ impl Scanners { Scanners { payable, + aware_of_unresolved_pending_payable: false, + initial_pending_payable_scan: true, pending_payable, receivable, } } + + pub fn start_new_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + automatic_scans_enabled: bool, + ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + if triggered_manually && automatic_scans_enabled { + return Err(StartScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); + } + if let Some(started_at) = self.payable.scan_started_at() { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at, + }); + } + + Self::start_correct_payable_scanner::( + &mut *self.payable, + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } + + // Note: This scanner cannot be started on its own. It always runs after the pending payable + // scan, but only if it is clear that a retry is needed. + pub fn start_retry_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(started_at) = self.payable.scan_started_at() { + unreachable!( + "Guards should ensure that no payable scanner can run if the pending payable \ + repetitive sequence is still ongoing. However, some other payable scan intruded \ + at {} and is still running at {}", + StartScanError::timestamp_as_string(started_at), + StartScanError::timestamp_as_string(SystemTime::now()) + ) + } + + Self::start_correct_payable_scanner::( + &mut *self.payable, + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } + + pub fn start_pending_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + automatic_scans_enabled: bool, + ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + self.check_general_conditions_for_pending_payable_scan( + triggered_manually, + automatic_scans_enabled, + )?; + match ( + self.pending_payable.scan_started_at(), + self.payable.scan_started_at(), + ) { + (Some(pp_timestamp), Some(p_timestamp)) => + // If you're wondering, then yes, this condition should be the sacred truth between + // PendingPayableScanner and NewPayableScanner. + { + unreachable!( + "Any payable-related scanners should never be allowed to run in parallel. \ + Scan for pending payables started at: {}, scan for payables started at: {}", + StartScanError::timestamp_as_string(pp_timestamp), + StartScanError::timestamp_as_string(p_timestamp) + ) + } + (Some(started_at), None) => { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at, + }) + } + (None, Some(started_at)) => { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: Some(ScanType::Payables), + started_at, + }) + } + (None, None) => (), + } + self.pending_payable + .start_scan(wallet, timestamp, response_skeleton_opt, logger) + } + + pub fn start_receivable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + automatic_scans_enabled: bool, + ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + if triggered_manually && automatic_scans_enabled { + return Err(StartScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); + } + if let Some(started_at) = self.receivable.scan_started_at() { + return Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at, + }); + } + + self.receivable + .start_scan(wallet, timestamp, response_skeleton_opt, logger) + } + + pub fn finish_payable_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { + let scan_result = self.payable.finish_scan(msg, logger); + match scan_result.result { + OperationOutcome::NewPendingPayable => self.aware_of_unresolved_pending_payable = true, + OperationOutcome::Failure => (), + }; + scan_result + } + + pub fn finish_pending_payable_scan( + &mut self, + msg: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanResult { + self.pending_payable.finish_scan(msg, logger) + } + + pub fn finish_receivable_scan( + &mut self, + msg: ReceivedPayments, + logger: &Logger, + ) -> Option { + self.receivable.finish_scan(msg, logger) + } + + pub fn acknowledge_scan_error(&mut self, error: &ScanError, logger: &Logger) { + match error.scan_type { + ScanType::Payables => { + self.payable.mark_as_ended(logger); + } + ScanType::PendingPayables => { + self.pending_payable.mark_as_ended(logger); + } + ScanType::Receivables => { + self.receivable.mark_as_ended(logger); + } + }; + } + + pub fn try_skipping_payable_adjustment( + &self, + msg: BlockchainAgentWithContextMessage, + logger: &Logger, + ) -> Result, String> { + self.payable.try_skipping_payment_adjustment(msg, logger) + } + + pub fn perform_payable_adjustment( + &self, + setup: PreparedAdjustment, + logger: &Logger, + ) -> OutboundPaymentsInstructions { + self.payable.perform_payment_adjustment(setup, logger) + } + + pub fn initial_pending_payable_scan(&self) -> bool { + self.initial_pending_payable_scan + } + + pub fn unset_initial_pending_payable_scan(&mut self) { + self.initial_pending_payable_scan = false + } + + // This is a helper function reducing a boilerplate of complex trait resolving where + // the compiler requires to specify which trigger message distinguish the scan to run. + // The payable scanner offers two modes through doubled implementations of StartableScanner + // which uses the trigger message type as the only distinction between them. + fn start_correct_payable_scanner<'a, TriggerMessage>( + scanner: &'a mut (dyn MultistageDualPayableScanner + 'a), + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result + where + TriggerMessage: Message, + (dyn MultistageDualPayableScanner + 'a): + StartableScanner, + { + <(dyn MultistageDualPayableScanner + 'a) as StartableScanner< + TriggerMessage, + QualifiedPayablesMessage, + >>::start_scan(scanner, wallet, timestamp, response_skeleton_opt, logger) + } + + fn check_general_conditions_for_pending_payable_scan( + &mut self, + triggered_manually: bool, + automatic_scans_enabled: bool, + ) -> Result<(), StartScanError> { + if triggered_manually && automatic_scans_enabled { + return Err(StartScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); + } + if self.initial_pending_payable_scan { + return Ok(()); + } + if triggered_manually && !self.aware_of_unresolved_pending_payable { + return Err(StartScanError::ManualTriggerError( + MTError::UnnecessaryRequest { + hint_opt: Some("Run the Payable scanner first.".to_string()), + }, + )); + } + if !self.aware_of_unresolved_pending_payable { + unreachable!( + "Automatic pending payable scan should never start if there are no pending \ + payables to process." + ) + } + + Ok(()) + } } -pub trait Scanner -where - BeginMessage: Message, +pub(in crate::accountant::scanners) trait PrivateScanner< + TriggerMessage, + StartMessage, + EndMessage, + ScanResult, +>: + StartableScanner + Scanner where + TriggerMessage: Message, + StartMessage: Message, EndMessage: Message, { - fn begin_scan( +} + +trait StartableScanner +where + TriggerMessage: Message, + StartMessage: Message, +{ + fn start_scan( &mut self, - wallet: Wallet, + wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result; - fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> Option; + ) -> Result; +} + +trait Scanner +where + EndMessage: Message, +{ + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); fn mark_as_ended(&mut self, logger: &Logger); @@ -125,7 +402,7 @@ where pub struct ScannerCommon { initiated_at_opt: Option, - pub payment_thresholds: Rc, + payment_thresholds: Rc, } impl ScannerCommon { @@ -159,6 +436,7 @@ impl ScannerCommon { } } +#[macro_export] macro_rules! time_marking_methods { ($scan_type_variant: ident) => { fn scan_started_at(&self) -> Option { @@ -180,26 +458,25 @@ macro_rules! time_marking_methods { } pub struct PayableScanner { + pub payable_threshold_gauge: Box, pub common: ScannerCommon, pub payable_dao: Box, pub pending_payable_dao: Box, - pub payable_threshold_gauge: Box, pub payment_adjuster: Box, } -impl Scanner for PayableScanner { - fn begin_scan( +impl MultistageDualPayableScanner for PayableScanner {} + +impl StartableScanner for PayableScanner { + fn start_scan( &mut self, - consuming_wallet: Wallet, + consuming_wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } + ) -> Result { self.mark_as_started(timestamp); - info!(logger, "Scanning for payables"); + info!(logger, "Scanning for new payables"); let all_non_pending_payables = self.payable_dao.non_pending_payables(); debug!( @@ -214,7 +491,7 @@ impl Scanner for PayableScanner { match qualified_payables.is_empty() { true => { self.mark_as_ended(logger); - Err(BeginScanError::NothingToProcess) + Err(StartScanError::NothingToProcess) } false => { info!( @@ -222,18 +499,32 @@ impl Scanner for PayableScanner { "Chose {} qualified debts to pay", qualified_payables.len() ); - let protected_payables = self.protect_payables(qualified_payables); + let outgoing_msg = QualifiedPayablesMessage::new( - protected_payables, - consuming_wallet, + qualified_payables, + consuming_wallet.clone(), response_skeleton_opt, ); Ok(outgoing_msg) } } } +} - fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { +impl StartableScanner for PayableScanner { + fn start_scan( + &mut self, + _consuming_wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + todo!("Complete me under GH-605") + } +} + +impl Scanner for PayableScanner { + fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> PayableScanResult { let (sent_payables, err_opt) = separate_errors(&message, logger); debug!( logger, @@ -247,12 +538,25 @@ impl Scanner for PayableScanner { self.handle_sent_payable_errors(err_opt, logger); self.mark_as_ended(logger); - message - .response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + + let ui_response_opt = + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + + let result = if !sent_payables.is_empty() { + OperationOutcome::NewPendingPayable + } else { + OperationOutcome::Failure + }; + + PayableScanResult { + ui_response_opt, + result, + } } time_marking_methods!(Payables); @@ -270,15 +574,11 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { .payment_adjuster .search_for_indispensable_adjustment(&msg, logger) { - Ok(None) => { - let protected = msg.protected_qualified_payables; - let unprotected = self.expose_payables(protected); - Ok(Either::Left(OutboundPaymentsInstructions::new( - unprotected, - msg.agent, - msg.response_skeleton_opt, - ))) - } + Ok(None) => Ok(Either::Left(OutboundPaymentsInstructions::new( + msg.qualified_payables, + msg.agent, + msg.response_skeleton_opt, + ))), Ok(Some(adjustment)) => Ok(Either::Right(PreparedAdjustment::new(msg, adjustment))), Err(_e) => todo!("be implemented with GH-711"), } @@ -294,8 +594,6 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { } } -impl MultistagePayableScanner for PayableScanner {} - impl PayableScanner { pub fn new( payable_dao: Box, @@ -398,7 +696,7 @@ impl PayableScanner { } let sent_payables_hashes = hashes.iter().copied().collect::>(); - if !PayableScanner::is_symmetrical(sent_payables_hashes, hashes_from_db) { + if !Self::is_symmetrical(sent_payables_hashes, hashes_from_db) { panic!( "Inconsistency in two maps, they cannot be matched by hashes. Data set directly \ sent from BlockchainBridge: {:?}, set derived from the DB: {:?}", @@ -546,14 +844,6 @@ impl PayableScanner { panic!("{}", msg) }; } - - fn protect_payables(&self, payables: Vec) -> Obfuscated { - Obfuscated::obfuscate_vector(payables) - } - - fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { - obfuscated.expose_vector() - } } pub struct PendingPayableScanner { @@ -564,24 +854,33 @@ pub struct PendingPayableScanner { pub financial_statistics: Rc>, } -impl Scanner for PendingPayableScanner { - fn begin_scan( +impl + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + > for PendingPayableScanner +{ +} + +impl StartableScanner + for PendingPayableScanner +{ + fn start_scan( &mut self, - _irrelevant_wallet: Wallet, + _wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } + ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for pending payable"); let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); match filtered_pending_payable.is_empty() { true => { self.mark_as_ended(logger); - Err(BeginScanError::NothingToProcess) + Err(StartScanError::NothingToProcess) } false => { debug!( @@ -590,22 +889,27 @@ impl Scanner for PendingP filtered_pending_payable.len() ); Ok(RequestTransactionReceipts { - pending_payable: filtered_pending_payable, + pending_payable_fingerprints: filtered_pending_payable, response_skeleton_opt, }) } } } +} +impl Scanner for PendingPayableScanner { fn finish_scan( &mut self, message: ReportTransactionReceipts, logger: &Logger, - ) -> Option { + ) -> PendingPayableScanResult { let response_skeleton_opt = message.response_skeleton_opt; - match message.fingerprints_with_receipts.is_empty() { - true => debug!(logger, "No transaction receipts found."), + let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { + true => { + warning!(logger, "No transaction receipts found."); + todo!("This requires the payment retry. GH-631 must be completed first"); + } false => { debug!( logger, @@ -613,15 +917,24 @@ impl Scanner for PendingP message.fingerprints_with_receipts.len() ); let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - self.process_transactions_by_reported_state(scan_report, logger); + let requires_payment_retry = + self.process_transactions_by_reported_state(scan_report, logger); + + self.mark_as_ended(&logger); + + requires_payment_retry } - } + }; - self.mark_as_ended(logger); - response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + if requires_payment_retry { + PendingPayableScanResult::PaymentRetryRequired + } else { + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) + } } time_marking_methods!(PendingPayables); @@ -683,10 +996,14 @@ impl PendingPayableScanner { &mut self, scan_report: PendingPayableScanReport, logger: &Logger, - ) { + ) -> bool { + let requires_payments_retry = scan_report.requires_payments_retry(); + self.confirm_transactions(scan_report.confirmed, logger); self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger) + self.update_remaining_fingerprints(scan_report.still_pending, logger); + + requires_payments_retry } fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { @@ -791,30 +1108,40 @@ pub struct ReceivableScanner { pub financial_statistics: Rc>, } -impl Scanner for ReceivableScanner { - fn begin_scan( +impl + PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + > for ReceivableScanner +{ +} + +impl StartableScanner for ReceivableScanner { + fn start_scan( &mut self, - earning_wallet: Wallet, + earning_wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } + ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for receivables to {}", earning_wallet); self.scan_for_delinquencies(timestamp, logger); Ok(RetrieveTransactions { - recipient: earning_wallet, + recipient: earning_wallet.clone(), response_skeleton_opt, }) } +} +impl Scanner> for ReceivableScanner { fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { self.handle_new_received_payments(&msg, logger); self.mark_as_ended(logger); + msg.response_skeleton_opt .map(|response_skeleton| NodeToUiMessage { target: MessageTarget::ClientId(response_skeleton.client_id), @@ -946,52 +1273,76 @@ impl ReceivableScanner { } } -#[derive(Debug, PartialEq, Eq)] -pub enum BeginScanError { +#[derive(Debug, PartialEq, Eq, Clone, VariantCount)] +pub enum StartScanError { NothingToProcess, NoConsumingWalletFound, - ScanAlreadyRunning(SystemTime), + ScanAlreadyRunning { + cross_scan_cause_opt: Option, + started_at: SystemTime, + }, CalledFromNullScanner, // Exclusive for tests + ManualTriggerError(MTError), } -impl BeginScanError { - pub fn handle_error( - &self, - logger: &Logger, - scan_type: ScanType, - is_externally_triggered: bool, - ) { - let log_message_opt = match self { - BeginScanError::NothingToProcess => Some(format!( +impl StartScanError { + pub fn log_error(&self, logger: &Logger, scan_type: ScanType, is_externally_triggered: bool) { + enum ErrorType { + Temporary(String), + Permanent(String), + } + + let log_message = match self { + StartScanError::NothingToProcess => ErrorType::Temporary(format!( "There was nothing to process during {:?} scan.", scan_type )), - BeginScanError::ScanAlreadyRunning(timestamp) => Some(format!( - "{:?} scan was already initiated at {}. \ - Hence, this scan request will be ignored.", + StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt, + started_at, + } => ErrorType::Temporary(Self::scan_already_running_msg( scan_type, - BeginScanError::timestamp_as_string(timestamp) + *cross_scan_cause_opt, + *started_at, )), - BeginScanError::NoConsumingWalletFound => Some(format!( + StartScanError::NoConsumingWalletFound => ErrorType::Permanent(format!( "Cannot initiate {:?} scan because no consuming wallet was found.", scan_type )), - BeginScanError::CalledFromNullScanner => match cfg!(test) { - true => None, + StartScanError::CalledFromNullScanner => match cfg!(test) { + true => ErrorType::Permanent(format!( + "Called from NullScanner, not the {:?} scanner.", + scan_type + )), false => panic!("Null Scanner shouldn't be running inside production code."), }, + StartScanError::ManualTriggerError(e) => match e { + MTError::AutomaticScanConflict => ErrorType::Permanent(format!( + "User requested {:?} scan was denied. Automatic mode prevents manual triggers.", + scan_type + )), + MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( + "User requested {:?} scan was denied expecting zero findings.{}", + scan_type, + match hint_opt { + Some(hint) => format!(" {}", hint), + None => "".to_string(), + } + )), + }, }; - if let Some(log_message) = log_message_opt { - match is_externally_triggered { - true => info!(logger, "{}", log_message), - false => debug!(logger, "{}", log_message), - } + match log_message { + ErrorType::Temporary(msg) => match is_externally_triggered { + true => info!(logger, "{}", msg), + false => debug!(logger, "{}", msg), + }, + ErrorType::Permanent(msg) => warning!(logger, "{}", msg), } } - fn timestamp_as_string(timestamp: &SystemTime) -> String { - let offset_date_time = OffsetDateTime::from(*timestamp); + fn timestamp_as_string(timestamp: SystemTime) -> String { + let offset_date_time = OffsetDateTime::from(timestamp); offset_date_time .format( &parse(TIME_FORMATTING_STRING) @@ -999,69 +1350,44 @@ impl BeginScanError { ) .expect("Error while formatting timestamp as string.") } -} -pub struct ScanSchedulers { - pub schedulers: HashMap>, -} + fn scan_already_running_msg( + request_of: ScanType, + cross_scan_cause_opt: Option, + scan_started: SystemTime, + ) -> String { + let (blocking_scanner, request_spec) = if let Some(cross_scan_cause) = cross_scan_cause_opt + { + (cross_scan_cause, format!("the {:?}", request_of)) + } else { + (request_of, "this".to_string()) + }; -impl ScanSchedulers { - pub fn new(scan_intervals: ScanIntervals) -> Self { - let schedulers = HashMap::from_iter([ - ( - ScanType::Payables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.payable_scan_interval, - }) as Box, - ), - ( - ScanType::PendingPayables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.pending_payable_scan_interval, - }), - ), - ( - ScanType::Receivables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.receivable_scan_interval, - }), - ), - ]); - ScanSchedulers { schedulers } + format!( + "{:?} scan was already initiated at {}. Hence, {} scan request will be ignored.", + blocking_scanner, + StartScanError::timestamp_as_string(scan_started), + request_spec + ) } } -pub struct PeriodicalScanScheduler { - pub handle: Box>, - pub interval: Duration, +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum MTError { + AutomaticScanConflict, + UnnecessaryRequest { hint_opt: Option }, } -pub trait ScanScheduler { - fn schedule(&self, ctx: &mut Context); - fn interval(&self) -> Duration { - intentionally_blank!() - } +pub trait RealScannerMarker {} - as_any_ref_in_trait!(); - as_any_mut_in_trait!(); +macro_rules! impl_real_scanner_marker { + ($($t:ty),*) => { + $(impl RealScannerMarker for $t {})* + } } -impl ScanScheduler for PeriodicalScanScheduler { - fn schedule(&self, ctx: &mut Context) { - // the default of the message implies response_skeleton_opt to be None - // because scheduled scans don't respond - let _ = self.handle.notify_later(T::default(), self.interval, ctx); - } - fn interval(&self) -> Duration { - self.interval - } +impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScanner); - as_any_ref_in_trait_impl!(); - as_any_mut_in_trait_impl!(); -} #[cfg(test)] mod tests { use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; @@ -1069,23 +1395,12 @@ mod tests { PendingPayable, PendingPayableDaoError, TransactionHashes, }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; - use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, ScanSchedulers, - Scanner, ScannerCommon, Scanners, - }; - use crate::accountant::test_utils::{ - make_custom_payment_thresholds, make_payable_account, make_payables, - make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, - BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, - PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, - PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, - ReceivableDaoMock, ReceivableScannerBuilder, - }; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; + use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; + use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; + use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1095,9 +1410,9 @@ mod tests { use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::mocks::ConfigDaoMock; - use crate::db_config::persistent_configuration::{PersistentConfigError}; + use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, + DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; @@ -1106,9 +1421,8 @@ mod tests { use actix::{Message, System}; use ethereum_types::U64; use masq_lib::logger::Logger; - use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::Regex; + use regex::{Regex}; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; use std::collections::HashSet; @@ -1119,8 +1433,88 @@ mod tests { use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H256}; use web3::Error; + use masq_lib::messages::ScanType; + use masq_lib::ui_gateway::NodeToUiMessage; + use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; + impl Scanners { + pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { + match replacement { + ScannerReplacement::Payable(ReplacementType::Real(scanner)) => { + self.payable = Box::new(scanner) + } + ScannerReplacement::Payable(ReplacementType::Mock(scanner)) => { + self.payable = Box::new(scanner) + } + ScannerReplacement::Payable(ReplacementType::Null) => { + self.payable = Box::new(NullScanner::default()) + } + ScannerReplacement::PendingPayable(ReplacementType::Real(scanner)) => { + self.pending_payable = Box::new(scanner) + } + ScannerReplacement::PendingPayable(ReplacementType::Mock(scanner)) => { + self.pending_payable = Box::new(scanner) + } + ScannerReplacement::PendingPayable(ReplacementType::Null) => { + self.pending_payable = Box::new(NullScanner::default()) + } + ScannerReplacement::Receivable(ReplacementType::Real(scanner)) => { + self.receivable = Box::new(scanner) + } + ScannerReplacement::Receivable(ReplacementType::Mock(scanner)) => { + self.receivable = Box::new(scanner) + } + ScannerReplacement::Receivable(ReplacementType::Null) => { + self.receivable = Box::new(NullScanner::default()) + } + } + } + + pub fn reset_scan_started(&mut self, scan_type: ScanType, value: MarkScanner) { + match scan_type { + ScanType::Payables => { + Self::simple_scanner_timestamp_treatment(&mut *self.payable, value) + } + ScanType::PendingPayables => { + Self::simple_scanner_timestamp_treatment(&mut *self.pending_payable, value) + } + ScanType::Receivables => { + Self::simple_scanner_timestamp_treatment(&mut *self.receivable, value) + } + } + } + + pub fn aware_of_unresolved_pending_payables(&self) -> bool { + self.aware_of_unresolved_pending_payable + } + + pub fn set_aware_of_unresolved_pending_payables(&mut self, value: bool) { + self.aware_of_unresolved_pending_payable = value + } + + fn simple_scanner_timestamp_treatment( + scanner: &mut Scanner, + value: MarkScanner, + ) where + Scanner: self::Scanner + ?Sized, + EndMessage: actix::Message, + { + match value { + MarkScanner::Ended(logger) => scanner.mark_as_ended(logger), + MarkScanner::Started(timestamp) => scanner.mark_as_started(timestamp), + } + } + + pub fn scan_started_at(&self, scan_type: ScanType) -> Option { + match scan_type { + ScanType::Payables => self.payable.scan_started_at(), + ScanType::PendingPayables => self.pending_payable.scan_started_at(), + ScanType::Receivables => self.receivable.scan_started_at(), + } + } + } + #[test] fn scanners_struct_can_be_constructed_with_the_respective_scanners() { let payable_dao_factory = PayableDaoFactoryMock::new() @@ -1179,6 +1573,8 @@ mod tests { &payment_thresholds ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); + assert_eq!(scanners.aware_of_unresolved_pending_payable, false); + assert_eq!(scanners.initial_pending_payable_scan, true); assert_eq!( pending_payable_scanner.when_pending_too_long_sec, when_pending_too_long_sec @@ -1214,54 +1610,159 @@ mod tests { vec![("start_block".to_string(), Some("136890".to_string()))] ); assert_eq!( - Rc::strong_count(&payment_thresholds_rc), - initial_rc_count + 3 + Rc::strong_count(&payment_thresholds_rc), + initial_rc_count + 3 + ); + } + + #[test] + fn new_payable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "new_payable_scanner_can_initiate_a_scan"; + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + let (qualified_payable_accounts, _, all_non_pending_payables) = + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + subject.payable = Box::new(payable_scanner); + + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + true, + ); + + let timestamp = subject.payable.scan_started_at(); + assert_eq!(timestamp, Some(now)); + assert_eq!( + result, + Ok(QualifiedPayablesMessage { + qualified_payables: qualified_payable_accounts.clone(), + consuming_wallet, + response_skeleton_opt: None, + }) + ); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for new payables"), + &format!( + "INFO: {test_name}: Chose {} qualified debts to pay", + qualified_payable_accounts.len() + ), + ]) + } + + #[test] + fn new_payable_scanner_cannot_be_initiated_if_it_is_already_running() { + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + SystemTime::now(), + &PaymentThresholds::default(), + ); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + subject.payable = Box::new(payable_scanner); + let previous_scan_started_at = SystemTime::now(); + let _ = subject.start_new_payable_scan_guarded( + &consuming_wallet, + previous_scan_started_at, + None, + &Logger::new("test"), + true, + ); + + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); + + let is_scan_running = subject.payable.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: previous_scan_started_at + }) ); } #[test] - fn protected_payables_can_be_cast_from_and_back_to_vec_of_payable_accounts_by_payable_scanner() - { - let initial_unprotected = vec![make_payable_account(123), make_payable_account(456)]; - let subject = PayableScannerBuilder::new().build(); + fn new_payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + let (_, unqualified_payable_accounts, _) = + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); + let mut subject = make_dull_subject(); + subject.payable = Box::new( + PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(), + ); - let protected = subject.protect_payables(initial_unprotected.clone()); - let again_unprotected: Vec = subject.expose_payables(protected); + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); - assert_eq!(initial_unprotected, again_unprotected) + let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); + assert_eq!(is_scan_running, false); + assert_eq!(result, Err(StartScanError::NothingToProcess)); } #[test] - fn payable_scanner_can_initiate_a_scan() { + fn retry_payable_scanner_can_initiate_a_scan() { init_test_logging(); - let test_name = "payable_scanner_can_initiate_a_scan"; + let test_name = "retry_payable_scanner_can_initiate_a_scan"; let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (qualified_payable_accounts, _, all_non_pending_payables) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let mut subject = PayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); + subject.payable = Box::new(payable_scanner); - let result = - subject.begin_scan(consuming_wallet.clone(), now, None, &Logger::new(test_name)); + let result = subject.start_retry_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + ); - let timestamp = subject.scan_started_at(); + let timestamp = subject.payable.scan_started_at(); assert_eq!(timestamp, Some(now)); assert_eq!( result, Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test( - qualified_payable_accounts.clone() - ), + qualified_payables: qualified_payable_accounts.clone(), consuming_wallet, response_skeleton_opt: None, }) ); TestLogHandler::new().assert_logs_match_in_order(vec![ - &format!("INFO: {test_name}: Scanning for payables"), + &format!("INFO: {test_name}: Scanning for retry-required payables"), &format!( "INFO: {test_name}: Chose {} qualified debts to pay", qualified_payable_accounts.len() @@ -1270,49 +1771,103 @@ mod tests { } #[test] - fn payable_scanner_throws_error_when_a_scan_is_already_running() { + fn retry_payable_scanner_panics_in_case_scan_is_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let now = SystemTime::now(); - let (_, _, all_non_pending_payables) = make_payables(now, &PaymentThresholds::default()); + let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + SystemTime::now(), + &PaymentThresholds::default(), + ); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let mut subject = PayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let _result = subject.begin_scan(consuming_wallet.clone(), now, None, &Logger::new("test")); - - let run_again_result = subject.begin_scan( - consuming_wallet, + subject.payable = Box::new(payable_scanner); + let before = SystemTime::now(); + let _ = subject.start_retry_payable_scan_guarded( + &consuming_wallet, SystemTime::now(), None, &Logger::new("test"), ); - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(is_scan_running, true); - assert_eq!( - run_again_result, - Err(BeginScanError::ScanAlreadyRunning(now)) + let caught_panic = catch_unwind(AssertUnwindSafe(|| { + let _: Result = subject + .start_retry_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + ); + })) + .unwrap_err(); + + let after = SystemTime::now(); + let panic_msg = caught_panic.downcast_ref::().unwrap(); + let expected_needle_1 = "internal error: entered unreachable code: Guard for pending \ + payables should've prevented running the tandem of scanners if the payable scanner was \ + still running. It started "; + assert!( + panic_msg.contains(expected_needle_1), + "We looked for {} but the actual string doesn't contain it: {}", + expected_needle_1, + panic_msg + ); + let expected_needle_2 = "and is still running at "; + assert!( + panic_msg.contains(expected_needle_2), + "We looked for {} but the actual string doesn't contain it: {}", + expected_needle_2, + panic_msg + ); + check_timestamps_in_panic_for_already_running_retry_payable_scanner( + &panic_msg, before, after, + ) + } + + fn check_timestamps_in_panic_for_already_running_retry_payable_scanner( + panic_msg: &str, + before: SystemTime, + after: SystemTime, + ) { + let system_times = parse_system_time_from_str(panic_msg); + let first_actual = system_times[0]; + let second_actual = system_times[1]; + + assert!( + before <= first_actual + && first_actual <= second_actual + && second_actual <= after, + "We expected this relationship before({:?}) <= first_actual({:?}) <= second_actual({:?}) \ + <= after({:?}), but it does not hold true", + before, + first_actual, + second_actual, + after ); } #[test] - fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + #[should_panic(expected = "Complete me with GH-605")] + fn retry_payable_scanner_panics_in_case_no_qualified_payable_is_found() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (_, unqualified_payable_accounts, _) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let result = subject.begin_scan(consuming_wallet, now, None, &Logger::new("test")); - - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(is_scan_running, false); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + let _ = Scanners::start_correct_payable_scanner::( + &mut subject, + &consuming_wallet, + now, + None, + &Logger::new("test"), + ); } #[test] @@ -1361,7 +1916,7 @@ mod tests { .mark_pending_payables_rowids_params(&mark_pending_payables_params_arc) .mark_pending_payables_rowids_result(Ok(())) .mark_pending_payables_rowids_result(Ok(())); - let mut subject = PayableScannerBuilder::new() + let mut payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .pending_payable_dao(pending_payable_dao) .build(); @@ -1374,13 +1929,26 @@ mod tests { ]), response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; - let message_opt = subject.finish_scan(sent_payable, &logger); + let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(message_opt, None); + let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; + assert_eq!( + payable_scan_result, + PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable + } + ); assert_eq!(is_scan_running, false); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, true); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -1748,7 +2316,7 @@ mod tests { }) .delete_fingerprints_params(&delete_fingerprints_params_arc) .delete_fingerprints_result(Ok(())); - let mut subject = PayableScannerBuilder::new() + let payable_scanner = PayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); let logger = Logger::new(test_name); @@ -1759,12 +2327,25 @@ mod tests { }), response_skeleton_opt: None, }; + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; - let result = subject.finish_scan(sent_payable, &logger); + let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; System::current().stop(); system.run(); - assert_eq!(result, None); + assert_eq!( + payable_scan_result, + PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::Failure + } + ); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -1798,10 +2379,17 @@ mod tests { )), response_skeleton_opt: None, }; - let mut subject = PayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; - subject.finish_scan(sent_payable, &Logger::new(test_name)); + subject.finish_payable_scan(sent_payable, &Logger::new(test_name)); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" @@ -2211,24 +2799,31 @@ mod tests { let fingerprints = vec![payable_fingerprint_1, payable_fingerprint_2]; let pending_payable_dao = PendingPayableDaoMock::new() .return_all_errorless_fingerprints_result(fingerprints.clone()); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); - - let result = pending_payable_scanner.begin_scan( - consuming_wallet, + // Important + subject.aware_of_unresolved_pending_payable = true; + subject.pending_payable = Box::new(pending_payable_scanner); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.payable = Box::new(payable_scanner); + + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, now, None, &Logger::new(test_name), + true, ); let no_of_pending_payables = fingerprints.len(); - let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, Ok(RequestTransactionReceipts { - pending_payable: fingerprints, + pending_payable_fingerprints: fingerprints, response_skeleton_opt: None }) ); @@ -2241,29 +2836,154 @@ mod tests { } #[test] - fn pending_payable_scanner_throws_error_in_case_scan_is_already_running() { + fn pending_payable_scanner_cannot_be_initiated_if_it_itself_is_already_running() { let now = SystemTime::now(); let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(vec![PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: make_tx_hash(1), - attempt: 1, - amount: 1_000_000, - process_error: None, - }]); - let mut subject = PendingPayableScannerBuilder::new() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); + // Important + subject.aware_of_unresolved_pending_payable = true; + subject.pending_payable = Box::new(pending_payable_scanner); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); - let _ = subject.begin_scan(consuming_wallet.clone(), now, None, &logger); + let _ = + subject.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger, true); - let result = subject.begin_scan(consuming_wallet, SystemTime::now(), None, &logger); + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + true, + ); - let is_scan_running = subject.scan_started_at().is_some(); + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); - assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: now + }) + ); + } + + #[test] + fn pending_payable_scanner_cannot_be_initiated_if_payable_scanner_is_still_running() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + // Important + subject.aware_of_unresolved_pending_payable = true; + subject.pending_payable = Box::new(pending_payable_scanner); + subject.payable = Box::new(payable_scanner); + let logger = Logger::new("test"); + let previous_scan_started_at = SystemTime::now(); + subject.payable.mark_as_started(previous_scan_started_at); + + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + true, + ); + + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); + assert_eq!(is_scan_running, false); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: Some(ScanType::Payables), + started_at: previous_scan_started_at + }) + ); + } + + #[test] + fn both_payable_scanners_cannot_be_detected_in_progress_at_the_same_time() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.pending_payable = Box::new(pending_payable_scanner); + subject.payable = Box::new(payable_scanner); + let timestamp_pending_payable_start = SystemTime::now() + .checked_sub(Duration::from_millis(12)) + .unwrap(); + let timestamp_payable_scanner_start = SystemTime::now(); + subject.aware_of_unresolved_pending_payable = true; + subject + .pending_payable + .mark_as_started(timestamp_pending_payable_start); + subject + .payable + .mark_as_started(timestamp_payable_scanner_start); + + let caught_panic = catch_unwind(AssertUnwindSafe(|| { + let _ = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); + })) + .unwrap_err(); + + let panic_msg = caught_panic.downcast_ref::().unwrap(); + let expected_msg_fragment_1 = "internal error: entered unreachable code: Any payable-\ + related scanners should never be allowed to run in parallel. Scan for pending payables \ + started at: "; + assert!( + panic_msg.contains(expected_msg_fragment_1), + "This fragment '{}' wasn't found in \ + '{}'", + expected_msg_fragment_1, + panic_msg + ); + let expected_msg_fragment_2 = ", scan for payables started at: "; + assert!( + panic_msg.contains(expected_msg_fragment_2), + "This fragment '{}' wasn't found in \ + '{}'", + expected_msg_fragment_2, + panic_msg + ); + assert_timestamps_from_str( + panic_msg, + vec![ + timestamp_pending_payable_start, + timestamp_payable_scanner_start, + ], + ) + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Automatic pending payable \ + scan should never start if there are no pending payables to process." + )] + fn pending_payable_scanner_bumps_into_zero_pending_payable_awareness_in_the_automatic_mode() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + subject.pending_payable = Box::new(pending_payable_scanner); + subject.aware_of_unresolved_pending_payable = false; + + let _ = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); } #[test] @@ -2277,13 +2997,24 @@ mod tests { .build(); let result = - pending_payable_scanner.begin_scan(consuming_wallet, now, None, &Logger::new("test")); + pending_payable_scanner.start_scan(&consuming_wallet, now, None, &Logger::new("test")); let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + assert_eq!(result, Err(StartScanError::NothingToProcess)); assert_eq!(is_scan_running, false); } + #[test] + fn check_general_conditions_for_pending_payable_scan_if_it_is_initial_pending_payable_scan() { + let mut subject = make_dull_subject(); + subject.initial_pending_payable_scan = true; + + let result = subject.check_general_conditions_for_pending_payable_scan(false, true); + + assert_eq!(result, Ok(())); + assert_eq!(subject.initial_pending_payable_scan, true); + } + fn assert_interpreting_none_status_for_pending_payable( test_name: &str, when_pending_too_long_sec: u64, @@ -2802,7 +3533,7 @@ mod tests { .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); let pending_payable_dao = PendingPayableDaoMock::new().delete_fingerprints_result(Ok(())); - let mut subject = PendingPayableScannerBuilder::new() + let mut pending_payable_scanner = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) .pending_payable_dao(pending_payable_dao) .build(); @@ -2851,17 +3582,22 @@ mod tests { ], response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(message_opt, None); + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ); assert_eq!( *transactions_confirmed_params, vec![vec![fingerprint_1, fingerprint_2]] ); - assert_eq!(subject.scan_started_at(), None); + assert_eq!(subject.scan_started_at(ScanType::PendingPayables), None); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!( "INFO: {}: Transactions {:?}, {:?} completed their confirmation process succeeding", @@ -2876,21 +3612,26 @@ mod tests { init_test_logging(); let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; - let mut subject = PendingPayableScannerBuilder::new().build(); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); let msg = ReportTransactionReceipts { fingerprints_with_receipts: vec![], response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(message_opt, None); + let is_scan_running = subject.scan_started_at(ScanType::PendingPayables).is_some(); + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ); assert_eq!(is_scan_running, false); let tlh = TestLogHandler::new(); tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: No transaction receipts found." + "WARN: {test_name}: No transaction receipts found." )); tlh.exists_log_matching(&format!( "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." @@ -2906,18 +3647,21 @@ mod tests { .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let earning_wallet = make_wallet("earning"); - let mut receivable_scanner = ReceivableScannerBuilder::new() + let mut subject = make_dull_subject(); + let receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .build(); + subject.receivable = Box::new(receivable_scanner); - let result = receivable_scanner.begin_scan( - earning_wallet.clone(), + let result = subject.start_receivable_scan_guarded( + &earning_wallet, now, None, &Logger::new(test_name), + true, ); - let is_scan_running = receivable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.receivable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, @@ -2938,22 +3682,36 @@ mod tests { .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let earning_wallet = make_wallet("earning"); - let mut receivable_scanner = ReceivableScannerBuilder::new() + let mut subject = make_dull_subject(); + let receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .build(); - let _ = - receivable_scanner.begin_scan(earning_wallet.clone(), now, None, &Logger::new("test")); + subject.receivable = Box::new(receivable_scanner); + let _ = subject.start_receivable_scan_guarded( + &earning_wallet, + now, + None, + &Logger::new("test"), + true, + ); - let result = receivable_scanner.begin_scan( - earning_wallet, + let result = subject.start_receivable_scan_guarded( + &earning_wallet, SystemTime::now(), None, &Logger::new("test"), + true, ); - let is_scan_running = receivable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.receivable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); - assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + assert_eq!( + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: now + }) + ); } #[test] @@ -2986,7 +3744,7 @@ mod tests { let logger = Logger::new("DELINQUENCY_TEST"); let now = SystemTime::now(); - let result = receivable_scanner.begin_scan(earning_wallet.clone(), now, None, &logger); + let result = receivable_scanner.start_scan(&earning_wallet, now, None, &logger); assert_eq!( result, @@ -3040,7 +3798,7 @@ mod tests { .start_block_result(Ok(None)) .set_start_block_params(&set_start_block_params_arc) .set_start_block_result(Ok(())); - let mut subject = ReceivableScannerBuilder::new() + let receivable_scanner = ReceivableScannerBuilder::new() .persistent_configuration(persistent_config) .build(); let msg = ReceivedPayments { @@ -3049,10 +3807,12 @@ mod tests { response_skeleton_opt: None, transactions: vec![], }; + let mut subject = make_dull_subject(); + subject.receivable = Box::new(receivable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_receivable_scan(msg, &Logger::new(test_name)); - assert_eq!(message_opt, None); + assert_eq!(ui_msg_opt, None); let set_start_block_params = set_start_block_params_arc.lock().unwrap(); assert_eq!(*set_start_block_params, vec![Some(4321)]); TestLogHandler::new().exists_log_containing(&format!( @@ -3084,7 +3844,6 @@ mod tests { response_skeleton_opt: None, transactions: vec![], }; - // Not necessary, rather for preciseness subject.mark_as_started(SystemTime::now()); @@ -3112,13 +3871,15 @@ mod tests { let receivable_dao = ReceivableDaoMock::new() .more_money_received_params(&more_money_received_params_arc) .more_money_received_result(transaction); - let mut subject = ReceivableScannerBuilder::new() + let mut receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .persistent_configuration(persistent_config) .build(); - let mut financial_statistics = subject.financial_statistics.borrow().clone(); + let mut financial_statistics = receivable_scanner.financial_statistics.borrow().clone(); financial_statistics.total_paid_receivable_wei += 2_222_123_123; - subject.financial_statistics.replace(financial_statistics); + receivable_scanner + .financial_statistics + .replace(financial_statistics); let receivables = vec![ BlockchainTransaction { block_number: 4578910, @@ -3137,16 +3898,23 @@ mod tests { response_skeleton_opt: None, transactions: receivables.clone(), }; - subject.mark_as_started(SystemTime::now()); + receivable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.receivable = Box::new(receivable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_receivable_scan(msg, &Logger::new(test_name)); - let total_paid_receivable = subject + let scanner_after = subject + .receivable + .as_any() + .downcast_ref::() + .unwrap(); + let total_paid_receivable = scanner_after .financial_statistics .borrow() .total_paid_receivable_wei; - assert_eq!(message_opt, None); - assert_eq!(subject.scan_started_at(), None); + assert_eq!(ui_msg_opt, None); + assert_eq!(scanner_after.scan_started_at(), None); assert_eq!(total_paid_receivable, 2_222_123_123 + 45_780 + 3_333_345); let more_money_received_params = more_money_received_params_arc.lock().unwrap(); assert_eq!(*more_money_received_params, vec![(now, receivables)]); @@ -3303,8 +4071,8 @@ mod tests { )); } - fn assert_elapsed_time_in_mark_as_ended( - subject: &mut dyn Scanner, + fn assert_elapsed_time_in_mark_as_ended( + subject: &mut dyn Scanner, scanner_name: &str, test_name: &str, logger: &Logger, @@ -3343,21 +4111,21 @@ mod tests { let logger = Logger::new(test_name); let log_handler = TestLogHandler::new(); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PayableScannerBuilder::new().build(), "Payables", test_name, &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PendingPayableScannerBuilder::new().build(), "PendingPayables", test_name, &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::>( &mut ReceivableScannerBuilder::new().build(), "Receivables", test_name, @@ -3367,38 +4135,227 @@ mod tests { } #[test] - fn scan_schedulers_can_be_properly_initialized() { - let scan_intervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(240), - pending_payable_scan_interval: Duration::from_secs(300), - receivable_scan_interval: Duration::from_secs(360), - }; + fn scan_already_running_msg_displays_correctly_if_blocked_by_requested_scan() { + test_scan_already_running_msg( + ScanType::PendingPayables, + None, + "PendingPayables scan was already initiated at", + ". Hence, this scan request will be ignored.", + ) + } + + #[test] + fn scan_already_running_msg_displays_correctly_if_blocked_by_other_scan_than_directly_requested( + ) { + test_scan_already_running_msg( + ScanType::PendingPayables, + Some(ScanType::Payables), + "Payables scan was already initiated at", + ". Hence, the PendingPayables scan request will be ignored.", + ) + } - let result = ScanSchedulers::new(scan_intervals); + fn test_scan_already_running_msg( + requested_scan: ScanType, + cross_scan_blocking_cause_opt: Option, + expected_leading_msg_fragment: &str, + expected_trailing_msg_fragment: &str, + ) { + let some_time = SystemTime::now(); - assert_eq!( - result - .schedulers - .get(&ScanType::Payables) - .unwrap() - .interval(), - scan_intervals.payable_scan_interval + let result = StartScanError::scan_already_running_msg( + requested_scan, + cross_scan_blocking_cause_opt, + some_time, ); - assert_eq!( + + assert!( + result.contains(expected_leading_msg_fragment), + "We expected {} but the msg is: {}", + expected_leading_msg_fragment, result - .schedulers - .get(&ScanType::PendingPayables) - .unwrap() - .interval(), - scan_intervals.pending_payable_scan_interval ); - assert_eq!( + assert!( + result.contains(expected_trailing_msg_fragment), + "We expected {} but the msg is: {}", + expected_trailing_msg_fragment, result - .schedulers - .get(&ScanType::Receivables) - .unwrap() - .interval(), - scan_intervals.receivable_scan_interval ); + assert_timestamps_from_str(&result, vec![some_time]); + } + + #[test] + fn acknowledge_scan_error_works() { + fn scan_error(scan_type: ScanType) -> ScanError { + ScanError { + scan_type, + response_skeleton_opt: None, + msg: "bluh".to_string(), + } + } + + init_test_logging(); + let test_name = "acknowledge_scan_error_works"; + let inputs: Vec<( + ScanType, + Box, + Box Option>, + )> = vec![ + ( + ScanType::Payables, + Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.payable.scan_started_at()), + ), + ( + ScanType::PendingPayables, + Box::new(|subject| subject.pending_payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.pending_payable.scan_started_at()), + ), + ( + ScanType::Receivables, + Box::new(|subject| subject.receivable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.receivable.scan_started_at()), + ), + ]; + let mut subject = make_dull_subject(); + subject.payable = Box::new(PayableScannerBuilder::new().build()); + subject.pending_payable = Box::new(PendingPayableScannerBuilder::new().build()); + subject.receivable = Box::new(ReceivableScannerBuilder::new().build()); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + + inputs + .into_iter() + .for_each(|(scan_type, set_started, get_started_at)| { + set_started(&mut subject); + let started_at_before = get_started_at(&subject); + + subject.acknowledge_scan_error(&scan_error(scan_type), &logger); + + let started_at_after = get_started_at(&subject); + assert!( + started_at_before.is_some(), + "Should've been started for {:?}", + scan_type + ); + assert_eq!( + started_at_after, None, + "Should've been unset for {:?}", + scan_type + ); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: The {:?} scan ended in", + scan_type + )); + }) + } + + #[test] + fn log_error_works_fine() { + init_test_logging(); + let test_name = "log_error_works_fine"; + let now = SystemTime::now(); + let input: Vec<(StartScanError, Box String>, &str, &str)> = vec![ + ( + StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: now, + }, + Box::new(|sev| { + format!( + "{sev}: {test_name}: Payables scan was already initiated at {}", + StartScanError::timestamp_as_string(now) + ) + }), + "INFO", + "DEBUG", + ), + ( + StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") + }), + "WARN", + "WARN", + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + hint_opt: Some("Wise words".to_string()), + }), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings. Wise words") + }), + "INFO", + "DEBUG", + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: None }), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings.") + }), + "INFO", + "DEBUG", + ), + ( + StartScanError::CalledFromNullScanner, + Box::new(|sev| { + format!( + "{sev}: {test_name}: Called from NullScanner, not the Payables scanner." + ) + }), + "WARN", + "WARN", + ), + ( + StartScanError::NoConsumingWalletFound, + Box::new(|sev| { + format!("{sev}: {test_name}: Cannot initiate Payables scan because no consuming wallet was found.") + }), + "WARN", + "WARN", + ), + ( + StartScanError::NothingToProcess, + Box::new(|sev| { + format!( + "{sev}: {test_name}: There was nothing to process during Payables scan." + ) + }), + "INFO", + "DEBUG", + ), + ]; + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + + input.into_iter().for_each( + |( + err, + form_expected_log_msg, + log_severity_for_externally_triggered_scans, + log_severity_for_automatic_scans, + )| { + let test_log_error_by_mode = + |is_externally_triggered: bool, expected_severity: &str| { + err.log_error(&logger, ScanType::Payables, is_externally_triggered); + let expected_log_msg = form_expected_log_msg(expected_severity); + test_log_handler.exists_log_containing(&expected_log_msg); + }; + + test_log_error_by_mode(true, log_severity_for_externally_triggered_scans); + + test_log_error_by_mode(false, log_severity_for_automatic_scans); + }, + ); + } + + fn make_dull_subject() -> Scanners { + Scanners { + payable: Box::new(NullScanner::new()), + aware_of_unresolved_pending_payable: false, + initial_pending_payable_scan: false, + pending_payable: Box::new(NullScanner::new()), + receivable: Box::new(NullScanner::new()), + } } } diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs similarity index 94% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs rename to node/src/accountant/scanners/payable_scanner_extension/agent_null.rs index e95673002..5f9811204 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; @@ -77,8 +77,8 @@ impl Default for BlockchainAgentNull { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_null::BlockchainAgentNull; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner_extension::agent_null::BlockchainAgentNull; + use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs similarity index 93% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs rename to node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs index 725e14f00..8acf40ef8 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use masq_lib::blockchains::chains::Chain; @@ -64,10 +64,10 @@ impl BlockchainAgentWeb3 { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::{ + use crate::accountant::scanners::payable_scanner_extension::agent_web3::{ BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, }; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::make_wallet; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs b/node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs similarity index 100% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs rename to node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs similarity index 62% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs rename to node/src/accountant/scanners/payable_scanner_extension/mod.rs index 257c88fde..649bc820f 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -7,22 +7,25 @@ pub mod msgs; pub mod test_utils; use crate::accountant::payment_adjuster::Adjustment; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::Scanner; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; +use crate::accountant::scanners::{Scanner, StartableScanner}; +use crate::accountant::{ScanForNewPayables, ScanForRetryPayables, SentPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use actix::Message; use itertools::Either; use masq_lib::logger::Logger; -pub trait MultistagePayableScanner: - Scanner + SolvencySensitivePaymentInstructor -where - BeginMessage: Message, - EndMessage: Message, +pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: + StartableScanner + + StartableScanner + + SolvencySensitivePaymentInstructor + + Scanner { } -pub trait SolvencySensitivePaymentInstructor { +pub(in crate::accountant::scanners) trait SolvencySensitivePaymentInstructor { fn try_skipping_payment_adjustment( &self, msg: BlockchainAgentWithContextMessage, @@ -55,7 +58,7 @@ impl PreparedAdjustment { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; + use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; impl Clone for PreparedAdjustment { fn clone(&self) -> Self { diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs similarity index 66% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs rename to node/src/accountant/scanners/payable_scanner_extension/msgs.rs index 41a1b3940..599f17390 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs @@ -1,27 +1,27 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; use crate::sub_lib::wallet::Wallet; use actix::Message; -use masq_lib::type_obfuscation::Obfuscated; use std::fmt::Debug; #[derive(Debug, Message, PartialEq, Eq, Clone)] pub struct QualifiedPayablesMessage { - pub protected_qualified_payables: Obfuscated, + pub qualified_payables: Vec, pub consuming_wallet: Wallet, pub response_skeleton_opt: Option, } impl QualifiedPayablesMessage { pub(in crate::accountant) fn new( - protected_qualified_payables: Obfuscated, + qualified_payables: Vec, consuming_wallet: Wallet, response_skeleton_opt: Option, ) -> Self { Self { - protected_qualified_payables, + qualified_payables, consuming_wallet, response_skeleton_opt, } @@ -36,20 +36,20 @@ impl SkeletonOptHolder for QualifiedPayablesMessage { #[derive(Message)] pub struct BlockchainAgentWithContextMessage { - pub protected_qualified_payables: Obfuscated, + pub qualified_payables: Vec, pub agent: Box, pub response_skeleton_opt: Option, } impl BlockchainAgentWithContextMessage { pub fn new( - qualified_payables: Obfuscated, - blockchain_agent: Box, + qualified_payables: Vec, + agent: Box, response_skeleton_opt: Option, ) -> Self { Self { - protected_qualified_payables: qualified_payables, - agent: blockchain_agent, + qualified_payables, + agent, response_skeleton_opt, } } @@ -58,8 +58,8 @@ impl BlockchainAgentWithContextMessage { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; impl Clone for BlockchainAgentWithContextMessage { fn clone(&self) -> Self { @@ -67,7 +67,7 @@ mod tests { let cloned_agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(original_agent_id); Self { - protected_qualified_payables: self.protected_qualified_payables.clone(), + qualified_payables: self.qualified_payables.clone(), agent: Box::new(cloned_agent), response_skeleton_opt: self.response_skeleton_opt, } diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs similarity index 96% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs rename to node/src/accountant/scanners/payable_scanner_extension/test_utils.rs index d3ab97284..def16f20e 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs @@ -2,7 +2,7 @@ #![cfg(test)] -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs new file mode 100644 index 000000000..77ac6646e --- /dev/null +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -0,0 +1,941 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::scanners::StartScanError; +use crate::accountant::{ + Accountant, ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, + ScanForRetryPayables, +}; +use crate::sub_lib::accountant::ScanIntervals; +use crate::sub_lib::utils::{ + NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal, +}; +use actix::{Actor, Context, Handler}; +use masq_lib::logger::Logger; +use masq_lib::messages::ScanType; +use std::fmt::{Debug, Display, Formatter}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub struct ScanSchedulers { + pub payable: PayableScanScheduler, + pub pending_payable: SimplePeriodicalScanScheduler, + pub receivable: SimplePeriodicalScanScheduler, + pub reschedule_on_error_resolver: Box, + pub automatic_scans_enabled: bool, +} + +impl ScanSchedulers { + pub fn new(scan_intervals: ScanIntervals, automatic_scans_enabled: bool) -> Self { + Self { + payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), + pending_payable: SimplePeriodicalScanScheduler::new( + scan_intervals.pending_payable_scan_interval, + ), + receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), + reschedule_on_error_resolver: Box::new(RescheduleScanOnErrorResolverReal::default()), + automatic_scans_enabled, + } + } +} + +#[derive(Debug, PartialEq)] +pub enum PayableScanSchedulerError { + ScanForNewPayableAlreadyScheduled, +} + +#[derive(Debug, PartialEq)] +pub enum ScanRescheduleAfterEarlyStop { + Schedule(ScanType), + DoNotSchedule, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum PayableSequenceScanner { + NewPayables, + RetryPayables, + PendingPayables { initial_pending_payable_scan: bool }, +} + +impl Display for PayableSequenceScanner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PayableSequenceScanner::NewPayables => write!(f, "NewPayables"), + PayableSequenceScanner::RetryPayables => write!(f, "RetryPayables"), + PayableSequenceScanner::PendingPayables { .. } => write!(f, "PendingPayables"), + } + } +} + +impl From for ScanType { + fn from(scanner: PayableSequenceScanner) -> Self { + match scanner { + PayableSequenceScanner::NewPayables => ScanType::Payables, + PayableSequenceScanner::RetryPayables => ScanType::Payables, + PayableSequenceScanner::PendingPayables { .. } => ScanType::PendingPayables, + } + } +} + +pub struct PayableScanScheduler { + pub new_payable_notify_later: Box>, + pub dyn_interval_computer: Box, + pub inner: Arc>, + pub new_payable_interval: Duration, + pub new_payable_notify: Box>, + pub retry_payable_notify: Box>, +} + +impl PayableScanScheduler { + fn new(new_payable_interval: Duration) -> Self { + Self { + new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), + dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::default()), + inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), + new_payable_interval, + new_payable_notify: Box::new(NotifyHandleReal::default()), + retry_payable_notify: Box::new(NotifyHandleReal::default()), + } + } + + pub fn schedule_new_payable_scan(&self, ctx: &mut Context, logger: &Logger) { + let inner = self.inner.lock().expect("couldn't acquire inner"); + let last_new_payable_scan_timestamp = inner.last_new_payable_scan_timestamp; + let new_payable_interval = self.new_payable_interval; + let now = SystemTime::now(); + if let Some(interval) = self.dyn_interval_computer.compute_interval( + now, + last_new_payable_scan_timestamp, + new_payable_interval, + ) { + debug!( + logger, + "Scheduling a new-payable scan in {}ms", + interval.as_millis() + ); + + let _ = self.new_payable_notify_later.notify_later( + ScanForNewPayables { + response_skeleton_opt: None, + }, + interval, + ctx, + ); + } else { + debug!(logger, "Scheduling a new-payable scan asap"); + + let _ = self.new_payable_notify.notify( + ScanForNewPayables { + response_skeleton_opt: None, + }, + ctx, + ); + } + } + + // This message ships into the Accountant's mailbox with no delay. + // Can also be triggered by command, following up after the PendingPayableScanner + // that requests it. That's why the response skeleton is possible to be used. + pub fn schedule_retry_payable_scan( + &self, + ctx: &mut Context, + response_skeleton_opt: Option, + logger: &Logger, + ) { + debug!(logger, "Scheduling a retry-payable scan asap"); + + self.retry_payable_notify.notify( + ScanForRetryPayables { + response_skeleton_opt, + }, + ctx, + ) + } +} + +pub struct PayableScanSchedulerInner { + pub last_new_payable_scan_timestamp: SystemTime, +} + +impl Default for PayableScanSchedulerInner { + fn default() -> Self { + Self { + last_new_payable_scan_timestamp: UNIX_EPOCH, + } + } +} + +pub trait NewPayableScanDynIntervalComputer { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option; +} + +#[derive(Default)] +pub struct NewPayableScanDynIntervalComputerReal {} + +impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option { + let elapsed = now + .duration_since(last_new_payable_scan_timestamp) + .unwrap_or_else(|_| { + panic!( + "Unexpected now ({:?}) earlier than past timestamp ({:?})", + now, last_new_payable_scan_timestamp + ) + }); + if elapsed >= interval { + None + } else { + Some(interval - elapsed) + } + } +} + +pub struct SimplePeriodicalScanScheduler { + pub handle: Box>, + pub interval: Duration, +} + +impl SimplePeriodicalScanScheduler +where + Message: actix::Message + Default + Debug + Send + 'static, + Accountant: Actor + Handler, +{ + fn new(interval: Duration) -> Self { + Self { + handle: Box::new(NotifyLaterHandleReal::default()), + interval, + } + } + pub fn schedule(&self, ctx: &mut Context, logger: &Logger) { + // The default of the message implies response_skeleton_opt to be None because scheduled + // scans don't respond + let msg = Message::default(); + + debug!( + logger, + "Scheduling a scan via {:?} in {}ms", + msg, + self.interval.as_millis() + ); + + let _ = self.handle.notify_later(msg, self.interval, ctx); + } +} + +// Scanners that take part in a scan sequence composed of different scanners must handle +// StartScanErrors delicately to maintain the continuity and periodicity of this process. Where +// possible, either the same, some other, but traditional, or even a totally unrelated scan chosen +// just in the event of emergency, may be scheduled. The intention is to prevent a full panic while +// ensuring no harmful, toxic issues are left behind for the future scans. Following that philosophy, +// panic is justified only if the error was thought to be impossible by design and contextual +// things but still happened. +pub trait RescheduleScanOnErrorResolver { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop; +} + +#[derive(Default)] +pub struct RescheduleScanOnErrorResolverReal {} + +impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop { + let reschedule_hint = match scanner { + PayableSequenceScanner::NewPayables => { + Self::resolve_new_payables(error, is_externally_triggered) + } + PayableSequenceScanner::RetryPayables => { + Self::resolve_retry_payables(error, is_externally_triggered) + } + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan, + } => Self::resolve_pending_payables( + error, + initial_pending_payable_scan, + is_externally_triggered, + ), + }; + + Self::log_rescheduling(scanner, is_externally_triggered, logger, &reschedule_hint); + + reschedule_hint + } +} + +impl RescheduleScanOnErrorResolverReal { + fn resolve_new_payables( + err: &StartScanError, + is_externally_triggered: bool, + ) -> ScanRescheduleAfterEarlyStop { + if is_externally_triggered { + ScanRescheduleAfterEarlyStop::DoNotSchedule + } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { + unreachable!( + "an automatic scan of NewPayableScanner should never interfere with itself {:?}", + err + ) + } else { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + } + } + + // Paradoxical at first, but this scanner is meant to be shielded by the scanner right before + // it. That should ensure this scanner will not be requested if there was already something + // fishy. We can impose strictness. + fn resolve_retry_payables( + err: &StartScanError, + is_externally_triggered: bool, + ) -> ScanRescheduleAfterEarlyStop { + if is_externally_triggered { + ScanRescheduleAfterEarlyStop::DoNotSchedule + } else { + unreachable!( + "{:?} should be impossible with RetryPayableScanner in automatic mode", + err + ) + } + } + + fn resolve_pending_payables( + err: &StartScanError, + initial_pending_payable_scan: bool, + is_externally_triggered: bool, + ) -> ScanRescheduleAfterEarlyStop { + if is_externally_triggered { + ScanRescheduleAfterEarlyStop::DoNotSchedule + } else if err == &StartScanError::NothingToProcess { + if initial_pending_payable_scan { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + } else { + unreachable!( + "the automatic pending payable scan should always be requested only in need, \ + which contradicts the current StartScanError::NothingToProcess" + ) + } + } else if err == &StartScanError::NoConsumingWalletFound { + if initial_pending_payable_scan { + // Cannot deduce there are strayed pending payables from the previous Node's run + // (StartScanError::NoConsumingWalletFound is thrown before + // StartScanError::NothingToProcess can be evaluated); but may be cautious and + // prevent starting the NewPayableScanner. Repeating this scan endlessly may alarm + // the user. + // TODO Correctly, a check-point during the bootstrap that wouldn't allow to come + // this far should be the solution. Part of the issue mentioned in GH-799 + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) + } else { + unreachable!( + "PendingPayableScanner called later than the initial attempt, but \ + the consuming wallet is still missing; this should not be possible" + ) + } + } else { + unreachable!( + "{:?} should be impossible with PendingPayableScanner in automatic mode", + err + ) + } + } + + fn log_rescheduling( + scanner: PayableSequenceScanner, + is_externally_triggered: bool, + logger: &Logger, + reschedule_hint: &ScanRescheduleAfterEarlyStop, + ) { + let scan_mode = if is_externally_triggered { + "Manual" + } else { + "Automatic" + }; + + debug!( + logger, + "{} {} scan failed - rescheduling strategy: \"{:?}\"", + scan_mode, + scanner, + reschedule_hint + ); + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::scan_schedulers::{ + NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, + PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, + }; + use crate::accountant::scanners::{MTError, StartScanError}; + use crate::sub_lib::accountant::ScanIntervals; + use itertools::Itertools; + use lazy_static::lazy_static; + use masq_lib::logger::Logger; + use masq_lib::messages::ScanType; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + #[test] + fn scan_schedulers_are_initialized_correctly() { + let scan_intervals = ScanIntervals { + payable_scan_interval: Duration::from_secs(14), + pending_payable_scan_interval: Duration::from_secs(2), + receivable_scan_interval: Duration::from_secs(7), + }; + let automatic_scans_enabled = true; + + let schedulers = ScanSchedulers::new(scan_intervals, automatic_scans_enabled); + + assert_eq!( + schedulers.payable.new_payable_interval, + scan_intervals.payable_scan_interval + ); + let payable_scheduler_inner = schedulers.payable.inner.lock().unwrap(); + assert_eq!( + payable_scheduler_inner.last_new_payable_scan_timestamp, + UNIX_EPOCH + ); + assert_eq!( + schedulers.pending_payable.interval, + scan_intervals.pending_payable_scan_interval + ); + assert_eq!( + schedulers.receivable.interval, + scan_intervals.receivable_scan_interval + ); + assert_eq!(schedulers.automatic_scans_enabled, automatic_scans_enabled) + } + + #[test] + fn scan_dyn_interval_computer_computes_remaining_time_to_standard_interval_correctly() { + let now = SystemTime::now(); + let inputs = vec![ + ( + now.checked_sub(Duration::from_secs(32)).unwrap(), + Duration::from_secs(100), + Duration::from_secs(68), + ), + ( + now.checked_sub(Duration::from_millis(1111)).unwrap(), + Duration::from_millis(3333), + Duration::from_millis(2222), + ), + ( + now.checked_sub(Duration::from_secs(200)).unwrap(), + Duration::from_secs(204), + Duration::from_secs(4), + ), + ]; + let subject = NewPayableScanDynIntervalComputerReal::default(); + + inputs + .into_iter() + .for_each(|(past_instant, standard_interval, expected_result)| { + let result = subject.compute_interval(now, past_instant, standard_interval); + assert_eq!( + result, + Some(expected_result), + "We expected Some({}) ms, but got {:?} ms", + expected_result.as_millis(), + result.map(|duration| duration.as_millis()) + ) + }) + } + + #[test] + fn scan_dyn_interval_computer_realizes_the_standard_interval_has_been_exceeded() { + let now = SystemTime::now(); + let inputs = vec![ + ( + now.checked_sub(Duration::from_millis(32001)).unwrap(), + Duration::from_secs(32), + ), + ( + now.checked_sub(Duration::from_millis(1112)).unwrap(), + Duration::from_millis(1111), + ), + ( + now.checked_sub(Duration::from_secs(200)).unwrap(), + Duration::from_secs(123), + ), + ]; + let subject = NewPayableScanDynIntervalComputerReal::default(); + + inputs + .into_iter() + .enumerate() + .for_each(|(idx, (past_instant, standard_interval))| { + let result = subject.compute_interval(now, past_instant, standard_interval); + assert_eq!( + result, + None, + "We expected None ms, but got {:?} ms at idx {}", + result.map(|duration| duration.as_millis()), + idx + ) + }) + } + + #[test] + fn scan_dyn_interval_computer_realizes_standard_interval_just_met() { + let now = SystemTime::now(); + let subject = NewPayableScanDynIntervalComputerReal::default(); + + let result = subject.compute_interval( + now, + now.checked_sub(Duration::from_secs(32)).unwrap(), + Duration::from_secs(32), + ); + + assert_eq!( + result, + None, + "We expected None ms, but got {:?} ms", + result.map(|duration| duration.as_millis()) + ) + } + + #[test] + #[should_panic( + expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier than past \ + timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + )] + fn scan_dyn_interval_computer_panics() { + let now = UNIX_EPOCH + .checked_add(Duration::from_secs(1_000_000)) + .unwrap(); + let subject = NewPayableScanDynIntervalComputerReal::default(); + + let _ = subject.compute_interval( + now.checked_sub(Duration::from_secs(1)).unwrap(), + now, + Duration::from_secs(32), + ); + } + + lazy_static! { + static ref ALL_START_SCAN_ERRORS: Vec = { + + let candidates = vec![ + StartScanError::NothingToProcess, + StartScanError::NoConsumingWalletFound, + StartScanError::ScanAlreadyRunning { cross_scan_cause_opt: None, started_at: SystemTime::now()}, + StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + StartScanError::CalledFromNullScanner + ]; + + + let mut check_vec = candidates + .iter() + .fold(vec![],|mut acc, current|{ + acc.push(ListOfStartScanErrors::number_variant(current)); + acc + }); + // Making sure we didn't count in one variant multiple times + check_vec.dedup(); + assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "The check on variant \ + exhaustiveness failed."); + candidates + }; + } + + struct ListOfStartScanErrors<'a> { + errors: Vec<&'a StartScanError>, + } + + impl<'a> Default for ListOfStartScanErrors<'a> { + fn default() -> Self { + Self { + errors: ALL_START_SCAN_ERRORS.iter().collect_vec(), + } + } + } + + impl<'a> ListOfStartScanErrors<'a> { + fn eliminate_already_tested_variants( + mut self, + errors_to_eliminate: Vec, + ) -> Self { + let error_variants_to_remove: Vec<_> = errors_to_eliminate + .iter() + .map(Self::number_variant) + .collect(); + self.errors + .retain(|err| !error_variants_to_remove.contains(&Self::number_variant(*err))); + self + } + + fn number_variant(error: &StartScanError) -> usize { + match error { + StartScanError::NothingToProcess => 1, + StartScanError::NoConsumingWalletFound => 2, + StartScanError::ScanAlreadyRunning { .. } => 3, + StartScanError::CalledFromNullScanner => 4, + StartScanError::ManualTriggerError(..) => 5, + } + } + } + + #[test] + fn resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let test_name = + "resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered"; + + test_what_if_externally_triggered( + &format!("{}(initial_pending_payable_scan = false)", test_name), + &subject, + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }, + ); + test_what_if_externally_triggered( + &format!("{}(initial_pending_payable_scan = true)", test_name), + &subject, + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }, + ); + } + + fn test_what_if_externally_triggered( + test_name: &str, + subject: &ScanSchedulers, + scanner: PayableSequenceScanner, + ) { + init_test_logging(); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + ALL_START_SCAN_ERRORS + .iter() + .enumerate() + .for_each(|(idx, error)| { + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error(scanner, error, true, &logger); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::DoNotSchedule, + "We expected DoNotSchedule but got {:?} at idx {} for {:?}", + result, + idx, + scanner + ); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Manual {} scan failed - rescheduling strategy: \ + \"DoNotSchedule\"", + scanner + )); + }) + } + + #[test] + fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( + ) { + init_test_logging(); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let test_name = "resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true"; + let logger = Logger::new(test_name); + + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }, + &StartScanError::NothingToProcess, + false, + &logger, + ); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got {:?}", + result, + ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Automatic PendingPayables scan failed - rescheduling strategy: \ + \"Schedule(Payables)\"" + )); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: the automatic pending payable scan \ + should always be requested only in need, which contradicts the current \ + StartScanError::NothingToProcess" + )] + fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( + ) { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }, + &StartScanError::NothingToProcess, + false, + &Logger::new("test"), + ); + } + + #[test] + fn resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan() { + init_test_logging(); + let test_name = "resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan"; + let logger = Logger::new(test_name); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }; + + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, + &logger, + ); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables), + "We expected Schedule(PendingPayables) but got {:?} for {:?}", + result, + scanner + ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Automatic PendingPayables scan failed - rescheduling strategy: \ + \"Schedule(PendingPayables)\"" + )); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: PendingPayableScanner called later \ + than the initial attempt, but the consuming wallet is still missing; this should not be \ + possible" + )] + fn pending_p_scan_attempt_if_no_consuming_wallet_found_mustnt_happen_if_not_initial_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }; + + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, + &Logger::new("test"), + ); + } + + #[test] + fn resolve_error_for_pending_payables_forbidden_states() { + fn test_forbidden_states( + subject: &ScanSchedulers, + inputs: &ListOfStartScanErrors, + initial_pending_payable_scan: bool, + ) { + inputs.errors.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan, + }, + *error, + false, + &Logger::new("test"), + ) + })) + .unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} should be impossible with \ + PendingPayableScanner in automatic mode", + error + ); + assert_eq!( + panic_msg, &expected_msg, + "We expected '{}' but got '{}' for initial_pending_payable_scan = {}", + expected_msg, panic_msg, initial_pending_payable_scan + ) + }) + } + + let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ + StartScanError::NothingToProcess, + StartScanError::NoConsumingWalletFound, + ]); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_forbidden_states(&subject, &inputs, false); + test_forbidden_states(&subject, &inputs, true); + } + + #[test] + fn resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered() { + let test_name = + "resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered"; + let subject = ScanSchedulers::new(ScanIntervals::default(), false); + + test_what_if_externally_triggered( + test_name, + &subject, + PayableSequenceScanner::RetryPayables {}, + ); + } + + #[test] + fn any_automatic_scan_with_start_scan_error_is_fatal_for_retry_payables() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + ALL_START_SCAN_ERRORS.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::RetryPayables, + error, + false, + &Logger::new("test"), + ) + })) + .unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} should be impossible \ + with RetryPayableScanner in automatic mode", + error + ); + assert_eq!( + panic_msg, &expected_msg, + "We expected '{}' but got '{}'", + expected_msg, panic_msg, + ) + }) + } + + #[test] + fn resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered() { + let test_name = + "resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered"; + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_what_if_externally_triggered( + test_name, + &subject, + PayableSequenceScanner::NewPayables {}, + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: an automatic scan of NewPayableScanner \ + should never interfere with itself ScanAlreadyRunning { cross_scan_cause_opt: None, started_at:" + )] + fn resolve_hint_for_new_payables_if_scan_is_already_running_error_and_is_automatic_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::NewPayables, + &StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: SystemTime::now(), + }, + false, + &Logger::new("test"), + ); + } + + #[test] + fn resolve_new_payables_with_error_cases_resulting_in_future_rescheduling() { + let test_name = "resolve_new_payables_with_error_cases_resulting_in_future_rescheduling"; + let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ + StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: SystemTime::now(), + }, + ]); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + inputs.errors.iter().for_each(|error| { + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_on_error( + PayableSequenceScanner::NewPayables, + *error, + false, + &logger, + ); + + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got '{:?}'", + result, + ); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Automatic NewPayables scan failed - rescheduling strategy: \ + \"Schedule(Payables)\"", + )); + }) + } + + #[test] + fn conversion_between_hintable_scanner_and_scan_type_works() { + assert_eq!( + ScanType::from(PayableSequenceScanner::NewPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::RetryPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false + }), + ScanType::PendingPayables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true + }), + ScanType::PendingPayables + ); + } +} diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index ce2851e28..4d2bf16e1 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -16,6 +16,7 @@ pub mod payable_scanner_utils { use std::time::SystemTime; use thousands::Separable; use web3::types::H256; + use masq_lib::ui_gateway::NodeToUiMessage; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -26,6 +27,18 @@ pub mod payable_scanner_utils { RemotelyCausedErrors(Vec), } + #[derive(Debug, PartialEq)] + pub struct PayableScanResult { + pub ui_response_opt: Option, + pub result: OperationOutcome, + } + + #[derive(Debug, PartialEq)] + pub enum OperationOutcome { + NewPendingPayable, + Failure, + } + //debugging purposes only pub fn investigate_debt_extremes( timestamp: SystemTime, @@ -312,6 +325,7 @@ pub mod pending_payable_scanner_utils { use crate::accountant::PendingPayableId; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use masq_lib::logger::Logger; + use masq_lib::ui_gateway::NodeToUiMessage; use std::time::SystemTime; #[derive(Debug, Default, PartialEq, Eq, Clone)] @@ -321,6 +335,18 @@ pub mod pending_payable_scanner_utils { pub confirmed: Vec, } + impl PendingPayableScanReport { + pub fn requires_payments_retry(&self) -> bool { + todo!("complete my within GH-642") + } + } + + #[derive(Debug, PartialEq)] + pub enum PendingPayableScanResult { + NoPendingPayablesLeft(Option), + PaymentRetryRequired, + } + pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { timestamp .elapsed() @@ -843,4 +869,64 @@ mod tests { "Got 0 properly sent payables of an unknown number of attempts" ) } + + #[test] + fn requires_payments_retry_says_yes() { + todo!("complete this test with GH-604") + // let cases = vec![ + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // ]; + // + // cases.into_iter().enumerate().for_each(|(idx, case)| { + // let result = case.requires_payments_retry(); + // assert_eq!( + // result, true, + // "We expected true, but got false for case of idx {}", + // idx + // ) + // }) + } + + #[test] + fn requires_payments_retry_says_no() { + todo!("complete this test with GH-604") + // let report = PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }; + // + // let result = report.requires_payments_retry(); + // + // assert_eq!(result, false) + } } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index c43d6f71b..26aa15dc3 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,9 +2,489 @@ #![cfg(test)] -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use masq_lib::type_obfuscation::Obfuscated; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +}; +use crate::accountant::scanners::payable_scanner_extension::{ + MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, +}; +use crate::accountant::scanners::scan_schedulers::{ + NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, + ScanRescheduleAfterEarlyStop, +}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; +use crate::accountant::scanners::{ + PayableScanner, PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, + Scanner, StartScanError, StartableScanner, +}; +use crate::accountant::{ + ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, + SentPayables, +}; +use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use crate::sub_lib::wallet::Wallet; +use actix::{Message, System}; +use itertools::Either; +use masq_lib::logger::{Logger, TIME_FORMATTING_STRING}; +use masq_lib::ui_gateway::NodeToUiMessage; +use regex::Regex; +use std::any::type_name; +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use time::{format_description, PrimitiveDateTime}; -pub fn protect_payables_in_test(payables: Vec) -> Obfuscated { - Obfuscated::obfuscate_vector(payables) +pub struct NullScanner {} + +impl + PrivateScanner for NullScanner +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ +} + +impl StartableScanner for NullScanner +where + TriggerMessage: Message, + StartMessage: Message, +{ + fn start_scan( + &mut self, + _wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + Err(StartScanError::CalledFromNullScanner) + } +} + +impl Scanner for NullScanner +where + EndMessage: Message, +{ + fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> ScanResult { + panic!("Called finish_scan() from NullScanner"); + } + + fn scan_started_at(&self) -> Option { + None + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + panic!("Called mark_as_started() from NullScanner"); + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + panic!("Called mark_as_ended() from NullScanner"); + } + + as_any_ref_in_trait_impl!(); +} + +impl MultistageDualPayableScanner for NullScanner {} + +impl SolvencySensitivePaymentInstructor for NullScanner { + fn try_skipping_payment_adjustment( + &self, + _msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + intentionally_blank!() + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } +} + +impl Default for NullScanner { + fn default() -> Self { + Self::new() + } +} + +impl NullScanner { + pub fn new() -> Self { + Self {} + } +} + +pub struct ScannerMock { + start_scan_params: + Arc, Logger, String)>>>, + start_scan_results: RefCell>>, + finish_scan_params: Arc>>, + finish_scan_results: RefCell>, + scan_started_at_results: RefCell>>, + stop_system_after_last_message: RefCell, +} + +impl + PrivateScanner + for ScannerMock +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ +} + +impl + StartableScanner + for ScannerMock +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ + fn start_scan( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.start_scan_params.lock().unwrap().push(( + wallet.clone(), + timestamp, + response_skeleton_opt, + logger.clone(), + // This serves for identification in scanners allowing different modes to start + // them up through. + type_name::().to_string(), + )); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.start_scan_results.borrow_mut().remove(0) + } +} + +impl Scanner + for ScannerMock +where + StartMessage: Message, + EndMessage: Message, +{ + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult { + self.finish_scan_params + .lock() + .unwrap() + .push((message, logger.clone())); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.finish_scan_results.borrow_mut().remove(0) + } + + fn scan_started_at(&self) -> Option { + self.scan_started_at_results.borrow_mut().remove(0) + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + intentionally_blank!() + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + +impl Default + for ScannerMock +{ + fn default() -> Self { + Self::new() + } +} + +impl ScannerMock { + pub fn new() -> Self { + Self { + start_scan_params: Arc::new(Mutex::new(vec![])), + start_scan_results: RefCell::new(vec![]), + finish_scan_params: Arc::new(Mutex::new(vec![])), + finish_scan_results: RefCell::new(vec![]), + scan_started_at_results: RefCell::new(vec![]), + stop_system_after_last_message: RefCell::new(false), + } + } + + pub fn start_scan_params( + mut self, + params: &Arc, Logger, String)>>>, + ) -> Self { + self.start_scan_params = params.clone(); + self + } + + pub fn start_scan_result(self, result: Result) -> Self { + self.start_scan_results.borrow_mut().push(result); + self + } + + pub fn scan_started_at_result(self, result: Option) -> Self { + self.scan_started_at_results.borrow_mut().push(result); + self + } + + pub fn finish_scan_params(mut self, params: &Arc>>) -> Self { + self.finish_scan_params = params.clone(); + self + } + + pub fn finish_scan_result(self, result: ScanResult) -> Self { + self.finish_scan_results.borrow_mut().push(result); + self + } + + pub fn stop_the_system_after_last_msg(self) -> Self { + self.stop_system_after_last_message.replace(true); + self + } + + pub fn is_allowed_to_stop_the_system(&self) -> bool { + *self.stop_system_after_last_message.borrow() + } + + pub fn is_last_message(&self) -> bool { + self.is_last_message_from_start_scan() || self.is_last_message_from_end_scan() + } + + pub fn is_last_message_from_start_scan(&self) -> bool { + self.start_scan_results.borrow().len() == 1 && self.finish_scan_results.borrow().is_empty() + } + + pub fn is_last_message_from_end_scan(&self) -> bool { + self.finish_scan_results.borrow().len() == 1 && self.start_scan_results.borrow().is_empty() + } +} + +impl MultistageDualPayableScanner + for ScannerMock +{ +} + +impl SolvencySensitivePaymentInstructor + for ScannerMock +{ + fn try_skipping_payment_adjustment( + &self, + msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + // Always passes... + // It would be quite inconvenient if we had to add specialized features to the generic + // mock, plus this functionality can be tested better with the other components mocked, + // not the scanner itself. + Ok(Either::Left(OutboundPaymentsInstructions { + affordable_accounts: msg.qualified_payables, + agent: msg.agent, + response_skeleton_opt: msg.response_skeleton_opt, + })) + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } +} + +pub trait ScannerMockMarker {} + +impl ScannerMockMarker for ScannerMock {} + +#[derive(Default)] +pub struct NewPayableScanDynIntervalComputerMock { + compute_interval_params: Arc>>, + compute_interval_results: RefCell>>, +} + +impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerMock { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option { + self.compute_interval_params.lock().unwrap().push(( + now, + last_new_payable_scan_timestamp, + interval, + )); + self.compute_interval_results.borrow_mut().remove(0) + } +} + +impl NewPayableScanDynIntervalComputerMock { + pub fn compute_interval_params( + mut self, + params: &Arc>>, + ) -> Self { + self.compute_interval_params = params.clone(); + self + } + + pub fn compute_interval_result(self, result: Option) -> Self { + self.compute_interval_results.borrow_mut().push(result); + self + } +} + +pub enum ReplacementType +where + ScannerReal: RealScannerMarker, + ScannerMock: ScannerMockMarker, +{ + Real(ScannerReal), + Mock(ScannerMock), + Null, +} + +// The scanners are categorized by types because we want them to become an abstract object +// represented by a private trait. Of course, such an object cannot be constructed directly in +// the outer world; therefore, we have to provide specific objects that will cast accordingly +// under the hood. +pub enum ScannerReplacement { + Payable( + ReplacementType< + PayableScanner, + ScannerMock, + >, + ), + PendingPayable( + ReplacementType< + PendingPayableScanner, + ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + >, + ), + Receivable( + ReplacementType< + ReceivableScanner, + ScannerMock>, + >, + ), +} + +pub enum MarkScanner<'a> { + Ended(&'a Logger), + Started(SystemTime), +} + +// Cautious: Don't compare to another timestamp on a full match; this timestamp is trimmed in +// nanoseconds down to three digits +pub fn parse_system_time_from_str(examined_str: &str) -> Vec { + let regex = Regex::new(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})").unwrap(); + let captures = regex.captures_iter(examined_str); + captures + .map(|captures| { + let captured_str_timestamp = captures.get(0).unwrap().as_str(); + let format = format_description::parse(TIME_FORMATTING_STRING).unwrap(); + let dt = PrimitiveDateTime::parse(captured_str_timestamp, &format).unwrap(); + let duration = Duration::from_secs(dt.assume_utc().unix_timestamp() as u64) + + Duration::from_nanos(dt.nanosecond() as u64); + UNIX_EPOCH + duration + }) + .collect() +} + +fn trim_expected_timestamp_to_three_digits_nanos(value: SystemTime) -> SystemTime { + let duration = value.duration_since(UNIX_EPOCH).unwrap(); + let full_nanos = duration.subsec_nanos(); + let diffuser = 10_u32.pow(6); + let trimmed_nanos = (full_nanos / diffuser) * diffuser; + let duration = duration + .checked_sub(Duration::from_nanos(full_nanos as u64)) + .unwrap() + .checked_add(Duration::from_nanos(trimmed_nanos as u64)) + .unwrap(); + UNIX_EPOCH + duration +} + +pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec) { + let parsed_timestamps = parse_system_time_from_str(examined_str); + if parsed_timestamps.len() != expected_timestamps.len() { + panic!( + "You supplied {} expected timestamps, but the examined text contains only {}", + expected_timestamps.len(), + parsed_timestamps.len() + ) + } + let zipped = parsed_timestamps + .into_iter() + .zip(expected_timestamps.into_iter()); + zipped.for_each(|(parsed_timestamp, expected_timestamp)| { + let expected_timestamp_trimmed = + trim_expected_timestamp_to_three_digits_nanos(expected_timestamp); + assert_eq!( + parsed_timestamp, expected_timestamp_trimmed, + "We expected this timestamp {:?} in this fragment '{}' but found {:?}", + expected_timestamp_trimmed, examined_str, parsed_timestamp + ) + }) +} + +#[derive(Default)] +pub struct RescheduleScanOnErrorResolverMock { + resolve_rescheduling_on_error_params: + Arc>>, + resolve_rescheduling_on_error_results: RefCell>, +} + +impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop { + self.resolve_rescheduling_on_error_params + .lock() + .unwrap() + .push(( + scanner, + error.clone(), + is_externally_triggered, + logger.clone(), + )); + self.resolve_rescheduling_on_error_results + .borrow_mut() + .remove(0) + } +} + +impl RescheduleScanOnErrorResolverMock { + pub fn resolve_rescheduling_on_error_params( + mut self, + params: &Arc>>, + ) -> Self { + self.resolve_rescheduling_on_error_params = params.clone(); + self + } + pub fn resolve_rescheduling_on_error_result( + self, + result: ScanRescheduleAfterEarlyStop, + ) -> Self { + self.resolve_rescheduling_on_error_results + .borrow_mut() + .push(result); + self + } } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index e7186d2cd..e0e5a6cdd 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -16,20 +16,11 @@ use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, }; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, -}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{ - MultistagePayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, -}; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; -use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, PeriodicalScanScheduler, - ReceivableScanner, ScanSchedulers, Scanner, -}; -use crate::accountant::{ - gwei_to_wei, Accountant, ResponseSkeleton, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC, -}; +use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; @@ -41,17 +32,13 @@ use crate::db_config::mocks::ConfigDaoMock; use crate::sub_lib::accountant::{DaoFactories, FinancialStatistics}; use crate::sub_lib::accountant::{MessageIdGenerator, PaymentThresholds}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use crate::sub_lib::utils::NotifyLaterHandle; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::make_bc_with_defaults; -use actix::{Message, System}; +use actix::System; use ethereum_types::H256; -use itertools::Either; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; -use masq_lib::ui_gateway::NodeToUiMessage; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; @@ -59,7 +46,7 @@ use std::fmt::Debug; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime}; +use std::time::SystemTime; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_unix_timestamp(SystemTime::now()); @@ -1260,7 +1247,7 @@ pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { } } -pub fn make_payables( +pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, ) -> ( @@ -1510,208 +1497,3 @@ impl PaymentAdjusterMock { self } } - -macro_rules! formal_traits_for_payable_mid_scan_msg_handling { - ($scanner:ty) => { - impl MultistagePayableScanner for $scanner {} - - impl SolvencySensitivePaymentInstructor for $scanner { - fn try_skipping_payment_adjustment( - &self, - _msg: BlockchainAgentWithContextMessage, - _logger: &Logger, - ) -> Result, String> { - intentionally_blank!() - } - - fn perform_payment_adjustment( - &self, - _setup: PreparedAdjustment, - _logger: &Logger, - ) -> OutboundPaymentsInstructions { - intentionally_blank!() - } - } - }; -} - -pub struct NullScanner {} - -impl Scanner for NullScanner -where - BeginMessage: Message, - EndMessage: Message, -{ - fn begin_scan( - &mut self, - _wallet_opt: Wallet, - _timestamp: SystemTime, - _response_skeleton_opt: Option, - _logger: &Logger, - ) -> Result { - Err(BeginScanError::CalledFromNullScanner) - } - - fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> Option { - panic!("Called finish_scan() from NullScanner"); - } - - fn scan_started_at(&self) -> Option { - panic!("Called scan_started_at() from NullScanner"); - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - panic!("Called mark_as_started() from NullScanner"); - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - panic!("Called mark_as_ended() from NullScanner"); - } - - as_any_ref_in_trait_impl!(); -} - -formal_traits_for_payable_mid_scan_msg_handling!(NullScanner); - -impl Default for NullScanner { - fn default() -> Self { - Self::new() - } -} - -impl NullScanner { - pub fn new() -> Self { - Self {} - } -} - -pub struct ScannerMock { - begin_scan_params: Arc, Logger)>>>, - begin_scan_results: RefCell>>, - end_scan_params: Arc>>, - end_scan_results: RefCell>>, - stop_system_after_last_message: RefCell, -} - -impl Scanner - for ScannerMock -where - BeginMessage: Message, - EndMessage: Message, -{ - fn begin_scan( - &mut self, - wallet: Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.begin_scan_params.lock().unwrap().push(( - wallet, - timestamp, - response_skeleton_opt, - logger.clone(), - )); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.begin_scan_results.borrow_mut().remove(0) - } - - fn finish_scan(&mut self, message: EndMessage, _logger: &Logger) -> Option { - self.end_scan_params.lock().unwrap().push(message); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.end_scan_results.borrow_mut().remove(0) - } - - fn scan_started_at(&self) -> Option { - intentionally_blank!() - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - intentionally_blank!() - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - intentionally_blank!() - } -} - -impl Default for ScannerMock { - fn default() -> Self { - Self::new() - } -} - -impl ScannerMock { - pub fn new() -> Self { - Self { - begin_scan_params: Arc::new(Mutex::new(vec![])), - begin_scan_results: RefCell::new(vec![]), - end_scan_params: Arc::new(Mutex::new(vec![])), - end_scan_results: RefCell::new(vec![]), - stop_system_after_last_message: RefCell::new(false), - } - } - - pub fn begin_scan_params( - mut self, - params: &Arc, Logger)>>>, - ) -> Self { - self.begin_scan_params = params.clone(); - self - } - - pub fn begin_scan_result(self, result: Result) -> Self { - self.begin_scan_results.borrow_mut().push(result); - self - } - - pub fn stop_the_system_after_last_msg(self) -> Self { - self.stop_system_after_last_message.replace(true); - self - } - - pub fn is_allowed_to_stop_the_system(&self) -> bool { - *self.stop_system_after_last_message.borrow() - } - - pub fn is_last_message(&self) -> bool { - self.is_last_message_from_begin_scan() || self.is_last_message_from_end_scan() - } - - pub fn is_last_message_from_begin_scan(&self) -> bool { - self.begin_scan_results.borrow().len() == 1 && self.end_scan_results.borrow().is_empty() - } - - pub fn is_last_message_from_end_scan(&self) -> bool { - self.end_scan_results.borrow().len() == 1 && self.begin_scan_results.borrow().is_empty() - } -} - -formal_traits_for_payable_mid_scan_msg_handling!(ScannerMock); - -impl ScanSchedulers { - pub fn update_scheduler( - &mut self, - scan_type: ScanType, - handle_opt: Option>>, - interval_opt: Option, - ) { - let scheduler = self - .schedulers - .get_mut(&scan_type) - .unwrap() - .as_any_mut() - .downcast_mut::>() - .unwrap(); - if let Some(new_handle) = handle_opt { - scheduler.handle = new_handle - } - if let Some(new_interval) = interval_opt { - scheduler.interval = new_interval - } - } -} diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 8ab3426cf..9e15f9c5d 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -1166,7 +1166,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: Some(ScanIntervals::default()), - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1241,7 +1241,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1544,7 +1544,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1730,7 +1730,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 1a01087e1..b93d8770c 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::{ @@ -36,7 +36,6 @@ use futures::Future; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; @@ -45,8 +44,9 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use ethabi::Hash; use web3::types::H256; +use masq_lib::messages::ScanType; use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; @@ -267,7 +267,7 @@ impl BlockchainBridge { .map_err(|e| format!("Blockchain agent build error: {:?}", e)) .and_then(move |agent| { let outgoing_message = BlockchainAgentWithContextMessage::new( - incoming_message.protected_qualified_payables, + incoming_message.qualified_payables, agent, incoming_message.response_skeleton_opt, ); @@ -430,7 +430,7 @@ impl BlockchainBridge { .expect("Accountant is unbound"); let transaction_hashes = msg - .pending_payable + .pending_payable_fingerprints .iter() .map(|finger_print| finger_print.hash) .collect::>(); @@ -443,7 +443,7 @@ impl BlockchainBridge { let pairs = transaction_receipts_results .into_iter() - .zip(msg.pending_payable.into_iter()) + .zip(msg.pending_payable_fingerprints.into_iter()) .collect_vec(); accountant_recipient @@ -549,9 +549,8 @@ mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; @@ -566,14 +565,13 @@ mod tests { make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; use crate::db_config::persistent_configuration::PersistentConfigError; - use crate::match_every_type_id; + use crate::match_lazily_every_type_id; use crate::node_test_utils::check_timestamp; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{ make_accountant_subs_from_recorder, make_recorder, peer_actors_builder, }; - use crate::test_utils::recorder_stop_conditions::StopCondition; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::unshared_test_utils::{ @@ -583,7 +581,6 @@ mod tests { use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; - use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; @@ -724,9 +721,8 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(qualified_payables.clone()); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables.clone(), + qualified_payables: qualified_payables.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -746,7 +742,7 @@ mod tests { let blockchain_agent_with_context_msg_actual: &BlockchainAgentWithContextMessage = accountant_received_payment.get_record(0); assert_eq!( - blockchain_agent_with_context_msg_actual.protected_qualified_payables, + blockchain_agent_with_context_msg_actual.qualified_payables, qualified_payables ); assert_eq!( @@ -808,9 +804,8 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(vec![]); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables, + qualified_payables: vec![make_payable_account(123)], consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -858,7 +853,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(SentPayables)) + .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); let wallet_account = make_wallet("blah"); let consuming_wallet = make_paying_wallet(b"consuming_wallet"); @@ -949,7 +944,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(SentPayables)) + .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); let wallet_account = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); @@ -1149,7 +1144,7 @@ mod tests { #[test] fn blockchain_bridge_processes_requests_for_a_complete_and_null_transaction_receipt() { let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); let hash_1 = pending_payable_fingerprint_1.hash; let hash_2 = make_tx_hash(78989); @@ -1184,7 +1179,7 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payable_fingerprints: vec![ pending_payable_fingerprint_1.clone(), pending_payable_fingerprint_2.clone(), ], @@ -1239,7 +1234,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); let received_payments_subs: Recipient = accountant_addr.recipient(); @@ -1308,7 +1303,10 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ReportTransactionReceipts, ScanError)) + .system_stop_conditions(match_lazily_every_type_id!( + ReportTransactionReceipts, + ScanError + )) .start(); let report_transaction_receipt_recipient: Recipient = accountant_addr.clone().recipient(); @@ -1362,7 +1360,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payable_fingerprints: vec![ fingerprint_1.clone(), fingerprint_2.clone(), fingerprint_3.clone(), @@ -1406,7 +1404,7 @@ mod tests { init_test_logging(); let (accountant, _, accountant_recording) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); let report_transaction_recipient: Recipient = @@ -1441,7 +1439,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![fingerprint_1, fingerprint_2], + pending_payable_fingerprints: vec![fingerprint_1, fingerprint_2], response_skeleton_opt: None, }; let system = System::new("test"); @@ -1615,7 +1613,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = - accountant.system_stop_conditions(match_every_type_id!(ReceivedPayments)); + accountant.system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)); let some_wallet = make_wallet("somewallet"); let recipient_wallet = make_wallet("recipient_wallet"); let amount = 996000000; @@ -1708,7 +1706,7 @@ mod tests { let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = - accountant.system_stop_conditions(match_every_type_id!(ReceivedPayments)); + accountant.system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)); let earning_wallet = make_wallet("earning_wallet"); let amount = 996000000; let blockchain_interface = make_blockchain_interface_web3(port); @@ -1792,7 +1790,8 @@ mod tests { .ok_response(expected_response_logs, 1) .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant_addr = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant_addr = + accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let earning_wallet = make_wallet("earning_wallet"); let mut blockchain_interface = make_blockchain_interface_web3(port); blockchain_interface.logger = logger; @@ -1849,7 +1848,7 @@ mod tests { .err_response(-32005, "Blockheight too far in the past. Check params passed to eth_getLogs or eth_call requests.Range of blocks allowed for your plan: 1000", 0) .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let earning_wallet = make_wallet("earning_wallet"); let blockchain_interface = make_blockchain_interface_web3(port); let set_max_block_count_params_arc = Arc::new(Mutex::new(vec![])); @@ -2024,7 +2023,7 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); @@ -2075,7 +2074,7 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 92f8e9145..852b02a4e 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,7 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -430,7 +430,7 @@ impl BlockchainInterfaceWeb3 { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index ee2e49786..7172987f4 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -2,8 +2,8 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::BlockchainAgentWeb3; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::agent_web3::BlockchainAgentWeb3; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, @@ -332,7 +332,7 @@ mod tests { use super::*; use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::gwei_to_wei; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::test_utils::{ make_payable_account, make_payable_account_with_wallet_and_balance_and_timestamp_opt, }; diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index ef1e8d373..bdcbf6a91 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,7 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index 9e4e72698..fa5a61f01 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -335,7 +335,7 @@ pub struct BootstrapperConfig { pub log_level: LevelFilter, pub dns_servers: Vec, pub scan_intervals_opt: Option, - pub suppress_initial_scans: bool, + pub automatic_scans_enabled: bool, pub when_pending_too_long_sec: u64, pub crash_point: CrashPoint, pub clandestine_discriminator_factories: Vec>, @@ -371,7 +371,7 @@ impl BootstrapperConfig { log_level: LevelFilter::Off, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, crash_point: CrashPoint::None, clandestine_discriminator_factories: vec![], ui_gateway_config: UiGatewayConfig { @@ -415,7 +415,7 @@ impl BootstrapperConfig { self.consuming_wallet_opt = unprivileged.consuming_wallet_opt; self.db_password_opt = unprivileged.db_password_opt; self.scan_intervals_opt = unprivileged.scan_intervals_opt; - self.suppress_initial_scans = unprivileged.suppress_initial_scans; + self.automatic_scans_enabled = unprivileged.automatic_scans_enabled; self.payment_thresholds_opt = unprivileged.payment_thresholds_opt; self.when_pending_too_long_sec = unprivileged.when_pending_too_long_sec; } @@ -1250,7 +1250,7 @@ mod tests { unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); unprivileged_config.scan_intervals_opt = Some(ScanIntervals::default()); - unprivileged_config.suppress_initial_scans = false; + unprivileged_config.automatic_scans_enabled = true; unprivileged_config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; privileged_config.merge_unprivileged(unprivileged_config); @@ -1275,7 +1275,7 @@ mod tests { privileged_config.scan_intervals_opt, Some(ScanIntervals::default()) ); - assert_eq!(privileged_config.suppress_initial_scans, false); + assert_eq!(privileged_config.automatic_scans_enabled, true); assert_eq!( privileged_config.when_pending_too_long_sec, DEFAULT_PENDING_TOO_LONG_SEC diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 1ef47777f..fd051e3e1 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -3440,10 +3440,13 @@ mod tests { #[test] fn scan_intervals_computed_default_persistent_config_unequal_to_default() { let mut scan_intervals = *DEFAULT_SCAN_INTERVALS; - scan_intervals.pending_payable_scan_interval = scan_intervals - .pending_payable_scan_interval + scan_intervals.payable_scan_interval = scan_intervals + .payable_scan_interval .add(Duration::from_secs(15)); scan_intervals.pending_payable_scan_interval = scan_intervals + .pending_payable_scan_interval + .add(Duration::from_secs(20)); + scan_intervals.receivable_scan_interval = scan_intervals .receivable_scan_interval .sub(Duration::from_secs(33)); diff --git a/node/src/db_config/persistent_configuration.rs b/node/src/db_config/persistent_configuration.rs index 532048a34..ebc4efba6 100644 --- a/node/src/db_config/persistent_configuration.rs +++ b/node/src/db_config/persistent_configuration.rs @@ -1949,10 +1949,10 @@ mod tests { fn scan_intervals_get_method_works() { persistent_config_plain_data_assertions_for_simple_get_method!( "scan_intervals", - "40|60|50", + "60|5|50", ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(40), payable_scan_interval: Duration::from_secs(60), + pending_payable_scan_interval: Duration::from_secs(5), receivable_scan_interval: Duration::from_secs(50), } ); diff --git a/node/src/node_configurator/configurator.rs b/node/src/node_configurator/configurator.rs index 19b0b958a..b025abef9 100644 --- a/node/src/node_configurator/configurator.rs +++ b/node/src/node_configurator/configurator.rs @@ -652,8 +652,8 @@ impl Configurator { }, start_block_opt, scan_intervals: UiScanIntervals { - pending_payable_sec, payable_sec, + pending_payable_sec, receivable_sec, }, }; @@ -2591,8 +2591,8 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 122, payable_sec: 125, + pending_payable_sec: 122, receivable_sec: 128 } } @@ -2610,8 +2610,8 @@ mod tests { exit_service_rate: 13, })) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(122), payable_scan_interval: Duration::from_secs(125), + pending_payable_scan_interval: Duration::from_secs(122), receivable_scan_interval: Duration::from_secs(128), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -2722,8 +2722,8 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 122, payable_sec: 125, + pending_payable_sec: 122, receivable_sec: 128 } } @@ -2760,8 +2760,8 @@ mod tests { exit_service_rate: 0, })) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Default::default(), payable_scan_interval: Default::default(), + pending_payable_scan_interval: Default::default(), receivable_scan_interval: Default::default(), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -2815,8 +2815,8 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 0, payable_sec: 0, + pending_payable_sec: 0, receivable_sec: 0 } } diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 4238bd8d5..801aa4456 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -504,12 +504,13 @@ fn configure_accountant_config( |pc: &dyn PersistentConfiguration| pc.scan_intervals(), |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), )?; - let suppress_initial_scans = - value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == *"off"; + + let automatic_scans_enabled = + value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == "on"; config.payment_thresholds_opt = Some(payment_thresholds); config.scan_intervals_opt = Some(scan_intervals); - config.suppress_initial_scans = suppress_initial_scans; + config.automatic_scans_enabled = automatic_scans_enabled; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; Ok(()) } @@ -1818,7 +1819,7 @@ mod tests { "--ip", "1.2.3.4", "--scan-intervals", - "180|150|130", + "180|50|130", "--payment-thresholds", "100000|10000|1000|20000|1000|20000", ]; @@ -1827,8 +1828,8 @@ mod tests { let mut persistent_configuration = configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(100), payable_scan_interval: Duration::from_secs(101), + pending_payable_scan_interval: Duration::from_secs(33), receivable_scan_interval: Duration::from_secs(102), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -1855,8 +1856,8 @@ mod tests { .unwrap(); let expected_scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(50), receivable_scan_interval: Duration::from_secs(130), }; let expected_payment_thresholds = PaymentThresholds { @@ -1872,13 +1873,13 @@ mod tests { Some(expected_payment_thresholds) ); assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); - assert_eq!(config.suppress_initial_scans, false); + assert_eq!(config.automatic_scans_enabled, true); assert_eq!( config.when_pending_too_long_sec, DEFAULT_PENDING_TOO_LONG_SEC ); let set_scan_intervals_params = set_scan_intervals_params_arc.lock().unwrap(); - assert_eq!(*set_scan_intervals_params, vec!["180|150|130".to_string()]); + assert_eq!(*set_scan_intervals_params, vec!["180|50|130".to_string()]); let set_payment_thresholds_params = set_payment_thresholds_params_arc.lock().unwrap(); assert_eq!( *set_payment_thresholds_params, @@ -1894,7 +1895,7 @@ mod tests { "--ip", "1.2.3.4", "--scan-intervals", - "180|150|130", + "180|15|130", "--payment-thresholds", "100000|1000|1000|20000|1000|20000", ]; @@ -1903,8 +1904,8 @@ mod tests { let mut persistent_configuration = configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -1935,11 +1936,11 @@ mod tests { unban_below_gwei: 20000, }; let expected_scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), }; - let expected_suppress_initial_scans = false; + let expected_automatic_scans_enabled = true; let expected_when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; assert_eq!( config.payment_thresholds_opt, @@ -1947,8 +1948,8 @@ mod tests { ); assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); assert_eq!( - config.suppress_initial_scans, - expected_suppress_initial_scans + config.automatic_scans_enabled, + expected_automatic_scans_enabled ); assert_eq!( config.when_pending_too_long_sec, @@ -2578,7 +2579,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, true); + assert_eq!(bootstrapper_config.automatic_scans_enabled, false); } #[test] @@ -2599,7 +2600,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } #[test] @@ -2620,7 +2621,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } fn make_persistent_config( diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index 5894991c5..373688139 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -1353,7 +1353,7 @@ impl Hostname { #[cfg(test)] mod tests { use super::*; - use crate::match_every_type_id; + use crate::match_lazily_every_type_id; use crate::proxy_server::protocol_pack::ServerImpersonator; use crate::proxy_server::server_impersonator_http::ServerImpersonatorHttp; use crate::proxy_server::server_impersonator_tls::ServerImpersonatorTls; @@ -1380,7 +1380,7 @@ mod tests { use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::{ make_request_payload, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; @@ -2627,8 +2627,8 @@ mod tests { let cryptde = main_cryptde(); let http_request = b"GET /index.html HTTP/1.1\r\nHost: nowhere.com\r\n\r\n"; let (proxy_server_mock, _, proxy_server_recording_arc) = make_recorder(); - let proxy_server_mock = - proxy_server_mock.system_stop_conditions(match_every_type_id!(AddRouteResultMessage)); + let proxy_server_mock = proxy_server_mock + .system_stop_conditions(match_lazily_every_type_id!(AddRouteResultMessage)); let route_query_response = None; let (neighborhood_mock, _, _) = make_recorder(); let neighborhood_mock = @@ -5230,7 +5230,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!(RouteQueryMessage)) + .system_stop_conditions(match_lazily_every_type_id!(RouteQueryMessage)) .route_query_response(Some(route_query_response_expected.clone())); let cryptde = main_cryptde(); let mut subject = ProxyServer::new( @@ -5400,7 +5400,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( RouteQueryMessage, RouteQueryMessage, RouteQueryMessage diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 4b005f713..f1f174e6e 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -3,7 +3,7 @@ use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoFactory; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, SentPayables, @@ -38,8 +38,8 @@ lazy_static! { unban_below_gwei: 500_000_000, }; pub static ref DEFAULT_SCAN_INTERVALS: ScanIntervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(600), payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(60), receivable_scan_interval: Duration::from_secs(600) }; } @@ -231,8 +231,8 @@ mod tests { unban_below_gwei: 500_000_000, }; let scan_intervals_expected = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(600), payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(60), receivable_scan_interval: Duration::from_secs(600), }; assert_eq!(*DEFAULT_SCAN_INTERVALS, scan_intervals_expected); diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 2ec840cc9..84aaabe48 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::peer_actors::BindMessage; diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index f70f60f0f..53a3e8488 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -177,8 +177,8 @@ impl CombinedParams { ScanIntervals, &parsed_values, Duration::from_secs, - "pending_payable_scan_interval", "payable_scan_interval", + "pending_payable_scan_interval", "receivable_scan_interval" ))) } @@ -208,8 +208,8 @@ impl From<&CombinedParams> for &[(&str, CombinedParamsDataTypes)] { ("unban_below_gwei", U64), ], CombinedParams::ScanIntervals(Uninitialized) => &[ - ("pending_payable_scan_interval", U64), ("payable_scan_interval", U64), + ("pending_payable_scan_interval", U64), ("receivable_scan_interval", U64), ], _ => panic!( @@ -225,8 +225,8 @@ impl Display for ScanIntervals { write!( f, "{}|{}|{}", - self.pending_payable_scan_interval.as_secs(), self.payable_scan_interval.as_secs(), + self.pending_payable_scan_interval.as_secs(), self.receivable_scan_interval.as_secs() ) } @@ -400,8 +400,8 @@ mod tests { assert_eq!( scan_interval, &[ - ("pending_payable_scan_interval", U64), ("payable_scan_interval", U64), + ("pending_payable_scan_interval", U64), ("receivable_scan_interval", U64), ] ); @@ -550,15 +550,15 @@ mod tests { #[test] fn scan_intervals_from_combined_params() { - let scan_intervals_str = "110|115|113"; + let scan_intervals_str = "115|55|113"; let result = ScanIntervals::try_from(scan_intervals_str).unwrap(); assert_eq!( result, ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(110), payable_scan_interval: Duration::from_secs(115), + pending_payable_scan_interval: Duration::from_secs(55), receivable_scan_interval: Duration::from_secs(113) } ) @@ -567,14 +567,14 @@ mod tests { #[test] fn scan_intervals_to_combined_params() { let scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(60), - payable_scan_interval: Duration::from_secs(70), + payable_scan_interval: Duration::from_secs(90), + pending_payable_scan_interval: Duration::from_secs(40), receivable_scan_interval: Duration::from_secs(100), }; let result = scan_intervals.to_string(); - assert_eq!(result, "60|70|100".to_string()); + assert_eq!(result, "90|40|100".to_string()); } #[test] diff --git a/node/src/sub_lib/peer_actors.rs b/node/src/sub_lib/peer_actors.rs index 3a51be868..571eca3fe 100644 --- a/node/src/sub_lib/peer_actors.rs +++ b/node/src/sub_lib/peer_actors.rs @@ -14,6 +14,8 @@ use std::fmt::Debug; use std::fmt::Formatter; use std::net::IpAddr; +// TODO This file should be test only + #[derive(Clone, PartialEq, Eq)] pub struct PeerActors { pub proxy_server: ProxyServerSubs, diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index d68d721bb..5bd7a655a 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -222,6 +222,7 @@ impl NLSpawnHandleHolder for NLSpawnHandleHolderReal { } } +#[derive(Default)] pub struct NotifyHandleReal { phantom: PhantomData, } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 1bf32b4b5..b36199b75 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -12,6 +12,7 @@ pub mod logfile_name_guard; pub mod neighborhood_test_utils; pub mod persistent_configuration_mock; pub mod recorder; +pub mod recorder_counter_msgs; pub mod recorder_stop_conditions; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; @@ -539,7 +540,7 @@ pub mod unshared_test_utils { use crate::test_utils::neighborhood_test_utils::MIN_HOPS_FOR_TEST; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{make_recorder, Recorder, Recording}; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; use actix::{Actor, Addr, AsyncContext, Context, Handler, Recipient, System}; use actix::{Message, SpawnHandle}; @@ -682,7 +683,7 @@ pub mod unshared_test_utils { pub fn make_bc_with_defaults() -> BootstrapperConfig { let mut config = BootstrapperConfig::new(); config.scan_intervals_opt = Some(ScanIntervals::default()); - config.suppress_initial_scans = false; + config.automatic_scans_enabled = true; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; config.payment_thresholds_opt = Some(PaymentThresholds::default()); config @@ -698,9 +699,9 @@ pub mod unshared_test_utils { { let (recorder, _, recording_arc) = make_recorder(); let recorder = match stopping_message { - Some(type_id) => recorder.system_stop_conditions(StopConditions::All(vec![ - StopCondition::StopOnType(type_id), - ])), // No need to write stop message after this + Some(type_id) => recorder.system_stop_conditions(StopConditions::AllLazily(vec![ + MsgIdentification::ByType(type_id), + ])), // This will take care of stopping the system None => recorder, }; let addr = recorder.start(); @@ -871,17 +872,23 @@ pub mod unshared_test_utils { pub mod notify_handlers { use super::*; + use std::fmt::Debug; pub struct NotifyLaterHandleMock { notify_later_params: Arc>>, + stop_system_on_count_received_opt: RefCell>, send_message_out: bool, + // To prove that no msg was tried to be scheduled + panic_on_schedule_attempt: bool, } impl Default for NotifyLaterHandleMock { fn default() -> Self { Self { notify_later_params: Arc::new(Mutex::new(vec![])), + stop_system_on_count_received_opt: RefCell::new(None), send_message_out: false, + panic_on_schedule_attempt: false, } } } @@ -892,15 +899,30 @@ pub mod unshared_test_utils { self } + pub fn stop_system_on_count_received(self, count: usize) -> Self { + if count == 0 { + panic!("Should be a none-zero value") + } + let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + system_killer.start(); + self.stop_system_on_count_received_opt.replace(Some(count)); + self + } + pub fn capture_msg_and_let_it_fly_on(mut self) -> Self { self.send_message_out = true; self } + + pub fn panic_on_schedule_attempt(mut self) -> Self { + self.panic_on_schedule_attempt = true; + self + } } impl NotifyLaterHandle for NotifyLaterHandleMock where - M: Message + 'static + Clone, + M: Message + Clone + Debug + Send + 'static, A: Actor> + Handler, { fn notify_later<'a>( @@ -909,10 +931,26 @@ pub mod unshared_test_utils { interval: Duration, ctx: &'a mut Context, ) -> Box { + if self.panic_on_schedule_attempt { + panic!( + "Message scheduling request for {:?} and interval {}ms, thought not \ + expected", + msg, + interval.as_millis() + ); + } self.notify_later_params .lock() .unwrap() .push((msg.clone(), interval)); + if let Some(remaining) = + self.stop_system_on_count_received_opt.borrow_mut().as_mut() + { + *remaining -= 1; + if remaining == &0 { + System::current().stop(); + } + } if self.send_message_out { let handle = ctx.notify_later(msg, interval); Box::new(NLSpawnHandleHolderReal::new(handle)) @@ -933,6 +971,8 @@ pub mod unshared_test_utils { pub struct NotifyHandleMock { notify_params: Arc>>, send_message_out: bool, + stop_system_on_count_received_opt: RefCell>, + panic_on_schedule_attempt: bool, } impl Default for NotifyHandleMock { @@ -940,6 +980,8 @@ pub mod unshared_test_utils { Self { notify_params: Arc::new(Mutex::new(vec![])), send_message_out: false, + stop_system_on_count_received_opt: RefCell::new(None), + panic_on_schedule_attempt: false, } } } @@ -950,19 +992,50 @@ pub mod unshared_test_utils { self } - pub fn permit_to_send_out(mut self) -> Self { + pub fn capture_msg_and_let_it_fly_on(mut self) -> Self { self.send_message_out = true; self } + + pub fn stop_system_on_count_received(self, msg_count: usize) -> Self { + if msg_count == 0 { + panic!("Should be a non-zero value") + } + let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + system_killer.start(); + self.stop_system_on_count_received_opt + .replace(Some(msg_count)); + self + } + + pub fn panic_on_schedule_attempt(mut self) -> Self { + self.panic_on_schedule_attempt = true; + self + } } impl NotifyHandle for NotifyHandleMock where - M: Message + 'static + Clone, + M: Message + Debug + Clone + 'static, A: Actor> + Handler, { fn notify<'a>(&'a self, msg: M, ctx: &'a mut Context) { + if self.panic_on_schedule_attempt { + panic!( + "Message scheduling request for {:?}, thought not expected", + msg + ) + } self.notify_params.lock().unwrap().push(msg.clone()); + if let Some(remaining) = + self.stop_system_on_count_received_opt.borrow_mut().as_mut() + { + *remaining -= 1; + if remaining == &0 { + System::current().stop(); + return; + } + } if self.send_message_out { ctx.notify(msg) } @@ -984,7 +1057,7 @@ pub mod unshared_test_utils { // you've pasted in before at the other end. // 3) Using raw pointers to link the real memory address to your objects does not lead to good // results in all cases (It was found confusing and hard to be done correctly or even impossible - // to implement especially for references pointing to a dereferenced Box that was originally + // to implement, especially for references pointing to a dereferenced Box that was originally // supplied as an owned argument into the testing environment at the beginning, or we can // suspect the memory link already broken because of moves of the owned boxed instance // around the subjected code) diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index f66125182..6633ee948 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -1,13 +1,13 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; -use crate::accountant::ReportTransactionReceipts; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::{ - ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForPayables, - ScanForPendingPayables, ScanForReceivables, SentPayables, + ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, + ScanForReceivables, SentPayables, }; +use crate::accountant::{ReportTransactionReceipts, ScanForPendingPayables, ScanForRetryPayables}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::daemon::crash_notification::CrashNotification; @@ -47,8 +47,11 @@ use crate::sub_lib::stream_handler_pool::DispatcherNodeQueryResponse; use crate::sub_lib::stream_handler_pool::TransmitDataMsg; use crate::sub_lib::ui_gateway::UiGatewaySubs; use crate::sub_lib::utils::MessageScheduler; +use crate::test_utils::recorder_counter_msgs::{ + CounterMessages, CounterMsgGear, SingleTypeCounterMsgSetup, +}; use crate::test_utils::recorder_stop_conditions::{ - ForcedMatchable, PretendedMatchableWrapper, StopCondition, StopConditions, + ForcedMatchable, MsgIdentification, PretendedMatchableWrapper, StopConditions, }; use crate::test_utils::to_millis; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; @@ -59,7 +62,7 @@ use actix::MessageResult; use actix::System; use actix::{Actor, Message}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; -use std::any::{Any, TypeId}; +use std::any::{type_name, Any, TypeId}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; @@ -70,6 +73,7 @@ pub struct Recorder { recording: Arc>, node_query_responses: Vec>, route_query_responses: Vec>, + counter_msgs_opt: Option, stop_conditions_opt: Option, } @@ -101,7 +105,7 @@ macro_rules! message_handler_common { macro_rules! matchable { ($message_type: ty) => { impl ForcedMatchable<$message_type> for $message_type { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::<$message_type>() } } @@ -162,7 +166,8 @@ recorder_message_handler_t_m_p!(ReportTransactionReceipts); recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); -recorder_message_handler_t_m_p!(ScanForPayables); +recorder_message_handler_t_m_p!(ScanForNewPayables); +recorder_message_handler_t_m_p!(ScanForRetryPayables); recorder_message_handler_t_m_p!(ScanForPendingPayables); recorder_message_handler_t_m_p!(ScanForReceivables); recorder_message_handler_t_m_p!(SentPayables); @@ -187,7 +192,7 @@ where OuterM: PartialEq + 'static, InnerM: PartialEq + Send + Message, { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::() } } @@ -210,6 +215,16 @@ impl Handler for Recorder { matchable!(RouteQueryMessage); +impl Handler for Recorder { + type Result = (); + + fn handle(&mut self, msg: SetUpCounterMsgs, _ctx: &mut Self::Context) -> Self::Result { + msg.setups + .into_iter() + .for_each(|msg_setup| self.add_counter_msg(msg_setup)) + } +} + fn extract_response(responses: &mut Vec, err_msg: &str) -> T where T: Clone, @@ -261,11 +276,21 @@ impl Recorder { self.start_system_killer(); self.stop_conditions_opt = Some(stop_conditions) } else { - panic!("Stop conditions must be set by a single method call. Consider to use StopConditions::All") + panic!("Stop conditions must be set by a single method call. Consider using StopConditions::All") }; self } + fn add_counter_msg(&mut self, counter_msg_setup: SingleTypeCounterMsgSetup) { + if let Some(counter_msgs) = self.counter_msgs_opt.as_mut() { + counter_msgs.add_msg(counter_msg_setup) + } else { + let mut counter_msgs = CounterMessages::default(); + counter_msgs.add_msg(counter_msg_setup); + self.counter_msgs_opt = Some(counter_msgs) + } + } + fn start_system_killer(&mut self) { let system_killer = SystemKillerActor::new(Duration::from_secs(15)); system_killer.start(); @@ -275,7 +300,9 @@ impl Recorder { where M: 'static + ForcedMatchable + Send, { - let kill_system = if let Some(stop_conditions) = &mut self.stop_conditions_opt { + let counter_msg_opt = self.check_on_counter_msg(&msg); + + let stop_system = if let Some(stop_conditions) = &mut self.stop_conditions_opt { stop_conditions.resolve_stop_conditions::(&msg) } else { false @@ -283,7 +310,11 @@ impl Recorder { self.record(msg); - if kill_system { + if let Some(sendable_msgs) = counter_msg_opt { + sendable_msgs.into_iter().for_each(|msg| msg.try_send()) + } + + if stop_system { System::current().stop() } } @@ -295,6 +326,17 @@ impl Recorder { { self.handle_msg_t_m_p(PretendedMatchableWrapper(msg)) } + + fn check_on_counter_msg(&mut self, msg: &M) -> Option>> + where + M: ForcedMatchable + 'static, + { + if let Some(counter_msgs) = self.counter_msgs_opt.as_mut() { + counter_msgs.search_for_msg_gear(msg) + } else { + None + } + } } impl Recording { @@ -344,14 +386,15 @@ impl Recording { match item_box.downcast_ref::() { Some(item) => Ok(item), None => { - // double-checking for an uncommon, yet possible other type of an actor message, which doesn't implement PartialEq + // double-checking for an uncommon, yet possible other type of actor message, which doesn't implement PartialEq let item_opt = item_box.downcast_ref::>(); match item_opt { Some(item) => Ok(&item.0), None => Err(format!( - "Message {:?} could not be downcast to the expected type", - item_box + "Message {:?} could not be downcast to the expected type {}.", + item_box, + type_name::() )), } } @@ -385,6 +428,27 @@ impl RecordAwaiter { } } +#[derive(Message)] +pub struct SetUpCounterMsgs { + // Trigger msg - it arrives at the Recorder from the Actor being tested and matches one of the + // msg ID methods. + // Counter msg - it is sent back from the Recorder when a trigger msg is recognized + // + // In general, the triggering is data driven. Shuffling with the setups of differently typed + // trigger messages can't have any adverse effect. + // + // However, setups of the same trigger message types compose clusters. + // Keep in mind these are tested over their ID method sequentially, according to the order + // in which they are fed into this vector, with the other messages ignored. + setups: Vec, +} + +impl SetUpCounterMsgs { + pub fn new(setups: Vec) -> Self { + Self { setups } + } +} + pub fn make_recorder() -> (Recorder, RecordAwaiter, Arc>) { let recorder = Recorder::new(); let awaiter = recorder.get_awaiter(); @@ -576,8 +640,9 @@ impl PeerActorsBuilder { self } - // This must be called after System.new and before System.run - pub fn build(self) -> PeerActors { + // This must be called after System.new and before System.run. + // These addresses may be helpful for setting up the Counter Messages. + pub fn build_and_provide_addresses(self) -> (PeerActors, PeerActorAddrs) { let proxy_server_addr = self.proxy_server.start(); let dispatcher_addr = self.dispatcher.start(); let hopper_addr = self.hopper.start(); @@ -588,27 +653,73 @@ impl PeerActorsBuilder { let blockchain_bridge_addr = self.blockchain_bridge.start(); let configurator_addr = self.configurator.start(); - PeerActors { - proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), - dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), - hopper: make_hopper_subs_from_recorder(&hopper_addr), - proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), - neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), - accountant: make_accountant_subs_from_recorder(&accountant_addr), - ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), - blockchain_bridge: make_blockchain_bridge_subs_from_recorder(&blockchain_bridge_addr), - configurator: make_configurator_subs_from_recorder(&configurator_addr), - } + ( + PeerActors { + proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), + dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), + hopper: make_hopper_subs_from_recorder(&hopper_addr), + proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), + neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), + accountant: make_accountant_subs_from_recorder(&accountant_addr), + ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), + blockchain_bridge: make_blockchain_bridge_subs_from_recorder( + &blockchain_bridge_addr, + ), + configurator: make_configurator_subs_from_recorder(&configurator_addr), + }, + PeerActorAddrs { + proxy_server_addr, + dispatcher_addr, + hopper_addr, + proxy_client_addr, + neighborhood_addr, + accountant_addr, + ui_gateway_addr, + blockchain_bridge_addr, + configurator_addr, + }, + ) + } + + // This must be called after System.new and before System.run + pub fn build(self) -> PeerActors { + let (peer_actors, _) = self.build_and_provide_addresses(); + peer_actors } } +pub struct PeerActorAddrs { + pub proxy_server_addr: Addr, + pub dispatcher_addr: Addr, + pub hopper_addr: Addr, + pub proxy_client_addr: Addr, + pub neighborhood_addr: Addr, + pub accountant_addr: Addr, + pub ui_gateway_addr: Addr, + pub blockchain_bridge_addr: Addr, + pub configurator_addr: Addr, +} + #[cfg(test)] mod tests { use super::*; - use crate::match_every_type_id; + use crate::blockchain::blockchain_bridge::BlockchainBridge; + use crate::sub_lib::neighborhood::{ConfigChange, Hops, WalletPair}; + use crate::test_utils::make_wallet; + use crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient; + use crate::{ + match_lazily_every_type_id, setup_for_counter_msg_triggered_via_specific_msg_id_method, + setup_for_counter_msg_triggered_via_type_id, + }; use actix::Message; use actix::System; + use masq_lib::messages::{ + SerializableLogLevel, ToMessageBody, UiLogBroadcast, UiUnmarshalError, + }; + use masq_lib::ui_gateway::MessageTarget; use std::any::TypeId; + use std::net::{IpAddr, Ipv4Addr}; + use std::vec; #[derive(Debug, PartialEq, Eq, Message)] struct FirstMessageType { @@ -669,7 +780,7 @@ mod tests { fn recorder_can_be_stopped_on_a_particular_message() { let system = System::new("recorder_can_be_stopped_on_a_particular_message"); let recorder = - Recorder::new().system_stop_conditions(match_every_type_id!(FirstMessageType)); + Recorder::new().system_stop_conditions(match_lazily_every_type_id!(FirstMessageType)); let recording_arc = recorder.get_recording(); let rec_addr: Addr = recorder.start(); @@ -705,4 +816,324 @@ mod tests { TypeId::of::>() ) } + + #[test] + fn counter_msgs_with_diff_id_methods_are_used_together_and_one_was_not_triggered() { + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( + ScanForReceivables, + NodeToUiMessage + )); + let respondent_addr = respondent.start(); + // Case 1 + // This msg will trigger as the recorder will detect the arrival of StartMessage (no more + // requirement). + let (trigger_message_1, cm_setup_1) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + // Taking an opportunity to test a setup via the macro for the simplest identification, + // by the TypeId. + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) + }; + // Case two + // This msg will not trigger as it is declared with a wrong TypeId of the supposed trigger + // msg. The supplied ID does not even belong to an Actor msg type. + let cm_setup_2 = { + let counter_msg_strayed = StartMessage {}; + let screwed_id = TypeId::of::(); + let id_method = MsgIdentification::ByType(screwed_id); + SingleTypeCounterMsgSetup::new( + screwed_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg_strayed, + respondent_addr.clone().recipient(), + ))], + ) + }; + // Case three + // This msg will not trigger as it is declared to have to be matched entirely (The message + // type, plus the data of the message). The expected msg and the actual sent msg bear + // different IP addresses. + let (trigger_msg_3_unmatching, cm_setup_3) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), + }; + let type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(4), + body: UiUnmarshalError { + message: "abc".to_string(), + bad_data: "456".to_string(), + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 6, 5, 4)), + }), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg, + respondent_addr.clone().recipient(), + ))], + ), + ) + }; + // Case four + // This msg will trigger as the performed msg is an exact match of the expected msg. + let (trigger_msg_4_matching, cm_setup_4, counter_msg_4) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + }; + let msg_type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(234), + body: UiLogBroadcast { + msg: "Good one".to_string(), + log_level: SerializableLogLevel::Error, + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(trigger_msg.clone()), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + msg_type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg.clone(), + respondent_addr.clone().recipient(), + ))], + ), + counter_msg, + ) + }; + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + // Supplying messages deliberately in a tangled manner to express that the mechanism is + // robust enough to compensate for it. + // This works because we don't supply overlapping setups, such as that could apply to + // a single trigger msg. + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![cm_setup_3, cm_setup_1, cm_setup_2, cm_setup_4], + }) + .unwrap(); + + subject_addr.try_send(trigger_message_1).unwrap(); + subject_addr + .try_send(trigger_msg_3_unmatching.clone()) + .unwrap(); + subject_addr + .try_send(trigger_msg_4_matching.clone()) + .unwrap(); + + system.run(); + // Actual counter-messages that flew in this test + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let _first_counter_msg_recorded = respondent_recording.get_record::(0); + let second_counter_msg_recorded = respondent_recording.get_record::(1); + assert_eq!(second_counter_msg_recorded, &counter_msg_4); + assert_eq!(respondent_recording.len(), 2); + // Recorded trigger messages + let subject_recording = subject_recording_arc.lock().unwrap(); + let _first_recorded_trigger_msg = subject_recording.get_record::(0); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_3_unmatching); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_4_matching); + assert_eq!(subject_recording.len(), 3) + } + + #[test] + fn counter_msgs_evaluate_lazily_so_the_msgs_with_the_same_triggers_are_eliminated_sequentially() + { + // This test demonstrates the need for caution in setups where multiple messages are sent + // at different times and should be responded to by different counter-messages. However, + // the trigger methods of these setups also apply to each other. Which setup gets + // triggered depends purely on the order used to supply them to the recorder + // in SetUpCounterMsgs. + + // Notice that three of the messages share the same data type, with one additional message + // serving a special purpose in assertions. Two of the three use only TypeId for + // identification. This already requires greater caution since you probably need the three + // messages to be dispatched in a specific sequence. However, this wasn't considered + // properly and, as you can see in the test, the trigger messages aren't sent in the same + // order as the counter-message setups were supplied. + + // This results in an inevitable mismatch. The first counter-message that was sent should + // have belonged to the second trigger message, but was triggered by the third trigger + // message (which actually introduces the test). Similarly, the second trigger message + // activates a message rightfully meant for the first trigger message. To complete + // the picture, even the first trigger message is matched with the third counter-message. + + // This shows how important it is to avoid ambiguous setups. When operating with multiple + // calls of the same typed message as triggers, it is highly recommended not to use + // MsgIdentification::ByTypeId but to use more specific, unmistakable settings instead: + // MsgIdentification::ByMatch or MsgIdentification::ByPredicate. + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( + ConfigChangeMsg, + ConfigChangeMsg, + ConfigChangeMsg + )); + let respondent_addr = respondent.start(); + // Case 1 + let (trigger_msg_1, cm_setup_1) = { + let trigger_msg = CrashNotification { + process_id: 7777777, + exit_code: None, + stderr: Some("blah".to_string()), + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateMinHops(Hops::SixHops), + }; + let id_method = MsgIdentification::ByPredicate { + predicate: Box::new(|msg_boxed| { + let msg = msg_boxed.downcast_ref::().unwrap(); + msg.process_id == 1010 + }), + }; + ( + trigger_msg, + // Taking an opportunity to test a setup via the macro allowing more specific + // identification methods. + setup_for_counter_msg_triggered_via_specific_msg_id_method!( + CrashNotification, + id_method, + counter_msg, + &respondent_addr + ), + ) + }; + // Case two + let (trigger_msg_2, cm_setup_2) = { + let trigger_msg = CrashNotification { + process_id: 1010, + exit_code: Some(11), + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdatePassword("betterPassword".to_string()), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) + }; + // Case three + let (trigger_msg_3, cm_setup_3) = { + let trigger_msg = CrashNotification { + process_id: 9999999, + exit_code: None, + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def"), + }), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) + }; + // Case four + let (trigger_msg_4, cm_setup_4) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) + }; + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + // Adding messages in standard order + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![cm_setup_1, cm_setup_2, cm_setup_3, cm_setup_4], + }) + .unwrap(); + + // Now the fun begins, the trigger messages are shuffled + subject_addr.try_send(trigger_msg_3.clone()).unwrap(); + // The fourth message demonstrates that the previous trigger didn't activate two messages + // at once, even though this trigger actually matches two different setups. This shows + // that each trigger can only be matched with one setup at a time, consuming it. If you + // want to trigger multiple messages in response, you must configure that setup with + // multiple counter-messages (a one-to-many scenario). + subject_addr.try_send(trigger_msg_4.clone()).unwrap(); + subject_addr.try_send(trigger_msg_2.clone()).unwrap(); + subject_addr.try_send(trigger_msg_1.clone()).unwrap(); + + system.run(); + // Actual counter-messages that flew in this test + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let first_counter_msg_recorded = respondent_recording.get_record::(0); + assert_eq!( + first_counter_msg_recorded.change, + ConfigChange::UpdatePassword("betterPassword".to_string()) + ); + let _ = respondent_recording.get_record::(1); + let third_counter_msg_recorded = respondent_recording.get_record::(2); + assert_eq!( + third_counter_msg_recorded.change, + ConfigChange::UpdateMinHops(Hops::SixHops) + ); + let fourth_counter_msg_recorded = respondent_recording.get_record::(3); + assert_eq!( + fourth_counter_msg_recorded.change, + ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def") + }) + ); + assert_eq!(respondent_recording.len(), 4); + // Recorded trigger messages + let subject_recording = subject_recording_arc.lock().unwrap(); + let first_recorded_trigger_msg = subject_recording.get_record::(0); + assert_eq!(first_recorded_trigger_msg, &trigger_msg_3); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_4); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_2); + let fourth_recorded_trigger_msg = subject_recording.get_record::(3); + assert_eq!(fourth_recorded_trigger_msg, &trigger_msg_1); + assert_eq!(subject_recording.len(), 4) + } } diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs new file mode 100644 index 000000000..9aa856fc2 --- /dev/null +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -0,0 +1,172 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use crate::test_utils::recorder_stop_conditions::{ForcedMatchable, MsgIdentification}; +use actix::{Message, Recipient}; +use std::any::TypeId; +use std::cell::RefCell; +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +// Counter-messages are a powerful tool that allows you to actively simulate communication within +// a system. They enable sending either a single message or multiple messages in response to +// a specific trigger, which is just another Actor message arriving at the Recorder. +// By trigger, we mean the moment when an incoming message is tested sequentially against collected +// identification methods and matches. Each counter-message must have its ID method attached when +// it is being prepared for storage in the Recorder. This bundle is called a setup. Each setup has +// one ID method but can contain multiple counter-messages that are all sent when triggered. + +// Counter-messages can be independently customized and targeted at different actors by +// providing their addresses, supporting complex interaction patterns. This design facilitates +// sophisticated testing scenarios by mimicking real communication flows between multiple Actors. +// The actual preparation of the Recorder needs to be carried out somewhat specifically during the +// late stage of configuring the test, when all participating Actors are already started and their +// addresses are known. The setup for counter-messages must be registered with the appropriate +// Recorder using a specially designated Actor message SetUpCounterMsgs. + +// If a trigger message matches multiple counter-message setups, the triggered setup depends +// on the order in which setups are provided. Consider using MsgIdentification::ByMatch +// or MsgIdentification::ByPredicate instead of MsgIdentification::ByTypeId to avoid confusion +// about setup ordering. + +pub trait CounterMsgGear: Send { + fn try_send(&self); +} + +pub struct SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + msg_opt: RefCell>, + recipient: Recipient, +} + +impl CounterMsgGear for SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + fn try_send(&self) { + let msg = self.msg_opt.take().unwrap(); + self.recipient.try_send(msg).unwrap() + } +} + +impl SendableCounterMsgWithRecipient +where + Msg: Message + Send + 'static, + Msg::Result: Send, +{ + pub fn new(msg: Msg, recipient: Recipient) -> SendableCounterMsgWithRecipient { + Self { + msg_opt: RefCell::new(Some(msg)), + recipient, + } + } +} + +pub struct SingleTypeCounterMsgSetup { + // Leave them private + trigger_msg_type_id: TriggerMsgTypeId, + trigger_msg_id_method: MsgIdentification, + // Responding by multiple outbound messages to a single incoming (trigger) message is supported. + // (Imitates a message handler whose execution implies a couple of message dispatches) + msg_gears: Vec>, +} + +impl SingleTypeCounterMsgSetup { + pub fn new( + trigger_msg_type_id: TriggerMsgTypeId, + trigger_msg_id_method: MsgIdentification, + msg_gears: Vec>, + ) -> Self { + Self { + trigger_msg_type_id, + trigger_msg_id_method, + msg_gears, + } + } +} + +pub type TriggerMsgTypeId = TypeId; + +#[derive(Default)] +pub struct CounterMessages { + msgs: HashMap>, +} + +impl CounterMessages { + pub fn search_for_msg_gear( + &mut self, + trigger_msg: &Msg, + ) -> Option>> + where + Msg: ForcedMatchable + 'static, + { + let type_id = trigger_msg.trigger_msg_type_id(); + if let Some(msgs_vec) = self.msgs.get_mut(&type_id) { + msgs_vec + .iter_mut() + .position(|cm_setup| { + cm_setup + .trigger_msg_id_method + .resolve_condition(trigger_msg) + }) + .map(|idx| msgs_vec.remove(idx).msg_gears) + } else { + None + } + } + + pub fn add_msg(&mut self, counter_msg_setup: SingleTypeCounterMsgSetup) { + let type_id = counter_msg_setup.trigger_msg_type_id; + match self.msgs.entry(type_id) { + Entry::Occupied(mut existing_vec) => existing_vec.get_mut().push(counter_msg_setup), + Entry::Vacant(vacancy) => { + vacancy.insert(vec![counter_msg_setup]); + } + } + } +} + +// Note that you're not limited to triggering only one message at a time, but you can supply more +// messages to this macro, all triggered by the same type id. +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_type_id{ + ($trigger_msg_type: ty, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + + crate::setup_for_counter_msg_triggered_via_specific_msg_id_method!( + $trigger_msg_type, + MsgIdentification::ByType(TypeId::of::<$trigger_msg_type>()), + $($owned_counter_msg, $respondent_actor_addr_ref),+ + ) + }; +} + +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_specific_msg_id_method{ + ($trigger_msg_type: ty, $msg_id_method: expr, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + // This macro returns a block of operations. That's why it begins with these curly brackets + { + let msg_gears: Vec< + Box + > = vec![ + // This part can be repeated as long as there are more expression pairs suplied + $(Box::new( + crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient::new( + $owned_counter_msg, + $respondent_actor_addr_ref.clone().recipient() + ) + )),+ + ]; + + SingleTypeCounterMsgSetup::new( + TypeId::of::<$trigger_msg_type>(), + $msg_id_method, + msg_gears + ) + } + }; +} diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index b3dca287d..9a3214eea 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] @@ -6,16 +6,21 @@ use itertools::Itertools; use std::any::{Any, TypeId}; pub enum StopConditions { - Any(Vec), - All(Vec), + Any(Vec), + // Single message can eliminate _multiple_ ID Methods (previously stop conditions) by matching + // on them. + AllGreedily(Vec), + // Single message can eliminate _only one_ ID Method (previously stop conditions) by matching + // on them. To remove others, a new message must be received. + AllLazily(Vec), } -pub enum StopCondition { - StopOnType(TypeId), - StopOnMatch { +pub enum MsgIdentification { + ByType(TypeId), + ByMatch { exemplar: BoxedMsgExpected, }, - StopOnPredicate { + ByPredicate { predicate: Box bool + Send>, }, } @@ -24,43 +29,48 @@ pub type BoxedMsgExpected = Box; pub type RefMsgExpected<'a> = &'a (dyn Any + Send); impl StopConditions { - pub fn resolve_stop_conditions + Send + 'static>( + pub fn resolve_stop_conditions + Send + 'static>( &mut self, - msg: &T, + msg: &Msg, ) -> bool { match self { - StopConditions::Any(conditions) => Self::resolve_any::(conditions, msg), - StopConditions::All(conditions) => Self::resolve_all::(conditions, msg), + StopConditions::Any(conditions) => Self::resolve_any::(conditions, msg), + StopConditions::AllGreedily(conditions) => { + Self::resolve_all_greedily::(conditions, msg) + } + StopConditions::AllLazily(conditions) => { + Self::resolve_all_lazily::(conditions, msg) + } } } - fn resolve_any + Send + 'static>( - conditions: &Vec, - msg: &T, + fn resolve_any + Send + 'static>( + conditions: &Vec, + msg: &Msg, ) -> bool { conditions .iter() - .any(|condition| condition.resolve_condition::(msg)) + .any(|condition| condition.resolve_condition::(msg)) } - fn resolve_all + Send + 'static>( - conditions: &mut Vec, - msg: &T, + fn resolve_all_greedily + Send + 'static>( + conditions: &mut Vec, + msg: &Msg, ) -> bool { let indexes_to_remove = Self::indexes_of_matched_conditions(conditions, msg); Self::remove_matched_conditions(conditions, indexes_to_remove); conditions.is_empty() } - fn indexes_of_matched_conditions + Send + 'static>( - conditions: &[StopCondition], - msg: &T, + fn indexes_of_matched_conditions + Send + 'static>( + conditions: &[MsgIdentification], + msg: &Msg, ) -> Vec { conditions .iter() .enumerate() .fold(vec![], |mut acc, (idx, condition)| { - let matches = condition.resolve_condition::(msg); + let matches = condition.resolve_condition::(msg); if matches { acc.push(idx) } @@ -68,8 +78,21 @@ impl StopConditions { }) } + fn resolve_all_lazily + Send + 'static>( + conditions: &mut Vec, + msg: &Msg, + ) -> bool { + if let Some(idx) = conditions + .iter() + .position(|condition| condition.resolve_condition::(msg)) + { + conditions.remove(idx); + } + conditions.is_empty() + } + fn remove_matched_conditions( - conditions: &mut Vec, + conditions: &mut Vec, indexes_to_remove: Vec, ) { if !indexes_to_remove.is_empty() { @@ -84,44 +107,42 @@ impl StopConditions { } } -impl StopCondition { - fn resolve_condition + Send + 'static>(&self, msg: &T) -> bool { +impl MsgIdentification { + pub fn resolve_condition + Send + 'static>(&self, msg: &Msg) -> bool { match self { - StopCondition::StopOnType(type_id) => Self::matches_stop_on_type::(msg, *type_id), - StopCondition::StopOnMatch { exemplar } => { - Self::matches_stop_on_match::(exemplar, msg) - } - StopCondition::StopOnPredicate { predicate } => { - Self::matches_stop_on_predicate(predicate.as_ref(), msg) + MsgIdentification::ByType(type_id) => Self::matches_by_type::(msg, *type_id), + MsgIdentification::ByMatch { exemplar } => Self::is_identical::(exemplar, msg), + MsgIdentification::ByPredicate { predicate } => { + Self::matches_by_predicate(predicate.as_ref(), msg) } } } - fn matches_stop_on_type>(msg: &T, expected_type_id: TypeId) -> bool { - let correct_msg_type_id = msg.correct_msg_type_id(); - correct_msg_type_id == expected_type_id + fn matches_by_type>(msg: &Msg, expected_type_id: TypeId) -> bool { + let trigger_msg_type_id = msg.trigger_msg_type_id(); + trigger_msg_type_id == expected_type_id } - fn matches_stop_on_match + 'static + Send>( + fn is_identical + 'static + Send>( exemplar: &BoxedMsgExpected, - msg: &T, + msg: &Msg, ) -> bool { - if let Some(downcast_exemplar) = exemplar.downcast_ref::() { + if let Some(downcast_exemplar) = exemplar.downcast_ref::() { return downcast_exemplar == msg; } false } - fn matches_stop_on_predicate( + fn matches_by_predicate( predicate: &dyn Fn(RefMsgExpected) -> bool, - msg: &T, + msg: &Msg, ) -> bool { predicate(msg as RefMsgExpected) } } -pub trait ForcedMatchable: PartialEq + Send { - fn correct_msg_type_id(&self) -> TypeId; +pub trait ForcedMatchable: PartialEq + Send { + fn trigger_msg_type_id(&self) -> TypeId; } pub struct PretendedMatchableWrapper(pub M); @@ -131,7 +152,7 @@ where OuterM: PartialEq, InnerM: Send, { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::() } } @@ -139,7 +160,7 @@ where impl PartialEq for PretendedMatchableWrapper { fn eq(&self, _other: &Self) -> bool { panic!( - r#"You requested StopCondition::StopOnMatch for message + r#"You requested MsgIdentification::ByMatch for message that does not implement PartialEq. Consider two other options: matching the type simply by its TypeId or using a predicate."# @@ -148,53 +169,59 @@ impl PartialEq for PretendedMatchableWrapper { } #[macro_export] -macro_rules! match_every_type_id{ +macro_rules! match_lazily_every_type_id{ ($($single_message: ident),+) => { - StopConditions::All(vec![$(StopCondition::StopOnType(TypeId::of::<$single_message>())),+]) + StopConditions::AllLazily(vec![ + $( + crate::test_utils::recorder_stop_conditions::MsgIdentification::ByType( + TypeId::of::<$single_message>() + ) + ),+ + ]) } } mod tests { - use crate::accountant::{ResponseSkeleton, ScanError, ScanForPayables}; + use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use masq_lib::messages::ScanType; use std::any::TypeId; - use std::net::{IpAddr, Ipv4Addr}; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::vec; #[test] fn remove_matched_conditions_works_with_unsorted_indexes() { let mut conditions = vec![ - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), ]; let indexes = vec![2, 0]; StopConditions::remove_matched_conditions(&mut conditions, indexes); assert_eq!(conditions.len(), 1); - let type_id = if let StopCondition::StopOnType(type_id) = conditions[0] { + let type_id = if let MsgIdentification::ByType(type_id) = conditions[0] { type_id } else { - panic!("expected StopOnType but got a different variant") + panic!("expected ByType but got a different variant") }; - assert_eq!(type_id, TypeId::of::()) + assert_eq!(type_id, TypeId::of::()) } #[test] fn stop_on_match_works() { - let mut cond1 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond1 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }]); - let mut cond2 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond2 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 8, 6, 4)), }), }]); - let mut cond3 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond3 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(44, 2, 3, 1)), }), @@ -219,7 +246,7 @@ mod tests { #[test] fn stop_on_predicate_works() { - let mut cond_set = StopConditions::All(vec![StopCondition::StopOnPredicate { + let mut cond_set = StopConditions::AllGreedily(vec![MsgIdentification::ByPredicate { predicate: Box::new(|msg| { let scan_err_msg: &ScanError = msg.downcast_ref().unwrap(); scan_err_msg.scan_type == ScanType::PendingPayables @@ -249,12 +276,12 @@ mod tests { #[test] fn match_any_works_with_every_matching_condition_and_no_need_to_take_elements_out() { let mut cond_set = StopConditions::Any(vec![ - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnMatch { + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }, ]); - let first_msg = ScanForPayables { + let first_msg = ScanForNewPayables { response_skeleton_opt: None, }; let second_msg = StartMessage {}; @@ -265,11 +292,16 @@ mod tests { }; let inspect_len_of_any = |cond_set: &StopConditions, msg_number: usize| match cond_set { StopConditions::Any(conditions) => conditions.len(), - StopConditions::All(_) => panic!("stage {}: expected Any but got All", msg_number), + StopConditions::AllGreedily(_) => { + panic!("stage {}: expected Any but got AllGreedily", msg_number) + } + StopConditions::AllLazily(_) => { + panic!("stage {}: expected Any but got AllLazily", msg_number) + } }; assert_eq!( - cond_set.resolve_stop_conditions::(&first_msg), + cond_set.resolve_stop_conditions::(&first_msg), false ); let len_after_stage_1 = inspect_len_of_any(&cond_set, 1); @@ -289,9 +321,9 @@ mod tests { } #[test] - fn match_all_with_conditions_gradually_eliminated_until_vector_is_emptied_and_it_is_match() { - let mut cond_set = StopConditions::All(vec![ - StopCondition::StopOnPredicate { + fn match_all_with_conditions_gradually_eliminated_greedily_until_empty() { + let mut cond_set = StopConditions::AllGreedily(vec![ + MsgIdentification::ByPredicate { predicate: Box::new(|msg| { if let Some(ip_msg) = msg.downcast_ref::() { ip_msg.new_ip.is_ipv4() @@ -300,34 +332,31 @@ mod tests { } }), }, - StopCondition::StopOnMatch { - exemplar: Box::new(ScanForPayables { + MsgIdentification::ByMatch { + exemplar: Box::new(ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, }), }), }, - StopCondition::StopOnType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), ]); - let tested_msg_1 = ScanForPayables { + let tested_msg_1 = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, }), }; - let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); assert_eq!(kill_system, false); - match &cond_set { - StopConditions::All(conds) => { - assert_eq!(conds.len(), 2); - assert!(matches!(conds[0], StopCondition::StopOnPredicate { .. })); - assert!(matches!(conds[1], StopCondition::StopOnType(_))); - } - StopConditions::Any(_) => panic!("Stage 1: expected StopConditions::All, not ...Any"), - } + assert_state_after_greedily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); let tested_msg_2 = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 4, 1)), }; @@ -335,11 +364,109 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); assert_eq!(kill_system, true); + assert_state_after_greedily_matched(2, &cond_set, |conds| assert!(conds.is_empty())) + } + + fn assert_state_after_greedily_matched( + stage: usize, + cond_set: &StopConditions, + apply_assertions: fn(&[MsgIdentification]), + ) { match cond_set { - StopConditions::All(conds) => { - assert!(conds.is_empty()) + StopConditions::AllGreedily(conds) => apply_assertions(conds), + StopConditions::Any(_) => { + panic!("Stage {stage}: expected StopConditions::AllGreedily, not Any") + } + StopConditions::AllLazily(_) => { + panic!("Stage {stage}: expected StopConditions::AllGreedily, not AllLazily") + } + } + } + + #[test] + fn match_all_with_conditions_gradually_eliminated_lazily_until_empty() { + let mut cond_set = StopConditions::AllLazily(vec![ + MsgIdentification::ByPredicate { + predicate: Box::new(|msg| { + if let Some(ip_msg) = msg.downcast_ref::() { + ip_msg.new_ip.is_ipv6() + } else { + false + } + }), + }, + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + ]); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage one + let tested_msg_1 = ScanForNewPayables { + response_skeleton_opt: None, + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 3); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + assert!(matches!(conds[2], MsgIdentification::ByType(_))); + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage two + let tested_msg_2 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(6, 7, 8, 9)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(2, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage three + let tested_msg_3 = NewPublicIp { + new_ip: IpAddr::V6(Ipv6Addr::new(1, 2, 4, 1, 4, 3, 2, 1)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_3); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(3, &cond_set, |conds| { + assert_eq!(conds.len(), 1); + assert!(matches!(conds[0], MsgIdentification::ByType(_))) + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage four + let tested_msg_4 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(45, 45, 45, 45)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_4); + + assert_eq!(kill_system, true); + assert_state_after_lazily_matched(4, &cond_set, |conds| { + assert!(conds.is_empty()); + }); + } + + fn assert_state_after_lazily_matched( + stage: usize, + cond_set: &StopConditions, + apply_assertions: fn(&[MsgIdentification]), + ) { + match &cond_set { + StopConditions::AllLazily(conds) => apply_assertions(conds), + StopConditions::Any(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not Any") + } + StopConditions::AllGreedily(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not AllGreedily") } - StopConditions::Any(_) => panic!("Stage 2: expected StopConditions::All, not ...Any"), } } } From c3aed7a05a6ee621c6b318788bc0e7a4197eb098 Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta <32920299+utkarshg6@users.noreply.github.com> Date: Thu, 19 Jun 2025 21:11:15 +0200 Subject: [PATCH 04/37] GH-631: Create `FailedPayables` Table (#646) * GH-631: store gas price wei in two columns * GH-631: more updates to the sent payable table * GH-631: eliminate all errors * GH-631: accept null values in db * GH-631: all tests passing * GH-631: use TransactionBlock everywhere * GH-631: enforce that block details are complete * GH-631: fix test constants_have_correct_values * GH-631: introduce failed payable table * GH-631: add db migration for failed payable table * GH-631: mid review changes * GH-631: wip: introduce failed_payable_dao.rs * GH-631: introduce FailedTxBuilder and updated trait for FailedPayableDao * GH-631: insert_new_txs() works * GH-631: txs can be retrieved from FailedPayables * GH-631: implement all trait fns for FailedPayables * GH-631: add a test for string conversion for FailureReason * GH-631: make improvements to failed_payable_dao * GH-631: remove usages of H256 from the tests of DAOs * GH-631: improve imports * GH-631: introduce a utility fn make_block_hash() * GH-631: add review changes for node/src/database/db_initializer.rs * GH-631: some more review changes * GH-631: some more review changes * GH-631: change checked to a boolean * GH-631: keep pushing more review changes * GH-631: display more info in the error statements * GH-631: and even more changes * GH-631: change checked to rechecked * GH-631: remove blank line * GH-631: minor self-review changes * GH-631: add another review changes * GH-631: add more review changes * GH-631: introduce the replace_record() in SentPayableDao * v0.9.0: version bump (#657) * GH-631: review 4 changes * GH-631: review 5 changes * GH-631: review 6 changes * GH-631: import the Itertools and make join work without collect * GH-631: eliminate clippy warnings; tests will still fail due to todo!() --- automap/Cargo.lock | 4 +- automap/Cargo.toml | 2 +- dns_utility/Cargo.lock | 4 +- dns_utility/Cargo.toml | 2 +- masq/Cargo.toml | 2 +- masq_lib/Cargo.toml | 2 +- multinode_integration_tests/Cargo.toml | 2 +- node/Cargo.lock | 10 +- node/Cargo.toml | 2 +- .../db_access_objects/failed_payable_dao.rs | 790 +++++++++++++++++ node/src/accountant/db_access_objects/mod.rs | 1 + .../db_access_objects/sent_payable_dao.rs | 800 +++++++++++------- .../db_access_objects/test_utils.rs | 107 ++- .../src/accountant/db_access_objects/utils.rs | 6 + node/src/accountant/mod.rs | 12 +- node/src/accountant/scanners/mod.rs | 2 +- .../accountant/scanners/scan_schedulers.rs | 8 +- .../src/accountant/scanners/scanners_utils.rs | 4 +- .../lower_level_interface_web3.rs | 2 +- node/src/blockchain/test_utils.rs | 10 +- node/src/database/db_initializer.rs | 151 +++- .../migrations/migration_10_to_11.rs | 37 +- node/src/database/test_utils/mod.rs | 43 +- node/src/test_utils/recorder_counter_msgs.rs | 10 +- port_exposer/Cargo.lock | 2 +- port_exposer/Cargo.toml | 2 +- 26 files changed, 1635 insertions(+), 382 deletions(-) create mode 100644 node/src/accountant/db_access_objects/failed_payable_dao.rs diff --git a/automap/Cargo.lock b/automap/Cargo.lock index 7fd8ef8bf..82258e87b 100644 --- a/automap/Cargo.lock +++ b/automap/Cargo.lock @@ -137,7 +137,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "automap" -version = "0.8.2" +version = "0.9.0" dependencies = [ "crossbeam-channel 0.5.8", "flexi_logger", @@ -1116,7 +1116,7 @@ dependencies = [ [[package]] name = "masq_lib" -version = "0.8.2" +version = "0.9.0" dependencies = [ "actix", "clap", diff --git a/automap/Cargo.toml b/automap/Cargo.toml index 21c1acf91..b379c8a55 100644 --- a/automap/Cargo.toml +++ b/automap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "automap" -version = "0.8.2" +version = "0.9.0" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" description = "Library full of code to make routers map ports through firewalls" diff --git a/dns_utility/Cargo.lock b/dns_utility/Cargo.lock index 872334417..32431ab07 100644 --- a/dns_utility/Cargo.lock +++ b/dns_utility/Cargo.lock @@ -457,7 +457,7 @@ dependencies = [ [[package]] name = "dns_utility" -version = "0.8.2" +version = "0.9.0" dependencies = [ "core-foundation", "ipconfig 0.2.2", @@ -919,7 +919,7 @@ dependencies = [ [[package]] name = "masq_lib" -version = "0.8.2" +version = "0.9.0" dependencies = [ "actix", "clap", diff --git a/dns_utility/Cargo.toml b/dns_utility/Cargo.toml index 50f358db8..c21f8a9ca 100644 --- a/dns_utility/Cargo.toml +++ b/dns_utility/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dns_utility" -version = "0.8.2" +version = "0.9.0" license = "GPL-3.0-only" authors = ["Dan Wiebe ", "MASQ"] copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved." diff --git a/masq/Cargo.toml b/masq/Cargo.toml index 0a6484895..0bef7d9ca 100644 --- a/masq/Cargo.toml +++ b/masq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masq" -version = "0.8.2" +version = "0.9.0" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" description = "Reference implementation of user interface for MASQ Node" diff --git a/masq_lib/Cargo.toml b/masq_lib/Cargo.toml index 01c3957a6..ca2ce815b 100644 --- a/masq_lib/Cargo.toml +++ b/masq_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masq_lib" -version = "0.8.2" +version = "0.9.0" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" description = "Code common to Node and masq; also, temporarily, to dns_utility" diff --git a/multinode_integration_tests/Cargo.toml b/multinode_integration_tests/Cargo.toml index 9720859f7..6a9d22533 100644 --- a/multinode_integration_tests/Cargo.toml +++ b/multinode_integration_tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multinode_integration_tests" -version = "0.8.2" +version = "0.9.0" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" description = "" diff --git a/node/Cargo.lock b/node/Cargo.lock index dec334a81..c825fda4b 100644 --- a/node/Cargo.lock +++ b/node/Cargo.lock @@ -182,7 +182,7 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "automap" -version = "0.8.2" +version = "0.9.0" dependencies = [ "crossbeam-channel 0.5.1", "flexi_logger 0.17.1", @@ -1868,7 +1868,7 @@ dependencies = [ [[package]] name = "masq" -version = "0.8.2" +version = "0.9.0" dependencies = [ "atty", "clap", @@ -1889,7 +1889,7 @@ dependencies = [ [[package]] name = "masq_lib" -version = "0.8.2" +version = "0.9.0" dependencies = [ "actix", "clap", @@ -2082,7 +2082,7 @@ dependencies = [ [[package]] name = "multinode_integration_tests" -version = "0.8.2" +version = "0.9.0" dependencies = [ "base64 0.13.0", "crossbeam-channel 0.5.1", @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "node" -version = "0.8.2" +version = "0.9.0" dependencies = [ "actix", "automap", diff --git a/node/Cargo.toml b/node/Cargo.toml index 3d8c8ea36..80d75b5ef 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node" -version = "0.8.2" +version = "0.9.0" license = "GPL-3.0-only" authors = ["Dan Wiebe ", "MASQ"] description = "MASQ Node is the foundation of MASQ Network, an open-source network that allows anyone to allocate spare computing resources to make the internet a free and fair place for the entire world." diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs new file mode 100644 index 000000000..ce93a1f17 --- /dev/null +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -0,0 +1,790 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCondition::UncheckedPendingTooLong; +use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers, VigilantRusqliteFlatten}; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::database::rusqlite_wrappers::ConnectionWrapper; +use masq_lib::utils::ExpectValue; +use std::collections::HashSet; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use web3::types::Address; + +#[derive(Debug, PartialEq, Eq)] +pub enum FailedPayableDaoError { + EmptyInput, + NoChange, + InvalidInput(String), + PartialExecution(String), + SqlExecutionFailed(String), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FailureReason { + PendingTooLong, + NonceIssue, +} + +impl FromStr for FailureReason { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "PendingTooLong" => Ok(FailureReason::PendingTooLong), + "NonceIssue" => Ok(FailureReason::NonceIssue), + _ => Err(format!("Invalid FailureReason: {}", s)), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FailedTx { + pub hash: TxHash, + pub receiver_address: Address, + pub amount: u128, + pub timestamp: i64, + pub gas_price_wei: u128, + pub nonce: u64, + pub reason: FailureReason, + pub rechecked: bool, +} + +pub enum FailureRetrieveCondition { + UncheckedPendingTooLong, +} + +impl Display for FailureRetrieveCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FailureRetrieveCondition::UncheckedPendingTooLong => { + write!(f, "WHERE reason = 'PendingTooLong' AND rechecked = 0",) + } + } + } +} + +pub trait FailedPayableDao { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> Vec; + fn mark_as_rechecked(&self) -> Result<(), FailedPayableDaoError>; + fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError>; +} + +#[derive(Debug)] +pub struct FailedPayableDaoReal<'a> { + conn: Box, +} + +impl<'a> FailedPayableDaoReal<'a> { + pub fn new(conn: Box) -> Self { + Self { conn } + } +} + +impl FailedPayableDao for FailedPayableDaoReal<'_> { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + let hashes_vec: Vec = hashes.iter().copied().collect(); + let sql = format!( + "SELECT tx_hash, rowid FROM failed_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)) + ); + + let mut stmt = self + .conn + .prepare(&sql) + .unwrap_or_else(|_| panic!("Failed to prepare SQL statement")); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let tx_hash = TxHash::from_str(&tx_hash_str[2..]).expect("Failed to parse TxHash"); + let row_id: u64 = row.get(1).expectv("row_id"); + + Ok((tx_hash, row_id)) + }) + .unwrap_or_else(|_| panic!("Failed to execute query")) + .vigilant_flatten() + .collect() + } + + fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError> { + if txs.is_empty() { + return Err(FailedPayableDaoError::EmptyInput); + } + + let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); + if unique_hashes.len() != txs.len() { + return Err(FailedPayableDaoError::InvalidInput(format!( + "Duplicate hashes found in the input. Input Transactions: {:?}", + txs + ))); + } + + let duplicates = self.get_tx_identifiers(&unique_hashes); + if !duplicates.is_empty() { + return Err(FailedPayableDaoError::InvalidInput(format!( + "Duplicates detected in the database: {:?}", + duplicates, + ))); + } + + if let Some(_rechecked_tx) = txs.iter().find(|tx| tx.rechecked) { + return Err(FailedPayableDaoError::InvalidInput(format!( + "Already rechecked transaction(s) provided: {:?}", + txs + ))); + } + + let sql = format!( + "INSERT INTO failed_payable (\ + tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + reason, \ + rechecked + ) VALUES {}", + comma_joined_stringifiable(txs, |tx| { + let amount_checked = checked_conversion::(tx.amount); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); + let (gas_price_wei_high_b, gas_price_wei_low_b) = + BigIntDivider::deconstruct(gas_price_wei_checked); + format!( + "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{:?}', {})", + tx.hash, + tx.receiver_address, + amount_high_b, + amount_low_b, + tx.timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + tx.nonce, + tx.reason, + tx.rechecked + ) + }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(inserted_rows) => { + if inserted_rows == txs.len() { + Ok(()) + } else { + Err(FailedPayableDaoError::PartialExecution(format!( + "Only {} out of {} records inserted", + inserted_rows, + txs.len() + ))) + } + } + Err(e) => Err(FailedPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn retrieve_txs(&self, condition: Option) -> Vec { + let raw_sql = "SELECT tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + reason, \ + rechecked \ + FROM failed_payable" + .to_string(); + let sql = match condition { + None => raw_sql, + Some(condition) => format!("{} {}", raw_sql, condition), + }; + + let mut stmt = self + .conn + .prepare(&sql) + .expect("Failed to prepare SQL statement"); + + stmt.query_map([], |row| { + let tx_hash_str: String = row.get(0).expectv("tx_hash"); + let hash = TxHash::from_str(&tx_hash_str[2..]).expect("Failed to parse TxHash"); + let receiver_address_str: String = row.get(1).expectv("receiver_address"); + let receiver_address = + Address::from_str(&receiver_address_str[2..]).expect("Failed to parse Address"); + let amount_high_b = row.get(2).expectv("amount_high_b"); + let amount_low_b = row.get(3).expectv("amount_low_b"); + let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let timestamp = row.get(4).expectv("timestamp"); + let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); + let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); + let gas_price_wei = + BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; + let nonce = row.get(7).expectv("nonce"); + let reason_str: String = row.get(8).expectv("reason"); + let reason = + FailureReason::from_str(&reason_str).expect("Failed to parse FailureReason"); + let rechecked_as_integer: u8 = row.get(9).expectv("rechecked"); + let rechecked = rechecked_as_integer == 1; + + Ok(FailedTx { + hash, + receiver_address, + amount, + timestamp, + gas_price_wei, + nonce, + reason, + rechecked, + }) + }) + .expect("Failed to execute query") + .vigilant_flatten() + .collect() + } + + fn mark_as_rechecked(&self) -> Result<(), FailedPayableDaoError> { + let txs = self.retrieve_txs(Some(UncheckedPendingTooLong)); + let hashes_vec: Vec = txs.iter().map(|tx| tx.hash).collect(); + let hashes_string = comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)); + + let sql = format!( + "UPDATE failed_payable SET rechecked = 1 WHERE tx_hash IN ({})", + hashes_string + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(rows_changed) => { + if rows_changed == txs.len() { + Ok(()) + } else { + // This should never occur because we retrieve transaction hashes + // under the condition that all retrieved transactions are unchecked. + Err(FailedPayableDaoError::PartialExecution(format!( + "Only {} of {} records has been marked as rechecked.", + rows_changed, + txs.len(), + ))) + } + } + Err(e) => Err(FailedPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError> { + if hashes.is_empty() { + return Err(FailedPayableDaoError::EmptyInput); + } + + let hashes_vec: Vec = hashes.iter().cloned().collect(); + let sql = format!( + "DELETE FROM failed_payable WHERE tx_hash IN ({})", + comma_joined_stringifiable(&hashes_vec, |hash| { format!("'{:?}'", hash) }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(deleted_rows) => { + if deleted_rows == hashes.len() { + Ok(()) + } else if deleted_rows == 0 { + Err(FailedPayableDaoError::NoChange) + } else { + Err(FailedPayableDaoError::PartialExecution(format!( + "Only {} of {} hashes has been deleted.", + deleted_rows, + hashes.len(), + ))) + } + } + Err(e) => Err(FailedPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::{ + NonceIssue, PendingTooLong, + }; + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, + FailureRetrieveCondition, + }; + use crate::accountant::db_access_objects::test_utils::{ + make_read_only_db_connection, FailedTxBuilder, + }; + use crate::accountant::db_access_objects::utils::current_unix_timestamp; + use crate::blockchain::test_utils::make_tx_hash; + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, + }; + use crate::database::test_utils::ConnectionWrapperMock; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::Connection; + use std::collections::HashSet; + use std::str::FromStr; + + #[test] + fn insert_new_records_works() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "insert_new_records_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .reason(NonceIssue) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(PendingTooLong) + .build(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let txs = vec![tx1, tx2]; + + let result = subject.insert_new_records(&txs); + + let retrieved_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(retrieved_txs, txs); + } + + #[test] + fn insert_new_records_throws_err_for_empty_input() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_err_for_empty_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let empty_input = vec![]; + + let result = subject.insert_new_records(&empty_input); + + assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); + } + + #[test] + fn insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(123); + let tx1 = FailedTxBuilder::default().hash(hash).build(); + let tx2 = FailedTxBuilder::default() + .hash(hash) + .rechecked(true) + .build(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + + let result = subject.insert_new_records(&vec![tx1, tx2]); + + assert_eq!( + result, + Err(FailedPayableDaoError::InvalidInput( + "Duplicate hashes found in the input. Input Transactions: \ + [FailedTx { \ + hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 0, gas_price_wei: 0, \ + nonce: 0, reason: PendingTooLong, rechecked: false }, \ + FailedTx { \ + hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 0, gas_price_wei: 0, \ + nonce: 0, reason: PendingTooLong, rechecked: true }]" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(123); + let tx1 = FailedTxBuilder::default().hash(hash).build(); + let tx2 = FailedTxBuilder::default() + .hash(hash) + .rechecked(true) + .build(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let initial_insertion_result = subject.insert_new_records(&vec![tx1]); + + let result = subject.insert_new_records(&vec![tx2]); + + assert_eq!(initial_insertion_result, Ok(())); + assert_eq!( + result, + Err(FailedPayableDaoError::InvalidInput( + "Duplicates detected in the database: \ + {0x000000000000000000000000000000000000000000000000000000000000007b: 1}" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_throws_err_if_an_already_rechecked_tx_is_supplied() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_throws_err_if_an_already_rechecked_tx_is_supplied", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .rechecked(true) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .rechecked(false) + .build(); + let input = vec![tx1, tx2]; + + let result = subject.insert_new_records(&input); + + assert_eq!( + result, + Err(FailedPayableDaoError::InvalidInput(format!( + "Already rechecked transaction(s) provided: {:?}", + input + ))) + ); + } + + #[test] + fn insert_new_records_returns_err_if_partially_executed() { + let setup_conn = Connection::open_in_memory().unwrap(); + setup_conn + .execute("CREATE TABLE example (id integer)", []) + .unwrap(); + let get_tx_identifiers_stmt = setup_conn.prepare("SELECT id FROM example").unwrap(); + let faulty_insert_stmt = { setup_conn.prepare("SELECT id FROM example").unwrap() }; + let wrapped_conn = ConnectionWrapperMock::default() + .prepare_result(Ok(get_tx_identifiers_stmt)) + .prepare_result(Ok(faulty_insert_stmt)); + let tx = FailedTxBuilder::default().build(); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(FailedPayableDaoError::PartialExecution( + "Only 0 out of 1 records inserted".to_string() + )) + ); + } + + #[test] + fn insert_new_records_can_throw_error() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "insert_new_records_can_throw_error", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let tx = FailedTxBuilder::default().build(); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(FailedPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn get_tx_identifiers_works() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "get_tx_identifiers_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let another_present_hash = make_tx_hash(3); + let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); + let present_tx = FailedTxBuilder::default().hash(present_hash).build(); + let another_present_tx = FailedTxBuilder::default() + .hash(another_present_hash) + .build(); + subject + .insert_new_records(&vec![present_tx, another_present_tx]) + .unwrap(); + + let result = subject.get_tx_identifiers(&hashset); + + assert_eq!(result.get(&present_hash), Some(&1u64)); + assert_eq!(result.get(&absent_hash), None); + assert_eq!(result.get(&another_present_hash), Some(&2u64)); + } + + #[test] + fn failure_reason_from_str_works() { + assert_eq!( + FailureReason::from_str("PendingTooLong"), + Ok(PendingTooLong) + ); + assert_eq!(FailureReason::from_str("NonceIssue"), Ok(NonceIssue)); + assert_eq!( + FailureReason::from_str("InvalidReason"), + Err("Invalid FailureReason: InvalidReason".to_string()) + ); + } + + #[test] + fn retrieve_condition_display_works() { + let expected_condition = "WHERE reason = 'PendingTooLong' AND rechecked = 0"; + assert_eq!( + FailureRetrieveCondition::UncheckedPendingTooLong.to_string(), + expected_condition + ); + } + + #[test] + fn can_retrieve_all_txs() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "can_retrieve_all_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .nonce(1) + .build(); + let tx3 = FailedTxBuilder::default().hash(make_tx_hash(3)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + subject.insert_new_records(&vec![tx3.clone()]).unwrap(); + + let result = subject.retrieve_txs(None); + + assert_eq!(result, vec![tx1, tx2, tx3]); + } + + #[test] + fn can_retrieve_unchecked_pending_too_long_txs() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "can_retrieve_unchecked_pending_too_long_txs", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let now = current_unix_timestamp(); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .reason(PendingTooLong) + .timestamp(now - 3600) + .rechecked(false) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(NonceIssue) + .rechecked(false) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .reason(PendingTooLong) + .rechecked(false) + .timestamp(now - 3000) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(FailureRetrieveCondition::UncheckedPendingTooLong)); + + assert_eq!(result, vec![tx1, tx3]); + } + + #[test] + fn mark_as_rechecked_works() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "mark_as_rechecked_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default() + .hash(make_tx_hash(1)) + .reason(NonceIssue) + .rechecked(false) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(PendingTooLong) + .rechecked(false) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .reason(PendingTooLong) + .rechecked(false) + .build(); + let tx1_pre_checked_state = tx1.rechecked; + let tx2_pre_checked_state = tx2.rechecked; + let tx3_pre_checked_state = tx3.rechecked; + subject + .insert_new_records(&vec![tx1, tx2.clone(), tx3.clone()]) + .unwrap(); + + let result = subject.mark_as_rechecked(); + + let updated_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(tx1_pre_checked_state, false); + assert_eq!(tx2_pre_checked_state, false); + assert_eq!(tx3_pre_checked_state, false); + assert_eq!(updated_txs[0].rechecked, false); + assert_eq!(updated_txs[1].rechecked, true); + assert_eq!(updated_txs[2].rechecked, true); + } + + #[test] + fn mark_as_rechecked_handles_sql_error() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "mark_as_rechecked_handles_sql_error", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.mark_as_rechecked(); + + assert_eq!( + result, + Err(FailedPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ); + } + + #[test] + fn txs_can_be_deleted() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "txs_can_be_deleted"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let tx1 = FailedTxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = FailedTxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = FailedTxBuilder::default().hash(make_tx_hash(3)).build(); + let tx4 = FailedTxBuilder::default().hash(make_tx_hash(4)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .unwrap(); + let hashset = HashSet::from([tx1.hash, tx3.hash]); + + let result = subject.delete_records(&hashset); + + let remaining_records = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(remaining_records, vec![tx2, tx4]); + } + + #[test] + fn delete_records_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + + let result = subject.delete_records(&HashSet::new()); + + assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); + } + + #[test] + fn delete_records_returns_error_when_no_records_are_deleted() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_error_when_no_records_are_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let non_existent_hash = make_tx_hash(999); + let hashset = HashSet::from([non_existent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!(result, Err(FailedPayableDaoError::NoChange)); + } + + #[test] + fn delete_records_returns_error_when_not_all_input_records_were_deleted() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_error_when_not_all_input_records_were_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let tx = FailedTxBuilder::default().hash(present_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hashset = HashSet::from([present_hash, absent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!( + result, + Err(FailedPayableDaoError::PartialExecution( + "Only 1 of 2 hashes has been deleted.".to_string() + )) + ); + } + + #[test] + fn delete_records_returns_a_general_error_from_sql() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "delete_records_returns_a_general_error_from_sql", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); + let hashes = HashSet::from([make_tx_hash(1)]); + + let result = subject.delete_records(&hashes); + + assert_eq!( + result, + Err(FailedPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } +} diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index cf1ca4611..ae165909a 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod banned_dao; +pub mod failed_payable_dao; pub mod payable_dao; pub mod pending_payable_dao; pub mod receivable_dao; 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 1ef307224..5cdc59047 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -3,13 +3,15 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -use ethereum_types::H256; +use ethereum_types::{H256, U64}; use web3::types::Address; use masq_lib::utils::ExpectValue; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers}; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TxStatus; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; use crate::database::rusqlite_wrappers::ConnectionWrapper; +use itertools::Itertools; #[derive(Debug, PartialEq, Eq)] pub enum SentPayableDaoError { @@ -20,26 +22,19 @@ pub enum SentPayableDaoError { SqlExecutionFailed(String), } -type TxHash = H256; -type RowId = u64; - -type TxIdentifiers = HashMap; -type TxUpdates = HashMap; - #[derive(Clone, Debug, PartialEq, Eq)] pub struct Tx { pub hash: TxHash, pub receiver_address: Address, pub amount: u128, pub timestamp: i64, - pub gas_price_wei: u64, - pub nonce: u32, - pub status: TxStatus, + pub gas_price_wei: u128, + pub nonce: u64, + pub block_opt: Option, } pub enum RetrieveCondition { IsPending, - ToRetry, ByHash(Vec), } @@ -47,10 +42,7 @@ impl Display for RetrieveCondition { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { RetrieveCondition::IsPending => { - write!(f, "WHERE status = 'Pending'") - } - RetrieveCondition::ToRetry => { - write!(f, "WHERE status = 'Failed'") + write!(f, "WHERE block_hash IS NULL") } RetrieveCondition::ByHash(tx_hashes) => { write!( @@ -67,7 +59,11 @@ pub trait SentPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError>; fn retrieve_txs(&self, condition: Option) -> Vec; - fn change_statuses(&self, hash_map: &TxUpdates) -> Result<(), SentPayableDaoError>; + fn update_tx_blocks( + &self, + hash_map: &HashMap, + ) -> Result<(), SentPayableDaoError>; + fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError>; fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; } @@ -114,35 +110,54 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); if unique_hashes.len() != txs.len() { - return Err(SentPayableDaoError::InvalidInput( - "Duplicate hashes found in the input".to_string(), - )); + return Err(SentPayableDaoError::InvalidInput(format!( + "Duplicate hashes found in the input. Input Transactions: {:?}", + txs + ))); } - if !self.get_tx_identifiers(&unique_hashes).is_empty() { - return Err(SentPayableDaoError::InvalidInput( - "Input hash is already present in the database".to_string(), - )); + let duplicates = self.get_tx_identifiers(&unique_hashes); + if !duplicates.is_empty() { + return Err(SentPayableDaoError::InvalidInput(format!( + "Duplicates detected in the database: {:?}", + duplicates, + ))); } let sql = format!( "INSERT INTO sent_payable (\ - tx_hash, receiver_address, amount_high_b, amount_low_b, \ - timestamp, gas_price_wei, nonce, status + tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + block_hash, \ + block_number ) VALUES {}", comma_joined_stringifiable(txs, |tx| { let amount_checked = checked_conversion::(tx.amount); - let (high_bytes, low_bytes) = BigIntDivider::deconstruct(amount_checked); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); + let (gas_price_wei_high_b, gas_price_wei_low_b) = + BigIntDivider::deconstruct(gas_price_wei_checked); + let block_details = match &tx.block_opt { + Some(block) => format!("'{:?}', {}", block.block_hash, block.block_number), + None => "null, null".to_string(), + }; format!( - "('{:?}', '{:?}', {}, {}, {}, {}, {}, '{}')", + "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, {})", tx.hash, tx.receiver_address, - high_bytes, - low_bytes, + amount_high_b, + amount_low_b, tx.timestamp, - tx.gas_price_wei, + gas_price_wei_high_b, + gas_price_wei_low_b, tx.nonce, - tx.status + block_details ) }) ); @@ -165,7 +180,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { fn retrieve_txs(&self, condition_opt: Option) -> Vec { let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ - timestamp, gas_price_wei, nonce, status FROM sent_payable" + timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, block_hash, block_number FROM sent_payable" .to_string(); let sql = match condition_opt { None => raw_sql, @@ -187,10 +202,29 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let amount_low_b = row.get(3).expectv("amount_low_b"); let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; let timestamp = row.get(4).expectv("timestamp"); - let gas_price_wei = row.get(5).expectv("gas_price_wei"); - let nonce = row.get(6).expectv("nonce"); - let status_str: String = row.get(7).expectv("status"); - let status = TxStatus::from_str(&status_str).expect("Failed to parse TxStatus"); + let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); + let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); + let gas_price_wei = + BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; + let nonce = row.get(7).expectv("nonce"); + let block_hash_opt: Option = { + let block_hash_str_opt: Option = row.get(8).expectv("block_hash"); + block_hash_str_opt + .map(|string| H256::from_str(&string[2..]).expect("Failed to parse H256")) + }; + let block_number_opt: Option = { + let block_number_i64_opt: Option = row.get(9).expectv("block_number"); + block_number_i64_opt.map(|v| u64::try_from(v).expect("Failed to parse u64")) + }; + + let block_opt = match (block_hash_opt, block_number_opt) { + (Some(block_hash), Some(block_number)) => Some(TransactionBlock { + block_hash, + block_number: U64::from(block_number), + }), + (None, None) => None, + _ => panic!("Invalid block details"), + }; Ok(Tx { hash, @@ -199,7 +233,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { timestamp, gas_price_wei, nonce, - status, + block_opt, }) }) .expect("Failed to execute query") @@ -207,15 +241,18 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn change_statuses(&self, hash_map: &TxUpdates) -> Result<(), SentPayableDaoError> { + fn update_tx_blocks( + &self, + hash_map: &HashMap, + ) -> Result<(), SentPayableDaoError> { if hash_map.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - for (hash, status) in hash_map { + for (hash, transaction_block) in hash_map { let sql = format!( - "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", - status, hash + "UPDATE sent_payable SET block_hash = '{:?}', block_number = {} WHERE tx_hash = '{:?}'", + transaction_block.block_hash, transaction_block.block_number, hash ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -238,6 +275,99 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Ok(()) } + fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError> { + if new_txs.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let build_case = |value_fn: fn(&Tx) -> String| { + new_txs + .iter() + .map(|tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx))) + .join(" ") + }; + + let tx_hash_cases = build_case(|tx| format!("'{:?}'", tx.hash)); + let receiver_address_cases = build_case(|tx| format!("'{:?}'", tx.receiver_address)); + let amount_high_b_cases = build_case(|tx| { + let amount_checked = checked_conversion::(tx.amount); + let (high, _) = BigIntDivider::deconstruct(amount_checked); + high.to_string() + }); + let amount_low_b_cases = build_case(|tx| { + let amount_checked = checked_conversion::(tx.amount); + let (_, low) = BigIntDivider::deconstruct(amount_checked); + low.to_string() + }); + let timestamp_cases = build_case(|tx| tx.timestamp.to_string()); + let gas_price_wei_high_b_cases = build_case(|tx| { + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (high, _) = BigIntDivider::deconstruct(gas_price_wei_checked); + high.to_string() + }); + let gas_price_wei_low_b_cases = build_case(|tx| { + let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); + low.to_string() + }); + let block_hash_cases = build_case(|tx| match &tx.block_opt { + Some(block) => format!("'{:?}'", block.block_hash), + None => "NULL".to_string(), + }); + let block_number_cases = build_case(|tx| match &tx.block_opt { + Some(block) => block.block_number.as_u64().to_string(), + None => "NULL".to_string(), + }); + + let nonces = comma_joined_stringifiable(new_txs, |tx| tx.nonce.to_string()); + + let sql = format!( + "UPDATE sent_payable \ + SET \ + tx_hash = CASE \ + {tx_hash_cases} \ + END, \ + receiver_address = CASE \ + {receiver_address_cases} \ + END, \ + amount_high_b = CASE \ + {amount_high_b_cases} \ + END, \ + amount_low_b = CASE \ + {amount_low_b_cases} \ + END, \ + timestamp = CASE \ + {timestamp_cases} \ + END, \ + gas_price_wei_high_b = CASE \ + {gas_price_wei_high_b_cases} \ + END, \ + gas_price_wei_low_b = CASE \ + {gas_price_wei_low_b_cases} \ + END, \ + block_hash = CASE \ + {block_hash_cases} \ + END, \ + block_number = CASE \ + {block_number_cases} \ + END \ + WHERE nonce IN ({nonces})", + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(updated_rows) => match updated_rows { + 0 => Err(SentPayableDaoError::NoChange), + count if count == new_txs.len() => Ok(()), + _ => Err(SentPayableDaoError::PartialExecution(format!( + "Only {} out of {} records updated", + updated_rows, + new_txs.len() + ))), + }, + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { if hashes.is_empty() { return Err(SentPayableDaoError::EmptyInput); @@ -271,19 +401,20 @@ impl SentPayableDao for SentPayableDaoReal<'_> { #[cfg(test)] mod tests { use std::collections::{HashMap, HashSet}; + use std::sync::{Arc, Mutex}; use crate::accountant::db_access_objects::sent_payable_dao::{RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal}; - use crate::accountant::db_access_objects::utils::current_unix_timestamp; use crate::database::db_initializer::{ - DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + DbInitializationConfig, DbInitializer, DbInitializerReal, }; - use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::database::test_utils::ConnectionWrapperMock; use ethereum_types::{ H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; - use rusqlite::{Connection, OpenFlags}; - use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending, ToRetry}; - use crate::accountant::db_access_objects::test_utils::TxBuilder; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxStatus}; + use rusqlite::{Connection}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending}; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; + use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; #[test] fn insert_new_records_works() { @@ -292,29 +423,19 @@ mod tests { let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let tx1 = TxBuilder::default() - .hash(H256::from_low_u64_le(1)) - .status(TxStatus::Pending) - .build(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); let tx2 = TxBuilder::default() - .hash(H256::from_low_u64_le(2)) - .status(TxStatus::Failed) - .build(); - let tx3 = TxBuilder::default() - .hash(H256::from_low_u64_le(3)) - .status(TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: Default::default(), - })) + .hash(make_tx_hash(2)) + .block(Default::default()) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); - let txs = vec![tx1, tx2, tx3]; + let txs = vec![tx1, tx2]; let result = subject.insert_new_records(&txs); let retrieved_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); - assert_eq!(retrieved_txs.len(), 3); + assert_eq!(retrieved_txs.len(), 2); assert_eq!(retrieved_txs, txs); } @@ -344,14 +465,15 @@ mod tests { let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let hash = H256::from_low_u64_be(1234567890); + let hash = make_tx_hash(1234); let tx1 = TxBuilder::default() .hash(hash) - .status(TxStatus::Pending) + .timestamp(1749204017) .build(); let tx2 = TxBuilder::default() .hash(hash) - .status(TxStatus::Failed) + .timestamp(1749204020) + .block(Default::default()) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); @@ -360,7 +482,20 @@ mod tests { assert_eq!( result, Err(SentPayableDaoError::InvalidInput( - "Duplicate hashes found in the input".to_string() + "Duplicate hashes found in the input. Input Transactions: \ + [Tx { \ + hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ + nonce: 0, block_opt: None }, \ + Tx { \ + hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount: 0, timestamp: 1749204020, gas_price_wei: 0, \ + nonce: 0, block_opt: Some(TransactionBlock { \ + block_hash: 0x0000000000000000000000000000000000000000000000000000000000000000, \ + block_number: 0 }) }]" + .to_string() )) ); } @@ -374,14 +509,11 @@ mod tests { let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let hash = H256::from_low_u64_be(1234567890); - let tx1 = TxBuilder::default() - .hash(hash) - .status(TxStatus::Pending) - .build(); + let hash = make_tx_hash(1234); + let tx1 = TxBuilder::default().hash(hash).build(); let tx2 = TxBuilder::default() .hash(hash) - .status(TxStatus::Failed) + .block(Default::default()) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); let initial_insertion_result = subject.insert_new_records(&vec![tx1]); @@ -392,7 +524,9 @@ mod tests { assert_eq!( result, Err(SentPayableDaoError::InvalidInput( - "Input hash is already present in the database".to_string() + "Duplicates detected in the database: \ + {0x00000000000000000000000000000000000000000000000000000000000004d2: 1}" + .to_string() )) ); } @@ -427,18 +561,8 @@ mod tests { "sent_payable_dao", "insert_new_records_can_throw_error", ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let read_only_conn = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(read_only_conn); let tx = TxBuilder::default().build(); + let wrapped_conn = make_read_only_db_connection(home_dir); let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); let result = subject.insert_new_records(&vec![tx]); @@ -459,9 +583,9 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let present_hash = H256::from_low_u64_le(1); - let absent_hash = H256::from_low_u64_le(2); - let another_present_hash = H256::from_low_u64_le(3); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let another_present_hash = make_tx_hash(3); let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); let present_tx = TxBuilder::default().hash(present_hash).build(); let another_present_tx = TxBuilder::default().hash(another_present_hash).build(); @@ -478,8 +602,7 @@ mod tests { #[test] fn retrieve_condition_display_works() { - assert_eq!(IsPending.to_string(), "WHERE status = 'Pending'"); - assert_eq!(ToRetry.to_string(), "WHERE status = 'Failed'"); + assert_eq!(IsPending.to_string(), "WHERE block_hash IS NULL"); assert_eq!( ByHash(vec![ H256::from_low_u64_be(0x123456789), @@ -502,39 +625,20 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let tx1 = TxBuilder::default() - .hash(H256::from_low_u64_le(1)) - .status(TxStatus::Pending) - .build(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); let tx2 = TxBuilder::default() - .hash(H256::from_low_u64_le(2)) - .status(TxStatus::Failed) + .hash(make_tx_hash(2)) + .block(Default::default()) .build(); - let tx3 = TxBuilder::default() - .hash(H256::from_low_u64_le(3)) - .status(TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: Default::default(), - })) - .build(); - let tx4 = TxBuilder::default() - .hash(H256::from_low_u64_le(4)) - .status(TxStatus::Pending) - .build(); - let tx5 = TxBuilder::default() - .hash(H256::from_low_u64_le(5)) - .status(TxStatus::Failed) - .build(); - subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone()]) - .unwrap(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); subject - .insert_new_records(&vec![tx4.clone(), tx5.clone()]) + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) .unwrap(); + subject.insert_new_records(&vec![tx3.clone()]).unwrap(); let result = subject.retrieve_txs(None); - assert_eq!(result, vec![tx1, tx2, tx3, tx4, tx5]); + assert_eq!(result, vec![tx1, tx2, tx3]); } #[test] @@ -545,27 +649,14 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let tx1 = TxBuilder::default() - .hash(H256::from_low_u64_le(1)) - .status(TxStatus::Pending) - .build(); - let tx2 = TxBuilder::default() - .hash(H256::from_low_u64_le(2)) - .status(TxStatus::Pending) - .build(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); let tx3 = TxBuilder::default() - .hash(H256::from_low_u64_le(3)) - .status(TxStatus::Failed) - .build(); - let tx4 = TxBuilder::default() - .hash(H256::from_low_u64_le(4)) - .status(TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: Default::default(), - })) + .hash(make_tx_hash(3)) + .block(Default::default()) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3, tx4]) + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3]) .unwrap(); let result = subject.retrieve_txs(Some(RetrieveCondition::IsPending)); @@ -574,166 +665,161 @@ mod tests { } #[test] - fn can_retrieve_txs_to_retry() { + fn tx_can_be_retrieved_by_hash() { let home_dir = - ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_txs_to_retry"); + ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_hash"); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let old_timestamp = current_unix_timestamp() - 60; // 1 minute old - let tx1 = TxBuilder::default() - .hash(H256::from_low_u64_le(3)) - .timestamp(old_timestamp) - .status(TxStatus::Pending) - .build(); - let tx2 = TxBuilder::default() - .hash(H256::from_low_u64_le(4)) - .timestamp(old_timestamp) - .status(TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: Default::default(), - })) - .build(); - // TODO: GH-631: Instead of fetching it from SentPayables, fetch it from the FailedPayables table - let tx3 = TxBuilder::default() // this should be picked for retry - .hash(H256::from_low_u64_le(5)) - .timestamp(old_timestamp) - .status(TxStatus::Failed) - .build(); - let tx4 = TxBuilder::default() // this should be picked for retry - .hash(H256::from_low_u64_le(6)) - .status(TxStatus::Failed) - .build(); - let tx5 = TxBuilder::default() - .hash(H256::from_low_u64_le(7)) - .timestamp(old_timestamp) - .status(TxStatus::Pending) - .build(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); subject - .insert_new_records(&vec![tx1, tx2, tx3.clone(), tx4.clone(), tx5]) + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) .unwrap(); - let result = subject.retrieve_txs(Some(RetrieveCondition::ToRetry)); + let result = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx3.hash]))); - assert_eq!(result, vec![tx3, tx4]); + assert_eq!(result, vec![tx1, tx3]); } #[test] - fn tx_can_be_retrieved_by_hash() { - let home_dir = - ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_hash"); + #[should_panic(expected = "Invalid block details")] + fn retrieve_txs_enforces_complete_block_details() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "retrieve_txs_enforces_complete_block_details", + ); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let subject = SentPayableDaoReal::new(wrapped_conn); - let tx1 = TxBuilder::default() - .hash(H256::from_low_u64_le(1)) - .status(TxStatus::Pending) - .build(); - let tx2 = TxBuilder::default() - .hash(H256::from_low_u64_le(2)) - .status(TxStatus::Failed) - .build(); - subject - .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + // Insert a record with block_hash but no block_number + { + let sql = "INSERT INTO sent_payable (\ + tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + block_hash, \ + block_number\ + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"; + let mut stmt = wrapped_conn.prepare(sql).unwrap(); + stmt.execute(rusqlite::params![ + "0x1234567890123456789012345678901234567890123456789012345678901234", + "0x1234567890123456789012345678901234567890", + 0, + 100, + 1234567890, + 0, + 1000000000, + 1, + "0x2345678901234567890123456789012345678901234567890123456789012345", + rusqlite::types::Null, + ]) .unwrap(); + } + let subject = SentPayableDaoReal::new(wrapped_conn); - let result = subject.retrieve_txs(Some(ByHash(vec![tx1.hash]))); - - assert_eq!(result, vec![tx1]); + // This should panic due to invalid block details + let _ = subject.retrieve_txs(None); } #[test] - fn change_statuses_works() { + fn update_tx_blocks_works() { let home_dir = - ensure_node_home_directory_exists("sent_payable_dao", "change_statuses_works"); + ensure_node_home_directory_exists("sent_payable_dao", "update_tx_blocks_works"); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let tx1 = TxBuilder::default() - .hash(H256::from_low_u64_le(1)) - .status(TxStatus::Pending) - .build(); - let tx2 = TxBuilder::default() - .hash(H256::from_low_u64_le(2)) - .status(TxStatus::Pending) - .build(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let pre_assert_is_block_details_present_tx1 = tx1.block_opt.is_some(); + let pre_assert_is_block_details_present_tx2 = tx2.block_opt.is_some(); subject .insert_new_records(&vec![tx1.clone(), tx2.clone()]) .unwrap(); + let tx_block_1 = TransactionBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), + }; + let tx_block_2 = TransactionBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), + }; let hash_map = HashMap::from([ - (tx1.hash, TxStatus::Failed), - ( - tx2.hash, - TxStatus::Succeeded(TransactionBlock { - block_hash: H256::from_low_u64_le(3), - block_number: U64::from(1), - }), - ), + (tx1.hash, tx_block_1.clone()), + (tx2.hash, tx_block_2.clone()), ]); - let result = subject.change_statuses(&hash_map); + let result = subject.update_tx_blocks(&hash_map); let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); assert_eq!(result, Ok(())); - assert_eq!(updated_txs[0].status, TxStatus::Failed); - assert_eq!( - updated_txs[1].status, - TxStatus::Succeeded(TransactionBlock { - block_hash: H256::from_low_u64_le(3), - block_number: U64::from(1), - }) - ) + assert_eq!(pre_assert_is_block_details_present_tx1, false); + assert_eq!(updated_txs[0].block_opt, Some(tx_block_1)); + assert_eq!(pre_assert_is_block_details_present_tx2, false); + assert_eq!(updated_txs[1].block_opt, Some(tx_block_2)); } #[test] - fn change_statuses_returns_error_when_input_is_empty() { + fn update_tx_blocks_returns_error_when_input_is_empty() { let home_dir = ensure_node_home_directory_exists( "sent_payable_dao", - "change_statuses_returns_error_when_input_is_empty", + "update_tx_blocks_returns_error_when_input_is_empty", ); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let existent_hash = H256::from_low_u64_le(1); - let tx = TxBuilder::default() - .hash(existent_hash) - .status(TxStatus::Pending) - .build(); + let existent_hash = make_tx_hash(1); + let tx = TxBuilder::default().hash(existent_hash).build(); subject.insert_new_records(&vec![tx]).unwrap(); let hash_map = HashMap::new(); - let result = subject.change_statuses(&hash_map); + let result = subject.update_tx_blocks(&hash_map); assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); } #[test] - fn change_statuses_returns_error_during_partial_execution() { + fn update_tx_blocks_returns_error_during_partial_execution() { let home_dir = ensure_node_home_directory_exists( "sent_payable_dao", - "change_statuses_returns_error_during_partial_execution", + "update_tx_blocks_returns_error_during_partial_execution", ); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let existent_hash = H256::from_low_u64_le(1); - let non_existent_hash = H256::from_low_u64_le(999); - let tx = TxBuilder::default() - .hash(existent_hash) - .status(TxStatus::Pending) - .build(); + let existent_hash = make_tx_hash(1); + let non_existent_hash = make_tx_hash(999); + let tx = TxBuilder::default().hash(existent_hash).build(); subject.insert_new_records(&vec![tx]).unwrap(); let hash_map = HashMap::from([ - (existent_hash, TxStatus::Failed), - (non_existent_hash, TxStatus::Failed), + ( + existent_hash, + TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }, + ), + ( + non_existent_hash, + TransactionBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), + }, + ), ]); - let result = subject.change_statuses(&hash_map); + let result = subject.update_tx_blocks(&hash_map); assert_eq!( result, @@ -745,27 +831,23 @@ mod tests { } #[test] - fn change_statuses_returns_error_when_an_error_occurs_while_executing_sql() { + fn update_tx_blocks_returns_error_when_an_error_occurs_while_executing_sql() { let home_dir = ensure_node_home_directory_exists( "sent_payable_dao", - "change_statuses_returns_error_when_an_error_occurs_while_executing_sql", + "update_tx_blocks_returns_error_when_an_error_occurs_while_executing_sql", ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let read_only_conn = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(read_only_conn); + let wrapped_conn = make_read_only_db_connection(home_dir); let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); - let hash = H256::from_low_u64_le(1); - let hash_map = HashMap::from([(hash, TxStatus::Failed)]); + let hash = make_tx_hash(1); + let hash_map = HashMap::from([( + hash, + TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), + }, + )]); - let result = subject.change_statuses(&hash_map); + let result = subject.update_tx_blocks(&hash_map); assert_eq!( result, @@ -782,24 +864,12 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let tx1 = TxBuilder::default() - .hash(H256::from_low_u64_le(1)) - .status(TxStatus::Pending) - .build(); - let tx2 = TxBuilder::default() - .hash(H256::from_low_u64_le(2)) - .status(TxStatus::Pending) - .build(); - let tx3 = TxBuilder::default() - .hash(H256::from_low_u64_le(3)) - .status(TxStatus::Failed) - .build(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); let tx4 = TxBuilder::default() - .hash(H256::from_low_u64_le(4)) - .status(TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: Default::default(), - })) + .hash(make_tx_hash(4)) + .block(Default::default()) .build(); subject .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) @@ -839,7 +909,7 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let non_existent_hash = H256::from_low_u64_le(999); + let non_existent_hash = make_tx_hash(999); let hashset = HashSet::from([non_existent_hash]); let result = subject.delete_records(&hashset); @@ -857,12 +927,9 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); - let present_hash = H256::from_low_u64_le(1); - let absent_hash = H256::from_low_u64_le(2); - let tx = TxBuilder::default() - .hash(present_hash) - .status(TxStatus::Failed) - .build(); + 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]); @@ -882,19 +949,9 @@ mod tests { "sent_payable_dao", "delete_records_returns_a_general_error_from_sql", ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let read_only_conn = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(read_only_conn); + let wrapped_conn = make_read_only_db_connection(home_dir); let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); - let hashes = HashSet::from([H256::from_low_u64_le(1)]); + let hashes = HashSet::from([make_tx_hash(1)]); let result = subject.delete_records(&hashes); @@ -905,4 +962,179 @@ mod tests { )) ) } + + #[test] + fn replace_records_works_as_expected() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_works_as_expected", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).nonce(3).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3]) + .unwrap(); + let new_tx2 = TxBuilder::default() + .hash(make_tx_hash(22)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(2) + .build(); + let new_tx3 = TxBuilder::default() + .hash(make_tx_hash(33)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(3) + .build(); + + let result = subject.replace_records(&[new_tx2.clone(), new_tx3.clone()]); + + let retrieved_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(retrieved_txs, vec![tx1, new_tx2, new_tx3]); + } + + #[test] + fn replace_records_uses_single_sql_statement() { + let prepare_params = Arc::new(Mutex::new(vec![])); + let setup_conn = Connection::open_in_memory().unwrap(); + setup_conn + .execute("CREATE TABLE example (id integer)", []) + .unwrap(); + let stmt = setup_conn.prepare("SELECT id FROM example").unwrap(); + let wrapped_conn = ConnectionWrapperMock::default() + .prepare_params(&prepare_params) + .prepare_result(Ok(stmt)); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).nonce(3).build(); + + let _ = subject.replace_records(&[tx1, tx2, tx3]); + + let captured_params = prepare_params.lock().unwrap(); + let sql = &captured_params[0]; + assert!(sql.starts_with("UPDATE sent_payable SET")); + assert!(sql.contains("tx_hash = CASE")); + assert!(sql.contains("receiver_address = CASE")); + assert!(sql.contains("amount_high_b = CASE")); + assert!(sql.contains("amount_low_b = CASE")); + assert!(sql.contains("timestamp = CASE")); + assert!(sql.contains("gas_price_wei_high_b = CASE")); + assert!(sql.contains("gas_price_wei_low_b = CASE")); + assert!(sql.contains("block_hash = CASE")); + assert!(sql.contains("block_number = CASE")); + assert!(sql.contains("WHERE nonce IN (1, 2, 3)")); + assert!(sql.contains("WHEN nonce = 1 THEN '0x0000000000000000000000000000000000000000000000000000000000000001'")); + assert!(sql.contains("WHEN nonce = 2 THEN '0x0000000000000000000000000000000000000000000000000000000000000002'")); + assert!(sql.contains("WHEN nonce = 3 THEN '0x0000000000000000000000000000000000000000000000000000000000000003'")); + assert_eq!(captured_params.len(), 1); + } + + #[test] + fn replace_records_throws_error_for_empty_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_throws_error_for_empty_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + subject.insert_new_records(&vec![tx1, tx2]).unwrap(); + + let result = subject.replace_records(&[]); + + assert_eq!(result, Err(EmptyInput)); + } + + #[test] + fn replace_records_throws_partial_execution_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_throws_partial_execution_error", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).nonce(2).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + let new_tx2 = TxBuilder::default() + .hash(make_tx_hash(22)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(2) + .build(); + let new_tx3 = TxBuilder::default() + .hash(make_tx_hash(33)) + .block(TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }) + .nonce(3) + .build(); + + let result = subject.replace_records(&[new_tx2, new_tx3]); + + assert_eq!( + result, + Err(PartialExecution( + "Only 1 out of 2 records updated".to_string() + )) + ); + } + + #[test] + fn replace_records_returns_no_change_error_when_no_rows_updated() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_returns_no_change_error_when_no_rows_updated", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx = TxBuilder::default().hash(make_tx_hash(1)).nonce(42).build(); + + let result = subject.replace_records(&[tx]); + + assert_eq!(result, Err(SentPayableDaoError::NoChange)); + } + + #[test] + fn replace_records_returns_a_general_error_from_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "replace_records_returns_a_general_error_from_sql", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let tx = TxBuilder::default().hash(make_tx_hash(1)).nonce(1).build(); + + let result = subject.replace_records(&[tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index 3a571ff6a..598a4121d 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -1,20 +1,25 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use web3::types::{Address, H256}; -use crate::accountant::db_access_objects::sent_payable_dao::Tx; -use crate::accountant::db_access_objects::utils::current_unix_timestamp; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TxStatus; +use std::path::PathBuf; +use rusqlite::{Connection, OpenFlags}; +use crate::accountant::db_access_objects::sent_payable_dao::{ Tx}; +use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use web3::types::{Address}; +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionBlock; +use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE}; +use crate::database::rusqlite_wrappers::ConnectionWrapperReal; #[derive(Default)] pub struct TxBuilder { - hash_opt: Option, + hash_opt: Option, receiver_address_opt: Option
, amount_opt: Option, timestamp_opt: Option, - gas_price_wei_opt: Option, - nonce_opt: Option, - status_opt: Option, + gas_price_wei_opt: Option, + nonce_opt: Option, + block_opt: Option, } impl TxBuilder { @@ -22,7 +27,7 @@ impl TxBuilder { Default::default() } - pub fn hash(mut self, hash: H256) -> Self { + pub fn hash(mut self, hash: TxHash) -> Self { self.hash_opt = Some(hash); self } @@ -32,8 +37,13 @@ impl TxBuilder { self } - pub fn status(mut self, status: TxStatus) -> Self { - self.status_opt = Some(status); + pub fn nonce(mut self, nonce: u64) -> Self { + self.nonce_opt = Some(nonce); + self + } + + pub fn block(mut self, block: TransactionBlock) -> Self { + self.block_opt = Some(block); self } @@ -45,7 +55,80 @@ impl TxBuilder { timestamp: self.timestamp_opt.unwrap_or_else(current_unix_timestamp), gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), - status: self.status_opt.unwrap_or(TxStatus::Pending), + block_opt: self.block_opt, + } + } +} + +#[derive(Default)] +pub struct FailedTxBuilder { + hash_opt: Option, + receiver_address_opt: Option
, + amount_opt: Option, + timestamp_opt: Option, + gas_price_wei_opt: Option, + nonce_opt: Option, + reason_opt: Option, + rechecked_opt: Option, +} + +impl FailedTxBuilder { + pub fn default() -> Self { + Default::default() + } + + pub fn hash(mut self, hash: TxHash) -> Self { + self.hash_opt = Some(hash); + self + } + + pub fn timestamp(mut self, timestamp: i64) -> Self { + self.timestamp_opt = Some(timestamp); + self + } + + pub fn nonce(mut self, nonce: u64) -> Self { + self.nonce_opt = Some(nonce); + self + } + + pub fn reason(mut self, reason: FailureReason) -> Self { + self.reason_opt = Some(reason); + self + } + + pub fn rechecked(mut self, rechecked: bool) -> Self { + self.rechecked_opt = Some(rechecked); + self + } + + pub fn build(self) -> FailedTx { + FailedTx { + hash: self.hash_opt.unwrap_or_default(), + receiver_address: self.receiver_address_opt.unwrap_or_default(), + amount: self.amount_opt.unwrap_or_default(), + timestamp: self.timestamp_opt.unwrap_or_default(), + gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + nonce: self.nonce_opt.unwrap_or_default(), + reason: self + .reason_opt + .unwrap_or_else(|| FailureReason::PendingTooLong), + rechecked: self.rechecked_opt.unwrap_or_else(|| false), } } } + +pub fn make_read_only_db_connection(home_dir: PathBuf) -> ConnectionWrapperReal { + { + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + } + let read_only_conn = Connection::open_with_flags( + home_dir.join(DATABASE_FILE), + OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .unwrap(); + + ConnectionWrapperReal::new(read_only_conn) +} diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index fe84712d7..8fbc875c2 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -9,11 +9,13 @@ use crate::database::db_initializer::{ }; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::accountant::PaymentThresholds; +use ethereum_types::H256; use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::messages::{ RangeQuery, TopRecordsConfig, TopRecordsOrdering, UiPayableAccount, UiReceivableAccount, }; use rusqlite::{Row, Statement, ToSql}; +use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::iter::FlatMap; use std::path::{Path, PathBuf}; @@ -21,6 +23,10 @@ use std::string::ToString; use std::time::Duration; use std::time::SystemTime; +pub type TxHash = H256; +pub type RowId = u64; +pub type TxIdentifiers = HashMap; + pub fn to_unix_timestamp(system_time: SystemTime) -> i64 { match system_time.duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => sign_conversion::(d.as_secs()).expect("MASQNode has expired"), diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 79f7c52f5..24dbdcc68 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1041,7 +1041,7 @@ impl Accountant { e.log_error(&self.logger, scanner.into(), is_externally_triggered); - response_skeleton_opt.map(|skeleton| { + if let Some(skeleton) = response_skeleton_opt { self.ui_message_sub_opt .as_ref() .expect("UiGateway is unbound") @@ -1050,7 +1050,7 @@ impl Accountant { body: UiScanResponse {}.tmb(skeleton.context_id), }) .expect("UiGateway is dead"); - }); + }; self.scan_schedulers .reschedule_on_error_resolver @@ -1084,7 +1084,7 @@ impl Accountant { response_skeleton_opt.is_some(), ); - response_skeleton_opt.map(|skeleton| { + if let Some(skeleton) = response_skeleton_opt { self.ui_message_sub_opt .as_ref() .expect("UiGateway is unbound") @@ -1093,7 +1093,7 @@ impl Accountant { body: UiScanResponse {}.tmb(skeleton.context_id), }) .expect("UiGateway is dead"); - }); + }; } } } @@ -3010,8 +3010,8 @@ mod tests { scan_for_pending_payables_notify_later_params_arc .lock() .unwrap(); - // PendingPayableScanner can only start after NewPayableScanner finishes and makes at least - // one transaction. The test stops before running NewPayableScanner, missing both + // PendingPayableScanner can only start after NewPayableScanner finishes and makes at least + // one transaction. The test stops before running NewPayableScanner, missing both // the second PendingPayableScanner run and its scheduling event. assert!( scan_for_pending_payables_notify_later_params.is_empty(), diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index ca1810290..349ffe3df 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -920,7 +920,7 @@ impl Scanner for PendingPay let requires_payment_retry = self.process_transactions_by_reported_state(scan_report, logger); - self.mark_as_ended(&logger); + self.mark_as_ended(logger); requires_payment_retry } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 77ac6646e..74e102aff 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -38,18 +38,18 @@ impl ScanSchedulers { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum PayableScanSchedulerError { ScanForNewPayableAlreadyScheduled, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum ScanRescheduleAfterEarlyStop { Schedule(ScanType), DoNotSchedule, } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum PayableSequenceScanner { NewPayables, RetryPayables, @@ -123,7 +123,7 @@ impl PayableScanScheduler { } else { debug!(logger, "Scheduling a new-payable scan asap"); - let _ = self.new_payable_notify.notify( + self.new_payable_notify.notify( ScanForNewPayables { response_skeleton_opt: None, }, diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 4d2bf16e1..b50a1388b 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -33,7 +33,7 @@ pub mod payable_scanner_utils { pub result: OperationOutcome, } - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Eq)] pub enum OperationOutcome { NewPendingPayable, Failure, @@ -341,7 +341,7 @@ pub mod pending_payable_scanner_utils { } } - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Eq)] pub enum PendingPayableScanResult { NoPendingPayablesLeft(Option), PaymentRetryRequired, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index 25a747907..b7353b7c2 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -77,7 +77,7 @@ pub struct TxReceipt { pub status: TxStatus, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct TransactionBlock { pub block_hash: H256, pub block_number: U64, diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 4124e283a..6259e8739 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -185,10 +185,18 @@ pub fn make_default_signed_transaction() -> SignedTransaction { } } -pub fn make_tx_hash(base: u32) -> H256 { +pub fn make_hash(base: u32) -> Hash { H256::from_uint(&U256::from(base)) } +pub fn make_tx_hash(base: u32) -> H256 { + make_hash(base) +} + +pub fn make_block_hash(base: u32) -> H256 { + make_hash(base + 1000000000) +} + pub fn all_chains() -> [Chain; 4] { [ Chain::EthMainnet, diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index 6e0090f0d..86e82aed1 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -136,6 +136,7 @@ impl DbInitializerReal { Self::initialize_config(conn, external_params); Self::create_payable_table(conn); Self::create_sent_payable_table(conn); + Self::create_failed_payable_table(conn); Self::create_pending_payable_table(conn); Self::create_receivable_table(conn); Self::create_banned_table(conn); @@ -268,9 +269,11 @@ impl DbInitializerReal { amount_high_b integer not null, amount_low_b integer not null, timestamp integer not null, - gas_price_wei integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, nonce integer not null, - status text not null + block_hash text null, + block_number integer null )", [], ) @@ -283,6 +286,32 @@ impl DbInitializerReal { .expect("Can't create transaction hash index in sent payments"); } + pub fn create_failed_payable_table(conn: &Connection) { + conn.execute( + "create table if not exists failed_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + reason text not null, + rechecked integer not null + )", + [], + ) + .expect("Can't create failed_payable table"); + + conn.execute( + "CREATE UNIQUE INDEX failed_payable_tx_hash_idx ON sent_payable (tx_hash)", + [], + ) + .expect("Can't create transaction hash index in failed payments"); + } + pub fn create_pending_payable_table(conn: &Connection) { conn.execute( "create table if not exists pending_payable ( @@ -646,7 +675,9 @@ impl Debug for DbInitializationConfig { mod tests { use super::*; use crate::database::db_initializer::InitializationError::SqliteError; - use crate::database::test_utils::SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE; + use crate::database::test_utils::{ + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + }; use crate::db_config::config_dao::{ConfigDao, ConfigDaoReal}; use crate::test_utils::database_utils::{ assert_create_table_stm_contains_all_parts, @@ -696,7 +727,7 @@ mod tests { let mut stmt = conn .prepare("select name, value, encrypted from config") .unwrap(); - let _ = stmt.query_map([], |_| Ok(42)).unwrap(); + let _ = stmt.execute([]); let expected_key_words: &[&[&str]] = &[ &["name", "text", "primary", "key"], &["value", "text"], @@ -718,9 +749,20 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let mut stmt = conn.prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable").unwrap(); - let mut payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(payable_contents.next().is_none()); + let mut stmt = conn + .prepare( + "SELECT rowid, + transaction_hash, + amount_high_b, + amount_low_b, + payable_timestamp, + attempt, + process_error + FROM pending_payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); let expected_key_words: &[&[&str]] = &[ &["rowid", "integer", "primary", "key"], &["transaction_hash", "text", "not", "null"], @@ -750,10 +792,24 @@ mod tests { let conn = subject .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - - let mut stmt = conn.prepare("select rowid, tx_hash, receiver_address, amount_high_b, amount_low_b, timestamp, gas_price_wei, nonce, status from sent_payable").unwrap(); - let mut sent_payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(sent_payable_contents.next().is_none()); + let mut stmt = conn + .prepare( + "SELECT rowid, + tx_hash, + receiver_address, + amount_high_b, + amount_low_b, + timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + nonce, + block_hash, + block_number + FROM sent_payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); assert_create_table_stm_contains_all_parts( &*conn, "sent_payable", @@ -767,6 +823,48 @@ mod tests { ) } + #[test] + fn db_initialize_creates_failed_payable_table() { + let home_dir = ensure_node_home_directory_does_not_exist( + "db_initializer", + "db_initialize_creates_failed_payable_table", + ); + let subject = DbInitializerReal::default(); + + let conn = subject + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let mut stmt = conn + .prepare( + "SELECT rowid, + tx_hash, + receiver_address, + amount_high_b, + amount_low_b, + timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + nonce, + reason, + rechecked + FROM failed_payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); + assert_create_table_stm_contains_all_parts( + &*conn, + "failed_payable", + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, + ); + let expected_key_words: &[&[&str]] = &[&["tx_hash"]]; + assert_index_stm_is_coupled_with_right_parameter( + conn.as_ref(), + "failed_payable_tx_hash_idx", + expected_key_words, + ) + } + #[test] fn db_initialize_creates_payable_table() { let home_dir = ensure_node_home_directory_does_not_exist( @@ -779,9 +877,18 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let mut stmt = conn.prepare ("select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid from payable").unwrap (); - let mut payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(payable_contents.next().is_none()); + let mut stmt = conn + .prepare( + "SELECT wallet_address, + balance_high_b, + balance_low_b, + last_paid_timestamp, + pending_payable_rowid + FROM payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); assert_table_created_as_strict(&*conn, "payable"); let expected_key_words: &[&[&str]] = &[ &["wallet_address", "text", "primary", "key"], @@ -807,10 +914,16 @@ mod tests { .unwrap(); let mut stmt = conn - .prepare("select wallet_address, balance_high_b, balance_low_b, last_received_timestamp from receivable") + .prepare( + "SELECT wallet_address, + balance_high_b, + balance_low_b, + last_received_timestamp + FROM receivable", + ) .unwrap(); - let mut receivable_contents = stmt.query_map([], |_| Ok(())).unwrap(); - assert!(receivable_contents.next().is_none()); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); assert_table_created_as_strict(&*conn, "receivable"); let expected_key_words: &[&[&str]] = &[ &["wallet_address", "text", "primary", "key"], @@ -836,8 +949,8 @@ mod tests { .unwrap(); let mut stmt = conn.prepare("select wallet_address from banned").unwrap(); - let mut banned_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(banned_contents.next().is_none()); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); let expected_key_words: &[&[&str]] = &[&["wallet_address", "text", "primary", "key"]]; assert_create_table_stm_contains_all_parts(conn.as_ref(), "banned", expected_key_words); assert_no_index_exists_for_table(conn.as_ref(), "banned") diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs index 8b7984673..4dbfd5b5e 100644 --- a/node/src/database/db_migrations/migrations/migration_10_to_11.rs +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -9,19 +9,38 @@ impl DatabaseMigration for Migrate_10_to_11 { &self, declaration_utils: Box, ) -> rusqlite::Result<()> { - let sql_statement = "create table if not exists sent_payable ( + let sql_statement_for_sent_payable = "create table if not exists sent_payable ( rowid integer primary key, tx_hash text not null, receiver_address text not null, amount_high_b integer not null, amount_low_b integer not null, timestamp integer not null, - gas_price_wei integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, nonce integer not null, - status text not null + block_hash text null, + block_number integer null )"; - declaration_utils.execute_upon_transaction(&[&sql_statement]) + let sql_statement_for_failed_payable = "create table if not exists failed_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + reason text not null, + rechecked integer not null + )"; + + declaration_utils.execute_upon_transaction(&[ + &sql_statement_for_sent_payable, + &sql_statement_for_failed_payable, + ]) } fn old_version(&self) -> usize { @@ -34,7 +53,9 @@ mod tests { use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; - use crate::database::test_utils::SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE; + use crate::database::test_utils::{ + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + }; use crate::test_utils::database_utils::{ assert_create_table_stm_contains_all_parts, assert_table_exists, bring_db_0_back_to_life_and_return_connection, make_external_data, @@ -72,11 +93,17 @@ mod tests { .unwrap(); assert_table_exists(connection.as_ref(), "sent_payable"); + assert_table_exists(connection.as_ref(), "failed_payable"); assert_create_table_stm_contains_all_parts( &*connection, "sent_payable", SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, ); + assert_create_table_stm_contains_all_parts( + &*connection, + "failed_payable", + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, + ); TestLogHandler::new().assert_logs_contain_in_order(vec![ "DbMigrator: Database successfully migrated from version 10 to 11", ]); diff --git a/node/src/database/test_utils/mod.rs b/node/src/database/test_utils/mod.rs index 4251d1588..6e88e1292 100644 --- a/node/src/database/test_utils/mod.rs +++ b/node/src/database/test_utils/mod.rs @@ -19,9 +19,25 @@ pub const SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE: &[&[&str]] = &[ &["amount_high_b", "integer", "not", "null"], &["amount_low_b", "integer", "not", "null"], &["timestamp", "integer", "not", "null"], - &["gas_price_wei", "integer", "not", "null"], + &["gas_price_wei_high_b", "integer", "not", "null"], + &["gas_price_wei_low_b", "integer", "not", "null"], &["nonce", "integer", "not", "null"], - &["status", "text", "not", "null"], + &["block_hash", "text", "null"], + &["block_number", "integer", "null"], +]; + +pub const SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE: &[&[&str]] = &[ + &["rowid", "integer", "primary", "key"], + &["tx_hash", "text", "not", "null"], + &["receiver_address", "text", "not", "null"], + &["amount_high_b", "integer", "not", "null"], + &["amount_low_b", "integer", "not", "null"], + &["timestamp", "integer", "not", "null"], + &["gas_price_wei_high_b", "integer", "not", "null"], + &["gas_price_wei_low_b", "integer", "not", "null"], + &["nonce", "integer", "not", "null"], + &["reason", "text", "not", "null"], + &["rechecked", "integer", "not", "null"], ]; #[derive(Debug, Default)] @@ -126,26 +142,3 @@ impl DbInitializerMock { self } } - -#[cfg(test)] -mod tests { - use crate::database::test_utils::SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE; - - #[test] - fn constants_have_correct_values() { - assert_eq!( - SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, - &[ - &["rowid", "integer", "primary", "key"], - &["tx_hash", "text", "not", "null"], - &["receiver_address", "text", "not", "null"], - &["amount_high_b", "integer", "not", "null"], - &["amount_low_b", "integer", "not", "null"], - &["timestamp", "integer", "not", "null"], - &["gas_price_wei", "integer", "not", "null"], - &["nonce", "integer", "not", "null"], - &["status", "text", "not", "null"], - ] - ); - } -} diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index 9aa856fc2..ee56936f6 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -13,8 +13,8 @@ use std::collections::HashMap; // a system. They enable sending either a single message or multiple messages in response to // a specific trigger, which is just another Actor message arriving at the Recorder. // By trigger, we mean the moment when an incoming message is tested sequentially against collected -// identification methods and matches. Each counter-message must have its ID method attached when -// it is being prepared for storage in the Recorder. This bundle is called a setup. Each setup has +// identification methods and matches. Each counter-message must have its ID method attached when +// it is being prepared for storage in the Recorder. This bundle is called a setup. Each setup has // one ID method but can contain multiple counter-messages that are all sent when triggered. // Counter-messages can be independently customized and targeted at different actors by @@ -25,9 +25,9 @@ use std::collections::HashMap; // addresses are known. The setup for counter-messages must be registered with the appropriate // Recorder using a specially designated Actor message SetUpCounterMsgs. -// If a trigger message matches multiple counter-message setups, the triggered setup depends -// on the order in which setups are provided. Consider using MsgIdentification::ByMatch -// or MsgIdentification::ByPredicate instead of MsgIdentification::ByTypeId to avoid confusion +// If a trigger message matches multiple counter-message setups, the triggered setup depends +// on the order in which setups are provided. Consider using MsgIdentification::ByMatch +// or MsgIdentification::ByPredicate instead of MsgIdentification::ByTypeId to avoid confusion // about setup ordering. pub trait CounterMsgGear: Send { diff --git a/port_exposer/Cargo.lock b/port_exposer/Cargo.lock index 210c0de54..1a65f5fc1 100644 --- a/port_exposer/Cargo.lock +++ b/port_exposer/Cargo.lock @@ -20,7 +20,7 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "port_exposer" -version = "0.8.2" +version = "0.9.0" dependencies = [ "default-net", ] diff --git a/port_exposer/Cargo.toml b/port_exposer/Cargo.toml index a5eab68f0..703fa9813 100644 --- a/port_exposer/Cargo.toml +++ b/port_exposer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "port_exposer" -version = "0.8.2" +version = "0.9.0" authors = ["Dan Wiebe ", "MASQ"] license = "GPL-3.0-only" copyright = "Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved." From f565cb29883e19dd54ee03fe2f45dba373dfd9cb Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:42:16 +0200 Subject: [PATCH 05/37] GH-638: Proper gas price management for both payable modes (#659) * GH-638: ploughed all over, but the main structure has been planted; now let's get rid of 50 c. errors * GH-638: interim commit; nice progress though * GH-638: lots done; but the computations will be hrder to test * GH-638: little rearrangement in the project tree before I bring in more files * GH-638: transmission log repair * GH-638: transmission log repair and some other tests * GH-638: tests for gas price ceiling confrontations written * GH-638: single test fixed * GH-638: tests full - all and fixed * GH-638: lint and formatting * GH-638: self-review * mend * GH-638: added warning * GH-638: another comment res * GH-638: ref tests with a lot of created accounts * GH-638: fixed chain records with more constants * GH-638: review two addressed --------- Co-authored-by: Bert --- .../src/blockchains/blockchain_records.rs | 48 +- masq_lib/src/blockchains/chains.rs | 1 + masq_lib/src/constants.rs | 24 + .../mock_blockchain_client_server.rs | 2 +- node/src/accountant/mod.rs | 66 +- node/src/accountant/payment_adjuster.rs | 9 +- node/src/accountant/scanners/mod.rs | 97 ++- .../payable_scanner_extension/agent_null.rs | 195 ----- .../payable_scanner_extension/agent_web3.rs | 135 --- .../scanners/payable_scanner_extension/mod.rs | 3 - .../payable_scanner_extension/msgs.rs | 73 +- .../payable_scanner_extension/test_utils.rs | 33 +- node/src/accountant/scanners/test_utils.rs | 6 +- node/src/accountant/test_utils.rs | 35 +- .../blockchain/blockchain_agent/agent_web3.rs | 817 ++++++++++++++++++ .../blockchain_agent/mod.rs} | 14 +- node/src/blockchain/blockchain_bridge.rs | 163 ++-- .../blockchain_interface_web3/mod.rs | 154 +++- .../blockchain_interface_web3/utils.rs | 346 +++++--- .../blockchain/blockchain_interface/mod.rs | 8 +- .../blockchain_interface_initializer.rs | 61 +- node/src/blockchain/mod.rs | 1 + node/src/hopper/routing_service.rs | 121 ++- node/src/sub_lib/blockchain_bridge.rs | 11 +- 24 files changed, 1676 insertions(+), 747 deletions(-) delete mode 100644 node/src/accountant/scanners/payable_scanner_extension/agent_null.rs delete mode 100644 node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs create mode 100644 node/src/blockchain/blockchain_agent/agent_web3.rs rename node/src/{accountant/scanners/payable_scanner_extension/blockchain_agent.rs => blockchain/blockchain_agent/mod.rs} (74%) diff --git a/masq_lib/src/blockchains/blockchain_records.rs b/masq_lib/src/blockchains/blockchain_records.rs index cc1198afa..00ac1bb66 100644 --- a/masq_lib/src/blockchains/blockchain_records.rs +++ b/masq_lib/src/blockchains/blockchain_records.rs @@ -2,63 +2,75 @@ use crate::blockchains::chains::Chain; use crate::constants::{ - BASE_MAINNET_CONTRACT_CREATION_BLOCK, BASE_MAINNET_FULL_IDENTIFIER, - BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, BASE_SEPOLIA_FULL_IDENTIFIER, DEV_CHAIN_FULL_IDENTIFIER, - ETH_MAINNET_CONTRACT_CREATION_BLOCK, ETH_MAINNET_FULL_IDENTIFIER, + BASE_GAS_PRICE_CEILING_WEI, BASE_MAINNET_CHAIN_ID, BASE_MAINNET_CONTRACT_CREATION_BLOCK, + BASE_MAINNET_FULL_IDENTIFIER, BASE_SEPOLIA_CHAIN_ID, BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, + BASE_SEPOLIA_FULL_IDENTIFIER, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, + DEV_GAS_PRICE_CEILING_WEI, ETH_GAS_PRICE_CEILING_WEI, ETH_MAINNET_CHAIN_ID, + ETH_MAINNET_CONTRACT_CREATION_BLOCK, ETH_MAINNET_FULL_IDENTIFIER, ETH_ROPSTEN_CHAIN_ID, ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, ETH_ROPSTEN_FULL_IDENTIFIER, - MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, POLYGON_AMOY_CONTRACT_CREATION_BLOCK, - POLYGON_AMOY_FULL_IDENTIFIER, POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, - POLYGON_MAINNET_FULL_IDENTIFIER, + MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, POLYGON_AMOY_CHAIN_ID, + POLYGON_AMOY_CONTRACT_CREATION_BLOCK, POLYGON_AMOY_FULL_IDENTIFIER, + POLYGON_GAS_PRICE_CEILING_WEI, POLYGON_MAINNET_CHAIN_ID, + POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, POLYGON_MAINNET_FULL_IDENTIFIER, }; use ethereum_types::{Address, H160}; +// TODO these should probably be a static (it's a shame that we construct the data every time anew +// when we ask for the chain specs), and dynamic initialization should be allowed as well pub const CHAINS: [BlockchainRecord; 7] = [ BlockchainRecord { self_id: Chain::PolyMainnet, - num_chain_id: 137, + num_chain_id: POLYGON_MAINNET_CHAIN_ID, literal_identifier: POLYGON_MAINNET_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::EthMainnet, - num_chain_id: 1, + num_chain_id: ETH_MAINNET_CHAIN_ID, literal_identifier: ETH_MAINNET_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::BaseMainnet, - num_chain_id: 8453, + num_chain_id: BASE_MAINNET_CHAIN_ID, literal_identifier: BASE_MAINNET_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::BaseSepolia, - num_chain_id: 84532, + num_chain_id: BASE_SEPOLIA_CHAIN_ID, literal_identifier: BASE_SEPOLIA_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::PolyAmoy, - num_chain_id: 80002, + num_chain_id: POLYGON_AMOY_CHAIN_ID, literal_identifier: POLYGON_AMOY_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::EthRopsten, - num_chain_id: 3, + num_chain_id: ETH_ROPSTEN_CHAIN_ID, literal_identifier: ETH_ROPSTEN_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::Dev, - num_chain_id: 2, + num_chain_id: DEV_CHAIN_ID, literal_identifier: DEV_CHAIN_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: DEV_GAS_PRICE_CEILING_WEI, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, }, @@ -69,6 +81,7 @@ pub struct BlockchainRecord { pub self_id: Chain, pub num_chain_id: u64, pub literal_identifier: &'static str, + pub gas_price_safe_ceiling_minor: u128, pub contract: Address, pub contract_creation_block: u64, } @@ -115,7 +128,7 @@ const POLYGON_MAINNET_CONTRACT_ADDRESS: Address = H160([ mod tests { use super::*; use crate::blockchains::chains::chain_from_chain_identifier_opt; - use crate::constants::BASE_MAINNET_CONTRACT_CREATION_BLOCK; + use crate::constants::{BASE_MAINNET_CONTRACT_CREATION_BLOCK, WEIS_IN_GWEI}; use std::collections::HashSet; use std::iter::FromIterator; @@ -195,6 +208,7 @@ mod tests { num_chain_id: 1, self_id: examined_chain, literal_identifier: "eth-mainnet", + gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -211,6 +225,7 @@ mod tests { num_chain_id: 3, self_id: examined_chain, literal_identifier: "eth-ropsten", + gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, } @@ -227,6 +242,7 @@ mod tests { num_chain_id: 137, self_id: examined_chain, literal_identifier: "polygon-mainnet", + gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -243,6 +259,7 @@ mod tests { num_chain_id: 80002, self_id: examined_chain, literal_identifier: "polygon-amoy", + gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, } @@ -259,6 +276,7 @@ mod tests { num_chain_id: 8453, self_id: examined_chain, literal_identifier: "base-mainnet", + gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -275,6 +293,7 @@ mod tests { num_chain_id: 84532, self_id: examined_chain, literal_identifier: "base-sepolia", + gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, } @@ -291,6 +310,7 @@ mod tests { num_chain_id: 2, self_id: examined_chain, literal_identifier: "dev", + gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, } diff --git a/masq_lib/src/blockchains/chains.rs b/masq_lib/src/blockchains/chains.rs index b7733b842..b7061d899 100644 --- a/masq_lib/src/blockchains/chains.rs +++ b/masq_lib/src/blockchains/chains.rs @@ -141,6 +141,7 @@ mod tests { num_chain_id: 0, self_id: Chain::PolyMainnet, literal_identifier: "", + gas_price_safe_ceiling_minor: 0, contract: Default::default(), contract_creation_block: 0, } diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index 6beea5748..67338f5a3 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -19,6 +19,7 @@ pub const CURRENT_LOGFILE_NAME: &str = "MASQNode_rCURRENT.log"; pub const MASQ_PROMPT: &str = "masq> "; pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really +pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; pub const WALLET_ADDRESS_LENGTH: usize = 42; pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; @@ -94,6 +95,13 @@ pub const CENTRAL_DELIMITER: char = '@'; pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; //chains +pub const POLYGON_MAINNET_CHAIN_ID: u64 = 137; +pub const POLYGON_AMOY_CHAIN_ID: u64 = 80002; +pub const BASE_MAINNET_CHAIN_ID: u64 = 8453; +pub const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; +pub const ETH_MAINNET_CHAIN_ID: u64 = 1; +pub const ETH_ROPSTEN_CHAIN_ID: u64 = 3; +pub const DEV_CHAIN_ID: u64 = 2; const POLYGON_FAMILY: &str = "polygon"; const ETH_FAMILY: &str = "eth"; const BASE_FAMILY: &str = "base"; @@ -106,6 +114,10 @@ pub const ETH_ROPSTEN_FULL_IDENTIFIER: &str = concatcp!(ETH_FAMILY, LINK, "ropst pub const BASE_MAINNET_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, MAINNET); pub const BASE_SEPOLIA_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, "sepolia"); pub const DEV_CHAIN_FULL_IDENTIFIER: &str = "dev"; +pub const POLYGON_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; +pub const ETH_GAS_PRICE_CEILING_WEI: u128 = 100_000_000_000; +pub const BASE_GAS_PRICE_CEILING_WEI: u128 = 50_000_000_000; +pub const DEV_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; #[cfg(test)] mod tests { @@ -124,6 +136,7 @@ mod tests { assert_eq!(CURRENT_LOGFILE_NAME, "MASQNode_rCURRENT.log"); assert_eq!(MASQ_PROMPT, "masq> "); assert_eq!(DEFAULT_GAS_PRICE, 1); + assert_eq!(DEFAULT_GAS_PRICE_MARGIN, 30); assert_eq!(WALLET_ADDRESS_LENGTH, 42); assert_eq!(MASQ_TOTAL_SUPPLY, 37_500_000); assert_eq!(WEIS_IN_GWEI, 1_000_000_000); @@ -169,6 +182,13 @@ mod tests { assert_eq!(VALUE_EXCEEDS_ALLOWED_LIMIT, ACCOUNTANT_PREFIX | 3); assert_eq!(CENTRAL_DELIMITER, '@'); assert_eq!(CHAIN_IDENTIFIER_DELIMITER, ':'); + assert_eq!(POLYGON_MAINNET_CHAIN_ID, 137); + assert_eq!(POLYGON_AMOY_CHAIN_ID, 80002); + assert_eq!(BASE_MAINNET_CHAIN_ID, 8453); + assert_eq!(BASE_SEPOLIA_CHAIN_ID, 84532); + assert_eq!(ETH_MAINNET_CHAIN_ID, 1); + assert_eq!(ETH_ROPSTEN_CHAIN_ID, 3); + assert_eq!(DEV_CHAIN_ID, 2); assert_eq!(POLYGON_FAMILY, "polygon"); assert_eq!(ETH_FAMILY, "eth"); assert_eq!(BASE_FAMILY, "base"); @@ -180,6 +200,10 @@ mod tests { assert_eq!(ETH_ROPSTEN_FULL_IDENTIFIER, "eth-ropsten"); assert_eq!(BASE_SEPOLIA_FULL_IDENTIFIER, "base-sepolia"); assert_eq!(DEV_CHAIN_FULL_IDENTIFIER, "dev"); + assert_eq!(POLYGON_GAS_PRICE_CEILING_WEI, 200_000_000_000); + assert_eq!(ETH_GAS_PRICE_CEILING_WEI, 100_000_000_000); + assert_eq!(BASE_GAS_PRICE_CEILING_WEI, 50_000_000_000); + assert_eq!(DEV_GAS_PRICE_CEILING_WEI, 200_000_000_000); assert_eq!( CLIENT_REQUEST_PAYLOAD_CURRENT_VERSION, DataVersion { major: 0, minor: 1 } diff --git a/masq_lib/src/test_utils/mock_blockchain_client_server.rs b/masq_lib/src/test_utils/mock_blockchain_client_server.rs index 424a4433d..80df649be 100644 --- a/masq_lib/src/test_utils/mock_blockchain_client_server.rs +++ b/masq_lib/src/test_utils/mock_blockchain_client_server.rs @@ -220,7 +220,7 @@ impl MockBlockchainClientServer { Err(e) if e.kind() == ErrorKind::TimedOut => (), Err(e) => panic!("MBCS accept() failed: {:?}", e), }; - thread::sleep(Duration::from_millis(100)); + thread::sleep(Duration::from_millis(50)); }; drop(listener); conn.set_nonblocking(true).unwrap(); diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 24dbdcc68..7237110a8 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1231,7 +1231,7 @@ mod tests { use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_pending_payable_fingerprint, make_qualified_and_unqualified_payables, make_unpriced_qualified_payables_for_retry_mode, make_priced_qualified_payables, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; @@ -1286,6 +1286,7 @@ mod tests { use std::sync::Mutex; use std::time::{Duration, UNIX_EPOCH}; use std::vec; + use crate::accountant::scanners::payable_scanner_extension::msgs::UnpricedQualifiedPayables; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; @@ -1541,7 +1542,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &QualifiedPayablesMessage { - qualified_payables: vec![payable_account], + qualified_payables: UnpricedQualifiedPayables::from(vec![payable_account]), consuming_wallet, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1631,9 +1632,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 accounts = vec![account_1, account_2]; + let qualified_payables = make_priced_qualified_payables(vec![ + (account_1, 1_000_000_001), + (account_2, 1_000_000_002), + ]); let msg = BlockchainAgentWithContextMessage { - qualified_payables: accounts.clone(), + qualified_payables: qualified_payables.clone(), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1649,7 +1653,7 @@ mod tests { is_adjustment_required_params.remove(0); assert_eq!( blockchain_agent_with_context_msg_actual.qualified_payables, - accounts.clone() + qualified_payables.clone() ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -1668,7 +1672,10 @@ mod tests { let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let payments_instructions = blockchain_bridge_recording.get_record::(0); - assert_eq!(payments_instructions.affordable_accounts, accounts); + assert_eq!( + payments_instructions.affordable_accounts, + qualified_payables + ); assert_eq!( payments_instructions.response_skeleton_opt, Some(ResponseSkeleton { @@ -1682,8 +1689,8 @@ mod tests { ); assert_eq!(blockchain_bridge_recording.len(), 1); assert_using_the_same_logger(&logger_clone, test_name, None) - // adjust_payments() did not need a prepared result, which means it wasn't reached - // because otherwise this test would've panicked + // The adjust_payments() function doesn't require prepared results, indicating it shouldn't + // have been reached during the test, or it would have caused a panic. } fn assert_using_the_same_logger( @@ -1732,8 +1739,10 @@ mod tests { let agent_id_stamp_first_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_first_phase); - let initial_unadjusted_accounts = - vec![unadjusted_account_1.clone(), unadjusted_account_2.clone()]; + let initial_unadjusted_accounts = make_priced_qualified_payables(vec![ + (unadjusted_account_1.clone(), 111_222_333), + (unadjusted_account_2.clone(), 222_333_444), + ]); let msg = BlockchainAgentWithContextMessage { qualified_payables: initial_unadjusted_accounts.clone(), agent: Box::new(agent), @@ -1744,7 +1753,10 @@ 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 = vec![adjusted_account_1.clone(), adjusted_account_2.clone()]; + let affordable_accounts = make_priced_qualified_payables(vec![ + (adjusted_account_1.clone(), 111_222_333), + (adjusted_account_2.clone(), 222_333_444), + ]); let payments_instructions = OutboundPaymentsInstructions { affordable_accounts: affordable_accounts.clone(), agent: Box::new(agent), @@ -1791,7 +1803,7 @@ mod tests { ); assert!( before <= captured_now && captured_now <= after, - "captured timestamp should have been between {:?} and {:?} but was {:?}", + "timestamp should be between {:?} and {:?} but was {:?}", before, after, captured_now @@ -2223,7 +2235,7 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - qualified_payables, + qualified_payables: UnpricedQualifiedPayables::from(qualified_payables), consuming_wallet, response_skeleton_opt: None, } @@ -2299,7 +2311,10 @@ mod tests { let consuming_wallet = make_wallet("abc"); subject.consuming_wallet_opt = Some(consuming_wallet.clone()); let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: vec![make_payable_account(789)], + qualified_payables: make_unpriced_qualified_payables_for_retry_mode(vec![ + (make_payable_account(789), 111_222_333), + (make_payable_account(888), 222_333_444), + ]), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; @@ -2706,9 +2721,13 @@ mod tests { 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: vec![make_payable_account(123)], + qualified_payables: make_unpriced_qualified_payables_for_retry_mode(vec![( + make_payable_account(123), + 555_666_777, + )]), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, })) @@ -3463,10 +3482,13 @@ 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 qualified_payable = vec![payable_account.clone()]; + 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 consuming_wallet = make_paying_wallet(b"consuming"); let counter_msg_1 = BlockchainAgentWithContextMessage { - qualified_payables: qualified_payable.clone(), + qualified_payables: priced_qualified_payables.clone(), agent: Box::new(BlockchainAgentMock::default()), response_skeleton_opt: None, }; @@ -3498,7 +3520,7 @@ mod tests { response_skeleton_opt: None, }; let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: qualified_payable.clone(), + qualified_payables: unpriced_qualified_payables, consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; @@ -3569,7 +3591,7 @@ mod tests { blockchain_bridge_recording.get_record::(1); assert_eq!( actual_outbound_payment_instructions_msg.affordable_accounts, - vec![payable_account] + priced_qualified_payables ); let actual_requested_receipts_1 = blockchain_bridge_recording.get_record::(2); @@ -3849,7 +3871,7 @@ mod tests { }); let now = to_unix_timestamp(SystemTime::now()); let qualified_payables = vec![ - // slightly above the minimum balance, to the right of the curve (time intersection) + // Slightly above the minimum balance, to the right of the curve (time intersection) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei( @@ -3864,7 +3886,7 @@ mod tests { ), pending_payable_opt: None, }, - // slightly above the curve (balance intersection), to the right of minimum time + // Slightly above the curve (balance intersection), to the right of minimum time PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), @@ -3905,7 +3927,7 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - qualified_payables, + qualified_payables: UnpricedQualifiedPayables::from(qualified_payables), consuming_wallet, response_skeleton_opt: None, } diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index 74c88690b..5062fc1ab 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -73,19 +73,18 @@ 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; + use crate::accountant::test_utils::{make_payable_account, make_priced_qualified_payables}; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; #[test] fn search_for_indispensable_adjustment_always_returns_none() { init_test_logging(); - let test_name = "is_adjustment_required_always_returns_none"; - let mut payable = make_payable_account(111); - payable.balance_wei = 100_000_000; + 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: vec![payable], + qualified_payables: make_priced_qualified_payables(vec![(payable, 111_111_111)]), 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 349ffe3df..cbd844bc4 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -45,7 +45,7 @@ use time::OffsetDateTime; use variant_count::VariantCount; use web3::types::H256; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; @@ -499,7 +499,7 @@ impl StartableScanner for PayableS "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(), @@ -1395,7 +1395,7 @@ mod tests { PendingPayable, PendingPayableDaoError, TransactionHashes, }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; @@ -1641,10 +1641,17 @@ 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::>(), + }; assert_eq!( result, Ok(QualifiedPayablesMessage { - qualified_payables: qualified_payable_accounts.clone(), + qualified_payables: expected_unpriced_qualified_payables, consuming_wallet, response_skeleton_opt: None, }) @@ -1653,7 +1660,7 @@ mod tests { &format!("INFO: {test_name}: Scanning for new payables"), &format!( "INFO: {test_name}: Chose {} qualified debts to pay", - qualified_payable_accounts.len() + qualified_payables_count ), ]) } @@ -1730,44 +1737,48 @@ mod tests { #[test] fn retry_payable_scanner_can_initiate_a_scan() { - 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: qualified_payable_accounts.clone(), - 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() - ), - ]) + 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() + // ), + // ]) } #[test] diff --git a/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs deleted file mode 100644 index 5f9811204..000000000 --- a/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; - -use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; -use crate::sub_lib::wallet::Wallet; -use ethereum_types::U256; -use masq_lib::blockchains::chains::Chain; -use masq_lib::logger::Logger; -use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; - -#[derive(Clone)] -pub struct BlockchainAgentNull { - wallet: Wallet, - logger: Logger, -} - -impl BlockchainAgent for BlockchainAgentNull { - fn estimated_transaction_fee_total(&self, _number_of_transactions: usize) -> u128 { - self.log_function_call("estimated_transaction_fee_total()"); - 0 - } - - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { - self.log_function_call("consuming_wallet_balances()"); - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::zero(), - masq_token_balance_in_minor_units: U256::zero(), - } - } - - fn agreed_fee_per_computation_unit(&self) -> u128 { - self.log_function_call("agreed_fee_per_computation_unit()"); - 0 - } - - fn consuming_wallet(&self) -> &Wallet { - self.log_function_call("consuming_wallet()"); - &self.wallet - } - - fn get_chain(&self) -> Chain { - self.log_function_call("get_chain()"); - TEST_DEFAULT_CHAIN - } - - #[cfg(test)] - fn dup(&self) -> Box { - intentionally_blank!() - } - - #[cfg(test)] - as_any_ref_in_trait_impl!(); -} - -impl BlockchainAgentNull { - pub fn new() -> Self { - Self { - wallet: Wallet::null(), - logger: Logger::new("BlockchainAgentNull"), - } - } - - fn log_function_call(&self, function_call: &str) { - error!( - self.logger, - "calling null version of {function_call} for BlockchainAgentNull will be without effect", - ); - } -} - -impl Default for BlockchainAgentNull { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::scanners::payable_scanner_extension::agent_null::BlockchainAgentNull; - use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; - - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; - use crate::sub_lib::wallet::Wallet; - - use masq_lib::logger::Logger; - use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; - use web3::types::U256; - - fn blockchain_agent_null_constructor_works(constructor: C) - where - C: Fn() -> BlockchainAgentNull, - { - init_test_logging(); - - let result = constructor(); - - assert_eq!(result.wallet, Wallet::null()); - warning!(result.logger, "blockchain_agent_null_constructor_works"); - TestLogHandler::default().exists_log_containing( - "WARN: BlockchainAgentNull: \ - blockchain_agent_null_constructor_works", - ); - } - - #[test] - fn blockchain_agent_null_constructor_works_for_new() { - blockchain_agent_null_constructor_works(BlockchainAgentNull::new) - } - - #[test] - fn blockchain_agent_null_constructor_works_for_default() { - blockchain_agent_null_constructor_works(BlockchainAgentNull::default) - } - - fn assert_error_log(test_name: &str, expected_operation: &str) { - TestLogHandler::default().exists_log_containing(&format!( - "ERROR: {test_name}: calling \ - null version of {expected_operation}() for BlockchainAgentNull \ - will be without effect" - )); - } - - #[test] - fn null_agent_estimated_transaction_fee_total() { - init_test_logging(); - let test_name = "null_agent_estimated_transaction_fee_total"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.estimated_transaction_fee_total(4); - - assert_eq!(result, 0); - assert_error_log(test_name, "estimated_transaction_fee_total"); - } - - #[test] - fn null_agent_consuming_wallet_balances() { - init_test_logging(); - let test_name = "null_agent_consuming_wallet_balances"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.consuming_wallet_balances(); - - assert_eq!( - result, - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::zero(), - masq_token_balance_in_minor_units: U256::zero() - } - ); - assert_error_log(test_name, "consuming_wallet_balances") - } - - #[test] - fn null_agent_agreed_fee_per_computation_unit() { - init_test_logging(); - let test_name = "null_agent_agreed_fee_per_computation_unit"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.agreed_fee_per_computation_unit(); - - assert_eq!(result, 0); - assert_error_log(test_name, "agreed_fee_per_computation_unit") - } - - #[test] - fn null_agent_consuming_wallet() { - init_test_logging(); - let test_name = "null_agent_consuming_wallet"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.consuming_wallet(); - - assert_eq!(result, &Wallet::null()); - assert_error_log(test_name, "consuming_wallet") - } - - #[test] - fn null_agent_get_chain() { - init_test_logging(); - let test_name = "null_agent_get_chain"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.get_chain(); - - assert_eq!(result, TEST_DEFAULT_CHAIN); - assert_error_log(test_name, "get_chain") - } -} diff --git a/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs deleted file mode 100644 index 8acf40ef8..000000000 --- a/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; -use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; -use crate::sub_lib::wallet::Wallet; -use masq_lib::blockchains::chains::Chain; - -#[derive(Debug, Clone)] -pub struct BlockchainAgentWeb3 { - gas_price_wei: u128, - gas_limit_const_part: u128, - maximum_added_gas_margin: u128, - consuming_wallet: Wallet, - consuming_wallet_balances: ConsumingWalletBalances, - chain: Chain, -} - -impl BlockchainAgent for BlockchainAgentWeb3 { - fn estimated_transaction_fee_total(&self, number_of_transactions: usize) -> u128 { - let gas_price = self.gas_price_wei; - let max_gas_limit = self.maximum_added_gas_margin + self.gas_limit_const_part; - number_of_transactions as u128 * gas_price * max_gas_limit - } - - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { - self.consuming_wallet_balances - } - - fn agreed_fee_per_computation_unit(&self) -> u128 { - self.gas_price_wei - } - - fn consuming_wallet(&self) -> &Wallet { - &self.consuming_wallet - } - - fn get_chain(&self) -> Chain { - self.chain - } -} - -// 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; - -impl BlockchainAgentWeb3 { - pub fn new( - gas_price_wei: u128, - gas_limit_const_part: u128, - consuming_wallet: Wallet, - consuming_wallet_balances: ConsumingWalletBalances, - chain: Chain, - ) -> Self { - Self { - gas_price_wei, - gas_limit_const_part, - consuming_wallet, - maximum_added_gas_margin: WEB3_MAXIMAL_GAS_LIMIT_MARGIN, - consuming_wallet_balances, - chain, - } - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::scanners::payable_scanner_extension::agent_web3::{ - BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, - }; - use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; - use crate::test_utils::make_wallet; - use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; - use web3::types::U256; - - #[test] - fn constants_are_correct() { - assert_eq!(WEB3_MAXIMAL_GAS_LIMIT_MARGIN, 3_328) - } - - #[test] - fn blockchain_agent_can_return_non_computed_input_values() { - let gas_price_gwei = 123; - let gas_limit_const_part = 44_000; - let consuming_wallet = make_wallet("abcde"); - let consuming_wallet_balances = ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::from(456_789), - masq_token_balance_in_minor_units: U256::from(123_000_000), - }; - - let subject = BlockchainAgentWeb3::new( - gas_price_gwei, - gas_limit_const_part, - consuming_wallet.clone(), - consuming_wallet_balances, - TEST_DEFAULT_CHAIN, - ); - - assert_eq!(subject.agreed_fee_per_computation_unit(), gas_price_gwei); - assert_eq!(subject.consuming_wallet(), &consuming_wallet); - assert_eq!( - subject.consuming_wallet_balances(), - consuming_wallet_balances - ); - assert_eq!(subject.get_chain(), TEST_DEFAULT_CHAIN); - } - - #[test] - fn estimated_transaction_fee_works() { - let consuming_wallet = make_wallet("efg"); - let consuming_wallet_balances = ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: Default::default(), - masq_token_balance_in_minor_units: Default::default(), - }; - let agent = BlockchainAgentWeb3::new( - 444, - 77_777, - consuming_wallet, - consuming_wallet_balances, - TEST_DEFAULT_CHAIN, - ); - - let result = agent.estimated_transaction_fee_total(3); - - assert_eq!(agent.gas_limit_const_part, 77_777); - assert_eq!( - agent.maximum_added_gas_margin, - WEB3_MAXIMAL_GAS_LIMIT_MARGIN - ); - assert_eq!( - result, - (3 * (77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN)) as u128 * 444 - ); - } -} diff --git a/node/src/accountant/scanners/payable_scanner_extension/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs index 649bc820f..1d1e8cb0b 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -1,8 +1,5 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod agent_null; -pub mod agent_web3; -pub mod blockchain_agent; pub mod msgs; pub mod test_utils; diff --git a/node/src/accountant/scanners/payable_scanner_extension/msgs.rs b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs index 599f17390..1e9dbe59d 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs @@ -1,22 +1,85 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; 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: Vec, + 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: Vec, + qualified_payables: UnpricedQualifiedPayables, consuming_wallet: Wallet, response_skeleton_opt: Option, ) -> Self { @@ -36,14 +99,14 @@ impl SkeletonOptHolder for QualifiedPayablesMessage { #[derive(Message)] pub struct BlockchainAgentWithContextMessage { - pub qualified_payables: Vec, + pub qualified_payables: PricedQualifiedPayables, pub agent: Box, pub response_skeleton_opt: Option, } impl BlockchainAgentWithContextMessage { pub fn new( - qualified_payables: Vec, + qualified_payables: PricedQualifiedPayables, agent: Box, response_skeleton_opt: Option, ) -> Self { diff --git a/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs index def16f20e..b8e83b78d 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs @@ -2,7 +2,10 @@ #![cfg(test)] -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, UnpricedQualifiedPayables, +}; +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; @@ -12,7 +15,7 @@ use std::cell::RefCell; pub struct BlockchainAgentMock { consuming_wallet_balances_results: RefCell>, - agreed_fee_per_computation_unit_results: RefCell>, + gas_price_results: RefCell>, consuming_wallet_result_opt: Option, arbitrary_id_stamp_opt: Option, get_chain_result_opt: Option, @@ -22,7 +25,7 @@ impl Default for BlockchainAgentMock { fn default() -> Self { BlockchainAgentMock { consuming_wallet_balances_results: RefCell::new(vec![]), - agreed_fee_per_computation_unit_results: RefCell::new(vec![]), + gas_price_results: RefCell::new(vec![]), consuming_wallet_result_opt: None, arbitrary_id_stamp_opt: None, get_chain_result_opt: None, @@ -31,18 +34,22 @@ impl Default for BlockchainAgentMock { } impl BlockchainAgent for BlockchainAgentMock { - fn estimated_transaction_fee_total(&self, _number_of_transactions: usize) -> u128 { - todo!("to be implemented by GH-711") + fn price_qualified_payables( + &self, + _qualified_payables: UnpricedQualifiedPayables, + ) -> PricedQualifiedPayables { + unimplemented!("not needed yet") } - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { + fn estimate_transaction_fee_total( + &self, + _qualified_payables: &PricedQualifiedPayables, + ) -> u128 { todo!("to be implemented by GH-711") } - fn agreed_fee_per_computation_unit(&self) -> u128 { - self.agreed_fee_per_computation_unit_results - .borrow_mut() - .remove(0) + fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { + todo!("to be implemented by GH-711") } fn consuming_wallet(&self) -> &Wallet { @@ -68,10 +75,8 @@ impl BlockchainAgentMock { self } - pub fn agreed_fee_per_computation_unit_result(self, result: u128) -> Self { - self.agreed_fee_per_computation_unit_results - .borrow_mut() - .push(result); + pub fn gas_price_result(self, result: u128) -> Self { + self.gas_price_results.borrow_mut().push(result); self } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 26aa15dc3..2445ff565 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -23,7 +23,7 @@ use crate::accountant::{ SentPayables, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; -use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, OutboundPaymentsInstructions}; use crate::sub_lib::wallet::Wallet; use actix::{Message, System}; use itertools::Either; @@ -488,3 +488,7 @@ impl RescheduleScanOnErrorResolverMock { self } } + +pub fn make_zeroed_consuming_wallet_balances() -> ConsumingWalletBalances { + ConsumingWalletBalances::new(0.into(), 0.into()) +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index e0e5a6cdd..a186ff016 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -16,7 +16,10 @@ use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, }; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayableWithGasPrice, + QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, +}; use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; @@ -1497,3 +1500,33 @@ 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/blockchain/blockchain_agent/agent_web3.rs b/node/src/blockchain/blockchain_agent/agent_web3.rs new file mode 100644 index 000000000..8899a0743 --- /dev/null +++ b/node/src/blockchain/blockchain_agent/agent_web3.rs @@ -0,0 +1,817 @@ +// 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::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 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 { + logger: Logger, + latest_gas_price_wei: u128, + gas_limit_const_part: u128, + consuming_wallet: Wallet, + consuming_wallet_balances: ConsumingWalletBalances, + chain: Chain, +} + +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, + } + } + + 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(); + (self.gas_limit_const_part + WEB3_MAXIMAL_GAS_LIMIT_MARGIN) * prices_sum + } + + fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { + self.consuming_wallet_balances + } + + fn consuming_wallet(&self) -> &Wallet { + &self.consuming_wallet + } + + fn get_chain(&self) -> Chain { + self.chain + } +} + +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; + +impl BlockchainAgentWeb3 { + pub fn new( + latest_gas_price_wei: u128, + gas_limit_const_part: u128, + consuming_wallet: Wallet, + consuming_wallet_balances: ConsumingWalletBalances, + chain: Chain, + ) -> BlockchainAgentWeb3 { + Self { + logger: Logger::new("BlockchainAgentWeb3"), + latest_gas_price_wei, + gas_limit_const_part, + consuming_wallet, + consuming_wallet_balances, + 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::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::blockchain::blockchain_agent::agent_web3::{ + BlockchainAgentWeb3, GasPriceAboveLimitWarningReporter, NewPayableWarningData, + RetryPayableWarningData, 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 masq_lib::blockchains::chains::Chain; + use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; + use thousands::Separable; + + #[test] + fn constants_are_correct() { + assert_eq!(WEB3_MAXIMAL_GAS_LIMIT_MARGIN, 3_328) + } + + #[test] + fn returns_correct_priced_qualified_payables_for_new_payable_scan() { + init_test_logging(); + let test_name = "returns_correct_priced_qualified_payables_for_new_payable_scan"; + let consuming_wallet = make_wallet("efg"); + 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 rpc_gas_price_wei = 555_666_777; + let chain = TEST_DEFAULT_CHAIN; + let mut subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = + subject.price_qualified_payables(unpriced_qualified_payables); + + 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}")); + } + + #[test] + fn returns_correct_priced_qualified_payables_for_retry_payable_scan() { + init_test_logging(); + let test_name = "returns_correct_priced_qualified_payables_for_retry_payable_scan"; + 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![ + rpc_gas_price_wei - 1, + rpc_gas_price_wei, + rpc_gas_price_wei + 1, + rpc_gas_price_wei - 123_456, + rpc_gas_price_wei + 456_789, + ] + .into_iter() + .enumerate() + .map(|(idx, previous_attempt_gas_price_wei)| { + let account = make_payable_account((idx as u64 + 1) * 3_000); + QualifiedPayablesBeforeGasPriceSelection::new( + account, + Some(previous_attempt_gas_price_wei), + ) + }) + .collect_vec(); + UnpricedQualifiedPayables { payables } + }; + 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, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = + subject.price_qualified_payables(unpriced_qualified_payables); + + let expected_result = { + let price_wei_for_accounts_from_1_to_5 = vec![ + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei + 1), + 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() { + panic!("Corrupted test") + } + PricedQualifiedPayables { + payables: accounts_from_1_to_5 + .into_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) + }) + .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)); + } + + #[test] + fn new_payables_gas_price_ceiling_test_if_latest_price_is_a_border_value() { + let test_name = "new_payables_gas_price_ceiling_test_if_latest_price_is_a_border_value"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + // This should be the value that would surplus the ceiling just slightly if the margin is + // applied. + // Adding just 1 didn't work, therefore 2 + 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); + + test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name, + chain, + rpc_gas_price_wei, + 50_000_000_001, + ); + + assert!( + check_value_wei > ceiling_gas_price_wei, + "should be {} > {} but isn't", + check_value_wei, + ceiling_gas_price_wei + ); + } + + #[test] + fn new_payables_gas_price_ceiling_test_if_latest_price_is_a_bit_bigger_even_with_no_margin() { + let test_name = "new_payables_gas_price_ceiling_test_if_latest_price_is_a_bit_bigger_even_with_no_margin"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + + test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name, + chain, + ceiling_gas_price_wei + 1, + 65_000_000_001, + ); + } + + #[test] + fn new_payables_gas_price_ceiling_test_if_latest_price_is_just_gigantic() { + let test_name = "new_payables_gas_price_ceiling_test_if_latest_price_is_just_gigantic"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + + test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name, + chain, + 10 * ceiling_gas_price_wei, + 650_000_000_000, + ); + } + + fn test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name: &str, + chain: Chain, + rpc_gas_price_wei: u128, + expected_calculated_surplus_value_wei: u128, + ) { + init_test_logging(); + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + let consuming_wallet = make_wallet("efg"); + 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 mut subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + + 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); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Calculated gas price {} wei for txs to {}, {} is over the spend \ + limit {} wei.", + expected_calculated_surplus_value_wei.separate_with_commas(), + account_1.wallet, + account_2.wallet, + chain + .rec() + .gas_price_safe_ceiling_minor + .separate_with_commas() + )); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_border_value_if_the_latest_fetch_being_bigger() { + let test_name = "retry_payables_gas_price_ceiling_test_of_border_value_if_the_latest_fetch_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + // This should be the value that would surplus the ceiling just slightly if the margin is + // applied. + // Adding just 1 didn't work, therefore 2 + 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"; + + 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, + ); + + assert!( + check_value_wei > ceiling_gas_price_wei, + "should be {} > {} but isn't", + check_value_wei, + ceiling_gas_price_wei + ); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_border_value_if_the_previous_attempt_being_bigger() + { + let test_name = "retry_payables_gas_price_ceiling_test_of_border_value_if_the_previous_attempt_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + // This should be the value that would surplus the ceiling just slightly if the margin is applied + let border_gas_price_wei = + (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"; + + 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, + ); + assert!(check_value_wei > ceiling_gas_price_wei); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_big_value_if_the_latest_fetch_being_bigger() { + let test_name = + "retry_payables_gas_price_ceiling_test_of_big_value_if_the_latest_fetch_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + 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"; + + 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, + ); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_big_value_if_the_previous_attempt_being_bigger() { + let test_name = "retry_payables_gas_price_ceiling_test_of_big_value_if_the_previous_attempt_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + 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"; + + 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, + ); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_giant_value_for_the_latest_fetch() { + let test_name = "retry_payables_gas_price_ceiling_test_of_giant_value_for_the_latest_fetch"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + let fetched_gas_price_wei = 10 * ceiling_gas_price_wei; + let account_1 = make_payable_account(12); + 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"; + + 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, + ); + } + + fn test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( + test_name: &str, + chain: Chain, + rpc_gas_price_wei: u128, + qualified_payables: UnpricedQualifiedPayables, + expected_surpluses_wallet_and_wei_as_text: &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 mut subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + + 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() + )); + } + + #[test] + fn returns_correct_non_computed_values() { + let gas_limit_const_part = 44_000; + let consuming_wallet = make_wallet("abcde"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + + let subject = BlockchainAgentWeb3::new( + 222_333_444, + gas_limit_const_part, + consuming_wallet.clone(), + consuming_wallet_balances, + TEST_DEFAULT_CHAIN, + ); + + assert_eq!(subject.consuming_wallet(), &consuming_wallet); + assert_eq!( + subject.consuming_wallet_balances(), + consuming_wallet_balances + ); + assert_eq!(subject.get_chain(), TEST_DEFAULT_CHAIN); + } + + #[test] + fn estimate_transaction_fee_total_works_for_new_payable() { + let consuming_wallet = make_wallet("efg"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + 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 subject = BlockchainAgentWeb3::new( + 444_555_666, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + + let result = subject.estimate_transaction_fee_total(&priced_qualified_payables); + + assert_eq!( + result, + (2 * (77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN)) + * increase_gas_price_by_margin(444_555_666) + ); + } + + #[test] + fn estimate_transaction_fee_total_works_for_retry_payable() { + 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![ + rpc_gas_price_wei - 1, + rpc_gas_price_wei, + rpc_gas_price_wei + 1, + rpc_gas_price_wei - 123_456, + rpc_gas_price_wei + 456_789, + ] + .into_iter() + .enumerate() + .map(|(idx, previous_attempt_gas_price_wei)| { + let account = make_payable_account((idx as u64 + 1) * 3_000); + QualifiedPayablesBeforeGasPriceSelection::new( + account, + Some(previous_attempt_gas_price_wei), + ) + }) + .collect_vec(); + UnpricedQualifiedPayables { payables } + }; + let subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + let priced_qualified_payables = + subject.price_qualified_payables(unpriced_qualified_payables); + + let result = subject.estimate_transaction_fee_total(&priced_qualified_payables); + + let gas_prices_for_accounts_from_1_to_5 = vec![ + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei + 1), + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei + 456_789), + ]; + let expected_result = gas_prices_for_accounts_from_1_to_5 + .into_iter() + .sum::() + * (77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN); + assert_eq!(result, expected_result) + } + + #[test] + fn blockchain_agent_web3_logs_with_right_name() { + let test_name = "blockchain_agent_web3_logs_with_right_name"; + let subject = BlockchainAgentWeb3::new( + 0, + 0, + make_wallet("abcde"), + make_zeroed_consuming_wallet_balances(), + TEST_DEFAULT_CHAIN, + ); + + info!(subject.logger, "{}", test_name); + + TestLogHandler::new() + .exists_log_containing(&format!("INFO: BlockchainAgentWeb3: {}", test_name)); + } +} diff --git a/node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs b/node/src/blockchain/blockchain_agent/mod.rs similarity index 74% rename from node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs rename to node/src/blockchain/blockchain_agent/mod.rs index 2f2af4015..fb8030a09 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs +++ b/node/src/blockchain/blockchain_agent/mod.rs @@ -1,10 +1,14 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod agent_web3; + +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, UnpricedQualifiedPayables, +}; use crate::arbitrary_id_stamp_in_trait; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use masq_lib::blockchains::chains::Chain; - // Table of chains by // // a) adoption of the fee market (variations on "gas price") @@ -22,11 +26,13 @@ use masq_lib::blockchains::chains::Chain; //* defaulted limit pub trait BlockchainAgent: Send { - fn estimated_transaction_fee_total(&self, number_of_transactions: usize) -> u128; + fn price_qualified_payables( + &self, + qualified_payables: UnpricedQualifiedPayables, + ) -> PricedQualifiedPayables; + fn estimate_transaction_fee_total(&self, qualified_payables: &PricedQualifiedPayables) -> u128; fn consuming_wallet_balances(&self) -> ConsumingWalletBalances; - fn agreed_fee_per_computation_unit(&self) -> u128; fn consuming_wallet(&self) -> &Wallet; - fn get_chain(&self) -> Chain; #[cfg(test)] diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index b93d8770c..e427ab934 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,8 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, -}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, PricedQualifiedPayables}; use crate::accountant::{ ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, @@ -44,9 +42,9 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use ethabi::Hash; use web3::types::H256; +use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::messages::ScanType; -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; @@ -263,11 +261,13 @@ impl BlockchainBridge { let accountant_recipient = self.payable_payments_setup_subs_opt.clone(); Box::new( self.blockchain_interface - .build_blockchain_agent(incoming_message.consuming_wallet) + .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( - incoming_message.qualified_payables, + priced_qualified_payables, agent, incoming_message.response_skeleton_opt, ); @@ -485,7 +485,7 @@ impl BlockchainBridge { fn process_payments( &self, agent: Box, - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { let new_fingerprints_recipient = self.new_fingerprints_recipient(); @@ -535,6 +535,10 @@ struct PendingTxInfo { when_sent: SystemTime, } +pub fn increase_gas_price_by_margin(gas_price: u128) -> u128 { + (gas_price * (100 + DEFAULT_GAS_PRICE_MARGIN as u128)) / 100 +} + pub struct BlockchainBridgeSubsFactoryReal {} impl SubsFactory for BlockchainBridgeSubsFactoryReal { @@ -549,10 +553,8 @@ mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; + use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint, make_priced_qualified_payables}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainAgentBuildError, PayableTransactionError, @@ -595,6 +597,7 @@ mod tests { use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160}; use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; + use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, QualifiedPayableWithGasPrice}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt}; impl Handler> for BlockchainBridge { @@ -676,15 +679,14 @@ mod tests { } #[test] - fn qualified_payables_msg_is_handled_and_new_msg_with_an_added_blockchain_agent_returns_to_accountant( - ) { + fn handles_qualified_payables_msg_in_new_payables_mode_and_sends_response_back_to_accountant() { let system = System::new( - "qualified_payables_msg_is_handled_and_new_msg_with_an_added_blockchain_agent_returns_to_accountant", - ); + "handles_qualified_payables_msg_in_new_payables_mode_and_sends_response_back_to_accountant"); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) - .ok_response("0x230000000".to_string(), 1) // 9395240960 - .ok_response("0x23".to_string(), 1) + // Fetching a recommended gas price + .ok_response("0x230000000".to_string(), 1) + .ok_response("0xAAAA".to_string(), 1) .ok_response( "0x000000000000000000000000000000000000000000000000000000000000FFFF".to_string(), 0, @@ -721,8 +723,10 @@ mod tests { false, ); 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: qualified_payables.clone(), + qualified_payables: unpriced_qualified_payables.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -737,42 +741,33 @@ mod tests { System::current().stop(); system.run(); - let accountant_received_payment = accountant_recording_arc.lock().unwrap(); let blockchain_agent_with_context_msg_actual: &BlockchainAgentWithContextMessage = 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(), + }; assert_eq!( blockchain_agent_with_context_msg_actual.qualified_payables, - qualified_payables - ); - assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .consuming_wallet(), - &consuming_wallet + expected_priced_qualified_payables ); + let actual_agent = blockchain_agent_with_context_msg_actual.agent.as_ref(); + assert_eq!(actual_agent.consuming_wallet(), &consuming_wallet); assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .agreed_fee_per_computation_unit(), - 0x230000000 - ); - assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .consuming_wallet_balances(), - ConsumingWalletBalances::new( - 35.into(), - 0x000000000000000000000000000000000000000000000000000000000000FFFF.into() - ) + actual_agent.consuming_wallet_balances(), + ConsumingWalletBalances::new(0xAAAA.into(), 0xFFFF.into()) ); - let gas_limit_const_part = - BlockchainInterfaceWeb3::web3_gas_limit_const_part(Chain::PolyMainnet); assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .estimated_transaction_fee_total(1), - (1 * 0x230000000 * (gas_limit_const_part + WEB3_MAXIMAL_GAS_LIMIT_MARGIN)) + actual_agent.estimate_transaction_fee_total( + &actual_agent.price_qualified_payables(unpriced_qualified_payables) + ), + 1_791_228_995_698_688 ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -785,9 +780,10 @@ mod tests { } #[test] - fn qualified_payables_msg_is_handled_but_fails_on_build_blockchain_agent() { - let system = - System::new("qualified_payables_msg_is_handled_but_fails_on_build_blockchain_agent"); + fn qualified_payables_msg_is_handled_but_fails_on_introduce_blockchain_agent() { + let system = System::new( + "qualified_payables_msg_is_handled_but_fails_on_introduce_blockchain_agent", + ); let port = find_free_port(); // build blockchain agent fails by not providing the third response. let _blockchain_client_server = MBCSBuilder::new(port) @@ -804,8 +800,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: vec![make_payable_account(123)], + qualified_payables, consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -820,7 +817,6 @@ mod tests { System::current().stop(); system.run(); - let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 0); let service_fee_balance_error = BlockchainAgentBuildError::ServiceFeeBalance( @@ -839,10 +835,10 @@ mod tests { } #[test] - fn handle_outbound_payments_instructions_sees_payments_happen_and_sends_payment_results_back_to_accountant( + fn handle_outbound_payments_instructions_sees_payment_happen_and_sends_payment_results_back_to_accountant( ) { let system = System::new( - "handle_outbound_payments_instructions_sees_payments_happen_and_sends_payment_results_back_to_accountant", + "handle_outbound_payments_instructions_sees_payment_happen_and_sends_payment_results_back_to_accountant", ); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) @@ -868,16 +864,15 @@ mod tests { let subject_subs = BlockchainBridge::make_subs_from(&addr); let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); - let accounts = vec![PayableAccount { + let account = PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, - }]; + }; let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) - .agreed_fee_per_computation_unit_result(123) .consuming_wallet_result(consuming_wallet) .get_chain_result(Chain::PolyMainnet); @@ -885,7 +880,10 @@ mod tests { let _ = addr .try_send(OutboundPaymentsInstructions { - affordable_accounts: accounts.clone(), + affordable_accounts: make_priced_qualified_payables(vec![( + account.clone(), + 111_222_333, + )]), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -905,9 +903,9 @@ mod tests { sent_payables_msg, &SentPayables { payment_procedure_result: Ok(vec![Correct(PendingPayable { - recipient_wallet: accounts[0].wallet.clone(), + recipient_wallet: account.wallet, hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) .unwrap() })]), @@ -923,10 +921,10 @@ mod tests { pending_payable_fingerprint_seeds_msg.hashes_and_balances, vec![HashAndAmount { hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) .unwrap(), - amount: accounts[0].balance_wei + amount: account.balance_wei }] ); assert_eq!(accountant_recording.len(), 2); @@ -958,22 +956,25 @@ mod tests { let subject_subs = BlockchainBridge::make_subs_from(&addr); let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); - let accounts = vec![PayableAccount { + let account = PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, - }]; + }; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let agent = BlockchainAgentMock::default() .consuming_wallet_result(consuming_wallet) - .agreed_fee_per_computation_unit_result(123) + .gas_price_result(123) .get_chain_result(Chain::PolyMainnet); send_bind_message!(subject_subs, peer_actors); let _ = addr .try_send(OutboundPaymentsInstructions { - affordable_accounts: accounts.clone(), + affordable_accounts: make_priced_qualified_payables(vec![( + account.clone(), + 111_222_333, + )]), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -999,10 +1000,10 @@ mod tests { pending_payable_fingerprint_seeds_msg.hashes_and_balances, vec![HashAndAmount { hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) .unwrap(), - amount: accounts[0].balance_wei + amount: account.balance_wei }] ); assert_eq!( @@ -1014,7 +1015,7 @@ mod tests { context_id: 4321 }), msg: format!( - "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions: 0x36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) } ); @@ -1036,13 +1037,17 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let accounts_1 = make_payable_account(1); let accounts_2 = make_payable_account(2); - let accounts = vec![accounts_1.clone(), accounts_2.clone()]; + let affordable_qualified_payables = make_priced_qualified_payables(vec![ + (accounts_1.clone(), 777_777_777), + (accounts_2.clone(), 999_999_999), + ]); let system = System::new(test_name); let agent = BlockchainAgentMock::default() .consuming_wallet_result(consuming_wallet) - .agreed_fee_per_computation_unit_result(1) + .gas_price_result(1) .get_chain_result(Chain::PolyMainnet); - let msg = OutboundPaymentsInstructions::new(accounts, Box::new(agent), None); + let msg = + OutboundPaymentsInstructions::new(affordable_qualified_payables, Box::new(agent), None); let persistent_config = PersistentConfigurationMock::new(); let mut subject = BlockchainBridge::new( Box::new(blockchain_interface_web3), @@ -1066,7 +1071,7 @@ mod tests { Correct(PendingPayable { recipient_wallet: accounts_1.wallet, hash: H256::from_str( - "cc73f3d5fe9fc3dac28b510ddeb157b0f8030b201e809014967396cdf365488a" + "c0756e8da662cee896ed979456c77931668b7f8456b9f978fc3305671f8f82ad" ) .unwrap() }) @@ -1076,7 +1081,7 @@ mod tests { Correct(PendingPayable { recipient_wallet: accounts_2.wallet, hash: H256::from_str( - "891d9ffa838aedc0bb2f6f7e9737128ce98bb33d07b4c8aa5645871e20d6cd13" + "9ba19f88ce43297d700b1f57ed8bc6274d01a5c366b78dd05167f9874c867ba0" ) .unwrap() }) @@ -1098,8 +1103,12 @@ mod tests { let agent = BlockchainAgentMock::default() .get_chain_result(TEST_DEFAULT_CHAIN) .consuming_wallet_result(consuming_wallet) - .agreed_fee_per_computation_unit_result(123); - let msg = OutboundPaymentsInstructions::new(vec![], Box::new(agent), None); + .gas_price_result(123); + let msg = OutboundPaymentsInstructions::new( + make_priced_qualified_payables(vec![(make_payable_account(111), 111_000_000)]), + Box::new(agent), + None, + ); let persistent_config = configure_default_persistent_config(ZERO); let mut subject = BlockchainBridge::new( Box::new(blockchain_interface_web3), @@ -2224,6 +2233,12 @@ mod tests { assert_on_initialization_with_panic_on_migration(&data_dir, &act); } + + #[test] + fn increase_gas_price_by_margin_works() { + assert_eq!(increase_gas_price_by_margin(1_000_000_000), 1_300_000_000); + assert_eq!(increase_gas_price_by_margin(9_000_000_000), 11_700_000_000); + } } #[cfg(test)] 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 852b02a4e..81c7fe62d 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,6 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -21,11 +20,18 @@ use actix::Recipient; use ethereum_types::U64; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, PricedQualifiedPayables}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{LowBlockchainIntWeb3, TransactionReceiptResult, TxReceipt, TxStatus}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::utils::{create_blockchain_agent_web3, send_payables_within_batch, BlockchainAgentFutureResult}; +// TODO We should probably begin to attach these constants to the interfaces more tightly, so that +// we aren't baffled by which interface they belong with. I suggest to declare them inside +// their inherent impl blocks. They will then need to be preceded by the class name +// of the respective interface if you want to use them. This could be a distinction we desire, +// despite the increased wordiness. + const CONTRACT_ABI: &str = indoc!( r#"[{ "constant":true, @@ -156,7 +162,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { ) } - fn build_blockchain_agent( + fn introduce_blockchain_agent( &self, consuming_wallet: Wallet, ) -> Box, Error = BlockchainAgentBuildError>> { @@ -193,8 +199,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { masq_token_balance, }; Ok(create_blockchain_agent_web3( - gas_limit_const_part, blockchain_agent_future_result, + gas_limit_const_part, consuming_wallet, chain, )) @@ -246,7 +252,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { logger: Logger, agent: Box, fingerprints_recipient: Recipient, - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { let consuming_wallet = agent.consuming_wallet().clone(); @@ -254,7 +260,6 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { let get_transaction_id = self .lower_interface() .get_transaction_id(consuming_wallet.address()); - let gas_price_wei = agent.agreed_fee_per_computation_unit(); let chain = agent.get_chain(); Box::new( @@ -266,7 +271,6 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { chain, &web3_batch, consuming_wallet, - gas_price_wei, pending_nonce, fingerprints_recipient, affordable_accounts, @@ -430,7 +434,6 @@ impl BlockchainInterfaceWeb3 { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, @@ -441,9 +444,7 @@ mod tests { BlockchainAgentBuildError, BlockchainError, BlockchainInterface, RetrievedBlockchainTransactions, }; - use crate::blockchain::test_utils::{ - all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder, - }; + use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; @@ -459,6 +460,9 @@ mod tests { use std::str::FromStr; use web3::transports::Http; use web3::types::{H256, U256}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayableWithGasPrice}; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; #[test] @@ -831,11 +835,96 @@ mod tests { } #[test] - fn blockchain_interface_web3_can_build_blockchain_agent() { + 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 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_estimated_transaction_fee_total = 190_652_800_000_000; + + test_blockchain_interface_web3_can_introduce_blockchain_agent( + unpriced_qualified_payables, + gas_price_wei_from_rpc_hex, + expected_priced_qualified_payables, + 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 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, + expected_estimated_transaction_fee_total, + ); + } + + fn test_blockchain_interface_web3_can_introduce_blockchain_agent( + unpriced_qualified_payables: UnpricedQualifiedPayables, + gas_price_wei_from_rpc_hex: &str, + expected_priced_qualified_payables: PricedQualifiedPayables, + expected_estimated_transaction_fee_total: u128, + ) { let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) // gas_price - .ok_response("0x3B9ACA00".to_string(), 0) // 1000000000 + .ok_response(gas_price_wei_from_rpc_hex.to_string(), 0) // transaction_fee_balance .ok_response("0xFFF0".to_string(), 0) // 65520 // masq_balance @@ -844,18 +933,16 @@ mod tests { 0, ) .start(); - let chain = Chain::PolyMainnet; let wallet = make_wallet("abc"); let subject = make_blockchain_interface_web3(port); let result = subject - .build_blockchain_agent(wallet.clone()) + .introduce_blockchain_agent(wallet.clone()) .wait() .unwrap(); let expected_transaction_fee_balance = U256::from(65_520); let expected_masq_balance = U256::from(65_535); - let expected_gas_price_wei = 1_000_000_000; assert_eq!(result.consuming_wallet(), &wallet); assert_eq!( result.consuming_wallet_balances(), @@ -864,17 +951,15 @@ mod tests { masq_token_balance_in_minor_units: expected_masq_balance } ); + let priced_qualified_payables = + result.price_qualified_payables(unpriced_qualified_payables); assert_eq!( - result.agreed_fee_per_computation_unit(), - expected_gas_price_wei + priced_qualified_payables, + expected_priced_qualified_payables ); - let expected_fee_estimation = (3 - * (BlockchainInterfaceWeb3::web3_gas_limit_const_part(chain) - + WEB3_MAXIMAL_GAS_LIMIT_MARGIN) - * expected_gas_price_wei) as u128; assert_eq!( - result.estimated_transaction_fee_total(3), - expected_fee_estimation + result.estimate_transaction_fee_total(&priced_qualified_payables), + expected_estimated_transaction_fee_total ) } @@ -886,7 +971,9 @@ mod tests { { let wallet = make_wallet("bcd"); let subject = make_blockchain_interface_web3(port); - let result = subject.build_blockchain_agent(wallet.clone()).wait(); + + let result = subject.introduce_blockchain_agent(wallet.clone()).wait(); + let err = match result { Err(e) => e, _ => panic!("we expected Err() but got Ok()"), @@ -899,15 +986,16 @@ mod tests { fn build_of_the_blockchain_agent_fails_on_fetching_gas_price() { let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); - let wallet = make_wallet("abc"); - let subject = make_blockchain_interface_web3(port); - - let err = subject.build_blockchain_agent(wallet).wait().err().unwrap(); + let expected_err_factory = |_wallet: &Wallet| { + BlockchainAgentBuildError::GasPrice(QueryFailed( + "Transport error: Error(IncompleteMessage)".to_string(), + )) + }; - let expected_err = BlockchainAgentBuildError::GasPrice(QueryFailed( - "Transport error: Error(IncompleteMessage)".to_string(), - )); - assert_eq!(err, expected_err) + build_of_the_blockchain_agent_fails_on_blockchain_interface_error( + port, + expected_err_factory, + ); } #[test] 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 7172987f4..00489febc 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -2,8 +2,9 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; -use crate::accountant::scanners::payable_scanner_extension::agent_web3::BlockchainAgentWeb3; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, @@ -17,6 +18,7 @@ use crate::sub_lib::wallet::Wallet; use actix::Recipient; 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; @@ -85,34 +87,59 @@ pub fn merged_output_data( pub fn transmission_log( chain: Chain, - accounts: &[PayableAccount], - gas_price_in_wei: u128, + qualified_payables: &PricedQualifiedPayables, + lowest_nonce_used: U256, ) -> String { - let chain_name = chain - .rec() - .literal_identifier - .chars() - .skip_while(|char| char != &'-') - .skip(1) - .collect::(); + 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 introduction = once(format!( - "\ - Paying to creditors...\n\ - Transactions in the batch:\n\ + "\n\ + Paying creditors\n\ + Transactions:\n\ \n\ - gas price: {} wei\n\ - chain: {}\n\ + {:first_column_width$} {}\n\ + {:first_column_width$} {}...{}\n\ \n\ - [wallet address] [payment in wei]\n", - gas_price_in_wei, chain_name + {:first_column_width$} {: SignedTransaction { let data = sign_transaction_data(amount, recipient_wallet); let gas_limit = gas_limit(data, chain); - // Warning: If you set gas_price or nonce to None in transaction_parameters, sign_transaction will start making RPC calls which we don't want (Do it at your own risk). + // 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), to: Some(chain.rec().contract), @@ -215,12 +243,12 @@ pub fn sign_and_append_multiple_payments( chain: Chain, web3_batch: &Web3>, consuming_wallet: Wallet, - gas_price_in_wei: u128, mut pending_nonce: U256, - accounts: &[PayableAccount], + accounts: &PricedQualifiedPayables, ) -> Vec { let mut hash_and_amount_list = vec![]; - accounts.iter().for_each(|payable| { + accounts.payables.iter().for_each(|payable_pack| { + let payable = &payable_pack.payable; debug!( logger, "Preparing payable future of {} wei to {} with nonce {}", @@ -235,7 +263,7 @@ pub fn sign_and_append_multiple_payments( payable, consuming_wallet.clone(), pending_nonce, - gas_price_in_wei, + payable_pack.gas_price_minor, ); pending_nonce = advance_used_nonce(pending_nonce); @@ -250,19 +278,17 @@ pub fn send_payables_within_batch( chain: Chain, web3_batch: &Web3>, consuming_wallet: Wallet, - gas_price_in_wei: u128, pending_nonce: U256, new_fingerprints_recipient: Recipient, - accounts: Vec, + accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError> + 'static> { debug!( logger, - "Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}, gas_price: {}", + "Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", consuming_wallet, chain.rec().contract, chain.rec().num_chain_id, - gas_price_in_wei ); let hashes_and_paid_amounts = sign_and_append_multiple_payments( @@ -270,7 +296,6 @@ pub fn send_payables_within_batch( chain, web3_batch, consuming_wallet, - gas_price_in_wei, pending_nonce, &accounts, ); @@ -279,7 +304,6 @@ pub fn send_payables_within_batch( let hashes_and_paid_amounts_error = hashes_and_paid_amounts.clone(); let hashes_and_paid_amounts_ok = hashes_and_paid_amounts.clone(); - // TODO: We are sending hashes_and_paid_amounts to the Accountant even if the payments fail. new_fingerprints_recipient .try_send(PendingPayableFingerprintSeeds { batch_wide_timestamp: timestamp, @@ -290,7 +314,7 @@ pub fn send_payables_within_batch( info!( logger, "{}", - transmission_log(chain, &accounts, gas_price_in_wei) + transmission_log(chain, &accounts, pending_nonce) ); Box::new( @@ -302,27 +326,30 @@ pub fn send_payables_within_batch( Ok(merged_output_data( batch_response, hashes_and_paid_amounts_ok, - accounts, + accounts.into(), )) }), ) } pub fn create_blockchain_agent_web3( - gas_limit_const_part: u128, blockchain_agent_future_result: BlockchainAgentFutureResult, + gas_limit_const_part: u128, wallet: Wallet, chain: Chain, ) -> Box { + let transaction_fee_balance_in_minor_units = + blockchain_agent_future_result.transaction_fee_balance; + let masq_token_balance_in_minor_units = blockchain_agent_future_result.masq_token_balance; + let cons_wallet_balances = ConsumingWalletBalances::new( + transaction_fee_balance_in_minor_units, + masq_token_balance_in_minor_units, + ); Box::new(BlockchainAgentWeb3::new( blockchain_agent_future_result.gas_price_wei.as_u128(), gas_limit_const_part, wallet, - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: blockchain_agent_future_result - .transaction_fee_balance, - masq_token_balance_in_minor_units: blockchain_agent_future_result.masq_token_balance, - }, + cons_wallet_balances, chain, )) } @@ -332,11 +359,12 @@ mod tests { use super::*; use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::gwei_to_wei; - use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::test_utils::{ make_payable_account, make_payable_account_with_wallet_and_balance_and_timestamp_opt, + make_priced_qualified_payables, }; 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, }; @@ -360,7 +388,6 @@ mod tests { 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; - use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use masq_lib::utils::find_free_port; use serde_json::Value; use std::net::Ipv4Addr; @@ -431,19 +458,20 @@ mod tests { .unwrap(); let web3_batch = Web3::new(Batch::new(transport)); let chain = DEFAULT_CHAIN; - let gas_price_in_gwei = DEFAULT_GAS_PRICE; 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 = vec![account_1, account_2]; + let accounts = make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 222_222_222), + ]); let result = sign_and_append_multiple_payments( &logger, chain, &web3_batch, consuming_wallet, - gwei_to_wei(gas_price_in_gwei), pending_nonce.into(), &accounts, ); @@ -453,14 +481,14 @@ mod tests { vec![ HashAndAmount { hash: H256::from_str( - "94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2" + "374b7d023f4ac7d99e612d82beda494b0747116e9b9dc975b33b865f331ee934" ) .unwrap(), amount: 1000000000 }, HashAndAmount { hash: H256::from_str( - "3811874d2b73cecd51234c94af46bcce918d0cb4de7d946c01d7da606fe761b5" + "5708afd876bc2573f9db984ec6d0e7f8ef222dd9f115643c9b9056d8bef8bbd9" ) .unwrap(), amount: 2000000000 @@ -470,49 +498,115 @@ mod tests { } #[test] - fn transmission_log_just_works() { - init_test_logging(); - let test_name = "transmission_log_just_works"; - let gas_price = 120; - let logger = Logger::new(test_name); - let amount_1 = gwei_to_wei(900_000_000_u64); - let account_1 = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - make_wallet("w123"), - amount_1, - None, - ); - let amount_2 = 123_456_789_u128; - let account_2 = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - make_wallet("w555"), - amount_2, - None, - ); - let amount_3 = gwei_to_wei(33_355_666_u64); - let account_3 = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - make_wallet("w987"), - amount_3, - None, + fn transmission_log_is_well_formatted() { + // This test only focuses on the formatting, but there are other tests asserting printing + // this in the logs + + // Case 1 + let payments = [ + gwei_to_wei(900_000_000_u64), + 123_456_789_u128, + gwei_to_wei(33_355_666_u64), + ]; + let pending_nonce = 123456789.into(); + let expected_format = "\n\ + Paying creditors\n\ + Transactions:\n\ + \n\ + chain: base-sepolia\n\ + nonces: 123,456,789...123,456,791\n\ + \n\ + [wallet address] [payment wei] [gas price wei]\n\ + 0x0000000000000000000000000077616c6c657430 900,000,000,000,000,000 246,913,578\n\ + 0x0000000000000000000000000077616c6c657431 123,456,789 493,827,156\n\ + 0x0000000000000000000000000077616c6c657432 33,355,666,000,000,000 740,740,734\n"; + + test_transmission_log( + 1, + payments, + Chain::BaseSepolia, + pending_nonce, + expected_format, ); - let accounts_to_process = vec![account_1, account_2, account_3]; - info!( - logger, - "{}", - transmission_log(TEST_DEFAULT_CHAIN, &accounts_to_process, gas_price) + // Case 2 + let payments = [ + gwei_to_wei(5_400_u64), + gwei_to_wei(10_000_u64), + 44_444_555_u128, + ]; + let pending_nonce = 100.into(); + let expected_format = "\n\ + Paying creditors\n\ + Transactions:\n\ + \n\ + chain: eth-mainnet\n\ + nonces: 100...102\n\ + \n\ + [wallet address] [payment wei] [gas price wei]\n\ + 0x0000000000000000000000000077616c6c657430 5,400,000,000,000 246,913,578\n\ + 0x0000000000000000000000000077616c6c657431 10,000,000,000,000 493,827,156\n\ + 0x0000000000000000000000000077616c6c657432 44,444,555 740,740,734\n"; + + test_transmission_log( + 2, + payments, + Chain::EthMainnet, + pending_nonce, + expected_format, ); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "INFO: transmission_log_just_works: Paying to creditors...\n\ - Transactions in the batch:\n\ + // Case 3 + let payments = [45_000_888, 1_999_999, 444_444_555]; + let pending_nonce = 1.into(); + let expected_format = "\n\ + Paying creditors\n\ + Transactions:\n\ \n\ - gas price: 120 wei\n\ - chain: sepolia\n\ + chain: polygon-mainnet\n\ + nonces: 1...3\n\ \n\ - [wallet address] [payment in wei]\n\ - 0x0000000000000000000000000000000077313233 900,000,000,000,000,000\n\ - 0x0000000000000000000000000000000077353535 123,456,789\n\ - 0x0000000000000000000000000000000077393837 33,355,666,000,000,000\n", + [wallet address] [payment wei] [gas price wei]\n\ + 0x0000000000000000000000000077616c6c657430 45,000,888 246,913,578\n\ + 0x0000000000000000000000000077616c6c657431 1,999,999 493,827,156\n\ + 0x0000000000000000000000000077616c6c657432 444,444,555 740,740,734\n"; + + test_transmission_log( + 3, + payments, + Chain::PolyMainnet, + pending_nonce, + expected_format, + ); + } + + fn test_transmission_log( + case: usize, + payments: [u128; 3], + chain: Chain, + pending_nonce: U256, + expected_result: &str, + ) { + let accounts_to_process_seeds = payments + .iter() + .enumerate() + .map(|(i, payment)| { + 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) + }) + .collect(); + let accounts_to_process = make_priced_qualified_payables(accounts_to_process_seeds); + + let result = transmission_log(chain, &accounts_to_process, pending_nonce); + + assert_eq!( + result, expected_result, + "Test case {}: we expected this format: \"{}\", but it was: \"{}\"", + case, expected_result, result ); } @@ -573,9 +667,9 @@ mod tests { ) } - fn execute_send_payables_test( + fn test_send_payables_within_batch( test_name: &str, - accounts: Vec, + accounts: PricedQualifiedPayables, expected_result: Result, PayableTransactionError>, port: u16, ) { @@ -585,7 +679,6 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let gas_price = 1_000_000_000; let pending_nonce: U256 = 1.into(); let web3_batch = Web3::new(Batch::new(transport)); let (accountant, _, accountant_recording) = make_recorder(); @@ -601,7 +694,6 @@ mod tests { chain, &web3_batch, consuming_wallet.clone(), - gas_price, pending_nonce, new_fingerprints_recipient, accounts.clone(), @@ -619,23 +711,23 @@ mod tests { assert!(timestamp_after >= ppfs_message.batch_wide_timestamp); let tlh = TestLogHandler::new(); tlh.exists_log_containing( - &format!("DEBUG: {test_name}: Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}, gas_price: {}", + &format!("DEBUG: {test_name}: Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", consuming_wallet, chain.rec().contract, chain.rec().num_chain_id, - gas_price ) ); tlh.exists_log_containing(&format!( "INFO: {test_name}: {}", - transmission_log(chain, &accounts, gas_price) + transmission_log(chain, &accounts, pending_nonce) )); assert_eq!(result, expected_result); } #[test] fn send_payables_within_batch_works() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + let account_1 = make_payable_account(1); + let account_2 = make_payable_account(2); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() @@ -646,24 +738,27 @@ mod tests { .start(); let expected_result = Ok(vec![ Correct(PendingPayable { - recipient_wallet: accounts[0].wallet.clone(), + recipient_wallet: account_1.wallet.clone(), hash: H256::from_str( - "35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4", + "6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2", ) .unwrap(), }), Correct(PendingPayable { - recipient_wallet: accounts[1].wallet.clone(), + recipient_wallet: account_2.wallet.clone(), hash: H256::from_str( - "7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3", + "b67a61b29c0c48d8b63a64fda73b3247e8e2af68082c710325675d4911e113d4", ) .unwrap(), }), ]); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_works", - accounts, + make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 222_222_222), + ]), expected_result, port, ); @@ -671,19 +766,22 @@ mod tests { #[test] fn send_payables_within_batch_fails_on_submit_batch_call() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + 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 expected_result = Err(Sending { msg: format!("Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", os_code, os_msg).to_string(), hashes: vec![ - H256::from_str("35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4").unwrap(), - H256::from_str("7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3").unwrap() + H256::from_str("ec7ac48060b75889f949f5e8d301b386198218e60e2635c95cb6b0934a0887ea").unwrap(), + H256::from_str("c2d5059db0ec2fbf15f83d9157eeb0d793d6242de5e73a607935fb5660e7e925").unwrap() ], }); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_fails_on_submit_batch_call", accounts, expected_result, @@ -693,7 +791,8 @@ mod tests { #[test] fn send_payables_within_batch_all_payments_fail() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + let account_1 = make_payable_account(1); + let account_2 = make_payable_account(2); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() @@ -718,8 +817,8 @@ mod tests { message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), data: None, }), - recipient_wallet: accounts[0].wallet.clone(), - hash: H256::from_str("35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4").unwrap(), + recipient_wallet: account_1.wallet.clone(), + hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -727,14 +826,17 @@ mod tests { message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), data: None, }), - recipient_wallet: accounts[1].wallet.clone(), - hash: H256::from_str("7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3").unwrap(), + recipient_wallet: account_2.wallet.clone(), + hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), }), ]); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_all_payments_fail", - accounts, + make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 111_111_111), + ]), expected_result, port, ); @@ -742,7 +844,8 @@ mod tests { #[test] fn send_payables_within_batch_one_payment_works_the_other_fails() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + let account_1 = make_payable_account(1); + let account_2 = make_payable_account(2); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() @@ -757,8 +860,8 @@ mod tests { .start(); let expected_result = Ok(vec![ Correct(PendingPayable { - recipient_wallet: accounts[0].wallet.clone(), - hash: H256::from_str("35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4").unwrap(), + recipient_wallet: account_1.wallet.clone(), + hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -766,14 +869,17 @@ mod tests { message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), data: None, }), - recipient_wallet: accounts[1].wallet.clone(), - hash: H256::from_str("7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3").unwrap(), + recipient_wallet: account_2.wallet.clone(), + hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), }), ]); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_one_payment_works_the_other_fails", - accounts, + make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 111_111_111), + ]), expected_result, port, ); diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index bdcbf6a91..242bf433f 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,6 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -15,7 +14,8 @@ use futures::Future; use masq_lib::blockchains::chains::Chain; use web3::types::Address; use masq_lib::logger::Logger; -use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -33,7 +33,7 @@ pub trait BlockchainInterface { recipient: Address, ) -> Box>; - fn build_blockchain_agent( + fn introduce_blockchain_agent( &self, consuming_wallet: Wallet, ) -> Box, Error = BlockchainAgentBuildError>>; @@ -48,7 +48,7 @@ pub trait BlockchainInterface { logger: Logger, agent: Box, fingerprints_recipient: Recipient, - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>>; as_any_ref_in_trait!(); diff --git a/node/src/blockchain/blockchain_interface_initializer.rs b/node/src/blockchain/blockchain_interface_initializer.rs index ee87519a0..d7f452311 100644 --- a/node/src/blockchain/blockchain_interface_initializer.rs +++ b/node/src/blockchain/blockchain_interface_initializer.rs @@ -10,8 +10,9 @@ use web3::transports::Http; pub(in crate::blockchain) struct BlockchainInterfaceInitializer {} impl BlockchainInterfaceInitializer { - // TODO when we have multiple chains of fundamentally different architectures and are able to switch them, - // this should probably be replaced by a HashMap of distinct interfaces for each chain + // TODO if we ever have multiple chains of fundamentally different architectures and are able + // to switch them, this should probably be replaced by a HashMap of distinct interfaces for + // each chain pub fn initialize_interface( &self, blockchain_service_url: &str, @@ -43,24 +44,25 @@ impl BlockchainInterfaceInitializer { #[cfg(test)] mod tests { - use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; - use masq_lib::blockchains::chains::Chain; - - use futures::Future; - use std::net::Ipv4Addr; - use web3::transports::Http; - - use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ - BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, }; - use crate::blockchain::blockchain_interface::BlockchainInterface; + 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 masq_lib::blockchains::chains::Chain; use masq_lib::constants::DEFAULT_CHAIN; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::utils::find_free_port; + use std::net::Ipv4Addr; #[test] fn initialize_web3_interface_works() { + // TODO this test should definitely assert on the web3 requests sent to the server, + // that's the best way to verify that this interface belongs to the web3 architecture + // (This test amplifies the importance of GH-543) let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .ok_response("0x3B9ACA00".to_string(), 0) // gas_price = 10000000000 @@ -71,22 +73,37 @@ mod tests { ) .ok_response("0x23".to_string(), 1) .start(); - let wallet = make_wallet("123"); let chain = Chain::PolyMainnet; let server_url = &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port); - let (event_loop_handle, transport) = - Http::with_max_parallel(server_url, REQUESTS_IN_PARALLEL).unwrap(); - let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, chain); - let blockchain_agent = subject - .build_blockchain_agent(wallet.clone()) + let result = BlockchainInterfaceInitializer {}.initialize_interface(server_url, chain); + + 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 payable_wallet = make_wallet("payable"); + let blockchain_agent = result + .introduce_blockchain_agent(payable_wallet.clone()) .wait() .unwrap(); - - assert_eq!(blockchain_agent.consuming_wallet(), &wallet); + assert_eq!(blockchain_agent.consuming_wallet(), &payable_wallet); + let priced_qualified_payables = + blockchain_agent.price_qualified_payables(unpriced_qualified_payables); + 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), + ], + }; + assert_eq!( + priced_qualified_payables, + expected_priced_qualified_payables + ); assert_eq!( - blockchain_agent.agreed_fee_per_computation_unit(), - 1_000_000_000 + blockchain_agent.estimate_transaction_fee_total(&priced_qualified_payables), + 190_652_800_000_000 ); } diff --git a/node/src/blockchain/mod.rs b/node/src/blockchain/mod.rs index 4c51e726e..48698c408 100644 --- a/node/src/blockchain/mod.rs +++ b/node/src/blockchain/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod bip32; pub mod bip39; +pub mod blockchain_agent; pub mod blockchain_bridge; pub mod blockchain_interface; pub mod blockchain_interface_initializer; diff --git a/node/src/hopper/routing_service.rs b/node/src/hopper/routing_service.rs index 05d8af9d6..478441159 100644 --- a/node/src/hopper/routing_service.rs +++ b/node/src/hopper/routing_service.rs @@ -529,6 +529,7 @@ mod tests { use masq_lib::test_utils::environment_guard::EnvironmentGuard; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; + use std::fmt::format; use std::net::SocketAddr; use std::str::FromStr; use std::time::SystemTime; @@ -593,6 +594,7 @@ mod tests { #[test] fn logs_and_ignores_message_that_cannot_be_deserialized() { init_test_logging(); + let test_name = "logs_and_ignores_message_that_cannot_be_deserialized"; let cryptdes = make_cryptde_pair(); let route = route_from_proxy_client(&cryptdes.main.public_key(), cryptdes.main); let lcp = LiveCoresPackage::new( @@ -610,7 +612,7 @@ mod tests { data: data_enc.into(), }; let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( cryptdes, RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -624,17 +626,19 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Couldn't expire CORES package with 35-byte payload to ProxyClient using main key", + &format!("ERROR: {test_name}: Couldn't expire CORES package with 35-byte payload to ProxyClient using main key"), ); } #[test] fn logs_and_ignores_message_that_cannot_be_decrypted() { init_test_logging(); + let test_name = "logs_and_ignores_message_that_cannot_be_decrypted"; let (main_cryptde, alias_cryptde) = { //initialization to real CryptDEs let pair = Bootstrapper::pub_initialize_cryptdes_for_testing(&None, &None); @@ -657,7 +661,7 @@ mod tests { data: data_enc.into(), }; let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -674,11 +678,12 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Couldn't expire CORES package with 51-byte payload to ProxyClient using main key: DecryptionError(OpeningFailed)", + &format!("ERROR: {test_name}: Couldn't expire CORES package with 51-byte payload to ProxyClient using main key: DecryptionError(OpeningFailed)") ); } @@ -809,6 +814,7 @@ mod tests { let _eg = EnvironmentGuard::new(); init_test_logging(); BAN_CACHE.clear(); + let test_name = "complains_about_live_message_for_nonexistent_proxy_client"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let route = route_to_proxy_client(&main_cryptde.public_key(), main_cryptde); @@ -838,7 +844,7 @@ mod tests { let system = System::new("converts_live_message_to_expired_for_proxy_client"); let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -855,6 +861,7 @@ mod tests { 0, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -862,7 +869,7 @@ mod tests { system.run(); let tlh = TestLogHandler::new(); tlh.exists_no_log_containing("Couldn't decode CORES package in 8-byte buffer"); - tlh.exists_log_containing("WARN: RoutingService: Received CORES package from 1.2.3.4:5678 for Proxy Client, but Proxy Client isn't running"); + tlh.exists_log_containing(&format!("WARN: {test_name}: Received CORES package from 1.2.3.4:5678 for Proxy Client, but Proxy Client isn't running")); } #[test] @@ -1281,6 +1288,8 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = + "route_logs_and_ignores_cores_package_that_demands_routing_without_paying_wallet"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let origin_key = PublicKey::new(&[1, 2]); @@ -1313,9 +1322,7 @@ mod tests { sequence_number: None, data: data_enc.into(), }; - let system = System::new( - "route_logs_and_ignores_cores_package_that_demands_routing_without_paying_wallet", - ); + let system = System::new(test_name); let (proxy_client, _, proxy_client_recording_arc) = make_recorder(); let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); @@ -1328,7 +1335,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1345,13 +1352,14 @@ mod tests { 200, true, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_matching( - "WARN: RoutingService: Refusing to route Live CORES package with \\d+-byte payload without paying wallet", + &format!("WARN: {test_name}: Refusing to route Live CORES package with \\d+-byte payload without paying wallet"), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1366,6 +1374,7 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = "route_logs_and_ignores_cores_package_that_demands_proxy_client_routing_with_paying_wallet_that_cant_pay"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let public_key = main_cryptde.public_key(); @@ -1414,9 +1423,7 @@ mod tests { sequence_number: None, data: data_enc.into(), }; - let system = System::new( - "route_logs_and_ignores_cores_package_that_demands_proxy_client_routing_with_paying_wallet_that_cant_pay", - ); + let system = System::new(test_name); let (proxy_client, _, proxy_client_recording_arc) = make_recorder(); let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); @@ -1429,7 +1436,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1446,13 +1453,14 @@ mod tests { 200, true, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_matching( - "WARN: RoutingService: Refusing to route Expired CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership.", + &format!("WARN: {test_name}: Refusing to route Expired CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership."), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1467,6 +1475,7 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = "route_logs_and_ignores_cores_package_that_demands_hopper_routing_with_paying_wallet_that_cant_pay"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let current_key = main_cryptde.public_key(); @@ -1512,9 +1521,7 @@ mod tests { encodex(main_cryptde, &destination_key, &payload).unwrap(), ); - let system = System::new( - "route_logs_and_ignores_cores_package_that_demands_hopper_routing_with_paying_wallet_that_cant_pay", - ); + let system = System::new(test_name); let (proxy_client, _, proxy_client_recording_arc) = make_recorder(); let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); @@ -1527,7 +1534,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1544,6 +1551,7 @@ mod tests { 200, true, ); + subject.logger = Logger::new(test_name); subject.route_data_externally( lcp, @@ -1554,7 +1562,7 @@ mod tests { System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_matching( - "WARN: RoutingService: Refusing to route Live CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership.", + &format!("WARN: {test_name}: Refusing to route Live CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership."), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1568,6 +1576,8 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = + "route_logs_and_ignores_cores_package_from_delinquent_that_demands_external_routing"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let paying_wallet = make_paying_wallet(b"wallet"); @@ -1603,7 +1613,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1620,6 +1630,7 @@ mod tests { rate_pack_routing_byte(103), false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -1630,7 +1641,7 @@ mod tests { assert_eq!(dispatcher_recording.len(), 0); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 0); - TestLogHandler::new().exists_log_containing("WARN: RoutingService: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 7-byte payload further"); + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 7-byte payload further")); } #[test] @@ -1638,6 +1649,8 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = + "route_logs_and_ignores_cores_package_from_delinquent_that_demands_internal_routing"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let paying_wallet = make_paying_wallet(b"wallet"); @@ -1677,7 +1690,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1694,6 +1707,7 @@ mod tests { rate_pack_routing_byte(103), false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -1704,12 +1718,14 @@ mod tests { assert_eq!(dispatcher_recording.len(), 0); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 0); - TestLogHandler::new().exists_log_containing("WARN: RoutingService: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 36-byte payload to ProxyServer"); + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 36-byte payload to ProxyServer")); } #[test] fn route_logs_and_ignores_inbound_client_data_that_doesnt_deserialize_properly() { init_test_logging(); + let test_name = + "route_logs_and_ignores_inbound_client_data_that_doesnt_deserialize_properly"; let inbound_client_data = InboundClientData { timestamp: SystemTime::now(), client_addr: SocketAddr::from_str("1.2.3.4:5678").unwrap(), @@ -1730,7 +1746,7 @@ mod tests { .neighborhood(neighborhood) .dispatcher(dispatcher) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -1744,13 +1760,14 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Couldn't decode CORES package in 0-byte buffer from 1.2.3.4:5678: DecryptionError(EmptyData)", + &format!("ERROR: {test_name}: Couldn't decode CORES package in 0-byte buffer from 1.2.3.4:5678: DecryptionError(EmptyData)"), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1761,6 +1778,7 @@ mod tests { #[test] fn route_logs_and_ignores_invalid_live_cores_package() { init_test_logging(); + let test_name = "route_logs_and_ignores_invalid_live_cores_package"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let lcp = LiveCoresPackage::new(Route { hops: vec![] }, CryptData::new(&[])); @@ -1788,7 +1806,7 @@ mod tests { .neighborhood(neighborhood) .dispatcher(dispatcher) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1805,14 +1823,15 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); - TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Invalid 67-byte CORES package: RoutingError(EmptyRoute)", - ); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Invalid 67-byte CORES package: RoutingError(EmptyRoute)" + )); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); assert_eq!(neighborhood_recording_arc.lock().unwrap().len(), 0); @@ -1822,8 +1841,9 @@ mod tests { #[test] fn route_data_around_again_logs_and_ignores_bad_lcp() { init_test_logging(); + let test_name = "route_data_around_again_logs_and_ignores_bad_lcp"; let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -1837,6 +1857,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let lcp = LiveCoresPackage::new(Route { hops: vec![] }, CryptData::new(&[])); let ibcd = InboundClientData { timestamp: SystemTime::now(), @@ -1850,9 +1871,9 @@ mod tests { subject.route_data_around_again(lcp, &ibcd); - TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: bad zero-hop route: RoutingError(EmptyRoute)", - ); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: bad zero-hop route: RoutingError(EmptyRoute)" + )); } fn make_routing_service_subs(peer_actors: PeerActors) -> RoutingServiceSubs { @@ -1985,9 +2006,10 @@ mod tests { #[test] fn route_expired_package_handles_unmigratable_gossip() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_gossip"; let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().neighborhood(neighborhood).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2001,6 +2023,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2008,7 +2031,7 @@ mod tests { MessageType::Gossip(VersionedData::test_new(dv!(0, 0), vec![])), 0, ); - let system = System::new("route_expired_package_handles_unmigratable_gossip"); + let system = System::new(test_name); subject.route_expired_package(Component::Neighborhood, expired_package, true); @@ -2017,7 +2040,7 @@ mod tests { let neighborhood_recording = neighborhood_recording_arc.lock().unwrap(); assert_eq!(neighborhood_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable Gossip: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable Gossip: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } @@ -2063,9 +2086,10 @@ mod tests { #[test] fn route_expired_package_handles_unmigratable_client_response() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_client_response"; let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().proxy_server(proxy_server).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2079,6 +2103,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2086,7 +2111,7 @@ mod tests { MessageType::ClientResponse(VersionedData::test_new(dv!(0, 0), vec![])), 0, ); - let system = System::new("route_expired_package_handles_unmigratable_client_response"); + let system = System::new(test_name); subject.route_expired_package(Component::ProxyServer, expired_package, true); @@ -2095,16 +2120,17 @@ mod tests { let proxy_server_recording = proxy_server_recording_arc.lock().unwrap(); assert_eq!(proxy_server_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable ClientResponsePayload: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable ClientResponsePayload: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } #[test] fn route_expired_package_handles_unmigratable_dns_resolve_failure() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_dns_resolve_failure"; let (hopper, _, hopper_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().hopper(hopper).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2118,6 +2144,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2125,7 +2152,7 @@ mod tests { MessageType::DnsResolveFailed(VersionedData::test_new(dv!(0, 0), vec![])), 0, ); - let system = System::new("route_expired_package_handles_unmigratable_dns_resolve_failure"); + let system = System::new(test_name); subject.route_expired_package(Component::ProxyServer, expired_package, true); @@ -2134,16 +2161,17 @@ mod tests { let hopper_recording = hopper_recording_arc.lock().unwrap(); assert_eq!(hopper_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable DnsResolveFailed: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable DnsResolveFailed: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } #[test] fn route_expired_package_handles_unmigratable_gossip_failure() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_gossip_failure"; let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().neighborhood(neighborhood).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2157,6 +2185,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2173,7 +2202,7 @@ mod tests { let neighborhood_recording = neighborhood_recording_arc.lock().unwrap(); assert_eq!(neighborhood_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable GossipFailure: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable GossipFailure: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } } diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 84aaabe48..669e37042 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -1,9 +1,10 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; -use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, QualifiedPayablesMessage, +}; use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::peer_actors::BindMessage; use actix::Message; @@ -41,14 +42,14 @@ impl Debug for BlockchainBridgeSubs { #[derive(Message)] pub struct OutboundPaymentsInstructions { - pub affordable_accounts: Vec, + pub affordable_accounts: PricedQualifiedPayables, pub agent: Box, pub response_skeleton_opt: Option, } impl OutboundPaymentsInstructions { pub fn new( - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, agent: Box, response_skeleton_opt: Option, ) -> Self { From 04515c5def258a78d675ae1995722b276fe59701 Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta <32920299+utkarshg6@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:41:16 +0530 Subject: [PATCH 06/37] GH-665: Introduce new states in FailedPayableDao (#666) * GH-605: add the steps to solve this card * GH-605: change the recheck to status * GH-605: all tests pass in failed_payable_dao.rs * GH-605: few more changes * GH-605: add instructions inside test retry_payable_scanner_can_initiate_a_scan * GH-605: introduce mocks for FailedPayableDAO * GH-665: add another variant for the reason: General * GH-665: add the string conversion for General * GH-665: review changes --- .../db_access_objects/failed_payable_dao.rs | 268 +++++++++++------- .../db_access_objects/test_utils.rs | 12 +- node/src/accountant/scanners/mod.rs | 14 + node/src/accountant/test_utils.rs | 159 ++++++++++- node/src/database/db_initializer.rs | 4 +- .../migrations/migration_10_to_11.rs | 2 +- node/src/database/test_utils/mod.rs | 2 +- node/src/hopper/routing_service.rs | 1 - 8 files changed, 350 insertions(+), 112 deletions(-) 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 ce93a1f17..78554557d 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -1,11 +1,13 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCondition::UncheckedPendingTooLong; -use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers, VigilantRusqliteFlatten}; +use crate::accountant::db_access_objects::utils::{ + DaoFactoryReal, TxHash, TxIdentifiers, VigilantRusqliteFlatten, +}; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; use crate::database::rusqlite_wrappers::ConnectionWrapper; +use itertools::Itertools; use masq_lib::utils::ExpectValue; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; use std::str::FromStr; use web3::types::Address; @@ -23,6 +25,26 @@ pub enum FailedPayableDaoError { pub enum FailureReason { PendingTooLong, NonceIssue, + General, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FailureStatus { + RetryRequired, + RecheckRequired, + Concluded, +} + +impl FromStr for FailureStatus { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "RetryRequired" => Ok(FailureStatus::RetryRequired), + "RecheckRequired" => Ok(FailureStatus::RecheckRequired), + "Concluded" => Ok(FailureStatus::Concluded), + _ => Err(format!("Invalid FailureStatus: {}", s)), + } + } } impl FromStr for FailureReason { @@ -32,6 +54,7 @@ impl FromStr for FailureReason { match s { "PendingTooLong" => Ok(FailureReason::PendingTooLong), "NonceIssue" => Ok(FailureReason::NonceIssue), + "General" => Ok(FailureReason::General), _ => Err(format!("Invalid FailureReason: {}", s)), } } @@ -46,18 +69,18 @@ pub struct FailedTx { pub gas_price_wei: u128, pub nonce: u64, pub reason: FailureReason, - pub rechecked: bool, + pub status: FailureStatus, } pub enum FailureRetrieveCondition { - UncheckedPendingTooLong, + ByStatus(FailureStatus), } impl Display for FailureRetrieveCondition { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - FailureRetrieveCondition::UncheckedPendingTooLong => { - write!(f, "WHERE reason = 'PendingTooLong' AND rechecked = 0",) + FailureRetrieveCondition::ByStatus(status) => { + write!(f, "WHERE status = '{:?}'", status) } } } @@ -67,7 +90,10 @@ pub trait FailedPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError>; fn retrieve_txs(&self, condition: Option) -> Vec; - fn mark_as_rechecked(&self) -> Result<(), FailedPayableDaoError>; + fn update_statuses( + &self, + status_updates: HashMap, + ) -> Result<(), FailedPayableDaoError>; fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError>; } @@ -128,13 +154,6 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { ))); } - if let Some(_rechecked_tx) = txs.iter().find(|tx| tx.rechecked) { - return Err(FailedPayableDaoError::InvalidInput(format!( - "Already rechecked transaction(s) provided: {:?}", - txs - ))); - } - let sql = format!( "INSERT INTO failed_payable (\ tx_hash, \ @@ -146,7 +165,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { gas_price_wei_low_b, \ nonce, \ reason, \ - rechecked + status ) VALUES {}", comma_joined_stringifiable(txs, |tx| { let amount_checked = checked_conversion::(tx.amount); @@ -155,7 +174,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { 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, @@ -165,7 +184,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { gas_price_wei_low_b, tx.nonce, tx.reason, - tx.rechecked + tx.status ) }) ); @@ -196,7 +215,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { gas_price_wei_low_b, \ nonce, \ reason, \ - rechecked \ + status \ FROM failed_payable" .to_string(); let sql = match condition { @@ -227,8 +246,9 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { let reason_str: String = row.get(8).expectv("reason"); let reason = FailureReason::from_str(&reason_str).expect("Failed to parse FailureReason"); - let rechecked_as_integer: u8 = row.get(9).expectv("rechecked"); - let rechecked = rechecked_as_integer == 1; + let status_str: String = row.get(9).expectv("status"); + let status = + FailureStatus::from_str(&status_str).expect("Failed to parse FailureStatus"); Ok(FailedTx { hash, @@ -238,7 +258,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { gas_price_wei, nonce, reason, - rechecked, + status, }) }) .expect("Failed to execute query") @@ -246,27 +266,40 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { .collect() } - fn mark_as_rechecked(&self) -> Result<(), FailedPayableDaoError> { - let txs = self.retrieve_txs(Some(UncheckedPendingTooLong)); - let hashes_vec: Vec = txs.iter().map(|tx| tx.hash).collect(); - let hashes_string = comma_joined_stringifiable(&hashes_vec, |hash| format!("'{:?}'", hash)); + fn update_statuses( + &self, + status_updates: HashMap, + ) -> Result<(), FailedPayableDaoError> { + if status_updates.is_empty() { + 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 sql = format!( - "UPDATE failed_payable SET rechecked = 1 WHERE tx_hash IN ({})", - hashes_string + "UPDATE failed_payable \ + SET \ + status = CASE \ + {case_statements} \ + END \ + WHERE tx_hash IN ({tx_hashes})" ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { Ok(rows_changed) => { - if rows_changed == txs.len() { + if rows_changed == status_updates.len() { Ok(()) } else { - // This should never occur because we retrieve transaction hashes - // under the condition that all retrieved transactions are unchecked. Err(FailedPayableDaoError::PartialExecution(format!( - "Only {} of {} records has been marked as rechecked.", + "Only {} of {} records had their status updated.", rows_changed, - txs.len(), + status_updates.len(), ))) } } @@ -304,14 +337,27 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { } } +pub trait FailedPayableDaoFactory { + fn make(&self) -> Box; +} + +impl FailedPayableDaoFactory for DaoFactoryReal { + fn make(&self) -> Box { + Box::new(FailedPayableDaoReal::new(self.make_connection())) + } +} + #[cfg(test)] mod tests { use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::{ - NonceIssue, PendingTooLong, + General, NonceIssue, PendingTooLong, + }; + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::{ + Concluded, RecheckRequired, RetryRequired, }; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, - FailureRetrieveCondition, + FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::test_utils::{ make_read_only_db_connection, FailedTxBuilder, @@ -324,7 +370,7 @@ mod tests { use crate::database::test_utils::ConnectionWrapperMock; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; - use std::collections::HashSet; + use std::collections::{HashMap, HashSet}; use std::str::FromStr; #[test] @@ -379,10 +425,13 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let hash = make_tx_hash(123); - let tx1 = FailedTxBuilder::default().hash(hash).build(); + let tx1 = FailedTxBuilder::default() + .hash(hash) + .status(RetryRequired) + .build(); let tx2 = FailedTxBuilder::default() .hash(hash) - .rechecked(true) + .status(RecheckRequired) .build(); let subject = FailedPayableDaoReal::new(wrapped_conn); @@ -396,12 +445,12 @@ mod tests { hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount: 0, timestamp: 0, gas_price_wei: 0, \ - nonce: 0, reason: PendingTooLong, rechecked: false }, \ + nonce: 0, reason: PendingTooLong, status: RetryRequired }, \ FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount: 0, timestamp: 0, gas_price_wei: 0, \ - nonce: 0, reason: PendingTooLong, rechecked: true }]" + nonce: 0, reason: PendingTooLong, status: RecheckRequired }]" .to_string() )) ); @@ -417,10 +466,13 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let hash = make_tx_hash(123); - let tx1 = FailedTxBuilder::default().hash(hash).build(); + let tx1 = FailedTxBuilder::default() + .hash(hash) + .status(RetryRequired) + .build(); let tx2 = FailedTxBuilder::default() .hash(hash) - .rechecked(true) + .status(RecheckRequired) .build(); let subject = FailedPayableDaoReal::new(wrapped_conn); let initial_insertion_result = subject.insert_new_records(&vec![tx1]); @@ -438,37 +490,6 @@ mod tests { ); } - #[test] - fn insert_new_records_throws_err_if_an_already_rechecked_tx_is_supplied() { - let home_dir = ensure_node_home_directory_exists( - "failed_payable_dao", - "insert_new_records_throws_err_if_an_already_rechecked_tx_is_supplied", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = FailedPayableDaoReal::new(wrapped_conn); - let tx1 = FailedTxBuilder::default() - .hash(make_tx_hash(1)) - .rechecked(true) - .build(); - let tx2 = FailedTxBuilder::default() - .hash(make_tx_hash(2)) - .rechecked(false) - .build(); - let input = vec![tx1, tx2]; - - let result = subject.insert_new_records(&input); - - assert_eq!( - result, - Err(FailedPayableDaoError::InvalidInput(format!( - "Already rechecked transaction(s) provided: {:?}", - input - ))) - ); - } - #[test] fn insert_new_records_returns_err_if_partially_executed() { let setup_conn = Connection::open_in_memory().unwrap(); @@ -547,18 +568,32 @@ mod tests { Ok(PendingTooLong) ); assert_eq!(FailureReason::from_str("NonceIssue"), Ok(NonceIssue)); + assert_eq!(FailureReason::from_str("General"), Ok(General)); assert_eq!( FailureReason::from_str("InvalidReason"), Err("Invalid FailureReason: InvalidReason".to_string()) ); } + #[test] + fn failure_status_from_str_works() { + assert_eq!(FailureStatus::from_str("RetryRequired"), Ok(RetryRequired)); + assert_eq!( + FailureStatus::from_str("RecheckRequired"), + Ok(RecheckRequired) + ); + assert_eq!(FailureStatus::from_str("Concluded"), Ok(Concluded)); + assert_eq!( + FailureStatus::from_str("InvalidStatus"), + Err("Invalid FailureStatus: InvalidStatus".to_string()) + ); + } + #[test] fn retrieve_condition_display_works() { - let expected_condition = "WHERE reason = 'PendingTooLong' AND rechecked = 0"; assert_eq!( - FailureRetrieveCondition::UncheckedPendingTooLong.to_string(), - expected_condition + FailureRetrieveCondition::ByStatus(RetryRequired).to_string(), + "WHERE status = 'RetryRequired'" ); } @@ -601,32 +636,38 @@ mod tests { .hash(make_tx_hash(1)) .reason(PendingTooLong) .timestamp(now - 3600) - .rechecked(false) + .status(RetryRequired) .build(); let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) .reason(NonceIssue) - .rechecked(false) + .timestamp(now - 3600) + .status(RetryRequired) .build(); let tx3 = FailedTxBuilder::default() .hash(make_tx_hash(3)) .reason(PendingTooLong) - .rechecked(false) + .status(RecheckRequired) + .build(); + let tx4 = FailedTxBuilder::default() + .hash(make_tx_hash(4)) + .reason(PendingTooLong) + .status(Concluded) .timestamp(now - 3000) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3, tx4]) .unwrap(); - let result = subject.retrieve_txs(Some(FailureRetrieveCondition::UncheckedPendingTooLong)); + let result = subject.retrieve_txs(Some(FailureRetrieveCondition::ByStatus(RetryRequired))); - assert_eq!(result, vec![tx1, tx3]); + assert_eq!(result, vec![tx1, tx2]); } #[test] - fn mark_as_rechecked_works() { + fn update_statuses_works() { let home_dir = - ensure_node_home_directory_exists("failed_payable_dao", "mark_as_rechecked_works"); + ensure_node_home_directory_exists("failed_payable_dao", "update_statuses_works"); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); @@ -634,47 +675,72 @@ mod tests { let tx1 = FailedTxBuilder::default() .hash(make_tx_hash(1)) .reason(NonceIssue) - .rechecked(false) + .status(RetryRequired) .build(); let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) .reason(PendingTooLong) - .rechecked(false) + .status(RetryRequired) .build(); let tx3 = FailedTxBuilder::default() .hash(make_tx_hash(3)) .reason(PendingTooLong) - .rechecked(false) + .status(RecheckRequired) + .build(); + let tx4 = FailedTxBuilder::default() + .hash(make_tx_hash(4)) + .reason(PendingTooLong) + .status(RecheckRequired) .build(); - let tx1_pre_checked_state = tx1.rechecked; - let tx2_pre_checked_state = tx2.rechecked; - let tx3_pre_checked_state = tx3.rechecked; subject - .insert_new_records(&vec![tx1, tx2.clone(), tx3.clone()]) + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4]) .unwrap(); + let hashmap = HashMap::from([ + (tx1.hash, Concluded), + (tx2.hash, RecheckRequired), + (tx3.hash, Concluded), + ]); - let result = subject.mark_as_rechecked(); + let result = subject.update_statuses(hashmap); let updated_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); - assert_eq!(tx1_pre_checked_state, false); - assert_eq!(tx2_pre_checked_state, false); - assert_eq!(tx3_pre_checked_state, false); - assert_eq!(updated_txs[0].rechecked, false); - assert_eq!(updated_txs[1].rechecked, true); - assert_eq!(updated_txs[2].rechecked, true); + assert_eq!(tx1.status, RetryRequired); + assert_eq!(updated_txs[0].status, Concluded); + assert_eq!(tx2.status, RetryRequired); + assert_eq!(updated_txs[1].status, RecheckRequired); + assert_eq!(tx3.status, RecheckRequired); + assert_eq!(updated_txs[2].status, Concluded); + assert_eq!(tx3.status, RecheckRequired); + assert_eq!(updated_txs[3].status, RecheckRequired); + } + + #[test] + fn update_statuses_handles_empty_input_error() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "update_statuses_handles_empty_input_error", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = FailedPayableDaoReal::new(wrapped_conn); + + let result = subject.update_statuses(HashMap::new()); + + assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); } #[test] - fn mark_as_rechecked_handles_sql_error() { + fn update_statuses_handles_sql_error() { let home_dir = ensure_node_home_directory_exists( "failed_payable_dao", - "mark_as_rechecked_handles_sql_error", + "update_statuses_handles_sql_error", ); let wrapped_conn = make_read_only_db_connection(home_dir); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.mark_as_rechecked(); + let result = subject.update_statuses(HashMap::from([(make_tx_hash(1), RecheckRequired)])); assert_eq!( result, diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index 598a4121d..004a76761 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -6,7 +6,7 @@ use rusqlite::{Connection, OpenFlags}; use crate::accountant::db_access_objects::sent_payable_dao::{ Tx}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; use web3::types::{Address}; -use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason}; +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason, FailureStatus}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionBlock; use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE}; use crate::database::rusqlite_wrappers::ConnectionWrapperReal; @@ -69,7 +69,7 @@ pub struct FailedTxBuilder { gas_price_wei_opt: Option, nonce_opt: Option, reason_opt: Option, - rechecked_opt: Option, + status_opt: Option, } impl FailedTxBuilder { @@ -97,8 +97,8 @@ impl FailedTxBuilder { self } - pub fn rechecked(mut self, rechecked: bool) -> Self { - self.rechecked_opt = Some(rechecked); + pub fn status(mut self, failure_status: FailureStatus) -> Self { + self.status_opt = Some(failure_status); self } @@ -113,7 +113,9 @@ impl FailedTxBuilder { reason: self .reason_opt .unwrap_or_else(|| FailureReason::PendingTooLong), - rechecked: self.rechecked_opt.unwrap_or_else(|| false), + status: self + .status_opt + .unwrap_or_else(|| FailureStatus::RetryRequired), } } } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index cbd844bc4..041fe2196 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -520,6 +520,9 @@ impl StartableScanner for Payabl _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 } } @@ -1737,6 +1740,17 @@ 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 diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index a186ff016..a09d734cd 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -3,6 +3,10 @@ #![cfg(test)] use crate::accountant::db_access_objects::banned_dao::{BannedDao, BannedDaoFactory}; +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, + FailureRetrieveCondition, FailureStatus, +}; use crate::accountant::db_access_objects::payable_dao::{ PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, }; @@ -13,7 +17,7 @@ use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; use crate::accountant::db_access_objects::utils::{ - from_unix_timestamp, to_unix_timestamp, CustomQuery, + 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::{ @@ -45,6 +49,7 @@ use masq_lib::logger::Logger; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::path::Path; use std::rc::Rc; @@ -1077,6 +1082,158 @@ impl PendingPayableDaoFactoryMock { } } +#[derive(Default)] +pub struct FailedPayableDaoMock { + get_tx_identifiers_params: Arc>>>, + get_tx_identifiers_results: RefCell>, + insert_new_records_params: Arc>>>, + insert_new_records_results: RefCell>>, + retrieve_txs_params: Arc>>>, + retrieve_txs_results: RefCell>>, + update_statuses_params: Arc>>>, + update_statuses_results: RefCell>>, + delete_records_params: Arc>>>, + delete_records_results: RefCell>>, +} + +impl FailedPayableDao for FailedPayableDaoMock { + fn get_tx_identifiers(&self, hashes: &HashSet) -> 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: &[FailedTx]) -> Result<(), FailedPayableDaoError> { + self.insert_new_records_params + .lock() + .unwrap() + .push(txs.to_vec()); + self.insert_new_records_results.borrow_mut().remove(0) + } + + fn retrieve_txs(&self, condition: Option) -> Vec { + self.retrieve_txs_params.lock().unwrap().push(condition); + self.retrieve_txs_results.borrow_mut().remove(0) + } + + fn update_statuses( + &self, + status_updates: HashMap, + ) -> Result<(), FailedPayableDaoError> { + self.update_statuses_params + .lock() + .unwrap() + .push(status_updates); + self.update_statuses_results.borrow_mut().remove(0) + } + + fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError> { + self.delete_records_params + .lock() + .unwrap() + .push(hashes.clone()); + self.delete_records_results.borrow_mut().remove(0) + } +} + +impl FailedPayableDaoMock { + pub fn new() -> Self { + Self::default() + } + + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + self.get_tx_identifiers_params = params.clone(); + self + } + + pub fn get_tx_identifiers_result(self, result: TxIdentifiers) -> Self { + self.get_tx_identifiers_results.borrow_mut().push(result); + self + } + + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + self.insert_new_records_params = params.clone(); + self + } + + pub fn insert_new_records_result(self, result: Result<(), FailedPayableDaoError>) -> Self { + self.insert_new_records_results.borrow_mut().push(result); + self + } + + pub fn retrieve_txs_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.retrieve_txs_params = params.clone(); + self + } + + pub fn retrieve_txs_result(self, result: Vec) -> Self { + self.retrieve_txs_results.borrow_mut().push(result); + self + } + + pub fn update_statuses_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.update_statuses_params = params.clone(); + self + } + + pub fn update_statuses_result(self, result: Result<(), FailedPayableDaoError>) -> Self { + self.update_statuses_results.borrow_mut().push(result); + self + } + + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + self.delete_records_params = params.clone(); + self + } + + pub fn delete_records_result(self, result: Result<(), FailedPayableDaoError>) -> Self { + self.delete_records_results.borrow_mut().push(result); + self + } +} + +pub struct FailedPayableDaoFactoryMock { + make_params: Arc>>, + make_results: RefCell>>, +} + +impl FailedPayableDaoFactory for FailedPayableDaoFactoryMock { + fn make(&self) -> Box { + if self.make_results.borrow().len() == 0 { + panic!("FailedPayableDao Missing.") + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) + } +} + +impl FailedPayableDaoFactoryMock { + 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: FailedPayableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); + self + } +} + pub struct PayableScannerBuilder { payable_dao: PayableDaoMock, pending_payable_dao: PendingPayableDaoMock, diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index 86e82aed1..18dded007 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -299,7 +299,7 @@ impl DbInitializerReal { gas_price_wei_low_b integer not null, nonce integer not null, reason text not null, - rechecked integer not null + status text not null )", [], ) @@ -846,7 +846,7 @@ mod tests { gas_price_wei_low_b, nonce, reason, - rechecked + status FROM failed_payable", ) .unwrap(); diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs index 4dbfd5b5e..bcbf192fc 100644 --- a/node/src/database/db_migrations/migrations/migration_10_to_11.rs +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -34,7 +34,7 @@ impl DatabaseMigration for Migrate_10_to_11 { gas_price_wei_low_b integer not null, nonce integer not null, reason text not null, - rechecked integer not null + status text not null )"; declaration_utils.execute_upon_transaction(&[ diff --git a/node/src/database/test_utils/mod.rs b/node/src/database/test_utils/mod.rs index 6e88e1292..b4924e77c 100644 --- a/node/src/database/test_utils/mod.rs +++ b/node/src/database/test_utils/mod.rs @@ -37,7 +37,7 @@ pub const SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE: &[&[&str]] = &[ &["gas_price_wei_low_b", "integer", "not", "null"], &["nonce", "integer", "not", "null"], &["reason", "text", "not", "null"], - &["rechecked", "integer", "not", "null"], + &["status", "text", "not", "null"], ]; #[derive(Debug, Default)] diff --git a/node/src/hopper/routing_service.rs b/node/src/hopper/routing_service.rs index 478441159..264ec29dc 100644 --- a/node/src/hopper/routing_service.rs +++ b/node/src/hopper/routing_service.rs @@ -529,7 +529,6 @@ mod tests { use masq_lib::test_utils::environment_guard::EnvironmentGuard; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; - use std::fmt::format; use std::net::SocketAddr; use std::str::FromStr; use std::time::SystemTime; From 3a11c6e2a10fa2841630eb297aa1ba0f0e9aa89b Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta <32920299+utkarshg6@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:18:11 +0530 Subject: [PATCH 07/37] GH-672: Improve FailureReason (#673) * GH-672: add more errors * GH-672: improve errors; compiling * GH-672: introduce the conversion of FailureReason * GH-672: all tests are passing :) * GH-672: From conversion is properly tested * GH-672: final touches * GH-672: return String as Error instead of an error from serde * GH-672: better error classification --- .../db_access_objects/failed_payable_dao.rs | 96 +++++++++---- node/src/blockchain/errors.rs | 127 ++++++++++++++++++ node/src/blockchain/mod.rs | 1 + 3 files changed, 197 insertions(+), 27 deletions(-) create mode 100644 node/src/blockchain/errors.rs 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 78554557d..00d16080e 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -4,9 +4,11 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::blockchain::errors::AppRpcError; 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::fmt::{Display, Formatter}; use std::str::FromStr; @@ -21,11 +23,29 @@ pub enum FailedPayableDaoError { SqlExecutionFailed(String), } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FailureReason { + Submission(AppRpcError), + Validation(AppRpcError), + Reverted, PendingTooLong, - NonceIssue, - General, +} + +impl Display for FailureReason { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match serde_json::to_string(self) { + Ok(json) => write!(f, "{}", json), + Err(_) => write!(f, ""), + } + } +} + +impl FromStr for FailureReason { + type Err = String; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(|e| e.to_string()) + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -47,19 +67,6 @@ impl FromStr for FailureStatus { } } -impl FromStr for FailureReason { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "PendingTooLong" => Ok(FailureReason::PendingTooLong), - "NonceIssue" => Ok(FailureReason::NonceIssue), - "General" => Ok(FailureReason::General), - _ => Err(format!("Invalid FailureReason: {}", s)), - } - } -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct FailedTx { pub hash: TxHash, @@ -174,7 +181,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { 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, @@ -350,7 +357,7 @@ impl FailedPayableDaoFactory for DaoFactoryReal { #[cfg(test)] mod tests { use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::{ - General, NonceIssue, PendingTooLong, + PendingTooLong, Reverted, }; use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::{ Concluded, RecheckRequired, RetryRequired, @@ -363,6 +370,7 @@ mod tests { make_read_only_db_connection, FailedTxBuilder, }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; + use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError}; use crate::blockchain::test_utils::make_tx_hash; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, @@ -382,7 +390,7 @@ mod tests { .unwrap(); let tx1 = FailedTxBuilder::default() .hash(make_tx_hash(1)) - .reason(NonceIssue) + .reason(Reverted) .build(); let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) @@ -563,15 +571,49 @@ mod tests { #[test] fn failure_reason_from_str_works() { + // Submission error assert_eq!( - FailureReason::from_str("PendingTooLong"), - Ok(PendingTooLong) + FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder":"Test decoder error"}}}"#) + .unwrap(), + FailureReason::Submission(AppRpcError::Local(LocalError::Decoder( + "Test decoder error".to_string() + ))) ); - assert_eq!(FailureReason::from_str("NonceIssue"), Ok(NonceIssue)); - assert_eq!(FailureReason::from_str("General"), Ok(General)); + + // Validation error + assert_eq!( + FailureReason::from_str(r#"{"Validation":{"Remote":{"Web3RpcError":{"code":42,"message":"Test RPC error"}}}}"#).unwrap(), + FailureReason::Validation(AppRpcError::Remote(RemoteError::Web3RpcError { + code: 42, + message: "Test RPC error".to_string() + })) + ); + + // Reverted + assert_eq!( + FailureReason::from_str(r#"{"Reverted":null}"#).unwrap(), + FailureReason::Reverted + ); + + // PendingTooLong + assert_eq!( + FailureReason::from_str(r#"{"PendingTooLong":null}"#).unwrap(), + FailureReason::PendingTooLong + ); + + // Invalid Variant + assert_eq!( + FailureReason::from_str(r#"{"UnknownReason":null}"#).unwrap_err(), + "unknown variant `UnknownReason`, \ + expected one of `Submission`, `Validation`, `Reverted`, `PendingTooLong` \ + at line 1 column 16" + .to_string() + ); + + // Invalid Input assert_eq!( - FailureReason::from_str("InvalidReason"), - Err("Invalid FailureReason: InvalidReason".to_string()) + FailureReason::from_str("random string").unwrap_err(), + "expected value at line 1 column 1".to_string() ); } @@ -640,7 +682,7 @@ mod tests { .build(); let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) - .reason(NonceIssue) + .reason(Reverted) .timestamp(now - 3600) .status(RetryRequired) .build(); @@ -674,7 +716,7 @@ mod tests { let subject = FailedPayableDaoReal::new(wrapped_conn); let tx1 = FailedTxBuilder::default() .hash(make_tx_hash(1)) - .reason(NonceIssue) + .reason(Reverted) .status(RetryRequired) .build(); let tx2 = FailedTxBuilder::default() diff --git a/node/src/blockchain/errors.rs b/node/src/blockchain/errors.rs new file mode 100644 index 000000000..865bea29c --- /dev/null +++ b/node/src/blockchain/errors.rs @@ -0,0 +1,127 @@ +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, Serialize, Deserialize)] +pub enum AppRpcError { + Local(LocalError), + Remote(RemoteError), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum LocalError { + Decoder(String), + Internal, + Io(String), + Signing(String), + Transport(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum RemoteError { + InvalidResponse(String), + Unreachable, + Web3RpcError { code: i64, message: String }, +} + +// EVM based errors +impl From for AppRpcError { + fn from(error: Web3Error) -> Self { + match error { + // Local Errors + Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), + Web3Error::Internal => AppRpcError::Local(LocalError::Internal), + Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), + Web3Error::Signing(error) => { + // This variant cannot be tested due to import limitations. + AppRpcError::Local(LocalError::Signing(error.to_string())) + } + Web3Error::Transport(error) => AppRpcError::Local(LocalError::Transport(error)), + + // Api Errors + Web3Error::InvalidResponse(response) => { + AppRpcError::Remote(RemoteError::InvalidResponse(response)) + } + Web3Error::Rpc(web3_rpc_error) => AppRpcError::Remote(RemoteError::Web3RpcError { + code: web3_rpc_error.code.code(), + message: web3_rpc_error.message, + }), + Web3Error::Unreachable => AppRpcError::Remote(RemoteError::Unreachable), + } + } +} + +mod tests { + use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError}; + use web3::error::Error as Web3Error; + + #[test] + fn web3_error_to_failure_reason_conversion_works() { + // Local Errors + assert_eq!( + AppRpcError::from(Web3Error::Decoder("Decoder error".to_string())), + AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())) + ); + assert_eq!( + AppRpcError::from(Web3Error::Internal), + AppRpcError::Local(LocalError::Internal) + ); + assert_eq!( + AppRpcError::from(Web3Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "IO error" + ))), + AppRpcError::Local(LocalError::Io("IO error".to_string())) + ); + assert_eq!( + AppRpcError::from(Web3Error::Transport("Transport error".to_string())), + AppRpcError::Local(LocalError::Transport("Transport error".to_string())) + ); + + // Api Errors + assert_eq!( + AppRpcError::from(Web3Error::InvalidResponse("Invalid response".to_string())), + AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())) + ); + assert_eq!( + AppRpcError::from(Web3Error::Rpc(jsonrpc_core::types::error::Error { + code: jsonrpc_core::types::error::ErrorCode::ServerError(42), + message: "RPC error".to_string(), + data: None, + })), + AppRpcError::Remote(RemoteError::Web3RpcError { + code: 42, + message: "RPC error".to_string(), + }) + ); + assert_eq!( + AppRpcError::from(Web3Error::Unreachable), + AppRpcError::Remote(RemoteError::Unreachable) + ); + } + + #[test] + fn app_rpc_error_serialization_deserialization() { + let errors = vec![ + // Local Errors + AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())), + AppRpcError::Local(LocalError::Internal), + AppRpcError::Local(LocalError::Io("IO error".to_string())), + AppRpcError::Local(LocalError::Signing("Signing error".to_string())), + AppRpcError::Local(LocalError::Transport("Transport error".to_string())), + // Remote Errors + AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())), + AppRpcError::Remote(RemoteError::Unreachable), + AppRpcError::Remote(RemoteError::Web3RpcError { + code: 42, + message: "RPC error".to_string(), + }), + ]; + + errors.into_iter().for_each(|error| { + let serialized = serde_json::to_string(&error).unwrap(); + let deserialized: AppRpcError = serde_json::from_str(&serialized).unwrap(); + assert_eq!(error, deserialized, "Error: {:?}", error); + }); + } +} diff --git a/node/src/blockchain/mod.rs b/node/src/blockchain/mod.rs index 48698c408..f3ef3d323 100644 --- a/node/src/blockchain/mod.rs +++ b/node/src/blockchain/mod.rs @@ -5,6 +5,7 @@ pub mod blockchain_agent; pub mod blockchain_bridge; pub mod blockchain_interface; pub mod blockchain_interface_initializer; +pub mod errors; pub mod payer; pub mod signature; #[cfg(test)] From 5860bd17c8864f90b7579196020a79f125586abb Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Thu, 24 Jul 2025 19:49:12 +0200 Subject: [PATCH 08/37] GH-674: Refinement of FailureReason and TxStatus according to the latest opinions (#675) --- .../db_access_objects/failed_payable_dao.rs | 148 +++--- .../db_access_objects/sent_payable_dao.rs | 435 +++++++++++------- .../db_access_objects/test_utils.rs | 27 +- .../lower_level_interface_web3.rs | 3 +- node/src/database/db_initializer.rs | 3 +- .../migrations/migration_10_to_11.rs | 3 +- node/src/database/test_utils/mod.rs | 3 +- 7 files changed, 380 insertions(+), 242 deletions(-) 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 00d16080e..7d9f2ae47 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -26,7 +26,6 @@ pub enum FailedPayableDaoError { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FailureReason { Submission(AppRpcError), - Validation(AppRpcError), Reverted, PendingTooLong, } @@ -35,6 +34,7 @@ impl Display for FailureReason { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match serde_json::to_string(self) { Ok(json) => write!(f, "{}", json), + // Untestable Err(_) => write!(f, ""), } } @@ -44,29 +44,40 @@ impl FromStr for FailureReason { type Err = String; fn from_str(s: &str) -> Result { - serde_json::from_str(s).map_err(|e| e.to_string()) + serde_json::from_str(s).map_err(|e| format!("{} in '{}'", e, s)) } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FailureStatus { RetryRequired, - RecheckRequired, + RecheckRequired(ValidationStatus), Concluded, } +impl Display for FailureStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match serde_json::to_string(self) { + Ok(json) => write!(f, "{}", json), + // Untestable + Err(_) => write!(f, ""), + } + } +} + impl FromStr for FailureStatus { type Err = String; fn from_str(s: &str) -> Result { - match s { - "RetryRequired" => Ok(FailureStatus::RetryRequired), - "RecheckRequired" => Ok(FailureStatus::RecheckRequired), - "Concluded" => Ok(FailureStatus::Concluded), - _ => Err(format!("Invalid FailureStatus: {}", s)), - } + serde_json::from_str(s).map_err(|e| format!("{} in '{}'", e, s)) } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ValidationStatus { + Waiting, + Reattempting { attempt: usize, error: AppRpcError }, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct FailedTx { pub hash: TxHash, @@ -87,7 +98,7 @@ impl Display for FailureRetrieveCondition { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { FailureRetrieveCondition::ByStatus(status) => { - write!(f, "WHERE status = '{:?}'", status) + write!(f, "WHERE status = '{}'", status) } } } @@ -181,7 +192,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { 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, @@ -283,7 +294,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { let case_statements = status_updates .iter() - .map(|(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{:?}'", hash, status)) + .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) @@ -364,7 +375,7 @@ mod tests { }; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, - FailureRetrieveCondition, FailureStatus, + FailureRetrieveCondition, FailureStatus, ValidationStatus, }; use crate::accountant::db_access_objects::test_utils::{ make_read_only_db_connection, FailedTxBuilder, @@ -439,7 +450,7 @@ mod tests { .build(); let tx2 = FailedTxBuilder::default() .hash(hash) - .status(RecheckRequired) + .status(RecheckRequired(ValidationStatus::Waiting)) .build(); let subject = FailedPayableDaoReal::new(wrapped_conn); @@ -458,7 +469,7 @@ mod tests { hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount: 0, timestamp: 0, gas_price_wei: 0, \ - nonce: 0, reason: PendingTooLong, status: RecheckRequired }]" + nonce: 0, reason: PendingTooLong, status: RecheckRequired(Waiting) }]" .to_string() )) ); @@ -480,7 +491,7 @@ mod tests { .build(); let tx2 = FailedTxBuilder::default() .hash(hash) - .status(RecheckRequired) + .status(RecheckRequired(ValidationStatus::Waiting)) .build(); let subject = FailedPayableDaoReal::new(wrapped_conn); let initial_insertion_result = subject.insert_new_records(&vec![tx1]); @@ -580,54 +591,67 @@ mod tests { ))) ); - // Validation error - assert_eq!( - FailureReason::from_str(r#"{"Validation":{"Remote":{"Web3RpcError":{"code":42,"message":"Test RPC error"}}}}"#).unwrap(), - FailureReason::Validation(AppRpcError::Remote(RemoteError::Web3RpcError { - code: 42, - message: "Test RPC error".to_string() - })) - ); - // Reverted assert_eq!( - FailureReason::from_str(r#"{"Reverted":null}"#).unwrap(), + FailureReason::from_str("\"Reverted\"").unwrap(), FailureReason::Reverted ); // PendingTooLong assert_eq!( - FailureReason::from_str(r#"{"PendingTooLong":null}"#).unwrap(), + FailureReason::from_str("\"PendingTooLong\"").unwrap(), FailureReason::PendingTooLong ); // Invalid Variant assert_eq!( - FailureReason::from_str(r#"{"UnknownReason":null}"#).unwrap_err(), + FailureReason::from_str("\"UnknownReason\"").unwrap_err(), "unknown variant `UnknownReason`, \ - expected one of `Submission`, `Validation`, `Reverted`, `PendingTooLong` \ - at line 1 column 16" - .to_string() + expected one of `Submission`, `Reverted`, `PendingTooLong` \ + at line 1 column 15 in '\"UnknownReason\"'" ); // Invalid Input assert_eq!( - FailureReason::from_str("random string").unwrap_err(), - "expected value at line 1 column 1".to_string() + FailureReason::from_str("not a failure reason").unwrap_err(), + "expected value at line 1 column 1 in 'not a failure reason'" ); } #[test] fn failure_status_from_str_works() { - assert_eq!(FailureStatus::from_str("RetryRequired"), Ok(RetryRequired)); assert_eq!( - FailureStatus::from_str("RecheckRequired"), - Ok(RecheckRequired) + FailureStatus::from_str("\"RetryRequired\"").unwrap(), + FailureStatus::RetryRequired ); - assert_eq!(FailureStatus::from_str("Concluded"), Ok(Concluded)); + + assert_eq!( + FailureStatus::from_str(r#"{"RecheckRequired":"Waiting"}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Waiting) + ); + assert_eq!( - FailureStatus::from_str("InvalidStatus"), - Err("Invalid FailureStatus: InvalidStatus".to_string()) + FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"attempt":2,"error":{"Remote":"Unreachable"}}}}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting { attempt: 2, error: AppRpcError::Remote(RemoteError::Unreachable) }) + ); + + assert_eq!( + FailureStatus::from_str("\"Concluded\"").unwrap(), + FailureStatus::Concluded + ); + + // Invalid Variant + assert_eq!( + FailureStatus::from_str("\"UnknownStatus\"").unwrap_err(), + "unknown variant `UnknownStatus`, \ + expected one of `RetryRequired`, `RecheckRequired`, `Concluded` \ + at line 1 column 15 in '\"UnknownStatus\"'" + ); + + // Invalid Input + assert_eq!( + FailureStatus::from_str("not a failure status").unwrap_err(), + "expected value at line 1 column 1 in 'not a failure status'" ); } @@ -635,7 +659,7 @@ mod tests { fn retrieve_condition_display_works() { assert_eq!( FailureRetrieveCondition::ByStatus(RetryRequired).to_string(), - "WHERE status = 'RetryRequired'" + "WHERE status = '\"RetryRequired\"'" ); } @@ -689,7 +713,10 @@ mod tests { let tx3 = FailedTxBuilder::default() .hash(make_tx_hash(3)) .reason(PendingTooLong) - .status(RecheckRequired) + .status(RecheckRequired(ValidationStatus::Reattempting { + attempt: 1, + error: AppRpcError::Remote(RemoteError::Unreachable), + })) .build(); let tx4 = FailedTxBuilder::default() .hash(make_tx_hash(4)) @@ -722,24 +749,30 @@ mod tests { let tx2 = FailedTxBuilder::default() .hash(make_tx_hash(2)) .reason(PendingTooLong) - .status(RetryRequired) + .status(RecheckRequired(ValidationStatus::Waiting)) .build(); let tx3 = FailedTxBuilder::default() .hash(make_tx_hash(3)) .reason(PendingTooLong) - .status(RecheckRequired) + .status(RetryRequired) .build(); let tx4 = FailedTxBuilder::default() .hash(make_tx_hash(4)) .reason(PendingTooLong) - .status(RecheckRequired) + .status(RecheckRequired(ValidationStatus::Waiting)) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4]) + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) .unwrap(); let hashmap = HashMap::from([ (tx1.hash, Concluded), - (tx2.hash, RecheckRequired), + ( + tx2.hash, + RecheckRequired(ValidationStatus::Reattempting { + attempt: 1, + error: AppRpcError::Remote(RemoteError::Unreachable), + }), + ), (tx3.hash, Concluded), ]); @@ -749,12 +782,21 @@ mod tests { assert_eq!(result, Ok(())); assert_eq!(tx1.status, RetryRequired); assert_eq!(updated_txs[0].status, Concluded); - assert_eq!(tx2.status, RetryRequired); - assert_eq!(updated_txs[1].status, RecheckRequired); - assert_eq!(tx3.status, RecheckRequired); + assert_eq!(tx2.status, RecheckRequired(ValidationStatus::Waiting)); + assert_eq!( + updated_txs[1].status, + RecheckRequired(ValidationStatus::Reattempting { + attempt: 1, + error: AppRpcError::Remote(RemoteError::Unreachable) + }) + ); + assert_eq!(tx3.status, RetryRequired); assert_eq!(updated_txs[2].status, Concluded); - assert_eq!(tx3.status, RecheckRequired); - assert_eq!(updated_txs[3].status, RecheckRequired); + assert_eq!(tx4.status, RecheckRequired(ValidationStatus::Waiting)); + assert_eq!( + updated_txs[3].status, + RecheckRequired(ValidationStatus::Waiting) + ); } #[test] @@ -782,7 +824,7 @@ mod tests { let wrapped_conn = make_read_only_db_connection(home_dir); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.update_statuses(HashMap::from([(make_tx_hash(1), RecheckRequired)])); + let result = subject.update_statuses(HashMap::from([(make_tx_hash(1), Concluded)])); assert_eq!( result, 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 5cdc59047..ac3fbec86 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -use ethereum_types::{H256, U64}; +use ethereum_types::{H256}; use web3::types::Address; use masq_lib::utils::ExpectValue; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; @@ -12,6 +12,8 @@ use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; use crate::database::rusqlite_wrappers::ConnectionWrapper; use itertools::Itertools; +use serde_derive::{Deserialize, Serialize}; +use crate::accountant::db_access_objects::failed_payable_dao::ValidationStatus; #[derive(Debug, PartialEq, Eq)] pub enum SentPayableDaoError { @@ -30,7 +32,58 @@ pub struct Tx { pub timestamp: i64, pub gas_price_wei: u128, pub nonce: u64, - pub block_opt: Option, + pub status: TxStatus, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum TxStatus { + Pending(ValidationStatus), + Confirmed { + block_hash: String, + block_number: u64, + detection: Detection, + }, +} + +impl FromStr for TxStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(|e| format!("{} in '{}'", e, s)) + } +} + +impl Display for TxStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match serde_json::to_string(self) { + Ok(json) => write!(f, "{}", json), + // Untestable + Err(_) => write!(f, ""), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Detection { + Normal, + Reclaim, +} + +impl From<&TxConfirmation> for TxStatus { + fn from(tx_confirmation: &TxConfirmation) -> Self { + TxStatus::Confirmed { + block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), + block_number: u64::try_from(tx_confirmation.block_info.block_number) + .expect("block number too big"), + detection: tx_confirmation.detection, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TxConfirmation { + block_info: TransactionBlock, + detection: Detection, } pub enum RetrieveCondition { @@ -42,7 +95,7 @@ impl Display for RetrieveCondition { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { RetrieveCondition::IsPending => { - write!(f, "WHERE block_hash IS NULL") + write!(f, r#"WHERE status LIKE '%"Pending":%'"#) } RetrieveCondition::ByHash(tx_hashes) => { write!( @@ -59,9 +112,9 @@ pub trait SentPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError>; fn retrieve_txs(&self, condition: Option) -> Vec; - fn update_tx_blocks( + fn confirm_tx( &self, - hash_map: &HashMap, + hash_map: &HashMap, ) -> Result<(), SentPayableDaoError>; fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError>; fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; @@ -134,8 +187,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { gas_price_wei_high_b, \ gas_price_wei_low_b, \ nonce, \ - block_hash, \ - block_number + status \ ) VALUES {}", comma_joined_stringifiable(txs, |tx| { let amount_checked = checked_conversion::(tx.amount); @@ -143,12 +195,8 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); let (gas_price_wei_high_b, gas_price_wei_low_b) = BigIntDivider::deconstruct(gas_price_wei_checked); - let block_details = match &tx.block_opt { - Some(block) => format!("'{:?}', {}", block.block_hash, block.block_number), - None => "null, null".to_string(), - }; format!( - "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, {})", + "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}')", tx.hash, tx.receiver_address, amount_high_b, @@ -157,7 +205,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { gas_price_wei_high_b, gas_price_wei_low_b, tx.nonce, - block_details + tx.status ) }) ); @@ -180,7 +228,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { fn retrieve_txs(&self, condition_opt: Option) -> Vec { let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ - timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, block_hash, block_number FROM sent_payable" + timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, status FROM sent_payable" .to_string(); let sql = match condition_opt { None => raw_sql, @@ -207,24 +255,8 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let gas_price_wei = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); - let block_hash_opt: Option = { - let block_hash_str_opt: Option = row.get(8).expectv("block_hash"); - block_hash_str_opt - .map(|string| H256::from_str(&string[2..]).expect("Failed to parse H256")) - }; - let block_number_opt: Option = { - let block_number_i64_opt: Option = row.get(9).expectv("block_number"); - block_number_i64_opt.map(|v| u64::try_from(v).expect("Failed to parse u64")) - }; - - let block_opt = match (block_hash_opt, block_number_opt) { - (Some(block_hash), Some(block_number)) => Some(TransactionBlock { - block_hash, - block_number: U64::from(block_number), - }), - (None, None) => None, - _ => panic!("Invalid block details"), - }; + let status_str: String = row.get(8).expectv("status"); + let status = TxStatus::from_str(&status_str).expect("Failed to parse TxStatus"); Ok(Tx { hash, @@ -233,7 +265,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { timestamp, gas_price_wei, nonce, - block_opt, + status, }) }) .expect("Failed to execute query") @@ -241,18 +273,19 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn update_tx_blocks( + fn confirm_tx( &self, - hash_map: &HashMap, + hash_map: &HashMap, ) -> Result<(), SentPayableDaoError> { if hash_map.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - for (hash, transaction_block) in hash_map { + for (hash, tx_confirmation) in hash_map { let sql = format!( - "UPDATE sent_payable SET block_hash = '{:?}', block_number = {} WHERE tx_hash = '{:?}'", - transaction_block.block_hash, transaction_block.block_number, hash + "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", + TxStatus::from(tx_confirmation), + hash ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -310,14 +343,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); low.to_string() }); - let block_hash_cases = build_case(|tx| match &tx.block_opt { - Some(block) => format!("'{:?}'", block.block_hash), - None => "NULL".to_string(), - }); - let block_number_cases = build_case(|tx| match &tx.block_opt { - Some(block) => block.block_number.as_u64().to_string(), - None => "NULL".to_string(), - }); + let status_cases = build_case(|tx| format!("'{}'", tx.status)); let nonces = comma_joined_stringifiable(new_txs, |tx| tx.nonce.to_string()); @@ -345,11 +371,8 @@ impl SentPayableDao for SentPayableDaoReal<'_> { gas_price_wei_low_b = CASE \ {gas_price_wei_low_b_cases} \ END, \ - block_hash = CASE \ - {block_hash_cases} \ - END, \ - block_number = CASE \ - {block_number_cases} \ + status = CASE \ + {status_cases} \ END \ WHERE nonce IN ({nonces})", ); @@ -401,8 +424,9 @@ impl SentPayableDao for SentPayableDaoReal<'_> { #[cfg(test)] mod tests { use std::collections::{HashMap, HashSet}; + use std::str::FromStr; use std::sync::{Arc, Mutex}; - use crate::accountant::db_access_objects::sent_payable_dao::{RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal}; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, TxConfirmation, TxStatus}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; @@ -410,10 +434,12 @@ mod tests { use ethereum_types::{ H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::{Connection}; + use crate::accountant::db_access_objects::failed_payable_dao::{ValidationStatus}; use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending}; use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; + use crate::blockchain::errors::{AppRpcError, RemoteError}; use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; #[test] @@ -426,7 +452,10 @@ mod tests { let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); let tx2 = TxBuilder::default() .hash(make_tx_hash(2)) - .block(Default::default()) + .status(TxStatus::Pending(ValidationStatus::Reattempting { + attempt: 2, + error: AppRpcError::Remote(RemoteError::Unreachable), + })) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); let txs = vec![tx1, tx2]; @@ -435,7 +464,6 @@ mod tests { let retrieved_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); - assert_eq!(retrieved_txs.len(), 2); assert_eq!(retrieved_txs, txs); } @@ -469,11 +497,16 @@ mod tests { let tx1 = TxBuilder::default() .hash(hash) .timestamp(1749204017) + .status(TxStatus::Pending(ValidationStatus::Waiting)) .build(); let tx2 = TxBuilder::default() .hash(hash) .timestamp(1749204020) - .block(Default::default()) + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(456)), + block_number: 7890123, + detection: Detection::Reclaim, + }) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); @@ -487,14 +520,14 @@ mod tests { hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ - nonce: 0, block_opt: None }, \ + nonce: 0, status: Pending(Waiting) }, \ Tx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount: 0, timestamp: 1749204020, gas_price_wei: 0, \ - nonce: 0, block_opt: Some(TransactionBlock { \ - block_hash: 0x0000000000000000000000000000000000000000000000000000000000000000, \ - block_number: 0 }) }]" + nonce: 0, status: Confirmed { block_hash: \ + \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ + block_number: 7890123, detection: Reclaim } }]" .to_string() )) ); @@ -511,10 +544,7 @@ mod tests { .unwrap(); let hash = make_tx_hash(1234); let tx1 = TxBuilder::default().hash(hash).build(); - let tx2 = TxBuilder::default() - .hash(hash) - .block(Default::default()) - .build(); + let tx2 = TxBuilder::default().hash(hash).build(); let subject = SentPayableDaoReal::new(wrapped_conn); let initial_insertion_result = subject.insert_new_records(&vec![tx1]); @@ -602,7 +632,7 @@ mod tests { #[test] fn retrieve_condition_display_works() { - assert_eq!(IsPending.to_string(), "WHERE block_hash IS NULL"); + assert_eq!(IsPending.to_string(), "WHERE status LIKE '%\"Pending\":%'"); assert_eq!( ByHash(vec![ H256::from_low_u64_be(0x123456789), @@ -626,10 +656,7 @@ mod tests { .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); - let tx2 = TxBuilder::default() - .hash(make_tx_hash(2)) - .block(Default::default()) - .build(); + let 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()]) @@ -649,11 +676,24 @@ 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 tx1 = TxBuilder::default() + .hash(make_tx_hash(1)) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(2)) + .status(TxStatus::Pending(ValidationStatus::Reattempting { + attempt: 1, + error: AppRpcError::Remote(RemoteError::Unreachable), + })) + .build(); let tx3 = TxBuilder::default() .hash(make_tx_hash(3)) - .block(Default::default()) + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(456)), + block_number: 456789, + detection: Detection::Normal, + }) .build(); subject .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3]) @@ -685,94 +725,74 @@ mod tests { } #[test] - #[should_panic(expected = "Invalid block details")] - fn retrieve_txs_enforces_complete_block_details() { - let home_dir = ensure_node_home_directory_exists( - "sent_payable_dao", - "retrieve_txs_enforces_complete_block_details", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - // Insert a record with block_hash but no block_number - { - let sql = "INSERT INTO sent_payable (\ - tx_hash, \ - receiver_address, \ - amount_high_b, \ - amount_low_b, \ - timestamp, \ - gas_price_wei_high_b, \ - gas_price_wei_low_b, \ - nonce, \ - block_hash, \ - block_number\ - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"; - let mut stmt = wrapped_conn.prepare(sql).unwrap(); - stmt.execute(rusqlite::params![ - "0x1234567890123456789012345678901234567890123456789012345678901234", - "0x1234567890123456789012345678901234567890", - 0, - 100, - 1234567890, - 0, - 1000000000, - 1, - "0x2345678901234567890123456789012345678901234567890123456789012345", - rusqlite::types::Null, - ]) - .unwrap(); - } - let subject = SentPayableDaoReal::new(wrapped_conn); - - // This should panic due to invalid block details - let _ = subject.retrieve_txs(None); - } - - #[test] - fn update_tx_blocks_works() { - let home_dir = - ensure_node_home_directory_exists("sent_payable_dao", "update_tx_blocks_works"); + fn confirm_tx_works() { + let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "confirm_tx_works"); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); let subject = SentPayableDaoReal::new(wrapped_conn); let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); - let pre_assert_is_block_details_present_tx1 = tx1.block_opt.is_some(); - let pre_assert_is_block_details_present_tx2 = tx2.block_opt.is_some(); subject .insert_new_records(&vec![tx1.clone(), tx2.clone()]) .unwrap(); - let tx_block_1 = TransactionBlock { - block_hash: make_block_hash(3), - block_number: U64::from(1), + let updated_pre_assert_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); + let pre_assert_status_tx1 = updated_pre_assert_txs[0].status.clone(); + let pre_assert_status_tx2 = updated_pre_assert_txs[1].status.clone(); + let tx_confirmation_1 = TxConfirmation { + block_info: TransactionBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), + }, + detection: Detection::Normal, }; - let tx_block_2 = TransactionBlock { - block_hash: make_block_hash(4), - block_number: U64::from(2), + let tx_confirmation_2 = TxConfirmation { + block_info: TransactionBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), + }, + detection: Detection::Reclaim, }; let hash_map = HashMap::from([ - (tx1.hash, tx_block_1.clone()), - (tx2.hash, tx_block_2.clone()), + (tx1.hash, tx_confirmation_1.clone()), + (tx2.hash, tx_confirmation_2.clone()), ]); - let result = subject.update_tx_blocks(&hash_map); + let result = subject.confirm_tx(&hash_map); let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); assert_eq!(result, Ok(())); - assert_eq!(pre_assert_is_block_details_present_tx1, false); - assert_eq!(updated_txs[0].block_opt, Some(tx_block_1)); - assert_eq!(pre_assert_is_block_details_present_tx2, false); - assert_eq!(updated_txs[1].block_opt, Some(tx_block_2)); + assert_eq!( + pre_assert_status_tx1, + TxStatus::Pending(ValidationStatus::Waiting) + ); + assert_eq!( + updated_txs[0].status, + TxStatus::Confirmed { + block_hash: format!("{:?}", tx_confirmation_1.block_info.block_hash), + block_number: tx_confirmation_1.block_info.block_number.as_u64(), + detection: tx_confirmation_1.detection + } + ); + assert_eq!( + pre_assert_status_tx2, + TxStatus::Pending(ValidationStatus::Waiting) + ); + assert_eq!( + updated_txs[1].status, + TxStatus::Confirmed { + block_hash: format!("{:?}", tx_confirmation_2.block_info.block_hash), + block_number: tx_confirmation_2.block_info.block_number.as_u64(), + detection: tx_confirmation_2.detection + } + ); } #[test] - fn update_tx_blocks_returns_error_when_input_is_empty() { + fn confirm_tx_returns_error_when_input_is_empty() { let home_dir = ensure_node_home_directory_exists( "sent_payable_dao", - "update_tx_blocks_returns_error_when_input_is_empty", + "confirm_tx_returns_error_when_input_is_empty", ); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) @@ -783,16 +803,16 @@ mod tests { subject.insert_new_records(&vec![tx]).unwrap(); let hash_map = HashMap::new(); - let result = subject.update_tx_blocks(&hash_map); + let result = subject.confirm_tx(&hash_map); assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); } #[test] - fn update_tx_blocks_returns_error_during_partial_execution() { + fn confirm_tx_returns_error_during_partial_execution() { let home_dir = ensure_node_home_directory_exists( "sent_payable_dao", - "update_tx_blocks_returns_error_during_partial_execution", + "confirm_tx_returns_error_during_partial_execution", ); let wrapped_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) @@ -805,21 +825,27 @@ mod tests { let hash_map = HashMap::from([ ( existent_hash, - TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), + TxConfirmation { + block_info: TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }, + detection: Detection::Normal, }, ), ( non_existent_hash, - TransactionBlock { - block_hash: make_block_hash(2), - block_number: U64::from(2), + TxConfirmation { + block_info: TransactionBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), + }, + detection: Detection::Normal, }, ), ]); - let result = subject.update_tx_blocks(&hash_map); + let result = subject.confirm_tx(&hash_map); assert_eq!( result, @@ -831,23 +857,26 @@ mod tests { } #[test] - fn update_tx_blocks_returns_error_when_an_error_occurs_while_executing_sql() { + fn confirm_tx_returns_error_when_an_error_occurs_while_executing_sql() { let home_dir = ensure_node_home_directory_exists( "sent_payable_dao", - "update_tx_blocks_returns_error_when_an_error_occurs_while_executing_sql", + "confirm_tx_returns_error_when_an_error_occurs_while_executing_sql", ); let wrapped_conn = make_read_only_db_connection(home_dir); let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); let hash = make_tx_hash(1); let hash_map = HashMap::from([( hash, - TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::default(), + TxConfirmation { + block_info: TransactionBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), + }, + detection: Detection::Normal, }, )]); - let result = subject.update_tx_blocks(&hash_map); + let result = subject.confirm_tx(&hash_map); assert_eq!( result, @@ -867,10 +896,7 @@ mod tests { let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); - let tx4 = TxBuilder::default() - .hash(make_tx_hash(4)) - .block(Default::default()) - .build(); + let tx4 = TxBuilder::default().hash(make_tx_hash(4)).build(); subject .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) .unwrap(); @@ -981,17 +1007,19 @@ mod tests { .unwrap(); let new_tx2 = TxBuilder::default() .hash(make_tx_hash(22)) - .block(TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: 45454545, + detection: Detection::Normal, }) .nonce(2) .build(); let new_tx3 = TxBuilder::default() .hash(make_tx_hash(33)) - .block(TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(789)), + block_number: 45454566, + detection: Detection::Reclaim, }) .nonce(3) .build(); @@ -1031,8 +1059,7 @@ mod tests { assert!(sql.contains("timestamp = CASE")); assert!(sql.contains("gas_price_wei_high_b = CASE")); assert!(sql.contains("gas_price_wei_low_b = CASE")); - assert!(sql.contains("block_hash = CASE")); - assert!(sql.contains("block_number = CASE")); + assert!(sql.contains("status = CASE")); assert!(sql.contains("WHERE nonce IN (1, 2, 3)")); assert!(sql.contains("WHEN nonce = 1 THEN '0x0000000000000000000000000000000000000000000000000000000000000001'")); assert!(sql.contains("WHEN nonce = 2 THEN '0x0000000000000000000000000000000000000000000000000000000000000002'")); @@ -1076,17 +1103,19 @@ mod tests { .unwrap(); let new_tx2 = TxBuilder::default() .hash(make_tx_hash(22)) - .block(TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(77777)), + block_number: 357913, + detection: Detection::Normal, }) .nonce(2) .build(); let new_tx3 = TxBuilder::default() .hash(make_tx_hash(33)) - .block(TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(66666)), + block_number: 353535, + detection: Detection::Reclaim, }) .nonce(3) .build(); @@ -1137,4 +1166,68 @@ mod tests { )) ) } + + #[test] + fn tx_status_from_str_works() { + assert_eq!( + TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Waiting) + ); + + assert_eq!( + TxStatus::from_str(r#"{"Pending":{"Reattempting":{"attempt":3,"error":{"Remote":{"InvalidResponse":"bluh"}}}}}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Reattempting { attempt: 3, error: AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())) }) + ); + + assert_eq!( + TxStatus::from_str(r#"{"Confirmed":{"block_hash":"0xb4bc263299d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a","block_number":456789,"detection":"Normal"}}"#).unwrap(), + TxStatus::Confirmed{ + block_hash: "0xb4bc263299d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a".to_string(), + block_number: 456789, + detection: Detection::Normal, + } + ); + + assert_eq!( + TxStatus::from_str(r#"{"Confirmed":{"block_hash":"0x6d0abc11e617442c26104c2bc63d1bc05e1e002e555aec4ab62a46e826b18f18","block_number":567890,"detection":"Reclaim"}}"#).unwrap(), + TxStatus::Confirmed{ + block_hash: "0x6d0abc11e617442c26104c2bc63d1bc05e1e002e555aec4ab62a46e826b18f18".to_string(), + block_number: 567890, + detection: Detection::Reclaim, + } + ); + + // Invalid Variant + assert_eq!( + TxStatus::from_str("\"UnknownStatus\"").unwrap_err(), + "unknown variant `UnknownStatus`, \ + expected `Pending` or `Confirmed` at line 1 column 15 in '\"UnknownStatus\"'" + ); + + // Invalid Input + assert_eq!( + TxStatus::from_str("not a failure status").unwrap_err(), + "expected value at line 1 column 1 in 'not a failure status'" + ); + } + + #[test] + fn tx_status_can_be_converted_from_tx_confirmation() { + let tx_confirmation = TxConfirmation { + block_info: TransactionBlock { + block_hash: make_block_hash(6), + block_number: 456789_u64.into(), + }, + detection: Detection::Normal, + }; + + assert_eq!( + TxStatus::from(&tx_confirmation), + TxStatus::Confirmed { + block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), + block_number: u64::try_from(tx_confirmation.block_info.block_number).unwrap(), + detection: tx_confirmation.detection, + } + ) + } } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index 004a76761..a1a2eeb31 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -1,15 +1,18 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use std::path::PathBuf; -use rusqlite::{Connection, OpenFlags}; -use crate::accountant::db_access_objects::sent_payable_dao::{ Tx}; +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, ValidationStatus, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{Tx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; -use web3::types::{Address}; -use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason, FailureStatus}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionBlock; -use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE}; +use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, +}; use crate::database::rusqlite_wrappers::ConnectionWrapperReal; +use rusqlite::{Connection, OpenFlags}; +use std::path::PathBuf; +use web3::types::Address; #[derive(Default)] pub struct TxBuilder { @@ -19,7 +22,7 @@ pub struct TxBuilder { timestamp_opt: Option, gas_price_wei_opt: Option, nonce_opt: Option, - block_opt: Option, + status_opt: Option, } impl TxBuilder { @@ -42,8 +45,8 @@ impl TxBuilder { self } - pub fn block(mut self, block: TransactionBlock) -> Self { - self.block_opt = Some(block); + pub fn status(mut self, status: TxStatus) -> Self { + self.status_opt = Some(status); self } @@ -55,7 +58,9 @@ impl TxBuilder { timestamp: self.timestamp_opt.unwrap_or_else(current_unix_timestamp), gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), - block_opt: self.block_opt, + status: self + .status_opt + .unwrap_or(TxStatus::Pending(ValidationStatus::Waiting)), } } } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index b7353b7c2..b91e2c924 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -6,6 +6,7 @@ use crate::blockchain::blockchain_interface::data_structures::errors::Blockchain use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use ethereum_types::{H256, U256, U64}; use futures::Future; +use serde_derive::{Deserialize, Serialize}; use serde_json::Value; use std::fmt::Display; use std::str::FromStr; @@ -77,7 +78,7 @@ pub struct TxReceipt { pub status: TxStatus, } -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct TransactionBlock { pub block_hash: H256, pub block_number: U64, diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index 18dded007..cbf6008c0 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -272,8 +272,7 @@ impl DbInitializerReal { gas_price_wei_high_b integer not null, gas_price_wei_low_b integer not null, nonce integer not null, - block_hash text null, - block_number integer null + status text not null )", [], ) diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs index bcbf192fc..5e4e18368 100644 --- a/node/src/database/db_migrations/migrations/migration_10_to_11.rs +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -19,8 +19,7 @@ impl DatabaseMigration for Migrate_10_to_11 { gas_price_wei_high_b integer not null, gas_price_wei_low_b integer not null, nonce integer not null, - block_hash text null, - block_number integer null + status text not null )"; let sql_statement_for_failed_payable = "create table if not exists failed_payable ( diff --git a/node/src/database/test_utils/mod.rs b/node/src/database/test_utils/mod.rs index b4924e77c..7b415b95b 100644 --- a/node/src/database/test_utils/mod.rs +++ b/node/src/database/test_utils/mod.rs @@ -22,8 +22,7 @@ pub const SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE: &[&[&str]] = &[ &["gas_price_wei_high_b", "integer", "not", "null"], &["gas_price_wei_low_b", "integer", "not", "null"], &["nonce", "integer", "not", "null"], - &["block_hash", "text", "null"], - &["block_number", "integer", "null"], + &["status", "text", "not", "null"], ]; pub const SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE: &[&[&str]] = &[ From b46c9b2483ea13bf0250fba1577ae30f9de000ef Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:21:24 +0200 Subject: [PATCH 09/37] scanner_directory_reorganization: finished (#676) Co-authored-by: Bert --- node/src/accountant/mod.rs | 2 +- node/src/accountant/scanners/mod.rs | 1176 +---------------- .../scanners/pending_payable_scanner/mod.rs | 804 +++++++++++ .../scanners/pending_payable_scanner/utils.rs | 191 +++ .../scanners/receivable_scanner/mod.rs | 195 +++ .../scanners/receivable_scanner/utils.rs | 39 + .../accountant/scanners/scan_schedulers.rs | 4 +- .../src/accountant/scanners/scanners_utils.rs | 221 ---- node/src/accountant/scanners/test_utils.rs | 2 +- node/src/accountant/test_utils.rs | 4 +- 10 files changed, 1267 insertions(+), 1371 deletions(-) create mode 100644 node/src/accountant/scanners/pending_payable_scanner/mod.rs create mode 100644 node/src/accountant/scanners/pending_payable_scanner/utils.rs create mode 100644 node/src/accountant/scanners/receivable_scanner/mod.rs create mode 100644 node/src/accountant/scanners/receivable_scanner/utils.rs diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 7237110a8..39ead2d76 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,9 +76,9 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 041fe2196..02aa19459 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,6 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod payable_scanner_extension; +pub mod pending_payable_scanner; +pub mod receivable_scanner; pub mod scan_schedulers; pub mod scanners_utils; pub mod test_utils; @@ -13,15 +15,12 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableT LocallyCausedError, RemotelyCausedErrors, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; -use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::accountant::db_access_objects::banned_dao::BannedDao; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::sub_lib::accountant::{ DaoFactories, FinancialStatistics, PaymentThresholds, @@ -46,9 +45,12 @@ use variant_count::VariantCount; use web3::types::H256; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, UnpricedQualifiedPayables}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; -use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; +use crate::db_config::persistent_configuration::{PersistentConfigurationReal}; // Leave the individual scanner objects private! pub struct Scanners { @@ -126,7 +128,7 @@ impl Scanners { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( - MTError::AutomaticScanConflict, + ManulTriggerError::AutomaticScanConflict, )); } if let Some(started_at) = self.payable.scan_started_at() { @@ -230,7 +232,7 @@ impl Scanners { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( - MTError::AutomaticScanConflict, + ManulTriggerError::AutomaticScanConflict, )); } if let Some(started_at) = self.receivable.scan_started_at() { @@ -336,7 +338,7 @@ impl Scanners { ) -> Result<(), StartScanError> { if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( - MTError::AutomaticScanConflict, + ManulTriggerError::AutomaticScanConflict, )); } if self.initial_pending_payable_scan { @@ -344,7 +346,7 @@ impl Scanners { } if triggered_manually && !self.aware_of_unresolved_pending_payable { return Err(StartScanError::ManualTriggerError( - MTError::UnnecessaryRequest { + ManulTriggerError::UnnecessaryRequest { hint_opt: Some("Run the Payable scanner first.".to_string()), }, )); @@ -849,433 +851,6 @@ impl PayableScanner { } } -pub struct PendingPayableScanner { - pub common: ScannerCommon, - pub payable_dao: Box, - pub pending_payable_dao: Box, - pub when_pending_too_long_sec: u64, - pub financial_statistics: Rc>, -} - -impl - PrivateScanner< - ScanForPendingPayables, - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - > for PendingPayableScanner -{ -} - -impl StartableScanner - for PendingPayableScanner -{ - fn start_scan( - &mut self, - _wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.mark_as_started(timestamp); - info!(logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); - match filtered_pending_payable.is_empty() { - true => { - self.mark_as_ended(logger); - Err(StartScanError::NothingToProcess) - } - false => { - debug!( - logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - Ok(RequestTransactionReceipts { - pending_payable_fingerprints: filtered_pending_payable, - response_skeleton_opt, - }) - } - } - } -} - -impl Scanner for PendingPayableScanner { - fn finish_scan( - &mut self, - message: ReportTransactionReceipts, - logger: &Logger, - ) -> PendingPayableScanResult { - let response_skeleton_opt = message.response_skeleton_opt; - - let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { - true => { - warning!(logger, "No transaction receipts found."); - todo!("This requires the payment retry. GH-631 must be completed first"); - } - false => { - debug!( - logger, - "Processing receipts for {} transactions", - message.fingerprints_with_receipts.len() - ); - let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - let requires_payment_retry = - self.process_transactions_by_reported_state(scan_report, logger); - - self.mark_as_ended(logger); - - requires_payment_retry - } - }; - - if requires_payment_retry { - PendingPayableScanResult::PaymentRetryRequired - } else { - let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }); - PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) - } - } - - time_marking_methods!(PendingPayables); - - as_any_ref_in_trait_impl!(); -} - -impl PendingPayableScanner { - pub fn new( - payable_dao: Box, - pending_payable_dao: Box, - payment_thresholds: Rc, - when_pending_too_long_sec: u64, - financial_statistics: Rc>, - ) -> Self { - Self { - common: ScannerCommon::new(payment_thresholds), - payable_dao, - pending_payable_dao, - when_pending_too_long_sec, - financial_statistics, - } - } - - fn handle_receipts_for_pending_transactions( - &self, - msg: ReportTransactionReceipts, - logger: &Logger, - ) -> PendingPayableScanReport { - let scan_report = PendingPayableScanReport::default(); - msg.fingerprints_with_receipts.into_iter().fold( - scan_report, - |scan_report_so_far, (receipt_result, fingerprint)| match receipt_result { - TransactionReceiptResult::RpcResponse(tx_receipt) => match tx_receipt.status { - TxStatus::Pending => handle_none_receipt( - scan_report_so_far, - fingerprint, - "none was given", - logger, - ), - TxStatus::Failed => { - handle_status_with_failure(scan_report_so_far, fingerprint, logger) - } - TxStatus::Succeeded(_) => { - handle_status_with_success(scan_report_so_far, fingerprint, logger) - } - }, - TransactionReceiptResult::LocalError(e) => handle_none_receipt( - scan_report_so_far, - fingerprint, - &format!("failed due to {}", e), - logger, - ), - }, - ) - } - - fn process_transactions_by_reported_state( - &mut self, - scan_report: PendingPayableScanReport, - logger: &Logger, - ) -> bool { - let requires_payments_retry = scan_report.requires_payments_retry(); - - self.confirm_transactions(scan_report.confirmed, logger); - self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger); - - requires_payments_retry - } - - fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.increment_scan_attempts(&rowids) { - Ok(_) => trace!( - logger, - "Updated records for rowids: {} ", - comma_joined_stringifiable(&rowids, |id| id.to_string()) - ), - Err(e) => panic!( - "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", - PendingPayableId::serialize_hashes_to_string(&ids), - e - ), - } - } - } - - fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - //TODO this function is imperfect. It waits for GH-663 - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.mark_failures(&rowids) { - Ok(_) => warning!( - logger, - "Broken transactions {} marked as an error. You should take over the care \ - of those to make sure your debts are going to be settled properly. At the moment, \ - there is no automated process fixing that without your assistance", - PendingPayableId::serialize_hashes_to_string(&ids) - ), - Err(e) => panic!( - "Unsuccessful attempt for transactions {} \ - to mark fatal error at payable fingerprint due to {:?}; database unreliable", - PendingPayableId::serialize_hashes_to_string(&ids), - e - ), - } - } - } - - fn confirm_transactions( - &mut self, - fingerprints: Vec, - logger: &Logger, - ) { - fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { - comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) - } - - if !fingerprints.is_empty() { - if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { - panic!( - "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ - records due to {:?}", serialize_hashes(&fingerprints), e - ) - } else { - self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); - let rowids = fingerprints - .iter() - .map(|fingerprint| fingerprint.rowid) - .collect::>(); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { - panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", - serialize_hashes(&fingerprints), e) - } else { - info!( - logger, - "Transactions {} completed their confirmation process succeeding", - serialize_hashes(&fingerprints) - ) - } - } - } - } - - fn add_to_the_total_of_paid_payable( - &mut self, - fingerprints: &[PendingPayableFingerprint], - serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, - logger: &Logger, - ) { - fingerprints.iter().for_each(|fingerprint| { - self.financial_statistics - .borrow_mut() - .total_paid_payable_wei += fingerprint.amount - }); - debug!( - logger, - "Confirmation of transactions {}; record for total paid payable was modified", - serialize_hashes(fingerprints) - ); - } -} - -pub struct ReceivableScanner { - pub common: ScannerCommon, - pub receivable_dao: Box, - pub banned_dao: Box, - pub persistent_configuration: Box, - pub financial_statistics: Rc>, -} - -impl - PrivateScanner< - ScanForReceivables, - RetrieveTransactions, - ReceivedPayments, - Option, - > for ReceivableScanner -{ -} - -impl StartableScanner for ReceivableScanner { - fn start_scan( - &mut self, - earning_wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.mark_as_started(timestamp); - info!(logger, "Scanning for receivables to {}", earning_wallet); - self.scan_for_delinquencies(timestamp, logger); - - Ok(RetrieveTransactions { - recipient: earning_wallet.clone(), - response_skeleton_opt, - }) - } -} - -impl Scanner> for ReceivableScanner { - fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { - self.handle_new_received_payments(&msg, logger); - self.mark_as_ended(logger); - - msg.response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - } - - time_marking_methods!(Receivables); - - as_any_ref_in_trait_impl!(); - as_any_mut_in_trait_impl!(); -} - -impl ReceivableScanner { - pub fn new( - receivable_dao: Box, - banned_dao: Box, - persistent_configuration: Box, - payment_thresholds: Rc, - financial_statistics: Rc>, - ) -> Self { - Self { - common: ScannerCommon::new(payment_thresholds), - receivable_dao, - banned_dao, - persistent_configuration, - financial_statistics, - } - } - - fn handle_new_received_payments( - &mut self, - received_payments_msg: &ReceivedPayments, - logger: &Logger, - ) { - if received_payments_msg.transactions.is_empty() { - info!( - logger, - "No newly received payments were detected during the scanning process." - ); - let new_start_block = received_payments_msg.new_start_block; - if let BlockMarker::Value(start_block_number) = new_start_block { - match self - .persistent_configuration - .set_start_block(Some(start_block_number)) - { - Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), - Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", - start_block_number, e - ), - } - } - } else { - let mut txn = self.receivable_dao.as_mut().more_money_received( - received_payments_msg.timestamp, - &received_payments_msg.transactions, - ); - let new_start_block = received_payments_msg.new_start_block; - if let BlockMarker::Value(start_block_number) = new_start_block { - match self - .persistent_configuration - .set_start_block_from_txn(Some(start_block_number), &mut txn) - { - Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), - Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", - start_block_number, e - ), - } - } else { - unreachable!("Failed to get start_block while transactions were present"); - } - match txn.commit() { - Ok(_) => { - debug!(logger, "Received payments have been commited to database"); - } - Err(e) => panic!("Commit of received transactions failed: {:?}", e), - } - let total_newly_paid_receivable = received_payments_msg - .transactions - .iter() - .fold(0, |so_far, now| so_far + now.wei_amount); - - self.financial_statistics - .borrow_mut() - .total_paid_receivable_wei += total_newly_paid_receivable; - } - } - - pub fn scan_for_delinquencies(&self, timestamp: SystemTime, logger: &Logger) { - info!(logger, "Scanning for delinquencies"); - self.find_and_ban_delinquents(timestamp, logger); - self.find_and_unban_reformed_nodes(timestamp, logger); - } - - fn find_and_ban_delinquents(&self, timestamp: SystemTime, logger: &Logger) { - self.receivable_dao - .new_delinquencies(timestamp, self.common.payment_thresholds.as_ref()) - .into_iter() - .for_each(|account| { - self.banned_dao.ban(&account.wallet); - let (balance_str_wei, age) = balance_and_age(timestamp, &account); - info!( - logger, - "Wallet {} (balance: {} gwei, age: {} sec) banned for delinquency", - account.wallet, - balance_str_wei, - age.as_secs() - ) - }); - } - - fn find_and_unban_reformed_nodes(&self, timestamp: SystemTime, logger: &Logger) { - self.receivable_dao - .paid_delinquencies(self.common.payment_thresholds.as_ref()) - .into_iter() - .for_each(|account| { - self.banned_dao.unban(&account.wallet); - let (balance_str_wei, age) = balance_and_age(timestamp, &account); - info!( - logger, - "Wallet {} (balance: {} gwei, age: {} sec) is no longer delinquent: unbanned", - account.wallet, - balance_str_wei, - age.as_secs() - ) - }); - } -} - #[derive(Debug, PartialEq, Eq, Clone, VariantCount)] pub enum StartScanError { NothingToProcess, @@ -1285,7 +860,7 @@ pub enum StartScanError { started_at: SystemTime, }, CalledFromNullScanner, // Exclusive for tests - ManualTriggerError(MTError), + ManualTriggerError(ManulTriggerError), } impl StartScanError { @@ -1320,18 +895,20 @@ impl StartScanError { false => panic!("Null Scanner shouldn't be running inside production code."), }, StartScanError::ManualTriggerError(e) => match e { - MTError::AutomaticScanConflict => ErrorType::Permanent(format!( + ManulTriggerError::AutomaticScanConflict => ErrorType::Permanent(format!( "User requested {:?} scan was denied. Automatic mode prevents manual triggers.", scan_type )), - MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( - "User requested {:?} scan was denied expecting zero findings.{}", - scan_type, - match hint_opt { - Some(hint) => format!(" {}", hint), - None => "".to_string(), - } - )), + ManulTriggerError::UnnecessaryRequest { hint_opt } => { + ErrorType::Temporary(format!( + "User requested {:?} scan was denied expecting zero findings.{}", + scan_type, + match hint_opt { + Some(hint) => format!(" {}", hint), + None => "".to_string(), + } + )) + } }, }; @@ -1376,7 +953,7 @@ impl StartScanError { } #[derive(Debug, PartialEq, Eq, Clone)] -pub enum MTError { +pub enum ManulTriggerError { AutomaticScanConflict, UnnecessaryRequest { hint_opt: Option }, } @@ -1400,8 +977,7 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; - use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -1438,6 +1014,7 @@ mod tests { use web3::Error; use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; + use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; @@ -2017,190 +1594,6 @@ mod tests { )); } - #[test] - fn entries_must_be_kept_consistent_and_aligned() { - let wallet_1 = make_wallet("abc"); - let hash_1 = make_tx_hash(123); - let wallet_2 = make_wallet("def"); - let hash_2 = make_tx_hash(345); - let wallet_3 = make_wallet("ghi"); - let hash_3 = make_tx_hash(546); - let wallet_4 = make_wallet("jkl"); - let hash_4 = make_tx_hash(678); - let pending_payables_owned = vec![ - PendingPayable::new(wallet_1.clone(), hash_1), - PendingPayable::new(wallet_2.clone(), hash_2), - PendingPayable::new(wallet_3.clone(), hash_3), - PendingPayable::new(wallet_4.clone(), hash_4), - ]; - let pending_payables_ref = pending_payables_owned - .iter() - .collect::>(); - let pending_payable_dao = - PendingPayableDaoMock::new().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(4, hash_4), (1, hash_1), (3, hash_3), (2, hash_2)], - no_rowid_results: vec![], - }); - let subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - let (existent, nonexistent) = - subject.separate_existent_and_nonexistent_fingerprints(&pending_payables_ref); - - assert_eq!( - existent, - vec![ - PendingPayableMetadata::new(&wallet_4, hash_4, Some(4)), - PendingPayableMetadata::new(&wallet_1, hash_1, Some(1)), - PendingPayableMetadata::new(&wallet_3, hash_3, Some(3)), - PendingPayableMetadata::new(&wallet_2, hash_2, Some(2)), - ] - ); - assert!(nonexistent.is_empty()) - } - - struct TestingMismatchedDataAboutPendingPayables { - pending_payables: Vec, - common_hash_1: H256, - common_hash_3: H256, - intruder_for_hash_2: H256, - } - - fn prepare_values_for_mismatched_setting() -> TestingMismatchedDataAboutPendingPayables { - let hash_1 = make_tx_hash(123); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(789); - let intruder = make_tx_hash(567); - let pending_payables = vec![ - PendingPayable::new(make_wallet("abc"), hash_1), - PendingPayable::new(make_wallet("def"), hash_2), - PendingPayable::new(make_wallet("ghi"), hash_3), - ]; - TestingMismatchedDataAboutPendingPayables { - pending_payables, - common_hash_1: hash_1, - common_hash_3: hash_3, - intruder_for_hash_2: intruder, - } - } - - #[test] - #[should_panic( - expected = "Inconsistency in two maps, they cannot be matched by hashes. \ - Data set directly sent from BlockchainBridge: \ - [PendingPayable { recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000616263) }, \ - hash: 0x000000000000000000000000000000000000000000000000000000000000007b }, \ - PendingPayable { recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000646566) }, \ - hash: 0x00000000000000000000000000000000000000000000000000000000000001c8 }, \ - PendingPayable { recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000676869) }, \ - hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }], \ - set derived from the DB: \ - TransactionHashes { rowid_results: \ - [(4, 0x000000000000000000000000000000000000000000000000000000000000007b), \ - (1, 0x0000000000000000000000000000000000000000000000000000000000000237), \ - (3, 0x0000000000000000000000000000000000000000000000000000000000000315)], \ - no_rowid_results: [] }" - )] - fn two_sourced_information_of_new_pending_payables_and_their_fingerprints_is_not_symmetrical() { - let vals = prepare_values_for_mismatched_setting(); - let pending_payables_ref = vals - .pending_payables - .iter() - .collect::>(); - let pending_payable_dao = - PendingPayableDaoMock::new().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (4, vals.common_hash_1), - (1, vals.intruder_for_hash_2), - (3, vals.common_hash_3), - ], - no_rowid_results: vec![], - }); - let subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - subject.separate_existent_and_nonexistent_fingerprints(&pending_payables_ref); - } - - #[test] - fn symmetry_check_happy_path() { - let hash_1 = make_tx_hash(123); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(789); - let pending_payables_sent_from_blockchain_bridge = vec![ - PendingPayable::new(make_wallet("abc"), hash_1), - PendingPayable::new(make_wallet("def"), hash_2), - PendingPayable::new(make_wallet("ghi"), hash_3), - ]; - let pending_payables_ref = pending_payables_sent_from_blockchain_bridge - .iter() - .map(|ppayable| ppayable.hash) - .collect::>(); - let hashes_from_fingerprints = vec![(hash_1, 3), (hash_2, 5), (hash_3, 6)] - .iter() - .map(|(hash, _id)| *hash) - .collect::>(); - - let result = PayableScanner::is_symmetrical(pending_payables_ref, hashes_from_fingerprints); - - assert_eq!(result, true) - } - - #[test] - fn symmetry_check_sad_path_for_intruder() { - let vals = prepare_values_for_mismatched_setting(); - let pending_payables_ref_from_blockchain_bridge = vals - .pending_payables - .iter() - .map(|ppayable| ppayable.hash) - .collect::>(); - let rowids_and_hashes_from_fingerprints = vec![ - (vals.common_hash_1, 3), - (vals.intruder_for_hash_2, 5), - (vals.common_hash_3, 6), - ] - .iter() - .map(|(hash, _rowid)| *hash) - .collect::>(); - - let result = PayableScanner::is_symmetrical( - pending_payables_ref_from_blockchain_bridge, - rowids_and_hashes_from_fingerprints, - ); - - assert_eq!(result, false) - } - - #[test] - fn symmetry_check_indifferent_to_wrong_order_on_the_input() { - let hash_1 = make_tx_hash(123); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(789); - let pending_payables_sent_from_blockchain_bridge = vec![ - PendingPayable::new(make_wallet("abc"), hash_1), - PendingPayable::new(make_wallet("def"), hash_2), - PendingPayable::new(make_wallet("ghi"), hash_3), - ]; - let bb_returned_p_payables_ref = pending_payables_sent_from_blockchain_bridge - .iter() - .map(|ppayable| ppayable.hash) - .collect::>(); - // Not in ascending order - let rowids_and_hashes_from_fingerprints = vec![(hash_1, 3), (hash_3, 5), (hash_2, 6)] - .iter() - .map(|(hash, _id)| *hash) - .collect::>(); - - let result = PayableScanner::is_symmetrical( - bb_returned_p_payables_ref, - rowids_and_hashes_from_fingerprints, - ); - - assert_eq!(result, true) - } - #[test] #[should_panic( expected = "Expected pending payable fingerprints for (tx: 0x0000000000000000000000000000000000000000000000000000000000000315, \ @@ -3040,515 +2433,6 @@ mod tests { assert_eq!(subject.initial_pending_payable_scan, true); } - fn assert_interpreting_none_status_for_pending_payable( - test_name: &str, - when_pending_too_long_sec: u64, - pending_payable_age_sec: u64, - rowid: u64, - hash: H256, - ) -> PendingPayableScanReport { - init_test_logging(); - let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: when_sent, - hash, - attempt: 1, - amount: 123, - process_error: None, - }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - - handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) - } - - fn assert_log_msg_and_elapsed_time_in_log_makes_sense( - expected_msg: &str, - elapsed_after: u64, - capture_regex: &str, - ) { - let log_handler = TestLogHandler::default(); - let log_idx = log_handler.exists_log_matching(expected_msg); - let log = log_handler.get_log_at(log_idx); - let capture = captures_for_regex_time_in_sec(&log, capture_regex); - assert!(capture <= elapsed_after) - } - - fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { - let capture_regex = Regex::new(capture_regex).unwrap(); - let time_str = capture_regex - .captures(stack) - .unwrap() - .get(1) - .unwrap() - .as_str(); - time_str.parse().unwrap() - } - - fn elapsed_since_secs_back(sec: u64) -> u64 { - SystemTime::now() - .sub(Duration::from_secs(sec)) - .elapsed() - .unwrap() - .as_secs() - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() - { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; - let hash = make_tx_hash(0x237); - let rowid = 466; - - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - DEFAULT_PENDING_TOO_LONG_SEC + 1, - rowid, - hash, - ); - - let elapsed_after = elapsed_since_secs_back(DEFAULT_PENDING_TOO_LONG_SEC + 1); - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(rowid, hash)], - confirmed: vec![] - } - ); - let capture_regex = "(\\d+){2}sec"; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "ERROR: {}: Pending transaction 0x00000000000000000000000000000000000000\ - 00000000000000000000000237 has exceeded the maximum pending time \\({}sec\\) with the age \ - \\d+sec and the confirmation process is going to be aborted now at the final attempt 1; manual \ - resolution is required from the user to complete the transaction" - , test_name, DEFAULT_PENDING_TOO_LONG_SEC, ), elapsed_after, capture_regex) - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; - let hash = make_tx_hash(0x7b); - let rowid = 333; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; - - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, - ); - - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 00000000000007b couldn't be confirmed at attempt 1 at \\d+ms after its sending"), elapsed_after_ms, capture_regex); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; - let hash = make_tx_hash(0x237); - let rowid = 466; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; - - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, - ); - - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 000000000000237 couldn't be confirmed at attempt 1 at \\d+ms after its sending", - ), elapsed_after_ms, capture_regex); - } - - #[test] - fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { - init_test_logging(); - let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = make_tx_hash(0xd7); - let fingerprint = PendingPayableFingerprint { - rowid: 777777, - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt: 5, - amount: 2222, - process_error: None, - }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - - let result = handle_status_with_failure(scan_report, fingerprint, &logger); - - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(777777, hash,)], - confirmed: vec![] - } - ); - TestLogHandler::new().exists_log_matching(&format!( - "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ - 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ - 1500\\d\\dms from the sending" - )); - } - - #[test] - fn handle_pending_txs_with_receipts_handles_none_for_receipt() { - init_test_logging(); - let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; - let subject = PendingPayableScannerBuilder::new().build(); - let rowid = 455; - let hash = make_tx_hash(0x913); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt: 3, - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }), - fingerprint.clone(), - )], - response_skeleton_opt: None, - }; - - let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); - - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {test_name}: Interpreting a receipt for transaction \ - 0x0000000000000000000000000000000000000000000000000000000000000913 \ - but none was given; attempt 3, 100\\d\\dms since sending" - )); - } - - #[test] - fn increment_scan_attempts_happy_path() { - let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let hash_1 = make_tx_hash(444888); - let rowid_1 = 3456; - let hash_2 = make_tx_hash(444888); - let rowid_2 = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) - .increment_scan_attempts_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); - let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); - - let _ = subject.update_remaining_fingerprints( - vec![transaction_id_1, transaction_id_2], - &Logger::new("test"), - ); - - let update_remaining_fingerprints_params = - update_remaining_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *update_remaining_fingerprints_params, - vec![vec![rowid_1, rowid_2]] - ) - } - - #[test] - #[should_panic( - expected = "Failure on incrementing scan attempts for fingerprints of \ - 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ - due to UpdateFailed(\"yeah, bad\")" - )] - fn increment_scan_attempts_sad_path() { - let hash = make_tx_hash(0x6c9d8); - let rowid = 3456; - let pending_payable_dao = - PendingPayableDaoMock::default().increment_scan_attempts_result(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let logger = Logger::new("test"); - let transaction_id = PendingPayableId::new(rowid, hash); - - let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); - } - - #[test] - fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { - let subject = PendingPayableScannerBuilder::new().build(); - - subject.update_remaining_fingerprints(vec![], &Logger::new("test")) - - //mocked pending payable DAO didn't panic which means we skipped the actual process - } - - #[test] - fn cancel_failed_transactions_works() { - init_test_logging(); - let test_name = "cancel_failed_transactions_works"; - let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failures_params(&mark_failures_params_arc) - .mark_failures_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); - let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); - - subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); - - let mark_failures_params = mark_failures_params_arc.lock().unwrap(); - assert_eq!(*mark_failures_params, vec![vec![2, 3]]); - TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ - 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ - the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ - process fixing that without your assistance", - )); - } - - #[test] - #[should_panic( - expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ - 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ - 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ - database unreliable" - )] - fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { - let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), - )); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); - let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); - let transaction_ids = vec![transaction_id_1, transaction_id_2]; - - subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); - } - - #[test] - fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { - let subject = PendingPayableScannerBuilder::new().build(); - - subject.cancel_failed_transactions(vec![], &Logger::new("test")) - - //mocked pending payable DAO didn't panic which means we skipped the actual process - } - - #[test] - #[should_panic( - expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ - 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ - 0000000000021a of verified transactions due to RecordDeletion(\"the database \ - is fooling around with us\")" - )] - fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { - let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Err( - PendingPayableDaoError::RecordDeletion( - "the database is fooling around with us".to_string(), - ), - )); - let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.rowid = 1; - fingerprint_1.hash = make_tx_hash(0x315); - let mut fingerprint_2 = make_pending_payable_fingerprint(); - fingerprint_2.rowid = 1; - fingerprint_2.hash = make_tx_hash(0x21a); - - subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); - } - - #[test] - fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { - let mut subject = PendingPayableScannerBuilder::new().build(); - - subject.confirm_transactions(vec![], &Logger::new("test")) - - //mocked payable DAO didn't panic which means we skipped the actual process - } - - #[test] - fn confirm_transactions_works() { - init_test_logging(); - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); - let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let rowid_1 = 2; - let rowid_2 = 5; - let pending_payable_fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: from_unix_timestamp(199_000_000), - hash: make_tx_hash(0x123), - attempt: 1, - amount: 4567, - process_error: None, - }; - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: from_unix_timestamp(200_000_000), - hash: make_tx_hash(0x567), - attempt: 1, - amount: 5555, - process_error: None, - }; - - subject.confirm_transactions( - vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), - ], - &Logger::new("confirm_transactions_works"), - ); - - let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!( - *confirm_transactions_params, - vec![vec![ - pending_payable_fingerprint_1, - pending_payable_fingerprint_2 - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "DEBUG: confirm_transactions_works: \ - Confirmation of transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567; \ - record for total paid payable was modified", - ); - log_handler.exists_log_containing( - "INFO: confirm_transactions_works: \ - Transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567 \ - completed their confirmation process succeeding", - ); - } - - #[test] - #[should_panic( - expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ - 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ - (\"record change not successful\")" - )] - fn confirm_transactions_panics_on_unchecking_payable_table() { - let hash = make_tx_hash(0x315); - let rowid = 3; - let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( - PayableDaoError::RusqliteError("record change not successful".to_string()), - )); - let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .build(); - let mut fingerprint = make_pending_payable_fingerprint(); - fingerprint.rowid = rowid; - fingerprint.hash = hash; - - subject.confirm_transactions(vec![fingerprint], &Logger::new("test")); - } - - #[test] - fn total_paid_payable_rises_with_each_bill_paid() { - let test_name = "total_paid_payable_rises_with_each_bill_paid"; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(189_999_888), - hash: make_tx_hash(56789), - attempt: 1, - amount: 5478, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 6, - timestamp: from_unix_timestamp(200_000_011), - hash: make_tx_hash(33333), - attempt: 1, - amount: 6543, - process_error: None, - }; - let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .build(); - let mut financial_statistics = subject.financial_statistics.borrow().clone(); - financial_statistics.total_paid_payable_wei += 1111; - subject.financial_statistics.replace(financial_statistics); - - subject.confirm_transactions( - vec![fingerprint_1.clone(), fingerprint_2.clone()], - &Logger::new(test_name), - ); - - let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; - assert_eq!(total_paid_payable, 1111 + 5478 + 6543); - } - #[test] fn pending_payable_scanner_handles_report_transaction_receipts_message() { init_test_logging(); @@ -4296,7 +3180,7 @@ mod tests { "DEBUG", ), ( - StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + StartScanError::ManualTriggerError(ManulTriggerError::AutomaticScanConflict), Box::new(|sev| { format!("{sev}: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") }), @@ -4304,7 +3188,7 @@ mod tests { "WARN", ), ( - StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + StartScanError::ManualTriggerError(ManulTriggerError::UnnecessaryRequest { hint_opt: Some("Wise words".to_string()), }), Box::new(|sev| { @@ -4314,7 +3198,9 @@ mod tests { "DEBUG", ), ( - StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: None }), + StartScanError::ManualTriggerError(ManulTriggerError::UnnecessaryRequest { + hint_opt: None, + }), Box::new(|sev| { format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings.") }), diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs new file mode 100644 index 000000000..cfb874f19 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -0,0 +1,804 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod utils; + +use std::cell::RefCell; +use std::rc::Rc; +use std::time::SystemTime; +use masq_lib::logger::Logger; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use crate::accountant::db_access_objects::payable_dao::PayableDao; +use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; +use crate::accountant::{comma_joined_stringifiable, PendingPayableId, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables}; +use crate::accountant::scanners::{PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner}; +use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; +use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; +use crate::sub_lib::wallet::Wallet; +use crate::time_marking_methods; + +pub struct PendingPayableScanner { + pub common: ScannerCommon, + pub payable_dao: Box, + pub pending_payable_dao: Box, + pub when_pending_too_long_sec: u64, + pub financial_statistics: Rc>, +} + +impl + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + > for PendingPayableScanner +{ +} + +impl StartableScanner + for PendingPayableScanner +{ + fn start_scan( + &mut self, + _wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.mark_as_started(timestamp); + info!(logger, "Scanning for pending payable"); + let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); + match filtered_pending_payable.is_empty() { + true => { + self.mark_as_ended(logger); + Err(StartScanError::NothingToProcess) + } + false => { + debug!( + logger, + "Found {} pending payables to process", + filtered_pending_payable.len() + ); + Ok(RequestTransactionReceipts { + pending_payable_fingerprints: filtered_pending_payable, + response_skeleton_opt, + }) + } + } + } +} + +impl Scanner for PendingPayableScanner { + fn finish_scan( + &mut self, + message: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanResult { + let response_skeleton_opt = message.response_skeleton_opt; + + let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { + true => { + warning!(logger, "No transaction receipts found."); + todo!("This requires the payment retry. GH-631 must be completed first"); + } + false => { + debug!( + logger, + "Processing receipts for {} transactions", + message.fingerprints_with_receipts.len() + ); + let scan_report = self.handle_receipts_for_pending_transactions(message, logger); + let requires_payment_retry = + self.process_transactions_by_reported_state(scan_report, logger); + + self.mark_as_ended(logger); + + requires_payment_retry + } + }; + + if requires_payment_retry { + PendingPayableScanResult::PaymentRetryRequired + } else { + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) + } + } + + time_marking_methods!(PendingPayables); + + as_any_ref_in_trait_impl!(); +} + +impl PendingPayableScanner { + pub fn new( + payable_dao: Box, + pending_payable_dao: Box, + payment_thresholds: Rc, + when_pending_too_long_sec: u64, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + payable_dao, + pending_payable_dao, + when_pending_too_long_sec, + financial_statistics, + } + } + + fn handle_receipts_for_pending_transactions( + &self, + msg: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanReport { + let scan_report = PendingPayableScanReport::default(); + msg.fingerprints_with_receipts.into_iter().fold( + scan_report, + |scan_report_so_far, (receipt_result, fingerprint)| match receipt_result { + TransactionReceiptResult::RpcResponse(tx_receipt) => match tx_receipt.status { + TxStatus::Pending => handle_none_receipt( + scan_report_so_far, + fingerprint, + "none was given", + logger, + ), + TxStatus::Failed => { + handle_status_with_failure(scan_report_so_far, fingerprint, logger) + } + TxStatus::Succeeded(_) => { + handle_status_with_success(scan_report_so_far, fingerprint, logger) + } + }, + TransactionReceiptResult::LocalError(e) => handle_none_receipt( + scan_report_so_far, + fingerprint, + &format!("failed due to {}", e), + logger, + ), + }, + ) + } + + fn process_transactions_by_reported_state( + &mut self, + scan_report: PendingPayableScanReport, + logger: &Logger, + ) -> bool { + let requires_payments_retry = scan_report.requires_payments_retry(); + + self.confirm_transactions(scan_report.confirmed, logger); + self.cancel_failed_transactions(scan_report.failures, logger); + self.update_remaining_fingerprints(scan_report.still_pending, logger); + + requires_payments_retry + } + + fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { + if !ids.is_empty() { + let rowids = PendingPayableId::rowids(&ids); + match self.pending_payable_dao.increment_scan_attempts(&rowids) { + Ok(_) => trace!( + logger, + "Updated records for rowids: {} ", + comma_joined_stringifiable(&rowids, |id| id.to_string()) + ), + Err(e) => panic!( + "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", + PendingPayableId::serialize_hashes_to_string(&ids), + e + ), + } + } + } + + fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { + if !ids.is_empty() { + //TODO this function is imperfect. It waits for GH-663 + let rowids = PendingPayableId::rowids(&ids); + match self.pending_payable_dao.mark_failures(&rowids) { + Ok(_) => warning!( + logger, + "Broken transactions {} marked as an error. You should take over the care \ + of those to make sure your debts are going to be settled properly. At the moment, \ + there is no automated process fixing that without your assistance", + PendingPayableId::serialize_hashes_to_string(&ids) + ), + Err(e) => panic!( + "Unsuccessful attempt for transactions {} \ + to mark fatal error at payable fingerprint due to {:?}; database unreliable", + PendingPayableId::serialize_hashes_to_string(&ids), + e + ), + } + } + } + + fn confirm_transactions( + &mut self, + fingerprints: Vec, + logger: &Logger, + ) { + fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { + comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) + } + + if !fingerprints.is_empty() { + if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { + panic!( + "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ + records due to {:?}", serialize_hashes(&fingerprints), e + ) + } else { + self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); + let rowids = fingerprints + .iter() + .map(|fingerprint| fingerprint.rowid) + .collect::>(); + if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { + panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", + serialize_hashes(&fingerprints), e) + } else { + info!( + logger, + "Transactions {} completed their confirmation process succeeding", + serialize_hashes(&fingerprints) + ) + } + } + } + } + + fn add_to_the_total_of_paid_payable( + &mut self, + fingerprints: &[PendingPayableFingerprint], + serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, + logger: &Logger, + ) { + fingerprints.iter().for_each(|fingerprint| { + self.financial_statistics + .borrow_mut() + .total_paid_payable_wei += fingerprint.amount + }); + debug!( + logger, + "Confirmation of transactions {}; record for total paid payable was modified", + serialize_hashes(fingerprints) + ); + } +} + +#[cfg(test)] +mod tests { + use std::ops::Sub; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; + use ethereum_types::{H256, U64}; + use regex::Regex; + use web3::types::TransactionReceipt; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use crate::accountant::{PendingPayableId, ReportTransactionReceipts, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::db_access_objects::payable_dao::PayableDaoError; + use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoError; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; + use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; + use crate::accountant::test_utils::{make_pending_payable_fingerprint, PayableDaoMock, PendingPayableDaoMock, PendingPayableScannerBuilder}; + use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxReceipt, TxStatus}; + use crate::blockchain::test_utils::make_tx_hash; + + fn assert_interpreting_none_status_for_pending_payable( + test_name: &str, + when_pending_too_long_sec: u64, + pending_payable_age_sec: u64, + rowid: u64, + hash: H256, + ) -> PendingPayableScanReport { + init_test_logging(); + let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); + let fingerprint = PendingPayableFingerprint { + rowid, + timestamp: when_sent, + hash, + attempt: 1, + amount: 123, + process_error: None, + }; + let logger = Logger::new(test_name); + let scan_report = PendingPayableScanReport::default(); + + handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) + } + + fn assert_log_msg_and_elapsed_time_in_log_makes_sense( + expected_msg: &str, + elapsed_after: u64, + capture_regex: &str, + ) { + let log_handler = TestLogHandler::default(); + let log_idx = log_handler.exists_log_matching(expected_msg); + let log = log_handler.get_log_at(log_idx); + let capture = captures_for_regex_time_in_sec(&log, capture_regex); + assert!(capture <= elapsed_after) + } + + fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { + let capture_regex = Regex::new(capture_regex).unwrap(); + let time_str = capture_regex + .captures(stack) + .unwrap() + .get(1) + .unwrap() + .as_str(); + time_str.parse().unwrap() + } + + fn elapsed_since_secs_back(sec: u64) -> u64 { + SystemTime::now() + .sub(Duration::from_secs(sec)) + .elapsed() + .unwrap() + .as_secs() + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() + { + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; + let hash = make_tx_hash(0x237); + let rowid = 466; + + let result = assert_interpreting_none_status_for_pending_payable( + test_name, + DEFAULT_PENDING_TOO_LONG_SEC, + DEFAULT_PENDING_TOO_LONG_SEC + 1, + rowid, + hash, + ); + + let elapsed_after = elapsed_since_secs_back(DEFAULT_PENDING_TOO_LONG_SEC + 1); + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![], + failures: vec![PendingPayableId::new(rowid, hash)], + confirmed: vec![] + } + ); + let capture_regex = "(\\d+){2}sec"; + assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( + "ERROR: {}: Pending transaction 0x00000000000000000000000000000000000000\ + 00000000000000000000000237 has exceeded the maximum pending time \\({}sec\\) with the age \ + \\d+sec and the confirmation process is going to be aborted now at the final attempt 1; manual \ + resolution is required from the user to complete the transaction" + , test_name, DEFAULT_PENDING_TOO_LONG_SEC, ), elapsed_after, capture_regex) + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; + let hash = make_tx_hash(0x7b); + let rowid = 333; + let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; + + let result = assert_interpreting_none_status_for_pending_payable( + test_name, + DEFAULT_PENDING_TOO_LONG_SEC, + pending_payable_age, + rowid, + hash, + ); + + let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(rowid, hash)], + failures: vec![], + confirmed: vec![] + } + ); + let capture_regex = r#"\s(\d+)ms"#; + assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( + "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ + 00000000000007b couldn't be confirmed at attempt 1 at \\d+ms after its sending"), elapsed_after_ms, capture_regex); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { + let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; + let hash = make_tx_hash(0x237); + let rowid = 466; + let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; + + let result = assert_interpreting_none_status_for_pending_payable( + test_name, + DEFAULT_PENDING_TOO_LONG_SEC, + pending_payable_age, + rowid, + hash, + ); + + let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(rowid, hash)], + failures: vec![], + confirmed: vec![] + } + ); + let capture_regex = r#"\s(\d+)ms"#; + assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( + "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ + 000000000000237 couldn't be confirmed at attempt 1 at \\d+ms after its sending", + ), elapsed_after_ms, capture_regex); + } + + #[test] + fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { + init_test_logging(); + let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; + let mut tx_receipt = TransactionReceipt::default(); + tx_receipt.status = Some(U64::from(0)); //failure + let hash = make_tx_hash(0xd7); + let fingerprint = PendingPayableFingerprint { + rowid: 777777, + timestamp: SystemTime::now().sub(Duration::from_millis(150000)), + hash, + attempt: 5, + amount: 2222, + process_error: None, + }; + let logger = Logger::new(test_name); + let scan_report = PendingPayableScanReport::default(); + + let result = handle_status_with_failure(scan_report, fingerprint, &logger); + + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![], + failures: vec![PendingPayableId::new(777777, hash,)], + confirmed: vec![] + } + ); + TestLogHandler::new().exists_log_matching(&format!( + "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ + 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ + 1500\\d\\dms from the sending" + )); + } + + #[test] + fn handle_pending_txs_with_receipts_handles_none_for_receipt() { + init_test_logging(); + let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; + let subject = PendingPayableScannerBuilder::new().build(); + let rowid = 455; + let hash = make_tx_hash(0x913); + let fingerprint = PendingPayableFingerprint { + rowid, + timestamp: SystemTime::now().sub(Duration::from_millis(10000)), + hash, + attempt: 3, + amount: 111, + process_error: None, + }; + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: hash, + status: TxStatus::Pending, + }), + fingerprint.clone(), + )], + response_skeleton_opt: None, + }; + + let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); + + assert_eq!( + result, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(rowid, hash)], + failures: vec![], + confirmed: vec![] + } + ); + TestLogHandler::new().exists_log_matching(&format!( + "DEBUG: {test_name}: Interpreting a receipt for transaction \ + 0x0000000000000000000000000000000000000000000000000000000000000913 \ + but none was given; attempt 3, 100\\d\\dms since sending" + )); + } + + #[test] + fn increment_scan_attempts_happy_path() { + let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let hash_1 = make_tx_hash(444888); + let rowid_1 = 3456; + let hash_2 = make_tx_hash(444888); + let rowid_2 = 3456; + let pending_payable_dao = PendingPayableDaoMock::default() + .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) + .increment_scan_attempts_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); + let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); + + let _ = subject.update_remaining_fingerprints( + vec![transaction_id_1, transaction_id_2], + &Logger::new("test"), + ); + + let update_remaining_fingerprints_params = + update_remaining_fingerprints_params_arc.lock().unwrap(); + assert_eq!( + *update_remaining_fingerprints_params, + vec![vec![rowid_1, rowid_2]] + ) + } + + #[test] + #[should_panic( + expected = "Failure on incrementing scan attempts for fingerprints of \ + 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ + due to UpdateFailed(\"yeah, bad\")" + )] + fn increment_scan_attempts_sad_path() { + let hash = make_tx_hash(0x6c9d8); + let rowid = 3456; + let pending_payable_dao = + PendingPayableDaoMock::default().increment_scan_attempts_result(Err( + PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), + )); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let logger = Logger::new("test"); + let transaction_id = PendingPayableId::new(rowid, hash); + + let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); + } + + #[test] + fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { + let subject = PendingPayableScannerBuilder::new().build(); + + subject.update_remaining_fingerprints(vec![], &Logger::new("test")) + + //mocked pending payable DAO didn't panic which means we skipped the actual process + } + + #[test] + fn cancel_failed_transactions_works() { + init_test_logging(); + let test_name = "cancel_failed_transactions_works"; + let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_dao = PendingPayableDaoMock::default() + .mark_failures_params(&mark_failures_params_arc) + .mark_failures_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); + let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); + + subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); + + let mark_failures_params = mark_failures_params_arc.lock().unwrap(); + assert_eq!(*mark_failures_params, vec![vec![2, 3]]); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ + 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ + the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ + process fixing that without your assistance", + )); + } + + #[test] + #[should_panic( + expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ + 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ + 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ + database unreliable" + )] + fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { + let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( + PendingPayableDaoError::UpdateFailed("no no no".to_string()), + )); + let subject = PendingPayableScannerBuilder::new() + .pending_payable_dao(pending_payable_dao) + .build(); + let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); + let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); + let transaction_ids = vec![transaction_id_1, transaction_id_2]; + + subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); + } + + #[test] + fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { + let subject = PendingPayableScannerBuilder::new().build(); + + subject.cancel_failed_transactions(vec![], &Logger::new("test")) + + //mocked pending payable DAO didn't panic which means we skipped the actual process + } + + #[test] + #[should_panic( + expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ + 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ + 0000000000021a of verified transactions due to RecordDeletion(\"the database \ + is fooling around with us\")" + )] + fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Err( + PendingPayableDaoError::RecordDeletion( + "the database is fooling around with us".to_string(), + ), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let mut fingerprint_1 = make_pending_payable_fingerprint(); + fingerprint_1.rowid = 1; + fingerprint_1.hash = make_tx_hash(0x315); + let mut fingerprint_2 = make_pending_payable_fingerprint(); + fingerprint_2.rowid = 1; + fingerprint_2.hash = make_tx_hash(0x21a); + + subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); + } + + #[test] + fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { + let mut subject = PendingPayableScannerBuilder::new().build(); + + subject.confirm_transactions(vec![], &Logger::new("test")) + + //mocked payable DAO didn't panic which means we skipped the actual process + } + + #[test] + fn confirm_transactions_works() { + init_test_logging(); + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transactions_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let pending_payable_dao = PendingPayableDaoMock::default() + .delete_fingerprints_params(&delete_fingerprints_params_arc) + .delete_fingerprints_result(Ok(())); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let rowid_1 = 2; + let rowid_2 = 5; + let pending_payable_fingerprint_1 = PendingPayableFingerprint { + rowid: rowid_1, + timestamp: from_unix_timestamp(199_000_000), + hash: make_tx_hash(0x123), + attempt: 1, + amount: 4567, + process_error: None, + }; + let pending_payable_fingerprint_2 = PendingPayableFingerprint { + rowid: rowid_2, + timestamp: from_unix_timestamp(200_000_000), + hash: make_tx_hash(0x567), + attempt: 1, + amount: 5555, + process_error: None, + }; + + subject.confirm_transactions( + vec![ + pending_payable_fingerprint_1.clone(), + pending_payable_fingerprint_2.clone(), + ], + &Logger::new("confirm_transactions_works"), + ); + + let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!( + *confirm_transactions_params, + vec![vec![ + pending_payable_fingerprint_1, + pending_payable_fingerprint_2 + ]] + ); + let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); + assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing( + "DEBUG: confirm_transactions_works: \ + Confirmation of transactions \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567; \ + record for total paid payable was modified", + ); + log_handler.exists_log_containing( + "INFO: confirm_transactions_works: \ + Transactions \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + completed their confirmation process succeeding", + ); + } + + #[test] + #[should_panic( + expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ + 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ + (\"record change not successful\")" + )] + fn confirm_transactions_panics_on_unchecking_payable_table() { + let hash = make_tx_hash(0x315); + let rowid = 3; + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( + PayableDaoError::RusqliteError("record change not successful".to_string()), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + let mut fingerprint = make_pending_payable_fingerprint(); + fingerprint.rowid = rowid; + fingerprint.hash = hash; + + subject.confirm_transactions(vec![fingerprint], &Logger::new("test")); + } + + #[test] + fn total_paid_payable_rises_with_each_bill_paid() { + let test_name = "total_paid_payable_rises_with_each_bill_paid"; + let fingerprint_1 = PendingPayableFingerprint { + rowid: 5, + timestamp: from_unix_timestamp(189_999_888), + hash: make_tx_hash(56789), + attempt: 1, + amount: 5478, + process_error: None, + }; + let fingerprint_2 = PendingPayableFingerprint { + rowid: 6, + timestamp: from_unix_timestamp(200_000_011), + hash: make_tx_hash(33333), + attempt: 1, + amount: 6543, + process_error: None, + }; + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .pending_payable_dao(pending_payable_dao) + .build(); + let mut financial_statistics = subject.financial_statistics.borrow().clone(); + financial_statistics.total_paid_payable_wei += 1111; + subject.financial_statistics.replace(financial_statistics); + + subject.confirm_transactions( + vec![fingerprint_1.clone(), fingerprint_2.clone()], + &Logger::new(test_name), + ); + + let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; + assert_eq!(total_paid_payable, 1111 + 5478 + 6543); + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs new file mode 100644 index 000000000..f277a1c91 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -0,0 +1,191 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::PendingPayableId; +use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use masq_lib::logger::Logger; +use masq_lib::ui_gateway::NodeToUiMessage; +use std::time::SystemTime; + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct PendingPayableScanReport { + pub still_pending: Vec, + pub failures: Vec, + pub confirmed: Vec, +} + +impl PendingPayableScanReport { + pub fn requires_payments_retry(&self) -> bool { + todo!("complete my within GH-642") + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PendingPayableScanResult { + NoPendingPayablesLeft(Option), + PaymentRetryRequired, +} + +pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { + timestamp + .elapsed() + .expect("time calculation for elapsed failed") + .as_millis() +} + +pub fn handle_none_status( + mut scan_report: PendingPayableScanReport, + fingerprint: PendingPayableFingerprint, + max_pending_interval: u64, + logger: &Logger, +) -> PendingPayableScanReport { + info!( + logger, + "Pending transaction {:?} couldn't be confirmed at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt, + elapsed_in_ms(fingerprint.timestamp) + ); + let elapsed = fingerprint + .timestamp + .elapsed() + .expect("we should be older now"); + let elapsed = elapsed.as_secs(); + if elapsed > max_pending_interval { + error!( + logger, + "Pending transaction {:?} has exceeded the maximum pending time \ + ({}sec) with the age {}sec and the confirmation process is going to be aborted now \ + at the final attempt {}; manual resolution is required from the \ + user to complete the transaction.", + fingerprint.hash, + max_pending_interval, + elapsed, + fingerprint.attempt + ); + scan_report.failures.push(fingerprint.into()) + } else { + scan_report.still_pending.push(fingerprint.into()) + } + scan_report +} + +pub fn handle_status_with_success( + mut scan_report: PendingPayableScanReport, + fingerprint: PendingPayableFingerprint, + logger: &Logger, +) -> PendingPayableScanReport { + info!( + logger, + "Transaction {:?} has been added to the blockchain; detected locally at attempt \ + {} at {}ms after its sending", + fingerprint.hash, + fingerprint.attempt, + elapsed_in_ms(fingerprint.timestamp) + ); + scan_report.confirmed.push(fingerprint); + scan_report +} + +//TODO: failures handling is going to need enhancement suggested by GH-693 +pub fn handle_status_with_failure( + mut scan_report: PendingPayableScanReport, + fingerprint: PendingPayableFingerprint, + logger: &Logger, +) -> PendingPayableScanReport { + error!( + logger, + "Pending transaction {:?} announced as a failure, interpreting attempt \ + {} after {}ms from the sending", + fingerprint.hash, + fingerprint.attempt, + elapsed_in_ms(fingerprint.timestamp) + ); + scan_report.failures.push(fingerprint.into()); + scan_report +} + +pub fn handle_none_receipt( + mut scan_report: PendingPayableScanReport, + payable: PendingPayableFingerprint, + error_msg: &str, + logger: &Logger, +) -> PendingPayableScanReport { + debug!( + logger, + "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", + payable.hash, + error_msg, + payable.attempt, + elapsed_in_ms(payable.timestamp) + ); + + scan_report + .still_pending + .push(PendingPayableId::new(payable.rowid, payable.hash)); + scan_report +} + +#[cfg(test)] +mod tests { + + #[test] + fn requires_payments_retry_says_yes() { + todo!("complete this test with GH-604") + // let cases = vec![ + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // ]; + // + // cases.into_iter().enumerate().for_each(|(idx, case)| { + // let result = case.requires_payments_retry(); + // assert_eq!( + // result, true, + // "We expected true, but got false for case of idx {}", + // idx + // ) + // }) + } + + #[test] + fn requires_payments_retry_says_no() { + todo!("complete this test with GH-604") + // let report = PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }; + // + // let result = report.requires_payments_retry(); + // + // assert_eq!(result, false) + } +} diff --git a/node/src/accountant/scanners/receivable_scanner/mod.rs b/node/src/accountant/scanners/receivable_scanner/mod.rs new file mode 100644 index 000000000..b7222df0d --- /dev/null +++ b/node/src/accountant/scanners/receivable_scanner/mod.rs @@ -0,0 +1,195 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod utils; + +use crate::accountant::db_access_objects::banned_dao::BannedDao; +use crate::accountant::db_access_objects::receivable_dao::ReceivableDao; +use crate::accountant::scanners::receivable_scanner::utils::balance_and_age; +use crate::accountant::scanners::{ + PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, +}; +use crate::accountant::{ReceivedPayments, ResponseSkeleton, ScanForReceivables}; +use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; +use crate::db_config::persistent_configuration::PersistentConfiguration; +use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; +use crate::sub_lib::wallet::Wallet; +use crate::time_marking_methods; +use masq_lib::logger::Logger; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use std::cell::RefCell; +use std::rc::Rc; +use std::time::SystemTime; + +pub struct ReceivableScanner { + pub common: ScannerCommon, + pub receivable_dao: Box, + pub banned_dao: Box, + pub persistent_configuration: Box, + pub financial_statistics: Rc>, +} + +impl + PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + > for ReceivableScanner +{ +} + +impl StartableScanner for ReceivableScanner { + fn start_scan( + &mut self, + earning_wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.mark_as_started(timestamp); + info!(logger, "Scanning for receivables to {}", earning_wallet); + self.scan_for_delinquencies(timestamp, logger); + + Ok(RetrieveTransactions { + recipient: earning_wallet.clone(), + response_skeleton_opt, + }) + } +} + +impl Scanner> for ReceivableScanner { + fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { + self.handle_new_received_payments(&msg, logger); + self.mark_as_ended(logger); + + msg.response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) + } + + time_marking_methods!(Receivables); + + as_any_ref_in_trait_impl!(); + as_any_mut_in_trait_impl!(); +} + +impl ReceivableScanner { + pub fn new( + receivable_dao: Box, + banned_dao: Box, + persistent_configuration: Box, + payment_thresholds: Rc, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + receivable_dao, + banned_dao, + persistent_configuration, + financial_statistics, + } + } + + fn handle_new_received_payments( + &mut self, + received_payments_msg: &ReceivedPayments, + logger: &Logger, + ) { + if received_payments_msg.transactions.is_empty() { + info!( + logger, + "No newly received payments were detected during the scanning process." + ); + let new_start_block = received_payments_msg.new_start_block; + if let BlockMarker::Value(start_block_number) = new_start_block { + match self + .persistent_configuration + .set_start_block(Some(start_block_number)) + { + Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), + Err(e) => panic!( + "Attempt to set new start block to {} failed due to: {:?}", + start_block_number, e + ), + } + } + } else { + let mut txn = self.receivable_dao.as_mut().more_money_received( + received_payments_msg.timestamp, + &received_payments_msg.transactions, + ); + let new_start_block = received_payments_msg.new_start_block; + if let BlockMarker::Value(start_block_number) = new_start_block { + match self + .persistent_configuration + .set_start_block_from_txn(Some(start_block_number), &mut txn) + { + Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), + Err(e) => panic!( + "Attempt to set new start block to {} failed due to: {:?}", + start_block_number, e + ), + } + } else { + unreachable!("Failed to get start_block while transactions were present"); + } + match txn.commit() { + Ok(_) => { + debug!(logger, "Received payments have been commited to database"); + } + Err(e) => panic!("Commit of received transactions failed: {:?}", e), + } + let total_newly_paid_receivable = received_payments_msg + .transactions + .iter() + .fold(0, |so_far, now| so_far + now.wei_amount); + + self.financial_statistics + .borrow_mut() + .total_paid_receivable_wei += total_newly_paid_receivable; + } + } + + pub fn scan_for_delinquencies(&self, timestamp: SystemTime, logger: &Logger) { + info!(logger, "Scanning for delinquencies"); + self.find_and_ban_delinquents(timestamp, logger); + self.find_and_unban_reformed_nodes(timestamp, logger); + } + + fn find_and_ban_delinquents(&self, timestamp: SystemTime, logger: &Logger) { + self.receivable_dao + .new_delinquencies(timestamp, self.common.payment_thresholds.as_ref()) + .into_iter() + .for_each(|account| { + self.banned_dao.ban(&account.wallet); + let (balance_str_wei, age) = balance_and_age(timestamp, &account); + info!( + logger, + "Wallet {} (balance: {} gwei, age: {} sec) banned for delinquency", + account.wallet, + balance_str_wei, + age.as_secs() + ) + }); + } + + fn find_and_unban_reformed_nodes(&self, timestamp: SystemTime, logger: &Logger) { + self.receivable_dao + .paid_delinquencies(self.common.payment_thresholds.as_ref()) + .into_iter() + .for_each(|account| { + self.banned_dao.unban(&account.wallet); + let (balance_str_wei, age) = balance_and_age(timestamp, &account); + info!( + logger, + "Wallet {} (balance: {} gwei, age: {} sec) is no longer delinquent: unbanned", + account.wallet, + balance_str_wei, + age.as_secs() + ) + }); + } +} diff --git a/node/src/accountant/scanners/receivable_scanner/utils.rs b/node/src/accountant/scanners/receivable_scanner/utils.rs new file mode 100644 index 000000000..45c8f6800 --- /dev/null +++ b/node/src/accountant/scanners/receivable_scanner/utils.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; +use crate::accountant::wei_to_gwei; +use std::time::{Duration, SystemTime}; +use thousands::Separable; + +pub fn balance_and_age(time: SystemTime, account: &ReceivableAccount) -> (String, Duration) { + let balance = wei_to_gwei::(account.balance_wei).separate_with_commas(); + let age = time + .duration_since(account.last_received_timestamp) + .unwrap_or_else(|_| Duration::new(0, 0)); + (balance, age) +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::receivable_scanner::utils::balance_and_age; + use crate::test_utils::make_wallet; + use std::time::SystemTime; + + #[test] + fn balance_and_age_is_calculated_as_expected() { + let now = SystemTime::now(); + let offset = 1000; + let receivable_account = ReceivableAccount { + wallet: make_wallet("wallet0"), + balance_wei: 10_000_000_000, + last_received_timestamp: from_unix_timestamp(to_unix_timestamp(now) - offset), + }; + + let (balance, age) = balance_and_age(now, &receivable_account); + + assert_eq!(balance, "10"); + assert_eq!(age.as_secs(), offset as u64); + } +} diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 74e102aff..7a99605b0 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -383,7 +383,7 @@ mod tests { NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, }; - use crate::accountant::scanners::{MTError, StartScanError}; + use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; use itertools::Itertools; use lazy_static::lazy_static; @@ -538,7 +538,7 @@ mod tests { StartScanError::NothingToProcess, StartScanError::NoConsumingWalletFound, StartScanError::ScanAlreadyRunning { cross_scan_cause_opt: None, started_at: SystemTime::now()}, - StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + StartScanError::ManualTriggerError(ManulTriggerError::AutomaticScanConflict), StartScanError::CalledFromNullScanner ]; diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index b50a1388b..971030e14 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -321,154 +321,10 @@ pub mod payable_scanner_utils { } } -pub mod pending_payable_scanner_utils { - use crate::accountant::PendingPayableId; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; - use masq_lib::logger::Logger; - use masq_lib::ui_gateway::NodeToUiMessage; - use std::time::SystemTime; - - #[derive(Debug, Default, PartialEq, Eq, Clone)] - pub struct PendingPayableScanReport { - pub still_pending: Vec, - pub failures: Vec, - pub confirmed: Vec, - } - - impl PendingPayableScanReport { - pub fn requires_payments_retry(&self) -> bool { - todo!("complete my within GH-642") - } - } - - #[derive(Debug, PartialEq, Eq)] - pub enum PendingPayableScanResult { - NoPendingPayablesLeft(Option), - PaymentRetryRequired, - } - - pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() - } - - pub fn handle_none_status( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, - ) -> PendingPayableScanReport { - info!( - logger, - "Pending transaction {:?} couldn't be confirmed at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let elapsed = elapsed.as_secs(); - if elapsed > max_pending_interval { - error!( - logger, - "Pending transaction {:?} has exceeded the maximum pending time \ - ({}sec) with the age {}sec and the confirmation process is going to be aborted now \ - at the final attempt {}; manual resolution is required from the \ - user to complete the transaction.", - fingerprint.hash, - max_pending_interval, - elapsed, - fingerprint.attempt - ); - scan_report.failures.push(fingerprint.into()) - } else { - scan_report.still_pending.push(fingerprint.into()) - } - scan_report - } - - pub fn handle_status_with_success( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, - ) -> PendingPayableScanReport { - info!( - logger, - "Transaction {:?} has been added to the blockchain; detected locally at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.confirmed.push(fingerprint); - scan_report - } - - //TODO: failures handling is going to need enhancement suggested by GH-693 - pub fn handle_status_with_failure( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, - ) -> PendingPayableScanReport { - error!( - logger, - "Pending transaction {:?} announced as a failure, interpreting attempt \ - {} after {}ms from the sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.failures.push(fingerprint.into()); - scan_report - } - - pub fn handle_none_receipt( - mut scan_report: PendingPayableScanReport, - payable: PendingPayableFingerprint, - error_msg: &str, - logger: &Logger, - ) -> PendingPayableScanReport { - debug!( - logger, - "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", - payable.hash, - error_msg, - payable.attempt, - elapsed_in_ms(payable.timestamp) - ); - - scan_report - .still_pending - .push(PendingPayableId::new(payable.rowid, payable.hash)); - scan_report - } -} - -pub mod receivable_scanner_utils { - use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; - use crate::accountant::wei_to_gwei; - use std::time::{Duration, SystemTime}; - use thousands::Separable; - - pub fn balance_and_age(time: SystemTime, account: &ReceivableAccount) -> (String, Duration) { - let balance = wei_to_gwei::(account.balance_wei).separate_with_commas(); - let age = time - .duration_since(account.last_received_timestamp) - .unwrap_or_else(|_| Duration::new(0, 0)); - (balance, age) - } -} - #[cfg(test)] mod tests { use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; - use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; @@ -477,7 +333,6 @@ mod tests { payables_debug_summary, separate_errors, PayableThresholdsGauge, PayableThresholdsGaugeReal, }; - use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; @@ -530,22 +385,6 @@ mod tests { assert_eq!(result, "Payable scan found 4 debts; the biggest is 2000000 owed for 10000sec, the oldest is 330 owed for 30000sec") } - #[test] - fn balance_and_age_is_calculated_as_expected() { - let now = SystemTime::now(); - let offset = 1000; - let receivable_account = ReceivableAccount { - wallet: make_wallet("wallet0"), - balance_wei: 10_000_000_000, - last_received_timestamp: from_unix_timestamp(to_unix_timestamp(now) - offset), - }; - - let (balance, age) = balance_and_age(now, &receivable_account); - - assert_eq!(balance, "10"); - assert_eq!(age.as_secs(), offset as u64); - } - #[test] fn separate_errors_works_for_no_errs_just_oks() { let correct_payment_1 = PendingPayable { @@ -869,64 +708,4 @@ mod tests { "Got 0 properly sent payables of an unknown number of attempts" ) } - - #[test] - fn requires_payments_retry_says_yes() { - todo!("complete this test with GH-604") - // let cases = vec![ - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // ]; - // - // cases.into_iter().enumerate().for_each(|(idx, case)| { - // let result = case.requires_payments_retry(); - // assert_eq!( - // result, true, - // "We expected true, but got false for case of idx {}", - // idx - // ) - // }) - } - - #[test] - fn requires_payments_retry_says_no() { - todo!("complete this test with GH-604") - // let report = PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }; - // - // let result = report.requires_payments_retry(); - // - // assert_eq!(result, false) - } } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 2445ff565..637325091 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -8,12 +8,12 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ use crate::accountant::scanners::payable_scanner_extension::{ MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, }; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, ScanRescheduleAfterEarlyStop, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::accountant::scanners::{ PayableScanner, PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, Scanner, StartScanError, StartableScanner, diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index a09d734cd..44c888ff7 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -25,8 +25,10 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, }; use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; -use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::scanners::PayableScanner; use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; From 7a664d6e6284e918368b4678dfa9bfffd37ce4cd Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:54:52 +0200 Subject: [PATCH 10/37] GH-683: Well structualized blockchain errors (#684) * GH-683: savepoint * GH-683: interim commit * GH-683: mostly done * GH-683: renamed error * GH-683: additional fix to renaming * GH-683: finished * GH-683: fixed for a review * GH-683: fixed screwed string replacement * GH-683: finished fixing it * GH-683: another fix...BlockchainError --------- Co-authored-by: Bert --- .../db_access_objects/failed_payable_dao.rs | 59 +++-- .../db_access_objects/sent_payable_dao.rs | 38 +++- node/src/accountant/mod.rs | 2 +- node/src/accountant/scanners/mod.rs | 16 +- .../src/accountant/scanners/scanners_utils.rs | 6 +- node/src/blockchain/blockchain_bridge.rs | 27 +-- .../lower_level_interface_web3.rs | 37 +-- .../blockchain_interface_web3/mod.rs | 36 +-- .../data_structures/errors.rs | 58 ++--- .../lower_level_interface.rs | 16 +- .../blockchain/blockchain_interface/mod.rs | 6 +- node/src/blockchain/errors.rs | 127 ----------- node/src/blockchain/errors/internal_errors.rs | 51 +++++ node/src/blockchain/errors/mod.rs | 49 ++++ node/src/blockchain/errors/rpc_errors.rs | 215 ++++++++++++++++++ .../blockchain/errors/validation_status.rs | 154 +++++++++++++ node/src/blockchain/test_utils.rs | 21 ++ node/src/database/db_initializer.rs | 3 +- 18 files changed, 657 insertions(+), 264 deletions(-) delete mode 100644 node/src/blockchain/errors.rs create mode 100644 node/src/blockchain/errors/internal_errors.rs create mode 100644 node/src/blockchain/errors/mod.rs create mode 100644 node/src/blockchain/errors/rpc_errors.rs create mode 100644 node/src/blockchain/errors/validation_status.rs 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 7d9f2ae47..296bfe8d2 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -4,7 +4,8 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::errors::AppRpcError; +use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; +use crate::blockchain::errors::validation_status::PreviousAttempts; use crate::database::rusqlite_wrappers::ConnectionWrapper; use itertools::Itertools; use masq_lib::utils::ExpectValue; @@ -25,7 +26,7 @@ pub enum FailedPayableDaoError { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FailureReason { - Submission(AppRpcError), + Submission(AppRpcErrorKind), Reverted, PendingTooLong, } @@ -75,7 +76,7 @@ impl FromStr for FailureStatus { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ValidationStatus { Waiting, - Reattempting { attempt: usize, error: AppRpcError }, + Reattempting(PreviousAttempts), } #[derive(Clone, Debug, PartialEq, Eq)] @@ -381,8 +382,12 @@ mod tests { make_read_only_db_connection, FailedTxBuilder, }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; - use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError}; - use crate::blockchain::test_utils::make_tx_hash; + use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_tx_hash, ValidationFailureClockMock}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; @@ -390,7 +395,9 @@ mod tests { use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; use std::collections::{HashMap, HashSet}; + use std::ops::Add; use std::str::FromStr; + use std::time::{Duration, SystemTime}; #[test] fn insert_new_records_works() { @@ -584,11 +591,8 @@ mod tests { fn failure_reason_from_str_works() { // Submission error assert_eq!( - FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder":"Test decoder error"}}}"#) - .unwrap(), - FailureReason::Submission(AppRpcError::Local(LocalError::Decoder( - "Test decoder error".to_string() - ))) + FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder"}}}"#).unwrap(), + FailureReason::Submission(AppRpcErrorKind::Decoder) ); // Reverted @@ -620,6 +624,11 @@ mod tests { #[test] fn failure_status_from_str_works() { + let validation_failure_clock = ValidationFailureClockMock::default().now_result( + SystemTime::UNIX_EPOCH + .add(Duration::from_secs(1755080031)) + .add(Duration::from_nanos(612180914)), + ); assert_eq!( FailureStatus::from_str("\"RetryRequired\"").unwrap(), FailureStatus::RetryRequired @@ -631,8 +640,8 @@ mod tests { ); assert_eq!( - FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"attempt":2,"error":{"Remote":"Unreachable"}}}}"#).unwrap(), - FailureStatus::RecheckRequired(ValidationStatus::Reattempting { attempt: 2, error: AppRpcError::Remote(RemoteError::Unreachable) }) + FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"ServerUnreachable":{"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}}}}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), &validation_failure_clock))) ); assert_eq!( @@ -713,10 +722,12 @@ mod tests { let tx3 = FailedTxBuilder::default() .hash(make_tx_hash(3)) .reason(PendingTooLong) - .status(RecheckRequired(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable), - })) + .status(RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ), + ))) .build(); let tx4 = FailedTxBuilder::default() .hash(make_tx_hash(4)) @@ -768,10 +779,10 @@ mod tests { (tx1.hash, Concluded), ( tx2.hash, - RecheckRequired(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable), - }), + RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ))), ), (tx3.hash, Concluded), ]); @@ -785,10 +796,10 @@ mod tests { assert_eq!(tx2.status, RecheckRequired(ValidationStatus::Waiting)); assert_eq!( updated_txs[1].status, - RecheckRequired(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable) - }) + RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default() + ))) ); assert_eq!(tx3.status, RetryRequired); assert_eq!(updated_txs[2].status, Concluded); 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 ac3fbec86..a79e8ffbd 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -424,8 +424,10 @@ impl SentPayableDao for SentPayableDaoReal<'_> { #[cfg(test)] mod tests { use std::collections::{HashMap, HashSet}; + use std::ops::Add; use std::str::FromStr; use std::sync::{Arc, Mutex}; + use std::time::{Duration, UNIX_EPOCH}; use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, TxConfirmation, TxStatus}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, @@ -439,8 +441,10 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; - use crate::blockchain::errors::{AppRpcError, RemoteError}; - use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; #[test] fn insert_new_records_works() { @@ -452,10 +456,16 @@ mod tests { let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); let tx2 = TxBuilder::default() .hash(make_tx_hash(2)) - .status(TxStatus::Pending(ValidationStatus::Reattempting { - attempt: 2, - error: AppRpcError::Remote(RemoteError::Unreachable), - })) + .status(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ), + ))) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); let txs = vec![tx1, tx2]; @@ -682,10 +692,12 @@ mod tests { .build(); let tx2 = TxBuilder::default() .hash(make_tx_hash(2)) - .status(TxStatus::Pending(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable), - })) + .status(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ), + ))) .build(); let tx3 = TxBuilder::default() .hash(make_tx_hash(3)) @@ -1169,14 +1181,16 @@ mod tests { #[test] fn tx_status_from_str_works() { + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); assert_eq!( TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), TxStatus::Pending(ValidationStatus::Waiting) ); assert_eq!( - TxStatus::from_str(r#"{"Pending":{"Reattempting":{"attempt":3,"error":{"Remote":{"InvalidResponse":"bluh"}}}}}"#).unwrap(), - TxStatus::Pending(ValidationStatus::Reattempting { attempt: 3, error: AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())) }) + TxStatus::from_str(r#"{"Pending":{"Reattempting":{"InvalidResponse":{"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}}}}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::InvalidResponse), &validation_failure_clock))) ); assert_eq!( diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 39ead2d76..2ca502557 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -4020,7 +4020,7 @@ 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("bluh".to_string())), + payment_procedure_result: Err(PayableTransactionError::Signing("blah".to_string())), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1122, context_id: 7788, diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 02aa19459..d49cf3efc 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -9,19 +9,18 @@ pub mod test_utils; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; use crate::accountant::db_access_objects::pending_payable_dao::{PendingPayable, PendingPayableDao}; -use crate::accountant::db_access_objects::receivable_dao::ReceivableDao; use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; +use crate::accountant::{ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; +use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; use crate::sub_lib::accountant::{ DaoFactories, FinancialStatistics, PaymentThresholds, }; @@ -48,7 +47,6 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAge use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfigurationReal}; @@ -976,10 +974,10 @@ mod tests { }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::{gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1004,13 +1002,11 @@ mod tests { use regex::{Regex}; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; - use std::collections::HashSet; - use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use web3::types::{TransactionReceipt, H256}; + use web3::types::{H256}; use web3::Error; use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; @@ -3099,7 +3095,7 @@ mod tests { ScanError { scan_type, response_skeleton_opt: None, - msg: "bluh".to_string(), + msg: "blah".to_string(), } } diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 971030e14..3747728ab 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -342,7 +342,7 @@ mod tests { use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::time::SystemTime; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; - use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; + use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; #[test] @@ -645,11 +645,11 @@ mod tests { #[test] fn count_total_errors_says_unknown_number_for_early_local_errors() { let early_local_errors = [ - PayableTransactionError::TransactionID(BlockchainError::QueryFailed( + PayableTransactionError::TransactionID(BlockchainInterfaceError::QueryFailed( "blah".to_string(), )), PayableTransactionError::MissingConsumingWallet, - PayableTransactionError::GasPriceQueryFailed(BlockchainError::QueryFailed( + PayableTransactionError::GasPriceQueryFailed(BlockchainInterfaceError::QueryFailed( "ouch".to_string(), )), PayableTransactionError::UnusableWallet("fooo".to_string()), diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index e427ab934..421fc6bd5 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -9,7 +9,7 @@ use crate::accountant::{ReportTransactionReceipts, RequestTransactionReceipts}; use crate::actor_system_factory::SubsFactory; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::{ - BlockchainError, PayableTransactionError, + BlockchainInterfaceError, PayableTransactionError, }; use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible; use crate::blockchain::blockchain_interface::BlockchainInterface; @@ -505,12 +505,13 @@ impl BlockchainBridge { .expect("Accountant unbound") } - pub fn extract_max_block_count(error: BlockchainError) -> Option { + pub fn extract_max_block_count(error: BlockchainInterfaceError) -> Option { let regex_result = Regex::new(r".* (max: |allowed for your plan: |is limited to |block range limit \(|exceeds max block range )(?P\d+).*") .expect("Invalid regex"); let max_block_count = match error { - BlockchainError::QueryFailed(msg) => match regex_result.captures(msg.as_str()) { + BlockchainInterfaceError::QueryFailed(msg) => match regex_result.captures(msg.as_str()) + { Some(captures) => match captures.name("max_block_count") { Some(m) => match m.as_str().parse::() { Ok(value) => Some(value), @@ -821,7 +822,7 @@ mod tests { assert_eq!(accountant_recording.len(), 0); let service_fee_balance_error = BlockchainAgentBuildError::ServiceFeeBalance( consuming_wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Api error: Transport error: Error(IncompleteMessage)".to_string(), ), ); @@ -1129,7 +1130,7 @@ mod tests { let error_result = result.unwrap_err(); assert_eq!( error_result, - TransactionID(BlockchainError::QueryFailed( + TransactionID(BlockchainInterfaceError::QueryFailed( "Decoder error: Error(\"0x prefix is missing\", line: 0, column: 0) for wallet 0x2581…7849".to_string() )) ); @@ -2127,7 +2128,7 @@ mod tests { #[test] fn extract_max_block_range_from_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs block range too large, range: 33636, max: 3500\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs block range too large, range: 33636, max: 3500\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2136,7 +2137,7 @@ mod tests { #[test] fn extract_max_block_range_from_pokt_error_response() { - let result = BlockchainError::QueryFailed("Rpc(Error { code: ServerError(-32001), message: \"Relay request failed validation: invalid relay request: eth_getLogs block range limit (100000 blocks) exceeded\", data: None })".to_string()); + let result = BlockchainInterfaceError::QueryFailed("Rpc(Error { code: ServerError(-32001), message: \"Relay request failed validation: invalid relay request: eth_getLogs block range limit (100000 blocks) exceeded\", data: None })".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2152,7 +2153,7 @@ mod tests { */ #[test] fn extract_max_block_range_for_ankr_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32600), message: \"block range is too wide\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32600), message: \"block range is too wide\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2165,7 +2166,7 @@ mod tests { */ #[test] fn extract_max_block_range_for_matic_vigil_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"Blockheight too far in the past. Check params passed to eth_getLogs or eth_call requests.Range of blocks allowed for your plan: 1000\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"Blockheight too far in the past. Check params passed to eth_getLogs or eth_call requests.Range of blocks allowed for your plan: 1000\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2178,7 +2179,7 @@ mod tests { */ #[test] fn extract_max_block_range_for_blockpi_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs is limited to 1024 block range. Please check the parameter requirements at https://docs.blockpi.io/documentations/api-reference\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs is limited to 1024 block range. Please check the parameter requirements at https://docs.blockpi.io/documentations/api-reference\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2193,7 +2194,7 @@ mod tests { #[test] fn extract_max_block_range_for_blastapi_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32601), message: \"Method not found\", data: \"'eth_getLogs' is not available on our public API. Head over to https://docs.blastapi.io/blast-documentation/tutorials-and-guides/using-blast-to-get-a-blockchain-endpoint for more information\" }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32601), message: \"Method not found\", data: \"'eth_getLogs' is not available on our public API. Head over to https://docs.blastapi.io/blast-documentation/tutorials-and-guides/using-blast-to-get-a-blockchain-endpoint for more information\" }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2202,7 +2203,7 @@ mod tests { #[test] fn extract_max_block_range_for_nodies_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: InvalidParams, message: \"query exceeds max block range 100000\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: InvalidParams, message: \"query exceeds max block range 100000\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2211,7 +2212,7 @@ mod tests { #[test] fn extract_max_block_range_for_expected_batch_got_single_error_response() { - let result = BlockchainError::QueryFailed( + let result = BlockchainInterfaceError::QueryFailed( "Got invalid response: Expected batch, got single.".to_string(), ); diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index b91e2c924..c93c07b53 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::blockchain::blockchain_interface::blockchain_interface_web3::CONTRACT_ABI; -use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError; -use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError::QueryFailed; +use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError; +use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use ethereum_types::{H256, U256, U64}; use futures::Future; @@ -115,7 +115,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_fee_balance( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.web3 .eth() @@ -127,7 +127,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_service_fee_balance( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.contract .query("balanceOf", address, None, Options::default(), None) @@ -135,7 +135,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { ) } - fn get_gas_price(&self) -> Box> { + fn get_gas_price(&self) -> Box> { Box::new( self.web3 .eth() @@ -144,7 +144,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { ) } - fn get_block_number(&self) -> Box> { + fn get_block_number(&self) -> Box> { Box::new( self.web3 .eth() @@ -156,7 +156,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_id( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.web3 .eth() @@ -168,7 +168,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_receipt_in_batch( &self, hash_vec: Vec, - ) -> Box>, Error = BlockchainError>> { + ) -> Box>, Error = BlockchainInterfaceError>> { hash_vec.into_iter().for_each(|hash| { self.web3_batch.eth().transaction_receipt(hash); }); @@ -188,7 +188,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_logs( &self, filter: Filter, - ) -> Box, Error = BlockchainError>> { + ) -> Box, Error = BlockchainInterfaceError>> { Box::new( self.web3 .eth() @@ -220,8 +220,8 @@ impl LowBlockchainIntWeb3 { #[cfg(test)] mod tests { use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_LITERAL; - use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError::QueryFailed; - use crate::blockchain::blockchain_interface::{BlockchainError, BlockchainInterface}; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; + use crate::blockchain::blockchain_interface::{BlockchainInterfaceError, BlockchainInterface}; use crate::blockchain::test_utils::make_blockchain_interface_web3; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; @@ -269,7 +269,9 @@ mod tests { .wait(); match result { - Err(BlockchainError::QueryFailed(msg)) if msg.contains("invalid hex character: Q") => { + Err(BlockchainInterfaceError::QueryFailed(msg)) + if msg.contains("invalid hex character: Q") => + { () } x => panic!("Expected complaint about hex character, but got {:?}", x), @@ -377,7 +379,9 @@ mod tests { .wait(); match result { - Err(BlockchainError::QueryFailed(msg)) if msg.contains("invalid hex character: Q") => { + Err(BlockchainInterfaceError::QueryFailed(msg)) + if msg.contains("invalid hex character: Q") => + { () } x => panic!("Expected complaint about hex character, but got {:?}", x), @@ -430,8 +434,11 @@ mod tests { .wait(); let err_msg = match result { - Err(BlockchainError::QueryFailed(msg)) => msg, - x => panic!("Expected BlockchainError::QueryFailed, but got {:?}", x), + Err(BlockchainInterfaceError::QueryFailed(msg)) => msg, + x => panic!( + "Expected BlockchainInterfaceError::QueryFailed, but got {:?}", + x + ), }; assert!( err_msg.contains(expected_err_msg), 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 81c7fe62d..bb9cde491 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,7 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; +use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; @@ -104,7 +104,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { start_block_marker: BlockMarker, scan_range: BlockScanRange, recipient: Address, - ) -> Box> { + ) -> Box> + { let lower_level_interface = self.lower_interface(); let logger = self.logger.clone(); let contract_address = lower_level_interface.get_contract_address(); @@ -213,7 +214,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { fn process_transaction_receipts( &self, transaction_hashes: Vec, - ) -> Box, Error = BlockchainError>> { + ) -> Box, Error = BlockchainInterfaceError>> + { Box::new( self.lower_interface() .get_transaction_receipt_in_batch(transaction_hashes.clone()) @@ -366,7 +368,7 @@ impl BlockchainInterfaceWeb3 { fn calculate_end_block_marker( start_block_marker: BlockMarker, scan_range: BlockScanRange, - rpc_block_number_result: Result, + rpc_block_number_result: Result, logger: &Logger, ) -> BlockMarker { let locally_determined_end_block_marker = match (start_block_marker, scan_range) { @@ -398,9 +400,9 @@ impl BlockchainInterfaceWeb3 { } fn handle_transaction_logs( - logs_result: Result, BlockchainError>, + logs_result: Result, BlockchainInterfaceError>, logger: &Logger, - ) -> Result, BlockchainError> { + ) -> Result, BlockchainInterfaceError> { let logs = logs_result?; let logs_len = logs.len(); if logs @@ -412,7 +414,7 @@ impl BlockchainInterfaceWeb3 { "Invalid response from blockchain server: {:?}", logs ); - Err(BlockchainError::InvalidResponse) + Err(BlockchainInterfaceError::InvalidResponse) } else { let transactions: Vec = Self::extract_transactions_from_logs(logs); @@ -438,10 +440,10 @@ mod tests { BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, }; - use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError::QueryFailed; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; use crate::blockchain::blockchain_interface::{ - BlockchainAgentBuildError, BlockchainError, BlockchainInterface, + BlockchainAgentBuildError, BlockchainInterfaceError, BlockchainInterface, RetrievedBlockchainTransactions, }; use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; @@ -733,7 +735,7 @@ mod tests { assert_eq!( result.expect_err("Expected an Err, got Ok"), - BlockchainError::InvalidResponse + BlockchainInterfaceError::InvalidResponse ); } @@ -757,7 +759,7 @@ mod tests { ) .wait(); - assert_eq!(result, Err(BlockchainError::InvalidResponse)); + assert_eq!(result, Err(BlockchainInterfaceError::InvalidResponse)); } #[test] @@ -1007,7 +1009,7 @@ mod tests { let expected_err_factory = |wallet: &Wallet| { BlockchainAgentBuildError::TransactionFeeBalance( wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Transport error: Error(IncompleteMessage)".to_string(), ), ) @@ -1029,7 +1031,7 @@ mod tests { let expected_err_factory = |wallet: &Wallet| { BlockchainAgentBuildError::ServiceFeeBalance( wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Api error: Transport error: Error(IncompleteMessage)".to_string(), ), ) @@ -1207,7 +1209,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Uninitialized, BlockScanRange::NoLimit, - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1225,7 +1227,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Uninitialized, BlockScanRange::Range(100), - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1243,7 +1245,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Value(50), BlockScanRange::NoLimit, - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1261,7 +1263,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Value(50), BlockScanRange::Range(100), - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Value(150) diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index 3084accfb..ffdfa4c20 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -11,24 +11,22 @@ const BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED: &str = "Uninitialized blockchain int being delinquency-banned, you should restart the Node with a value for blockchain-service-url"; #[derive(Clone, Debug, PartialEq, Eq, VariantCount)] -pub enum BlockchainError { +pub enum BlockchainInterfaceError { InvalidUrl, InvalidAddress, InvalidResponse, QueryFailed(String), - UninitializedBlockchainInterface, + UninitializedInterface, } -impl Display for BlockchainError { +impl Display for BlockchainInterfaceError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let err_spec = match self { Self::InvalidUrl => Either::Left("Invalid url"), Self::InvalidAddress => Either::Left("Invalid address"), Self::InvalidResponse => Either::Left("Invalid response"), Self::QueryFailed(msg) => Either::Right(format!("Query failed: {}", msg)), - Self::UninitializedBlockchainInterface => { - Either::Left(BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED) - } + Self::UninitializedInterface => Either::Left(BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED), }; write!(f, "Blockchain error: {}", err_spec) } @@ -37,12 +35,12 @@ impl Display for BlockchainError { #[derive(Clone, Debug, PartialEq, Eq, VariantCount)] pub enum PayableTransactionError { MissingConsumingWallet, - GasPriceQueryFailed(BlockchainError), - TransactionID(BlockchainError), + GasPriceQueryFailed(BlockchainInterfaceError), + TransactionID(BlockchainInterfaceError), UnusableWallet(String), Signing(String), Sending { msg: String, hashes: Vec }, - UninitializedBlockchainInterface, + UninitializedInterface, } impl Display for PayableTransactionError { @@ -69,7 +67,7 @@ impl Display for PayableTransactionError { msg, comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) ), - Self::UninitializedBlockchainInterface => { + Self::UninitializedInterface => { write!(f, "{}", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED) } } @@ -78,10 +76,10 @@ impl Display for PayableTransactionError { #[derive(Clone, Debug, PartialEq, Eq, VariantCount)] pub enum BlockchainAgentBuildError { - GasPrice(BlockchainError), - TransactionFeeBalance(Address, BlockchainError), - ServiceFeeBalance(Address, BlockchainError), - UninitializedBlockchainInterface, + GasPrice(BlockchainInterfaceError), + TransactionFeeBalance(Address, BlockchainInterfaceError), + ServiceFeeBalance(Address, BlockchainInterfaceError), + UninitializedInterface, } impl Display for BlockchainAgentBuildError { @@ -98,7 +96,7 @@ impl Display for BlockchainAgentBuildError { "masq balance for our earning wallet {:#x} due to {}", address, blockchain_e )), - Self::UninitializedBlockchainInterface => { + Self::UninitializedInterface => { Either::Right(BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED.to_string()) } }; @@ -119,7 +117,9 @@ mod tests { use crate::blockchain::blockchain_interface::data_structures::errors::{ PayableTransactionError, BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED, }; - use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainError}; + 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}; @@ -136,20 +136,20 @@ mod tests { #[test] fn blockchain_error_implements_display() { let original_errors = [ - BlockchainError::InvalidUrl, - BlockchainError::InvalidAddress, - BlockchainError::InvalidResponse, - BlockchainError::QueryFailed( + BlockchainInterfaceError::InvalidUrl, + BlockchainInterfaceError::InvalidAddress, + BlockchainInterfaceError::InvalidResponse, + BlockchainInterfaceError::QueryFailed( "Don't query so often, it gives me a headache".to_string(), ), - BlockchainError::UninitializedBlockchainInterface, + BlockchainInterfaceError::UninitializedInterface, ]; let actual_error_msgs = original_errors.iter().map(to_string).collect::>(); assert_eq!( original_errors.len(), - BlockchainError::VARIANT_COUNT, + BlockchainInterfaceError::VARIANT_COUNT, "you forgot to add all variants in this test" ); assert_eq!( @@ -168,10 +168,10 @@ mod tests { fn payable_payment_error_implements_display() { let original_errors = [ PayableTransactionError::MissingConsumingWallet, - PayableTransactionError::GasPriceQueryFailed(BlockchainError::QueryFailed( + PayableTransactionError::GasPriceQueryFailed(BlockchainInterfaceError::QueryFailed( "Gas halves shut, no drop left".to_string(), )), - PayableTransactionError::TransactionID(BlockchainError::InvalidResponse), + PayableTransactionError::TransactionID(BlockchainInterfaceError::InvalidResponse), PayableTransactionError::UnusableWallet( "This is a LEATHER wallet, not LEDGER wallet, stupid.".to_string(), ), @@ -182,7 +182,7 @@ mod tests { msg: "Sending to cosmos belongs elsewhere".to_string(), hashes: vec![make_tx_hash(0x6f), make_tx_hash(0xde)], }, - PayableTransactionError::UninitializedBlockchainInterface, + PayableTransactionError::UninitializedInterface, ]; let actual_error_msgs = original_errors.iter().map(to_string).collect::>(); @@ -213,16 +213,16 @@ mod tests { fn blockchain_agent_build_error_implements_display() { let wallet = make_wallet("abc"); let original_errors = [ - BlockchainAgentBuildError::GasPrice(BlockchainError::InvalidResponse), + BlockchainAgentBuildError::GasPrice(BlockchainInterfaceError::InvalidResponse), BlockchainAgentBuildError::TransactionFeeBalance( wallet.address(), - BlockchainError::InvalidResponse, + BlockchainInterfaceError::InvalidResponse, ), BlockchainAgentBuildError::ServiceFeeBalance( wallet.address(), - BlockchainError::InvalidAddress, + BlockchainInterfaceError::InvalidAddress, ), - BlockchainAgentBuildError::UninitializedBlockchainInterface, + BlockchainAgentBuildError::UninitializedInterface, ]; let actual_error_msgs = original_errors.iter().map(to_string).collect::>(); diff --git a/node/src/blockchain/blockchain_interface/lower_level_interface.rs b/node/src/blockchain/blockchain_interface/lower_level_interface.rs index c8653f985..6ae07dca2 100644 --- a/node/src/blockchain/blockchain_interface/lower_level_interface.rs +++ b/node/src/blockchain/blockchain_interface/lower_level_interface.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError; +use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError; use ethereum_types::{H256, U64}; use futures::Future; use serde_json::Value; @@ -15,33 +15,33 @@ pub trait LowBlockchainInt { fn get_transaction_fee_balance( &self, address: Address, - ) -> Box>; + ) -> Box>; fn get_service_fee_balance( &self, address: Address, - ) -> Box>; + ) -> Box>; - fn get_gas_price(&self) -> Box>; + fn get_gas_price(&self) -> Box>; - fn get_block_number(&self) -> Box>; + fn get_block_number(&self) -> Box>; fn get_transaction_id( &self, address: Address, - ) -> Box>; + ) -> Box>; fn get_transaction_receipt_in_batch( &self, hash_vec: Vec, - ) -> Box>, Error = BlockchainError>>; + ) -> Box>, Error = BlockchainInterfaceError>>; fn get_contract_address(&self) -> Address; fn get_transaction_logs( &self, filter: Filter, - ) -> Box, Error = BlockchainError>>; + ) -> Box, Error = BlockchainInterfaceError>>; fn get_web3_batch(&self) -> Web3>; } diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index 242bf433f..eb736b2a3 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,7 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; +use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::sub_lib::wallet::Wallet; @@ -31,7 +31,7 @@ pub trait BlockchainInterface { start_block: BlockMarker, scan_range: BlockScanRange, recipient: Address, - ) -> Box>; + ) -> Box>; fn introduce_blockchain_agent( &self, @@ -41,7 +41,7 @@ pub trait BlockchainInterface { fn process_transaction_receipts( &self, transaction_hashes: Vec, - ) -> Box, Error = BlockchainError>>; + ) -> Box, Error = BlockchainInterfaceError>>; fn submit_payables_in_batch( &self, diff --git a/node/src/blockchain/errors.rs b/node/src/blockchain/errors.rs deleted file mode 100644 index 865bea29c..000000000 --- a/node/src/blockchain/errors.rs +++ /dev/null @@ -1,127 +0,0 @@ -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, Serialize, Deserialize)] -pub enum AppRpcError { - Local(LocalError), - Remote(RemoteError), -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum LocalError { - Decoder(String), - Internal, - Io(String), - Signing(String), - Transport(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RemoteError { - InvalidResponse(String), - Unreachable, - Web3RpcError { code: i64, message: String }, -} - -// EVM based errors -impl From for AppRpcError { - fn from(error: Web3Error) -> Self { - match error { - // Local Errors - Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), - Web3Error::Internal => AppRpcError::Local(LocalError::Internal), - Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), - Web3Error::Signing(error) => { - // This variant cannot be tested due to import limitations. - AppRpcError::Local(LocalError::Signing(error.to_string())) - } - Web3Error::Transport(error) => AppRpcError::Local(LocalError::Transport(error)), - - // Api Errors - Web3Error::InvalidResponse(response) => { - AppRpcError::Remote(RemoteError::InvalidResponse(response)) - } - Web3Error::Rpc(web3_rpc_error) => AppRpcError::Remote(RemoteError::Web3RpcError { - code: web3_rpc_error.code.code(), - message: web3_rpc_error.message, - }), - Web3Error::Unreachable => AppRpcError::Remote(RemoteError::Unreachable), - } - } -} - -mod tests { - use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError}; - use web3::error::Error as Web3Error; - - #[test] - fn web3_error_to_failure_reason_conversion_works() { - // Local Errors - assert_eq!( - AppRpcError::from(Web3Error::Decoder("Decoder error".to_string())), - AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())) - ); - assert_eq!( - AppRpcError::from(Web3Error::Internal), - AppRpcError::Local(LocalError::Internal) - ); - assert_eq!( - AppRpcError::from(Web3Error::Io(std::io::Error::new( - std::io::ErrorKind::Other, - "IO error" - ))), - AppRpcError::Local(LocalError::Io("IO error".to_string())) - ); - assert_eq!( - AppRpcError::from(Web3Error::Transport("Transport error".to_string())), - AppRpcError::Local(LocalError::Transport("Transport error".to_string())) - ); - - // Api Errors - assert_eq!( - AppRpcError::from(Web3Error::InvalidResponse("Invalid response".to_string())), - AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())) - ); - assert_eq!( - AppRpcError::from(Web3Error::Rpc(jsonrpc_core::types::error::Error { - code: jsonrpc_core::types::error::ErrorCode::ServerError(42), - message: "RPC error".to_string(), - data: None, - })), - AppRpcError::Remote(RemoteError::Web3RpcError { - code: 42, - message: "RPC error".to_string(), - }) - ); - assert_eq!( - AppRpcError::from(Web3Error::Unreachable), - AppRpcError::Remote(RemoteError::Unreachable) - ); - } - - #[test] - fn app_rpc_error_serialization_deserialization() { - let errors = vec![ - // Local Errors - AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())), - AppRpcError::Local(LocalError::Internal), - AppRpcError::Local(LocalError::Io("IO error".to_string())), - AppRpcError::Local(LocalError::Signing("Signing error".to_string())), - AppRpcError::Local(LocalError::Transport("Transport error".to_string())), - // Remote Errors - AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())), - AppRpcError::Remote(RemoteError::Unreachable), - AppRpcError::Remote(RemoteError::Web3RpcError { - code: 42, - message: "RPC error".to_string(), - }), - ]; - - errors.into_iter().for_each(|error| { - let serialized = serde_json::to_string(&error).unwrap(); - let deserialized: AppRpcError = serde_json::from_str(&serialized).unwrap(); - assert_eq!(error, deserialized, "Error: {:?}", error); - }); - } -} diff --git a/node/src/blockchain/errors/internal_errors.rs b/node/src/blockchain/errors/internal_errors.rs new file mode 100644 index 000000000..fb6a4bf63 --- /dev/null +++ b/node/src/blockchain/errors/internal_errors.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Eq)] +pub enum InternalError { + PendingTooLongNotReplaced, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum InternalErrorKind { + PendingTooLongNotReplaced, +} + +impl From<&InternalError> for InternalErrorKind { + fn from(error: &InternalError) -> Self { + match error { + InternalError::PendingTooLongNotReplaced => { + InternalErrorKind::PendingTooLongNotReplaced + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn conversion_between_internal_error_and_internal_error_kind_works() { + assert_eq!( + InternalErrorKind::from(&InternalError::PendingTooLongNotReplaced), + InternalErrorKind::PendingTooLongNotReplaced + ); + } + + #[test] + fn app_rpc_error_kind_serialization_deserialization() { + let errors = vec![InternalErrorKind::PendingTooLongNotReplaced]; + + errors.into_iter().for_each(|error| { + let serialized = serde_json::to_string(&error).unwrap(); + let deserialized: InternalErrorKind = serde_json::from_str(&serialized).unwrap(); + assert_eq!( + error, deserialized, + "Failed serde attempt for {:?} that should look like {:?}", + deserialized, error + ); + }); + } +} diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs new file mode 100644 index 000000000..ab18ae9df --- /dev/null +++ b/node/src/blockchain/errors/mod.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::internal_errors::{InternalError, InternalErrorKind}; +use crate::blockchain::errors::rpc_errors::{AppRpcError, AppRpcErrorKind}; +use serde_derive::{Deserialize, Serialize}; + +pub mod internal_errors; +pub mod rpc_errors; +pub mod validation_status; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BlockchainError { + AppRpc(AppRpcError), + Internal(InternalError), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum BlockchainErrorKind { + AppRpc(AppRpcErrorKind), + Internal(InternalErrorKind), +} + +#[cfg(test)] +mod tests { + use crate::blockchain::errors::internal_errors::InternalErrorKind; + use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::BlockchainErrorKind; + + #[test] + fn blockchain_error_serialization_deserialization() { + vec![ + ( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + "{\"AppRpc\":\"Decoder\"}", + ), + ( + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + "{\"Internal\":\"PendingTooLongNotReplaced\"}", + ), + ] + .into_iter() + .for_each(|(err, expected_json)| { + let json = serde_json::to_string(&err).unwrap(); + assert_eq!(json, expected_json); + let deserialized_err = serde_json::from_str::(&json).unwrap(); + assert_eq!(deserialized_err, err); + }) + } +} diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs new file mode 100644 index 000000000..4d8482e17 --- /dev/null +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -0,0 +1,215 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +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)] +pub enum AppRpcError { + Local(LocalError), + Remote(RemoteError), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LocalError { + Decoder(String), + Internal, + Io(String), + Signing(String), + Transport(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RemoteError { + InvalidResponse(String), + Unreachable, + Web3RpcError { code: i64, message: String }, +} + +// EVM based errors +impl From for AppRpcError { + fn from(error: Web3Error) -> Self { + match error { + // Local Errors + Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), + Web3Error::Internal => AppRpcError::Local(LocalError::Internal), + Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), + Web3Error::Signing(error) => { + // This variant cannot be tested due to import limitations. + AppRpcError::Local(LocalError::Signing(error.to_string())) + } + Web3Error::Transport(error) => AppRpcError::Local(LocalError::Transport(error)), + + // Api Errors + Web3Error::InvalidResponse(response) => { + AppRpcError::Remote(RemoteError::InvalidResponse(response)) + } + Web3Error::Rpc(web3_rpc_error) => AppRpcError::Remote(RemoteError::Web3RpcError { + code: web3_rpc_error.code.code(), + message: web3_rpc_error.message, + }), + Web3Error::Unreachable => AppRpcError::Remote(RemoteError::Unreachable), + } + } +} + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum AppRpcErrorKind { + // Local + Decoder, + Internal, + IO, + Signing, + Transport, + + // Remote + InvalidResponse, + ServerUnreachable, + Web3RpcError(i64), // Keep only the stable error code +} + +impl From<&AppRpcError> for AppRpcErrorKind { + fn from(err: &AppRpcError) -> Self { + match err { + AppRpcError::Local(local) => match local { + LocalError::Decoder(_) => Self::Decoder, + LocalError::Internal => Self::Internal, + LocalError::Io(_) => Self::IO, + LocalError::Signing(_) => Self::Signing, + LocalError::Transport(_) => Self::Transport, + }, + AppRpcError::Remote(remote) => match remote { + RemoteError::InvalidResponse(_) => Self::InvalidResponse, + RemoteError::Unreachable => Self::ServerUnreachable, + RemoteError::Web3RpcError { code, .. } => Self::Web3RpcError(*code), + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, RemoteError, + }; + use web3::error::Error as Web3Error; + + #[test] + fn web3_error_to_failure_reason_conversion_works() { + // Local Errors + assert_eq!( + AppRpcError::from(Web3Error::Decoder("Decoder error".to_string())), + AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())) + ); + assert_eq!( + AppRpcError::from(Web3Error::Internal), + AppRpcError::Local(LocalError::Internal) + ); + assert_eq!( + AppRpcError::from(Web3Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "IO error" + ))), + AppRpcError::Local(LocalError::Io("IO error".to_string())) + ); + assert_eq!( + AppRpcError::from(Web3Error::Transport("Transport error".to_string())), + AppRpcError::Local(LocalError::Transport("Transport error".to_string())) + ); + + // Api Errors + assert_eq!( + AppRpcError::from(Web3Error::InvalidResponse("Invalid response".to_string())), + AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())) + ); + assert_eq!( + AppRpcError::from(Web3Error::Rpc(jsonrpc_core::types::error::Error { + code: jsonrpc_core::types::error::ErrorCode::ServerError(42), + message: "RPC error".to_string(), + data: None, + })), + AppRpcError::Remote(RemoteError::Web3RpcError { + code: 42, + message: "RPC error".to_string(), + }) + ); + assert_eq!( + AppRpcError::from(Web3Error::Unreachable), + AppRpcError::Remote(RemoteError::Unreachable) + ); + } + + #[test] + fn conversion_between_app_rpc_error_and_app_rpc_error_kind_works() { + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Decoder( + "Decoder error".to_string() + ))), + AppRpcErrorKind::Decoder + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Internal)), + AppRpcErrorKind::Internal + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Io("IO error".to_string()))), + AppRpcErrorKind::IO + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Signing( + "Signing error".to_string() + ))), + AppRpcErrorKind::Signing + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Transport( + "Transport error".to_string() + ))), + AppRpcErrorKind::Transport + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::InvalidResponse( + "Invalid response".to_string() + ))), + AppRpcErrorKind::InvalidResponse + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Unreachable)), + AppRpcErrorKind::ServerUnreachable + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Web3RpcError { + code: 55, + message: "Booga".to_string() + })), + AppRpcErrorKind::Web3RpcError(55) + ); + } + + #[test] + fn app_rpc_error_kind_serialization_deserialization() { + let errors = vec![ + // Local Errors + AppRpcErrorKind::Decoder, + AppRpcErrorKind::Internal, + AppRpcErrorKind::IO, + AppRpcErrorKind::Signing, + AppRpcErrorKind::Transport, + // Remote Errors + AppRpcErrorKind::InvalidResponse, + AppRpcErrorKind::ServerUnreachable, + AppRpcErrorKind::Web3RpcError(42), + ]; + + errors.into_iter().for_each(|error| { + let serialized = serde_json::to_string(&error).unwrap(); + let deserialized: AppRpcErrorKind = serde_json::from_str(&serialized).unwrap(); + assert_eq!( + error, deserialized, + "Failed serde attempt for {:?} that should look \ + like {:?}", + deserialized, error + ); + }); + } +} diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs new file mode 100644 index 000000000..72ca28346 --- /dev/null +++ b/node/src/blockchain/errors/validation_status.rs @@ -0,0 +1,154 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; +use crate::blockchain::errors::BlockchainErrorKind; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::SystemTime; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ValidationStatus { + Waiting, + Reattempting(PreviousAttempts), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PreviousAttempts { + #[serde(flatten)] + inner: HashMap, +} + +impl PreviousAttempts { + pub fn new(error: BlockchainErrorKind, clock: &dyn ValidationFailureClock) -> Self { + Self { + inner: hashmap!(error => ErrorStats::now(clock)), + } + } + + pub fn add_attempt( + mut self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Self { + self.inner + .entry(error) + .and_modify(|stats| stats.increment()) + .or_insert_with(|| ErrorStats::now(clock)); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ErrorStats { + #[serde(rename = "firstSeen")] + pub first_seen: SystemTime, + pub attempts: u16, +} + +impl ErrorStats { + pub fn now(clock: &dyn ValidationFailureClock) -> Self { + Self { + first_seen: clock.now(), + attempts: 1, + } + } + + pub fn increment(&mut self) { + self.attempts += 1; + } +} + +pub trait ValidationFailureClock { + fn now(&self) -> SystemTime; +} + +#[derive(Default)] +pub struct ValidationFailureClockReal {} + +impl ValidationFailureClock for ValidationFailureClockReal { + fn now(&self) -> SystemTime { + SystemTime::now() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blockchain::errors::internal_errors::InternalErrorKind; + + #[test] + fn previous_attempts_and_validation_failure_clock_work_together_fine() { + let validation_failure_clock = ValidationFailureClockReal::default(); + // new() + let timestamp_a = SystemTime::now(); + let subject = PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + &validation_failure_clock, + ); + // add_attempt() + let timestamp_b = SystemTime::now(); + let subject = subject.add_attempt( + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + &validation_failure_clock, + ); + let timestamp_c = SystemTime::now(); + let subject = subject.add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + &validation_failure_clock, + ); + let timestamp_d = SystemTime::now(); + let subject = subject.add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + &validation_failure_clock, + ); + let subject = subject.add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + &validation_failure_clock, + ); + + let decoder_error_stats = subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder)) + .unwrap(); + assert!( + timestamp_a <= decoder_error_stats.first_seen + && decoder_error_stats.first_seen <= timestamp_b, + "Was expected from {:?} to {:?} but was {:?}", + timestamp_a, + timestamp_b, + decoder_error_stats.first_seen + ); + assert_eq!(decoder_error_stats.attempts, 2); + let internal_error_stats = subject + .inner + .get(&BlockchainErrorKind::Internal( + InternalErrorKind::PendingTooLongNotReplaced, + )) + .unwrap(); + assert!( + timestamp_b <= internal_error_stats.first_seen + && internal_error_stats.first_seen <= timestamp_c, + "Was expected from {:?} to {:?} but was {:?}", + timestamp_b, + timestamp_c, + internal_error_stats.first_seen + ); + assert_eq!(internal_error_stats.attempts, 1); + let io_error_stats = subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO)) + .unwrap(); + assert!( + timestamp_c <= io_error_stats.first_seen && io_error_stats.first_seen <= timestamp_d, + "Was expected from {:?} to {:?} but was {:?}", + timestamp_c, + timestamp_d, + io_error_stats.first_seen + ); + assert_eq!(io_error_stats.attempts, 2); + let other_error_stats = subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Signing)); + assert_eq!(other_error_stats, None); + } +} diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 6259e8739..f3b354931 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -5,6 +5,7 @@ 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}; @@ -13,8 +14,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}; @@ -225,3 +228,21 @@ pub fn transport_error_message() -> String { "Connection refused".to_string() } } + +#[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/database/db_initializer.rs b/node/src/database/db_initializer.rs index cbf6008c0..674786766 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -802,8 +802,7 @@ mod tests { gas_price_wei_high_b, gas_price_wei_low_b, nonce, - block_hash, - block_number + status FROM sent_payable", ) .unwrap(); From 24e0a8aa1b996cddecf55507c65da14d59e02e3e Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:57:58 +0200 Subject: [PATCH 11/37] GH-598-json-hotfix (#699) * GH-598-json-hotfix: corrected * GH-598-json-hotfix: little fix * GH-598-json-hotfix: rearrangement * GH-598-json-hotfix: addressing the review * GH-598-json-hotfix: finished addressing review --------- Co-authored-by: Bert --- .../db_access_objects/failed_payable_dao.rs | 33 +- .../db_access_objects/sent_payable_dao.rs | 18 +- node/src/blockchain/errors/mod.rs | 8 +- node/src/blockchain/errors/rpc_errors.rs | 80 ++-- .../blockchain/errors/validation_status.rs | 197 +++++++++- node/src/test_utils/mod.rs | 1 + node/src/test_utils/serde_serializer_mock.rs | 348 ++++++++++++++++++ 7 files changed, 613 insertions(+), 72 deletions(-) create mode 100644 node/src/test_utils/serde_serializer_mock.rs 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 296bfe8d2..3202807b3 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -382,7 +382,7 @@ mod tests { make_read_only_db_connection, FailedTxBuilder, }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ PreviousAttempts, ValidationFailureClockReal, }; @@ -591,8 +591,8 @@ mod tests { fn failure_reason_from_str_works() { // Submission error assert_eq!( - FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder"}}}"#).unwrap(), - FailureReason::Submission(AppRpcErrorKind::Decoder) + FailureReason::from_str(r#"{"Submission":{"Local":"Decoder"}}"#).unwrap(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Decoder)) ); // Reverted @@ -640,8 +640,8 @@ mod tests { ); assert_eq!( - FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"ServerUnreachable":{"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}}}}"#).unwrap(), - FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), &validation_failure_clock))) + FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":[{"error":{"AppRpc":{"Remote":"Unreachable"}},"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}]}}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &validation_failure_clock))) ); assert_eq!( @@ -652,9 +652,8 @@ mod tests { // Invalid Variant assert_eq!( FailureStatus::from_str("\"UnknownStatus\"").unwrap_err(), - "unknown variant `UnknownStatus`, \ - expected one of `RetryRequired`, `RecheckRequired`, `Concluded` \ - at line 1 column 15 in '\"UnknownStatus\"'" + "unknown variant `UnknownStatus`, expected one of `RetryRequired`, `RecheckRequired`, \ + `Concluded` at line 1 column 15 in '\"UnknownStatus\"'" ); // Invalid Input @@ -724,7 +723,9 @@ mod tests { .reason(PendingTooLong) .status(RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -775,13 +776,19 @@ mod tests { subject .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) .unwrap(); + let timestamp = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp) + .now_result(timestamp); let hashmap = HashMap::from([ (tx1.hash, Concluded), ( tx2.hash, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), - &ValidationFailureClockReal::default(), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &clock, ))), ), (tx3.hash, Concluded), @@ -797,8 +804,8 @@ mod tests { assert_eq!( updated_txs[1].status, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), - &ValidationFailureClockReal::default() + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &clock ))) ); assert_eq!(tx3.status, RetryRequired); 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 a79e8ffbd..09e293edf 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -442,7 +442,7 @@ mod tests { use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; @@ -458,11 +458,15 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ) .add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -694,7 +698,9 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -1189,8 +1195,8 @@ mod tests { ); assert_eq!( - TxStatus::from_str(r#"{"Pending":{"Reattempting":{"InvalidResponse":{"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}}}}"#).unwrap(), - TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::InvalidResponse), &validation_failure_clock))) + TxStatus::from_str(r#"{"Pending":{"Reattempting":[{"error":{"AppRpc":{"Remote":"InvalidResponse"}},"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}]}}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &validation_failure_clock))) ); assert_eq!( diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index ab18ae9df..5cd1a6f3c 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -23,19 +23,19 @@ pub enum BlockchainErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::internal_errors::InternalErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; use crate::blockchain::errors::BlockchainErrorKind; #[test] fn blockchain_error_serialization_deserialization() { vec![ ( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), - "{\"AppRpc\":\"Decoder\"}", + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + r#"{"AppRpc":{"Local":"Decoder"}}"#, ), ( BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), - "{\"Internal\":\"PendingTooLongNotReplaced\"}", + r#"{"Internal":"PendingTooLongNotReplaced"}"#, ), ] .into_iter() diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index 4d8482e17..e717fbf25 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -14,7 +14,7 @@ pub enum AppRpcError { pub enum LocalError { Decoder(String), Internal, - Io(String), + IO(String), Signing(String), Transport(String), } @@ -33,7 +33,7 @@ impl From for AppRpcError { // Local Errors Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), Web3Error::Internal => AppRpcError::Local(LocalError::Internal), - Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), + Web3Error::Io(error) => AppRpcError::Local(LocalError::IO(error.to_string())), Web3Error::Signing(error) => { // This variant cannot be tested due to import limitations. AppRpcError::Local(LocalError::Signing(error.to_string())) @@ -53,18 +53,25 @@ impl From for AppRpcError { } } -#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AppRpcErrorKind { - // Local + Local(LocalErrorKind), + Remote(RemoteErrorKind), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum LocalErrorKind { Decoder, Internal, IO, Signing, Transport, +} - // Remote +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RemoteErrorKind { InvalidResponse, - ServerUnreachable, + Unreachable, Web3RpcError(i64), // Keep only the stable error code } @@ -72,16 +79,18 @@ impl From<&AppRpcError> for AppRpcErrorKind { fn from(err: &AppRpcError) -> Self { match err { AppRpcError::Local(local) => match local { - LocalError::Decoder(_) => Self::Decoder, - LocalError::Internal => Self::Internal, - LocalError::Io(_) => Self::IO, - LocalError::Signing(_) => Self::Signing, - LocalError::Transport(_) => Self::Transport, + LocalError::Decoder(_) => Self::Local(LocalErrorKind::Decoder), + LocalError::Internal => Self::Local(LocalErrorKind::Internal), + LocalError::IO(_) => Self::Local(LocalErrorKind::IO), + LocalError::Signing(_) => Self::Local(LocalErrorKind::Signing), + LocalError::Transport(_) => Self::Local(LocalErrorKind::Transport), }, AppRpcError::Remote(remote) => match remote { - RemoteError::InvalidResponse(_) => Self::InvalidResponse, - RemoteError::Unreachable => Self::ServerUnreachable, - RemoteError::Web3RpcError { code, .. } => Self::Web3RpcError(*code), + RemoteError::InvalidResponse(_) => Self::Remote(RemoteErrorKind::InvalidResponse), + RemoteError::Unreachable => Self::Remote(RemoteErrorKind::Unreachable), + RemoteError::Web3RpcError { code, .. } => { + Self::Remote(RemoteErrorKind::Web3RpcError(*code)) + } }, } } @@ -90,7 +99,7 @@ impl From<&AppRpcError> for AppRpcErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::rpc_errors::{ - AppRpcError, AppRpcErrorKind, LocalError, RemoteError, + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, RemoteErrorKind, }; use web3::error::Error as Web3Error; @@ -110,7 +119,7 @@ mod tests { std::io::ErrorKind::Other, "IO error" ))), - AppRpcError::Local(LocalError::Io("IO error".to_string())) + AppRpcError::Local(LocalError::IO("IO error".to_string())) ); assert_eq!( AppRpcError::from(Web3Error::Transport("Transport error".to_string())), @@ -145,60 +154,58 @@ mod tests { AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Decoder( "Decoder error".to_string() ))), - AppRpcErrorKind::Decoder + AppRpcErrorKind::Local(LocalErrorKind::Decoder) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Internal)), - AppRpcErrorKind::Internal + AppRpcErrorKind::Local(LocalErrorKind::Internal) ); assert_eq!( - AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Io("IO error".to_string()))), - AppRpcErrorKind::IO + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::IO("IO error".to_string()))), + AppRpcErrorKind::Local(LocalErrorKind::IO) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Signing( "Signing error".to_string() ))), - AppRpcErrorKind::Signing + AppRpcErrorKind::Local(LocalErrorKind::Signing) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Transport( "Transport error".to_string() ))), - AppRpcErrorKind::Transport + AppRpcErrorKind::Local(LocalErrorKind::Transport) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::InvalidResponse( "Invalid response".to_string() ))), - AppRpcErrorKind::InvalidResponse + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Unreachable)), - AppRpcErrorKind::ServerUnreachable + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Web3RpcError { code: 55, message: "Booga".to_string() })), - AppRpcErrorKind::Web3RpcError(55) + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(55)) ); } #[test] fn app_rpc_error_kind_serialization_deserialization() { let errors = vec![ - // Local Errors - AppRpcErrorKind::Decoder, - AppRpcErrorKind::Internal, - AppRpcErrorKind::IO, - AppRpcErrorKind::Signing, - AppRpcErrorKind::Transport, - // Remote Errors - AppRpcErrorKind::InvalidResponse, - AppRpcErrorKind::ServerUnreachable, - AppRpcErrorKind::Web3RpcError(42), + AppRpcErrorKind::Local(LocalErrorKind::Decoder), + AppRpcErrorKind::Local(LocalErrorKind::Internal), + AppRpcErrorKind::Local(LocalErrorKind::IO), + AppRpcErrorKind::Local(LocalErrorKind::Signing), + AppRpcErrorKind::Local(LocalErrorKind::Transport), + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse), + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable), + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(42)), ]; errors.into_iter().for_each(|error| { @@ -206,8 +213,7 @@ mod tests { let deserialized: AppRpcErrorKind = serde_json::from_str(&serialized).unwrap(); assert_eq!( error, deserialized, - "Failed serde attempt for {:?} that should look \ - like {:?}", + "Failed serde attempt for {:?} that should look like {:?}", deserialized, error ); }); diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 72ca28346..34cb2c5e3 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -1,9 +1,14 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; use crate::blockchain::errors::BlockchainErrorKind; +use serde::de::{SeqAccess, Visitor}; +use serde::ser::SerializeSeq; +use serde::{ + Deserialize as ManualDeserialize, Deserializer, Serialize as ManualSerialize, Serializer, +}; use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt::Formatter; use std::time::SystemTime; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -12,12 +17,74 @@ pub enum ValidationStatus { Reattempting(PreviousAttempts), } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PreviousAttempts { - #[serde(flatten)] inner: HashMap, } +// had to implement it manually in an array JSON layout, as the original, default HashMap +// serialization threw errors because the values of keys were represented by nested enums that +// serde doesn't translate into a complex JSON value (unlike the plain string required for a key) +impl ManualSerialize for PreviousAttempts { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct Entry<'a> { + #[serde(rename = "error")] + error_kind: &'a BlockchainErrorKind, + #[serde(flatten)] + stats: &'a ErrorStats, + } + + let mut seq = serializer.serialize_seq(Some(self.inner.len()))?; + for (error_kind, stats) in self.inner.iter() { + seq.serialize_element(&Entry { error_kind, stats })?; + } + seq.end() + } +} + +impl<'de> ManualDeserialize<'de> for PreviousAttempts { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(PreviousAttemptsVisitor) + } +} + +struct PreviousAttemptsVisitor; + +impl<'de> Visitor<'de> for PreviousAttemptsVisitor { + type Value = PreviousAttempts; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("PreviousAttempts") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + #[derive(Deserialize)] + struct EntryOwned { + #[serde(rename = "error")] + error_kind: BlockchainErrorKind, + #[serde(flatten)] + stats: ErrorStats, + } + + let mut error_stats_map: HashMap = hashmap!(); + while let Some(entry) = seq.next_element::()? { + error_stats_map.insert(entry.error_kind, entry.stats); + } + Ok(PreviousAttempts { + inner: error_stats_map, + }) + } +} + impl PreviousAttempts { pub fn new(error: BlockchainErrorKind, clock: &dyn ValidationFailureClock) -> Self { Self { @@ -75,6 +142,11 @@ impl ValidationFailureClock for ValidationFailureClockReal { mod tests { use super::*; 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}; #[test] fn previous_attempts_and_validation_failure_clock_work_together_fine() { @@ -82,7 +154,7 @@ mod tests { // new() let timestamp_a = SystemTime::now(); let subject = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); // add_attempt() @@ -93,22 +165,24 @@ mod tests { ); let timestamp_c = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), &validation_failure_clock, ); let timestamp_d = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), &validation_failure_clock, ); let decoder_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Decoder, + ))) .unwrap(); assert!( timestamp_a <= decoder_error_stats.first_seen @@ -136,7 +210,9 @@ mod tests { assert_eq!(internal_error_stats.attempts, 1); let io_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::IO, + ))) .unwrap(); assert!( timestamp_c <= io_error_stats.first_seen && io_error_stats.first_seen <= timestamp_d, @@ -146,9 +222,106 @@ mod tests { io_error_stats.first_seen ); assert_eq!(io_error_stats.attempts, 2); - let other_error_stats = subject - .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Signing)); + let other_error_stats = + subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Signing, + ))); assert_eq!(other_error_stats, None); } + + #[test] + fn previous_attempts_custom_serialize_seq_happy_path() { + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = serde_json::to_string(&PreviousAttempts::new(err, &clock)).unwrap(); + + assert_eq!( + result, + r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":{"secs_since_epoch":1234567890,"nanos_since_epoch":0},"attempts":1}]"# + ); + } + + #[test] + fn previous_attempts_custom_serialize_seq_initialization_err() { + let mock = SerdeSerializerMock::default() + .serialize_seq_result(Err(serde_json::Error::custom("lethally acid bobbles"))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "lethally acid bobbles"); + } + + #[test] + fn previous_attempts_custom_serialize_seq_element_err() { + let mock = SerdeSerializerMock::default() + .serialize_seq_result(Ok(SerializeSeqMock::default().serialize_element_result( + Err(serde_json::Error::custom("jelly gummies gone off")), + ))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "jelly gummies gone off"); + } + + #[test] + fn previous_attempts_custom_serialize_end_err() { + let mock = + SerdeSerializerMock::default().serialize_seq_result(Ok(SerializeSeqMock::default() + .serialize_element_result(Ok(())) + .end_result(Err(serde_json::Error::custom("funny belly ache"))))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "funny belly ache"); + } + + #[test] + fn previous_attempts_custom_deserialize_happy_path() { + let str = r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":{"secs_since_epoch":1234567890,"nanos_since_epoch":0},"attempts":1}]"#; + + let result = serde_json::from_str::(str); + + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + assert_eq!( + result.unwrap().inner, + hashmap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) + ); + } + + #[test] + fn previous_attempts_custom_deserialize_sad_path() { + let str = + r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":"Yesterday","attempts":1}]"#; + + let result = serde_json::from_str::(str); + + assert_eq!( + result.unwrap_err().to_string(), + "invalid type: string \"Yesterday\", expected struct SystemTime at line 1 column 79" + ); + } } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index b36199b75..588eb87e6 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -14,6 +14,7 @@ pub mod persistent_configuration_mock; pub mod recorder; pub mod recorder_counter_msgs; pub mod recorder_stop_conditions; +pub mod serde_serializer_mock; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; pub mod tokio_wrapper_mocks; diff --git a/node/src/test_utils/serde_serializer_mock.rs b/node/src/test_utils/serde_serializer_mock.rs new file mode 100644 index 000000000..7130cd0c0 --- /dev/null +++ b/node/src/test_utils/serde_serializer_mock.rs @@ -0,0 +1,348 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use serde::ser::{ + SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, + SerializeTupleStruct, SerializeTupleVariant, +}; +use serde::{Serialize, Serializer}; +use serde_json::Error; +use std::cell::RefCell; + +#[derive(Default)] +pub struct SerdeSerializerMock { + serialize_seq_results: RefCell>>, +} + +impl Serializer for SerdeSerializerMock { + type Ok = (); + type Error = Error; + type SerializeSeq = SerializeSeqMock; + type SerializeTuple = SerializeTupleMock; + type SerializeTupleStruct = SerializeTupleStructMock; + type SerializeTupleVariant = SerializeTupleVariantMock; + type SerializeMap = SerializeMapMock; + type SerializeStruct = SerializeStructMock; + type SerializeStructVariant = SerializeStructVariantMock; + + fn serialize_bool(self, _v: bool) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i8(self, _v: i8) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i16(self, _v: i16) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i32(self, _v: i32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i64(self, _v: i64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u8(self, _v: u8) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u16(self, _v: u16) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u32(self, _v: u32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u64(self, _v: u64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_f32(self, _v: f32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_f64(self, _v: f64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_char(self, _v: char) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_str(self, _v: &str) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_none(self) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_some(self, _value: &T) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_unit(self) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_seq(self, _len: Option) -> Result { + self.serialize_seq_results.borrow_mut().remove(0) + } + + fn serialize_tuple(self, _len: usize) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_map(self, _len: Option) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } +} + +impl SerdeSerializerMock { + pub fn serialize_seq_result(self, serializer: Result) -> Self { + self.serialize_seq_results.borrow_mut().push(serializer); + self + } +} + +#[derive(Default)] +pub struct SerializeSeqMock { + serialize_element_results: RefCell>>, + end_results: RefCell>>, +} + +impl SerializeSeq for SerializeSeqMock { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + self.serialize_element_results.borrow_mut().remove(0) + } + + fn end(self) -> Result { + self.end_results.borrow_mut().remove(0) + } +} + +impl SerializeSeqMock { + pub fn serialize_element_result(self, result: Result<(), Error>) -> Self { + self.serialize_element_results.borrow_mut().push(result); + self + } + + pub fn end_result(self, result: Result<(), Error>) -> Self { + self.end_results.borrow_mut().push(result); + self + } +} + +pub struct SerializeTupleMock {} + +impl SerializeTuple for SerializeTupleMock { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeTupleStructMock {} + +impl SerializeTupleStruct for SerializeTupleStructMock { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeTupleVariantMock {} + +impl SerializeTupleVariant for SerializeTupleVariantMock { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeMapMock {} + +impl SerializeMap for SerializeMapMock { + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, _key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_value(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeStructMock {} + +impl SerializeStruct for SerializeStructMock { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeStructVariantMock {} + +impl SerializeStructVariant for SerializeStructVariantMock { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} From ca6cb36a6ee16bd772ff1be0c0920db92354a51f Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:15:27 +0200 Subject: [PATCH 12/37] GH-642: Redesigning PendingPayableScanner (#677) * GH-642: interim commit * GH-642: interim commit * GH-642: interim commit * GH-642: big initial messy reconstruction continuing... * GH-642: big initial messy reconstruction... just realized I may've forgotten to update with the last changes from the other card * GH-642: tests compiling...failing lots of them * GH-642: fn confirm_transactions has been reimplemented * GH-642: fn handle_failed_transactions has been reimplemented * GH-642: fixed mainly internal, but smaller functions in the pending payable scanner; various From and Display implementations and these sorts * GH-642: progressed quite greatelly; fixed many tests; took action against the mark pending payable rowid fn * GH-642: another bunch fixed...down to 24 * GH-642: another bunch fixed...down to 10 * GH-642: the base of this card is done * GH-642: lots of fixes in names * GH-642: filling cache with failed txs to recheck at startup * GH-642: rpc failers during receipt checks can be handled now * GH-642: interim commit * GH-642: first I need to finish the impl of the db system of tx statuses...opened in its own PR and then will start from here on * GH-642: pending payable scanner machinery has been given the true skeleton * GH-642: before creating a new whole folder for scanner utils * GH-642: interim commit * GH-642: preparing tests before writing the guts of the core fns * GH-642: integration of the caches...100% at start_scan, 90% finish_scan * GH-642: another big portion of work in interpreting the receipts * GH-642: finishing tests for the receipt interpretation but I should rearrange the code a bit - maybe to add a separative class * GH-642: mod structure changed, new file for TxReceiptInterpreter * GH-642: fixed two unreliable tests * GH-642: interim commit * GH-642: worked away on the implementation of handling failed txs * GH-642: more todos!() gone * GH-642: processing failures is done; next tx confiramtions * GH-642: tx reclaim implemented * GH-642: finished the brain functions in PPS * GH-642: ValidationStatus extension - huge chunk of work; still some failing tests remain * GH-642: interim commit (some of the Validation error stuff will have to be fixed) * GH-683: savepoint * GH-683: interim commit * GH-683: mostly done * GH-683: renamed error * GH-683: additional fix to renaming * GH-683: finished * GH-642: finished * GH-683: fixed for a review * GH-683: fixed screwed string replacement * GH-683: finished fixing it * GH-683: another fix...BlockchainError * GH-642: added unreachable! * GH-642: before bigger issue addressing * GH-642: got rid of the BlockchainFailure::Unrecognized layer * GH-642: savepoint * GH-642: hashmap for receipt status result deployed * GH-642: finally solid... as much as under this card, tests fixed * GH-598-json-hotfix: interim commit * GH-642: dragging the failing tests down to bare minimum * GH-642: interim commit * GH-642: before fixing the todo!() left over * GH-642: finished * GH-642: cosmetics * GH-642: grrr - cosmetics - forgot e * GH-642: review 2 addressed * GH-642: added the cache clean-up on getting a scan error --------- Co-authored-by: Bert --- masq_lib/src/utils.rs | 122 +- .../db_access_objects/failed_payable_dao.rs | 104 +- node/src/accountant/db_access_objects/mod.rs | 2 +- .../db_access_objects/payable_dao.rs | 1014 +++---- .../db_access_objects/pending_payable_dao.rs | 1407 +++++----- .../db_access_objects/receivable_dao.rs | 6 +- ...able_and_failed_payable_data_conversion.rs | 137 + .../db_access_objects/sent_payable_dao.rs | 489 +++- .../db_access_objects/test_utils.rs | 17 +- .../src/accountant/db_access_objects/utils.rs | 4 + .../db_big_integer/big_int_db_processor.rs | 1 + node/src/accountant/mod.rs | 1172 +++++---- node/src/accountant/scanners/mod.rs | 1162 ++++---- .../scanners/pending_payable_scanner/mod.rs | 2339 +++++++++++++---- .../pending_payable_scanner/test_utils.rs | 23 + .../tx_receipt_interpreter.rs | 706 +++++ .../scanners/pending_payable_scanner/utils.rs | 1299 +++++++-- .../scanners/receivable_scanner/mod.rs | 2 +- .../accountant/scanners/scan_schedulers.rs | 36 +- .../src/accountant/scanners/scanners_utils.rs | 173 +- node/src/accountant/scanners/test_utils.rs | 94 +- node/src/accountant/test_utils.rs | 513 ++-- node/src/actor_system_factory.rs | 6 +- node/src/blockchain/blockchain_bridge.rs | 394 ++- .../lower_level_interface_web3.rs | 262 +- .../blockchain_interface_web3/mod.rs | 219 +- .../blockchain_interface_web3/utils.rs | 330 +-- .../data_structures/errors.rs | 30 +- .../data_structures/mod.rs | 99 +- .../blockchain/blockchain_interface/mod.rs | 36 +- node/src/blockchain/errors/internal_errors.rs | 2 +- node/src/blockchain/errors/mod.rs | 2 +- node/src/blockchain/errors/rpc_errors.rs | 6 +- node/src/blockchain/test_utils.rs | 49 + .../migrations/migration_4_to_5.rs | 4 +- node/src/database/rusqlite_wrappers.rs | 12 +- .../test_utils/transaction_wrapper_mock.rs | 6 +- node/src/stream_handler_pool.rs | 4 +- node/src/sub_lib/accountant.rs | 15 +- node/src/test_utils/mod.rs | 15 +- node/src/test_utils/recorder.rs | 12 +- 41 files changed, 8176 insertions(+), 4149 deletions(-) create mode 100644 node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs create mode 100644 node/src/accountant/scanners/pending_payable_scanner/test_utils.rs create mode 100644 node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs diff --git a/masq_lib/src/utils.rs b/masq_lib/src/utils.rs index 8d563ef37..9355d0624 100644 --- a/masq_lib/src/utils.rs +++ b/masq_lib/src/utils.rs @@ -463,6 +463,25 @@ macro_rules! test_only_use { } } +#[macro_export(local_inner_macros)] +macro_rules! btreemap { + () => { + ::std::collections::BTreeMap::new() + }; + ($($key:expr => $val:expr,)+) => { + btreemap!($($key => $val),+) + }; + ($($key:expr => $value:expr),+) => { + { + let mut _btm = ::std::collections::BTreeMap::new(); + $( + let _ = _btm.insert($key, $value); + )* + _btm + } + }; +} + #[macro_export(local_inner_macros)] macro_rules! hashmap { () => { @@ -482,10 +501,30 @@ macro_rules! hashmap { }; } +#[macro_export(local_inner_macros)] +macro_rules! hashset { + () => { + ::std::collections::HashSet::new() + }; + ($($val:expr,)+) => { + hashset!($($val),+) + }; + ($($value:expr),+) => { + { + let mut _hs = ::std::collections::HashSet::new(); + $( + let _ = _hs.insert($value); + )* + _hs + } + }; +} + #[cfg(test)] mod tests { use super::*; - use std::collections::HashMap; + use itertools::Itertools; + use std::collections::{BTreeMap, HashMap, HashSet}; use std::env::current_dir; use std::fmt::Write; use std::fs::{create_dir_all, File, OpenOptions}; @@ -814,7 +853,8 @@ mod tests { let hashmap_with_one_element = hashmap!(1 => 2); let hashmap_with_multiple_elements = hashmap!(1 => 2, 10 => 20, 12 => 42); let hashmap_with_trailing_comma = hashmap!(1 => 2, 10 => 20,); - let hashmap_of_string = hashmap!("key" => "val"); + let hashmap_of_string = hashmap!("key_1" => "val_a", "key_2" => "val_b"); + let hashmap_with_duplicate = hashmap!(1 => 2, 1 => 2); let expected_empty_hashmap: HashMap = HashMap::new(); let mut expected_hashmap_with_one_element = HashMap::new(); @@ -827,7 +867,10 @@ mod tests { expected_hashmap_with_trailing_comma.insert(1, 2); expected_hashmap_with_trailing_comma.insert(10, 20); let mut expected_hashmap_of_string = HashMap::new(); - expected_hashmap_of_string.insert("key", "val"); + expected_hashmap_of_string.insert("key_1", "val_a"); + expected_hashmap_of_string.insert("key_2", "val_b"); + let mut expected_hashmap_with_duplicate = HashMap::new(); + expected_hashmap_with_duplicate.insert(1, 2); assert_eq!(empty_hashmap, expected_empty_hashmap); assert_eq!(hashmap_with_one_element, expected_hashmap_with_one_element); assert_eq!( @@ -839,5 +882,78 @@ mod tests { expected_hashmap_with_trailing_comma ); assert_eq!(hashmap_of_string, expected_hashmap_of_string); + assert_eq!(hashmap_with_duplicate, expected_hashmap_with_duplicate); + } + + #[test] + fn btreemap_macro_works() { + let empty_btm: BTreeMap = btreemap!(); + let btm_with_one_element = btreemap!("ABC" => "234"); + let btm_with_multiple_elements = btreemap!("Bobble" => 2, "Hurrah" => 20, "Boom" => 42); + let btm_with_trailing_comma = btreemap!(12 => 1, 22 =>2,); + let btm_with_duplicate = btreemap!("A"=>123, "A"=>222); + + let expected_empty_btm: BTreeMap = BTreeMap::new(); + let mut expected_btm_with_one_element = BTreeMap::new(); + expected_btm_with_one_element.insert("ABC", "234"); + let mut expected_btm_with_multiple_elements = BTreeMap::new(); + expected_btm_with_multiple_elements.insert("Bobble", 2); + expected_btm_with_multiple_elements.insert("Hurrah", 20); + expected_btm_with_multiple_elements.insert("Boom", 42); + let mut expected_btm_with_trailing_comma = BTreeMap::new(); + expected_btm_with_trailing_comma.insert(12, 1); + expected_btm_with_trailing_comma.insert(22, 2); + let mut expected_btm_with_duplicate = BTreeMap::new(); + expected_btm_with_duplicate.insert("A", 222); + assert_eq!(empty_btm, expected_empty_btm); + assert_eq!(btm_with_one_element, expected_btm_with_one_element); + assert_eq!( + btm_with_multiple_elements, + expected_btm_with_multiple_elements + ); + assert_eq!( + btm_with_multiple_elements.into_iter().collect_vec(), + vec![("Bobble", 2), ("Boom", 42), ("Hurrah", 20)] + ); + assert_eq!(btm_with_trailing_comma, expected_btm_with_trailing_comma); + assert_eq!(btm_with_duplicate, expected_btm_with_duplicate); + } + + #[test] + fn hashset_macro_works() { + let empty_hashset: HashSet = hashset!(); + let hashset_with_one_element = hashset!(2); + let hashset_with_multiple_elements = hashset!(2, 20, 42); + let hashset_with_trailing_comma = hashset!(2, 20,); + let hashset_of_string = hashset!("val_a", "val_b"); + let hashset_with_duplicate = hashset!(2, 2); + + let expected_empty_hashset: HashSet = HashSet::new(); + let mut expected_hashset_with_one_element = HashSet::new(); + expected_hashset_with_one_element.insert(2); + let mut expected_hashset_with_multiple_elements = HashSet::new(); + expected_hashset_with_multiple_elements.insert(2); + expected_hashset_with_multiple_elements.insert(20); + expected_hashset_with_multiple_elements.insert(42); + let mut expected_hashset_with_trailing_comma = HashSet::new(); + expected_hashset_with_trailing_comma.insert(2); + expected_hashset_with_trailing_comma.insert(20); + let mut expected_hashset_of_string = HashSet::new(); + expected_hashset_of_string.insert("val_a"); + expected_hashset_of_string.insert("val_b"); + let mut expected_hashset_with_duplicate = HashSet::new(); + expected_hashset_with_duplicate.insert(2); + assert_eq!(empty_hashset, expected_empty_hashset); + assert_eq!(hashset_with_one_element, expected_hashset_with_one_element); + assert_eq!( + hashset_with_multiple_elements, + expected_hashset_with_multiple_elements + ); + assert_eq!( + hashset_with_trailing_comma, + expected_hashset_with_trailing_comma + ); + assert_eq!(hashset_of_string, expected_hashset_of_string); + assert_eq!(hashset_with_duplicate, expected_hashset_with_duplicate); } } diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 3202807b3..7a6e509bc 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -1,11 +1,11 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::utils::{ - DaoFactoryReal, TxHash, TxIdentifiers, VigilantRusqliteFlatten, + DaoFactoryReal, TxHash, TxIdentifiers, TxRecordWithHash, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; -use crate::blockchain::errors::validation_status::PreviousAttempts; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; use itertools::Itertools; use masq_lib::utils::ExpectValue; @@ -73,46 +73,61 @@ impl FromStr for FailureStatus { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum ValidationStatus { - Waiting, - Reattempting(PreviousAttempts), -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct FailedTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub reason: FailureReason, pub status: FailureStatus, } +impl TxRecordWithHash for FailedTx { + fn hash(&self) -> TxHash { + self.hash + } +} + +#[derive(Debug, PartialEq, Eq)] pub enum FailureRetrieveCondition { + ByTxHash(Vec), ByStatus(FailureStatus), + EveryRecheckRequiredRecord, } impl Display for FailureRetrieveCondition { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { + FailureRetrieveCondition::ByTxHash(hashes) => { + write!( + f, + "WHERE tx_hash IN ({})", + comma_joined_stringifiable(hashes, |hash| format!("'{:?}'", hash)) + ) + } FailureRetrieveCondition::ByStatus(status) => { write!(f, "WHERE status = '{}'", status) } + FailureRetrieveCondition::EveryRecheckRequiredRecord => { + write!(f, "WHERE status LIKE 'RecheckRequired%'") + } } } } pub trait FailedPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + //TODO potentially atomically fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError>; fn retrieve_txs(&self, condition: Option) -> Vec; fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError>; } @@ -187,11 +202,11 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { status ) VALUES {}", comma_joined_stringifiable(txs, |tx| { - let amount_checked = checked_conversion::(tx.amount); - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let amount_checked = checked_conversion::(tx.amount_minor); + let gas_price_minor_checked = checked_conversion::(tx.gas_price_minor); let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); let (gas_price_wei_high_b, gas_price_wei_low_b) = - BigIntDivider::deconstruct(gas_price_wei_checked); + BigIntDivider::deconstruct(gas_price_minor_checked); format!( "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}', '{}')", tx.hash, @@ -255,11 +270,11 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { Address::from_str(&receiver_address_str[2..]).expect("Failed to parse Address"); let amount_high_b = row.get(2).expectv("amount_high_b"); let amount_low_b = row.get(3).expectv("amount_low_b"); - let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let amount_minor = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; let timestamp = row.get(4).expectv("timestamp"); let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); - let gas_price_wei = + let gas_price_minor = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); let reason_str: String = row.get(8).expectv("reason"); @@ -272,9 +287,9 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { Ok(FailedTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, reason, status, @@ -287,7 +302,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { if status_updates.is_empty() { return Err(FailedPayableDaoError::EmptyInput); @@ -376,15 +391,16 @@ mod tests { }; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, - FailureRetrieveCondition, FailureStatus, ValidationStatus, + FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::test_utils::{ make_read_only_db_connection, FailedTxBuilder, }; - use crate::accountant::db_access_objects::utils::current_unix_timestamp; + use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxRecordWithHash}; + use crate::accountant::test_utils::make_failed_tx; use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClockReal, + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, }; use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::{make_tx_hash, ValidationFailureClockMock}; @@ -470,12 +486,12 @@ mod tests { [FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 0, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ nonce: 0, reason: PendingTooLong, status: RetryRequired }, \ FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 0, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ nonce: 0, reason: PendingTooLong, status: RecheckRequired(Waiting) }]" .to_string() )) @@ -587,6 +603,29 @@ mod tests { assert_eq!(result.get(&another_present_hash), Some(&2u64)); } + #[test] + fn display_for_failure_retrieve_condition_works() { + let tx_hash_1 = make_tx_hash(123); + let tx_hash_2 = make_tx_hash(456); + assert_eq!(FailureRetrieveCondition::ByTxHash(vec![tx_hash_1, tx_hash_2]).to_string(), + "WHERE tx_hash IN ('0x000000000000000000000000000000000000000000000000000000000000007b', \ + '0x00000000000000000000000000000000000000000000000000000000000001c8')" + ); + assert_eq!( + FailureRetrieveCondition::ByStatus(RetryRequired).to_string(), + "WHERE status = '\"RetryRequired\"'" + ); + assert_eq!( + FailureRetrieveCondition::ByStatus(RecheckRequired(ValidationStatus::Waiting)) + .to_string(), + "WHERE status = '{\"RecheckRequired\":\"Waiting\"}'" + ); + assert_eq!( + FailureRetrieveCondition::EveryRecheckRequiredRecord.to_string(), + "WHERE status LIKE 'RecheckRequired%'" + ); + } + #[test] fn failure_reason_from_str_works() { // Submission error @@ -794,7 +833,7 @@ mod tests { (tx3.hash, Concluded), ]); - let result = subject.update_statuses(hashmap); + let result = subject.update_statuses(&hashmap); let updated_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); @@ -815,6 +854,7 @@ mod tests { updated_txs[3].status, RecheckRequired(ValidationStatus::Waiting) ); + assert_eq!(updated_txs.len(), 4); } #[test] @@ -828,7 +868,7 @@ mod tests { .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let result = subject.update_statuses(HashMap::new()); + let result = subject.update_statuses(&HashMap::new()); assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); } @@ -842,7 +882,7 @@ mod tests { let wrapped_conn = make_read_only_db_connection(home_dir); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.update_statuses(HashMap::from([(make_tx_hash(1), Concluded)])); + let result = subject.update_statuses(&HashMap::from([(make_tx_hash(1), Concluded)])); assert_eq!( result, @@ -955,4 +995,14 @@ mod tests { )) ) } + + #[test] + fn tx_record_with_hash_is_implemented_for_failed_tx() { + let failed_tx = make_failed_tx(1234); + let hash = failed_tx.hash; + + let hash_from_trait = failed_tx.hash(); + + assert_eq!(hash_from_trait, hash); + } } diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index ae165909a..0141e8796 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -3,8 +3,8 @@ pub mod banned_dao; pub mod failed_payable_dao; pub mod payable_dao; -pub mod pending_payable_dao; pub mod receivable_dao; +pub mod sent_payable_and_failed_payable_data_conversion; pub mod sent_payable_dao; mod test_utils; pub mod utils; diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index c7d438a41..0226c0c68 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -1,24 +1,24 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::{ - PendingPayableRowid, WalletAddress, -}; -use crate::accountant::db_big_integer::big_int_db_processor::{BigIntDbProcessor, BigIntDbProcessorReal, BigIntSqlConfig, DisplayableRusqliteParamPair, ParamByUse, SQLParamsBuilder, TableNameDAO, WeiChange, WeiChangeDirection}; -use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_access_objects::utils; use crate::accountant::db_access_objects::utils::{ sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, - RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten, + RangeStmConfig, RowId, TopStmConfig, TxHash, VigilantRusqliteFlatten, }; -use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::{ - compose_case_expression, execute_command, serialize_wallets, +use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; +use crate::accountant::db_big_integer::big_int_db_processor::{ + BigIntDbProcessor, BigIntDbProcessorReal, BigIntSqlConfig, DisplayableRusqliteParamPair, + ParamByUse, SQLParamsBuilder, TableNameDAO, WeiChange, WeiChangeDirection, }; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, sign_conversion, PendingPayableId}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; +use ethabi::Address; #[cfg(test)] use ethereum_types::{BigEndianHash, U256}; +use itertools::Either; use masq_lib::utils::ExpectValue; #[cfg(test)] use rusqlite::OptionalExtension; @@ -26,7 +26,6 @@ use rusqlite::{Error, Row}; use std::fmt::Debug; use std::str::FromStr; use std::time::SystemTime; -use itertools::Either; use web3::types::H256; #[derive(Debug, PartialEq, Eq)] @@ -48,18 +47,15 @@ pub trait PayableDao: Debug + Send { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError>; fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], + mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError>; - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], - ) -> Result<(), PayableDaoError>; + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError>; fn non_pending_payables(&self) -> Vec; @@ -81,6 +77,11 @@ impl PayableDaoFactory for DaoFactoryReal { } } +pub struct MarkPendingPayableID { + pub receiver_wallet: Address, + pub rowid: RowId, +} + #[derive(Debug)] pub struct PayableDaoReal { conn: Box, @@ -92,7 +93,7 @@ impl PayableDao for PayableDaoReal { &self, timestamp: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError> { let main_sql = "insert into payable (wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid) \ values (:wallet, :balance_high_b, :balance_low_b, :last_paid_timestamp, null) on conflict (wallet_address) do update set \ @@ -105,7 +106,7 @@ impl PayableDao for PayableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( @@ -123,46 +124,42 @@ impl PayableDao for PayableDaoReal { fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], + _mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError> { - if wallets_and_rowids.is_empty() { - panic!("broken code: empty input is not permit to enter this method") - } - - let case_expr = compose_case_expression(wallets_and_rowids); - let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); - //the Wallet type is secure against SQL injections - let sql = format!( - "update payable set \ - pending_payable_rowid = {} \ - where - pending_payable_rowid is null and wallet_address in ({}) - returning - pending_payable_rowid", - case_expr, wallets, - ); - execute_command(&*self.conn, wallets_and_rowids, &sql) + todo!("Will be an object of removal in GH-662") + // if wallets_and_rowids.is_empty() { + // panic!("broken code: empty input is not permit to enter this method") + // } + // + // let case_expr = compose_case_expression(wallets_and_rowids); + // let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); + // //the Wallet type is secure against SQL injections + // let sql = format!( + // "update payable set \ + // pending_payable_rowid = {} \ + // where + // pending_payable_rowid is null and wallet_address in ({}) + // returning + // pending_payable_rowid", + // case_expr, wallets, + // ); + // execute_command(&*self.conn, wallets_and_rowids, &sql) } - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], - ) -> Result<(), PayableDaoError> { - confirmed_payables.iter().try_for_each(|pending_payable_fingerprint| { - + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError> { + confirmed_payables.iter().try_for_each(|confirmed_payable| { let main_sql = "update payable set \ balance_high_b = balance_high_b + :balance_high_b, balance_low_b = balance_low_b + :balance_low_b, \ - last_paid_timestamp = :last_paid, pending_payable_rowid = null where pending_payable_rowid = :rowid"; + last_paid_timestamp = :last_paid, pending_payable_rowid = null where wallet_address = :wallet"; let update_clause_with_compensated_overflow = "update payable set \ balance_high_b = :balance_high_b, balance_low_b = :balance_low_b, last_paid_timestamp = :last_paid, \ - pending_payable_rowid = null where pending_payable_rowid = :rowid"; + pending_payable_rowid = null where wallet_address = :wallet"; - let i64_rowid = checked_conversion::(pending_payable_fingerprint.rowid); - let last_paid = to_unix_timestamp(pending_payable_fingerprint.timestamp); + let wallet = format!("{:?}", confirmed_payable.receiver_address); let params = SQLParamsBuilder::default() - .key( PendingPayableRowid(&i64_rowid)) - .wei_change(WeiChange::new( "balance", pending_payable_fingerprint.amount, WeiChangeDirection::Subtraction)) - .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &last_paid))]) + .key( WalletAddress(&wallet)) + .wei_change(WeiChange::new("balance", confirmed_payable.amount_minor, WeiChangeDirection::Subtraction)) + .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &confirmed_payable.timestamp))]) .build(); self.big_int_db_processor.execute(Either::Left(self.conn.as_ref()), BigIntSqlConfig::new( @@ -389,175 +386,182 @@ impl TableNameDAO for PayableDaoReal { } } -mod mark_pending_payable_associated_functions { - use crate::accountant::comma_joined_stringifiable; - use crate::accountant::db_access_objects::payable_dao::PayableDaoError; - use crate::accountant::db_access_objects::utils::{ - update_rows_and_return_valid_count, VigilantRusqliteFlatten, - }; - use crate::database::rusqlite_wrappers::ConnectionWrapper; - use crate::sub_lib::wallet::Wallet; - use itertools::Itertools; - use rusqlite::Row; - use std::fmt::Display; - - pub fn execute_command( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - sql: &str, - ) -> Result<(), PayableDaoError> { - let mut stm = conn.prepare(sql).expect("Internal Error"); - let validator = validate_row_updated; - let rows_affected_res = update_rows_and_return_valid_count(&mut stm, validator); - - match rows_affected_res { - Ok(rows_affected) => match rows_affected { - num if num == wallets_and_rowids.len() => Ok(()), - num => mismatched_row_count_panic(conn, wallets_and_rowids, num), - }, - Err(errs) => { - let err_msg = format!( - "Multi-row update to mark pending payable hit these errors: {:?}", - errs - ); - Err(PayableDaoError::RusqliteError(err_msg)) - } - } - } - - pub fn compose_case_expression(wallets_and_rowids: &[(&Wallet, u64)]) -> String { - //the Wallet type is secure against SQL injections - fn when_clause((wallet, rowid): &(&Wallet, u64)) -> String { - format!("when wallet_address = '{wallet}' then {rowid}") - } - - format!( - "case {} end", - wallets_and_rowids.iter().map(when_clause).join("\n") - ) - } - - pub fn serialize_wallets( - wallets_and_rowids: &[(&Wallet, u64)], - quotes_opt: Option, - ) -> String { - wallets_and_rowids - .iter() - .map(|(wallet, _)| match quotes_opt { - Some(char) => format!("{}{}{}", char, wallet, char), - None => wallet.to_string(), - }) - .join(", ") - } - - fn validate_row_updated(row: &Row) -> Result { - row.get::>(0).map(|opt| opt.is_some()) - } - - fn mismatched_row_count_panic( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - actual_count: usize, - ) -> ! { - let serialized_wallets = serialize_wallets(wallets_and_rowids, None); - let expected_count = wallets_and_rowids.len(); - let extension = explanatory_extension(conn, wallets_and_rowids); - panic!( - "Marking pending payable rowid for wallets {serialized_wallets} affected \ - {actual_count} rows but expected {expected_count}. {extension}" - ) - } - - pub(super) fn explanatory_extension( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> String { - let resulting_pairs_collection = - query_resulting_pairs_of_wallets_and_rowids(conn, wallets_and_rowids); - let resulting_pairs_summary = if resulting_pairs_collection.is_empty() { - "".to_string() - } else { - pairs_in_pretty_string(&resulting_pairs_collection, |rowid_opt: &Option| { - match rowid_opt { - Some(rowid) => Box::new(*rowid), - None => Box::new("N/A"), - } - }) - }; - let wallets_and_non_optional_rowids = - pairs_in_pretty_string(wallets_and_rowids, |rowid: &u64| Box::new(*rowid)); - format!( - "\ - The demanded data according to {} looks different from the resulting state {}!. Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ - points to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ - probably had not managed to complete successfully before another payment was requested: preventive measures failed.\n", - wallets_and_non_optional_rowids, resulting_pairs_summary) - } - - fn query_resulting_pairs_of_wallets_and_rowids( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> Vec<(Wallet, Option)> { - let select_dealt_accounts = - format!( - "select wallet_address, pending_payable_rowid from payable where wallet_address in ({})", - serialize_wallets(wallets_and_rowids, Some('\'')) - ); - let row_processor = |row: &Row| { - Ok(( - row.get::(0) - .expect("database corrupt: wallet addresses found in bad format"), - row.get::>(1) - .expect("database_corrupt: rowid found in bad format"), - )) - }; - conn.prepare(&select_dealt_accounts) - .expect("select failed") - .query_map([], row_processor) - .expect("no args yet binding failed") - .vigilant_flatten() - .collect() - } - - fn pairs_in_pretty_string( - pairs: &[(W, R)], - rowid_pretty_writer: fn(&R) -> Box, - ) -> String { - comma_joined_stringifiable(pairs, |(wallet, rowid)| { - format!( - "( Wallet: {}, Rowid: {} )", - wallet, - rowid_pretty_writer(rowid) - ) - }) - } -} +// TODO Will be an object of removal in GH-662 +// mod mark_pending_payable_associated_functions { +// use crate::accountant::comma_joined_stringifiable; +// use crate::accountant::db_access_objects::payable_dao::{MarkPendingPayableID, PayableDaoError}; +// use crate::accountant::db_access_objects::utils::{ +// update_rows_and_return_valid_count, VigilantRusqliteFlatten, +// }; +// use crate::database::rusqlite_wrappers::ConnectionWrapper; +// use crate::sub_lib::wallet::Wallet; +// use itertools::Itertools; +// use rusqlite::Row; +// use std::fmt::Display; +// +// pub fn execute_command( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// sql: &str, +// ) -> Result<(), PayableDaoError> { +// let mut stm = conn.prepare(sql).expect("Internal Error"); +// let validator = validate_row_updated; +// let rows_affected_res = update_rows_and_return_valid_count(&mut stm, validator); +// +// match rows_affected_res { +// Ok(rows_affected) => match rows_affected { +// num if num == wallets_and_rowids.len() => Ok(()), +// num => mismatched_row_count_panic(conn, wallets_and_rowids, num), +// }, +// Err(errs) => { +// let err_msg = format!( +// "Multi-row update to mark pending payable hit these errors: {:?}", +// errs +// ); +// Err(PayableDaoError::RusqliteError(err_msg)) +// } +// } +// } +// +// pub fn compose_case_expression(wallets_and_rowids: &[(&Wallet, u64)]) -> String { +// //the Wallet type is secure against SQL injections +// fn when_clause((wallet, rowid): &(&Wallet, u64)) -> String { +// format!("when wallet_address = '{wallet}' then {rowid}") +// } +// +// format!( +// "case {} end", +// wallets_and_rowids.iter().map(when_clause).join("\n") +// ) +// } +// +// pub fn serialize_wallets( +// wallets_and_rowids: &[MarkPendingPayableID], +// quotes_opt: Option, +// ) -> String { +// wallets_and_rowids +// .iter() +// .map(|(wallet, _)| match quotes_opt { +// Some(char) => format!("{}{}{}", char, wallet, char), +// None => wallet.to_string(), +// }) +// .join(", ") +// } +// +// fn validate_row_updated(row: &Row) -> Result { +// row.get::>(0).map(|opt| opt.is_some()) +// } +// +// fn mismatched_row_count_panic( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// actual_count: usize, +// ) -> ! { +// let serialized_wallets = serialize_wallets(wallets_and_rowids, None); +// let expected_count = wallets_and_rowids.len(); +// let extension = explanatory_extension(conn, wallets_and_rowids); +// panic!( +// "Marking pending payable rowid for wallets {serialized_wallets} affected \ +// {actual_count} rows but expected {expected_count}. {extension}" +// ) +// } +// +// pub(super) fn explanatory_extension( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// ) -> String { +// let resulting_pairs_collection = +// query_resulting_pairs_of_wallets_and_rowids(conn, wallets_and_rowids); +// let resulting_pairs_summary = if resulting_pairs_collection.is_empty() { +// "".to_string() +// } else { +// pairs_in_pretty_string(&resulting_pairs_collection, |rowid_opt: &Option| { +// match rowid_opt { +// Some(rowid) => Box::new(*rowid), +// None => Box::new("N/A"), +// } +// }) +// }; +// let wallets_and_non_optional_rowids = +// pairs_in_pretty_string(wallets_and_rowids, |rowid: &u64| Box::new(*rowid)); +// format!( +// "\ +// The demanded data according to {} looks different from the resulting state {}!. Operation failed.\n\ +// Notes:\n\ +// a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ +// b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ +// points to figure out if you were put in danger of double payment,\n\ +// c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ +// The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ +// probably had not managed to complete successfully before another payment was requested: preventive measures failed.\n", +// wallets_and_non_optional_rowids, resulting_pairs_summary) +// } +// +// fn query_resulting_pairs_of_wallets_and_rowids( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// ) -> Vec<(Wallet, Option)> { +// let select_dealt_accounts = +// format!( +// "select wallet_address, pending_payable_rowid from payable where wallet_address in ({})", +// serialize_wallets(wallets_and_rowids, Some('\'')) +// ); +// let row_processor = |row: &Row| { +// Ok(( +// row.get::(0) +// .expect("database corrupt: wallet addresses found in bad format"), +// row.get::>(1) +// .expect("database_corrupt: rowid found in bad format"), +// )) +// }; +// conn.prepare(&select_dealt_accounts) +// .expect("select failed") +// .query_map([], row_processor) +// .expect("no args yet binding failed") +// .vigilant_flatten() +// .collect() +// } +// +// fn pairs_in_pretty_string( +// pairs: &[(W, R)], +// rowid_pretty_writer: fn(&R) -> Box, +// ) -> String { +// comma_joined_stringifiable(pairs, |(wallet, rowid)| { +// format!( +// "( Wallet: {}, Rowid: {} )", +// wallet, +// rowid_pretty_writer(rowid) +// ) +// }) +// } +// } #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::{from_unix_timestamp, current_unix_timestamp, to_unix_timestamp}; + use crate::accountant::db_access_objects::sent_payable_dao::SentTx; + use crate::accountant::db_access_objects::utils::{ + current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, + }; use crate::accountant::gwei_to_wei; - use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::explanatory_extension; - use crate::accountant::test_utils::{assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_pending_payable_fingerprint, trick_rusqlite_with_read_only_conn}; + use crate::accountant::test_utils::{ + assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_sent_tx, + trick_rusqlite_with_read_only_conn, + }; use crate::blockchain::test_utils::make_tx_hash; - use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; + use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::test_utils::make_wallet; + use itertools::Itertools; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::ToSql; use rusqlite::{Connection, OpenFlags}; - use rusqlite::{ToSql}; use std::path::Path; use std::str::FromStr; - use crate::database::test_utils::ConnectionWrapperMock; + use std::time::Duration; #[test] fn more_money_payable_works_for_new_address() { @@ -704,260 +708,271 @@ mod tests { fn mark_pending_payables_marks_pending_transactions_for_new_addresses() { //the extra unchanged record checks the safety of right count of changed rows; //experienced serious troubles in the past - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_marks_pending_transactions_for_new_addresses", - ); - let wallet_0 = make_wallet("wallet"); - let wallet_1 = make_wallet("booga"); - let pending_payable_rowid_1 = 656; - let wallet_2 = make_wallet("bagaboo"); - let pending_payable_rowid_2 = 657; - let boxed_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ - last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; - let mut stm = boxed_conn.prepare(insert).unwrap(); - let params = [ - [&wallet_0 as &dyn ToSql, &12345, &1, &45678], - [&wallet_1, &0, &i64::MAX, &150_000_000], - [&wallet_2, &3, &0, &151_000_000], - ] - .into_iter() - .flatten() - .collect::>(); - stm.execute(params.as_slice()).unwrap(); - } - let subject = PayableDaoReal::new(boxed_conn); - - subject - .mark_pending_payables_rowids(&[ - (&wallet_1, pending_payable_rowid_1), - (&wallet_2, pending_payable_rowid_2), - ]) - .unwrap(); - - let account_statuses = [&wallet_0, &wallet_1, &wallet_2] - .iter() - .map(|wallet| subject.account_status(wallet).unwrap()) - .collect::>(); - assert_eq!( - account_statuses, - vec![ - PayableAccount { - wallet: wallet_0, - balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), - last_paid_timestamp: from_unix_timestamp(45678), - pending_payable_opt: None, - }, - PayableAccount { - wallet: wallet_1, - balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), - last_paid_timestamp: from_unix_timestamp(150_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_1, - make_tx_hash(0) - )), - }, - //notice the hashes are garbage generated by a test method not knowing doing better - PayableAccount { - wallet: wallet_2, - balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), - last_paid_timestamp: from_unix_timestamp(151_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_2, - make_tx_hash(0) - )) - } - ] - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_marks_pending_transactions_for_new_addresses", + // ); + // let wallet_0 = make_wallet("wallet"); + // let wallet_1 = make_wallet("booga"); + // let pending_payable_rowid_1 = 656; + // let wallet_2 = make_wallet("bagaboo"); + // let pending_payable_rowid_2 = 657; + // let boxed_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // { + // let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ + // last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; + // let mut stm = boxed_conn.prepare(insert).unwrap(); + // let params = [ + // [&wallet_0 as &dyn ToSql, &12345, &1, &45678], + // [&wallet_1, &0, &i64::MAX, &150_000_000], + // [&wallet_2, &3, &0, &151_000_000], + // ] + // .into_iter() + // .flatten() + // .collect::>(); + // stm.execute(params.as_slice()).unwrap(); + // } + // let subject = PayableDaoReal::new(boxed_conn); + // + // subject + // .mark_pending_payables_rowids(&[ + // (&wallet_1, pending_payable_rowid_1), + // (&wallet_2, pending_payable_rowid_2), + // ]) + // .unwrap(); + // + // let account_statuses = [&wallet_0, &wallet_1, &wallet_2] + // .iter() + // .map(|wallet| subject.account_status(wallet).unwrap()) + // .collect::>(); + // assert_eq!( + // account_statuses, + // vec![ + // PayableAccount { + // wallet: wallet_0, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(45678), + // pending_payable_opt: None, + // }, + // PayableAccount { + // wallet: wallet_1, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(150_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_1, + // make_tx_hash(0) + // )), + // }, + // //notice the hashes are garbage generated by a test method not knowing doing better + // PayableAccount { + // wallet: wallet_2, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(151_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_2, + // make_tx_hash(0) + // )) + // } + // ] + // ) } #[test] - #[should_panic(expected = "\ - Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ - 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ - The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ - ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ - points to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ - probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] + // #[should_panic(expected = "\ + // Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ + // 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ + // ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ + // points to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ + // probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] fn mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified( ) { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let first_wallet = make_wallet("booga"); - let first_rowid = 456; - insert_payable_record_fn( - &*conn, - &first_wallet.to_string(), - 123456, - 789789, - Some(first_rowid), - ); - let subject = PayableDaoReal::new(conn); - - let _ = subject.mark_pending_payables_rowids(&[ - (&first_wallet, first_rowid as u64), - (&make_wallet("yahoo"), 789), - ]); + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let first_wallet = make_wallet("booga"); + // let first_rowid = 456; + // insert_payable_record_fn( + // &*conn, + // &first_wallet.to_string(), + // 123456, + // 789789, + // Some(first_rowid), + // ); + // let subject = PayableDaoReal::new(conn); + // + // let _ = subject.mark_pending_payables_rowids(&[ + // (&first_wallet, first_rowid as u64), + // (&make_wallet("yahoo"), 789), + // ]); } #[test] fn explanatory_extension_shows_resulting_account_with_unpopulated_rowid() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", - ); - let wallet_1 = make_wallet("hooga"); - let rowid_1 = 550; - let wallet_2 = make_wallet("booga"); - let rowid_2 = 555; - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let record_seeds = [ - (&wallet_1.to_string(), 12345, 1_000_000_000, None), - (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), - ]; - record_seeds - .into_iter() - .for_each(|(wallet, balance, timestamp, rowid_opt)| { - insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) - }); - - let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); - - assert_eq!(result, "\ - The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ - ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ - ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ - Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double \ - payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be \ - suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for \ - this account\nprobably had not managed to complete successfully before another payment was \ - requested: preventive measures failed.\n".to_string()) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", + // ); + // let wallet_1 = make_wallet("hooga"); + // let rowid_1 = 550; + // let wallet_2 = make_wallet("booga"); + // let rowid_2 = 555; + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let record_seeds = [ + // (&wallet_1.to_string(), 12345, 1_000_000_000, None), + // (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), + // ]; + // record_seeds + // .into_iter() + // .for_each(|(wallet, balance, timestamp, rowid_opt)| { + // insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) + // }); + // + // let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); + // + // assert_eq!(result, "\ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ + // ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ + // ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ + // Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double \ + // payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be \ + // suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for \ + // this account\nprobably had not managed to complete successfully before another payment was \ + // requested: preventive measures failed.\n".to_string()) } #[test] fn mark_pending_payables_rowids_handles_general_sql_error() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_handles_general_sql_error", - ); - let wallet = make_wallet("booga"); - let rowid = 656; - let conn = payable_read_only_conn(&home_dir); - let conn_wrapped = ConnectionWrapperReal::new(conn); - let subject = PayableDaoReal::new(Box::new(conn_wrapped)); - - let result = subject.mark_pending_payables_rowids(&[(&wallet, rowid)]); - - assert_eq!( - result, - Err(PayableDaoError::RusqliteError( - "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ - Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ - database\"))]" - .to_string() - )) - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_handles_general_sql_error", + // ); + // let wallet = make_wallet("booga"); + // let rowid = 656; + // let single_mark_instruction = MarkPendingPayableID::new(wallet.address(), rowid); + // let conn = payable_read_only_conn(&home_dir); + // let conn_wrapped = ConnectionWrapperReal::new(conn); + // let subject = PayableDaoReal::new(Box::new(conn_wrapped)); + // + // let result = subject.mark_pending_payables_rowids(&[single_mark_instruction]); + // + // assert_eq!( + // result, + // Err(PayableDaoError::RusqliteError( + // "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ + // Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ + // database\"))]" + // .to_string() + // )) + // ) } #[test] - #[should_panic(expected = "broken code: empty input is not permit to enter this method")] + //#[should_panic(expected = "broken code: empty input is not permit to enter this method")] fn mark_pending_payables_rowids_is_strict_about_empty_input() { - let wrapped_conn = ConnectionWrapperMock::default(); - let subject = PayableDaoReal::new(Box::new(wrapped_conn)); - - let _ = subject.mark_pending_payables_rowids(&[]); + // TODO Will be an object of removal in GH-662 + // let wrapped_conn = ConnectionWrapperMock::default(); + // let subject = PayableDaoReal::new(Box::new(wrapped_conn)); + // + // let _ = subject.mark_pending_payables_rowids(&[]); } struct TestSetupValuesHolder { - fingerprint_1: PendingPayableFingerprint, - fingerprint_2: PendingPayableFingerprint, - wallet_1: Wallet, - wallet_2: Wallet, - previous_timestamp_1: SystemTime, - previous_timestamp_2: SystemTime, + account_1: TxWalletAndTimestamp, + account_2: TxWalletAndTimestamp, + } + + struct TxWalletAndTimestamp { + pending_payable: SentTx, + previous_timestamp: SystemTime, } - fn make_fingerprint_pair_and_insert_initial_payable_records( + struct TestInputs { + hash: TxHash, + previous_timestamp: SystemTime, + new_payable_timestamp: SystemTime, + receiver_wallet: Address, + initial_amount_wei: u128, + balance_change: u128, + } + + fn insert_initial_payable_records_and_return_sent_txs( conn: &dyn ConnectionWrapper, - initial_amount_1: u128, - initial_amount_2: u128, - balance_change_1: u128, - balance_change_2: u128, + (initial_amount_1, balance_change_1): (u128, u128), + (initial_amount_2, balance_change_2): (u128, u128), ) -> TestSetupValuesHolder { - let hash_1 = make_tx_hash(12345); - let rowid_1 = 789; - let previous_timestamp_1_s = 190_000_000; - let new_payable_timestamp_1 = from_unix_timestamp(199_000_000); - let wallet_1 = make_wallet("bobble"); - let hash_2 = make_tx_hash(54321); - let rowid_2 = 792; - let previous_timestamp_2_s = 187_100_000; - let new_payable_timestamp_2 = from_unix_timestamp(191_333_000); - let wallet_2 = make_wallet("booble bobble"); - { + let now = SystemTime::now(); + let (account_1, account_2) = [ + TestInputs { + hash: make_tx_hash(12345), + previous_timestamp: now.checked_sub(Duration::from_secs(45_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("bobbles").address(), + initial_amount_wei: initial_amount_1, + balance_change: balance_change_1, + }, + TestInputs { + hash: make_tx_hash(54321), + previous_timestamp: now.checked_sub(Duration::from_secs(22_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("yet more bobbles").address(), + initial_amount_wei: initial_amount_2, + balance_change: balance_change_2, + }, + ] + .into_iter() + .enumerate() + .map(|(idx, test_inputs)| { insert_payable_record_fn( conn, - &wallet_1.to_string(), - i128::try_from(initial_amount_1).unwrap(), - previous_timestamp_1_s, - Some(rowid_1 as i64), + &format!("{:?}", test_inputs.receiver_wallet), + i128::try_from(test_inputs.initial_amount_wei).unwrap(), + to_unix_timestamp(test_inputs.previous_timestamp), + // TODO argument will be eliminated in GH-662 + None, ); - insert_payable_record_fn( - conn, - &wallet_2.to_string(), - i128::try_from(initial_amount_2).unwrap(), - previous_timestamp_2_s, - Some(rowid_2 as i64), - ) - } - let fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: new_payable_timestamp_1, - hash: hash_1, - attempt: 1, - amount: balance_change_1, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: new_payable_timestamp_2, - hash: hash_2, - attempt: 1, - amount: balance_change_2, - process_error: None, - }; - let previous_timestamp_1 = from_unix_timestamp(previous_timestamp_1_s); - let previous_timestamp_2 = from_unix_timestamp(previous_timestamp_2_s); + let mut sent_tx = make_sent_tx((idx as u64 + 1) * 1234); + sent_tx.hash = test_inputs.hash; + sent_tx.amount_minor = test_inputs.balance_change; + sent_tx.receiver_address = test_inputs.receiver_wallet; + sent_tx.timestamp = to_unix_timestamp(test_inputs.new_payable_timestamp); + sent_tx.amount_minor = test_inputs.balance_change; + + TxWalletAndTimestamp { + pending_payable: sent_tx, + previous_timestamp: test_inputs.previous_timestamp, + } + }) + .collect_tuple() + .unwrap(); + TestSetupValuesHolder { - fingerprint_1, - fingerprint_2, - wallet_1, - wallet_2, - previous_timestamp_1, - previous_timestamp_2, + account_1, + account_2, } } @@ -968,7 +983,7 @@ mod tests { //initial (1, 9999) let initial_changing_end_resulting_values = (initial, 11111, initial as u128 - 11111); //change (-1, abs(i64::MIN) - 11111) - transaction_confirmed_works( + test_transaction_confirmed_works( "transaction_confirmed_works_without_overflow", initial_changing_end_resulting_values, ) @@ -981,77 +996,80 @@ mod tests { //initial (0, 10000) //change (-1, abs(i64::MIN) - 111) //10000 + (abs(i64::MIN) - 111) > i64::MAX -> overflow - transaction_confirmed_works( + test_transaction_confirmed_works( "transaction_confirmed_works_hitting_overflow", initial_changing_end_resulting_values, ) } - fn transaction_confirmed_works( + fn test_transaction_confirmed_works( test_name: &str, (initial_amount_1, balance_change_1, expected_balance_after_1): (u128, u128, u128), ) { let home_dir = ensure_node_home_directory_exists("payable_dao", test_name); - //a hardcoded set that just makes a complement to the crucial, supplied one; this points to the ability of - //handling multiple transactions together + // A hardcoded set that just makes a complement to the crucial, supplied first one; this + // shows the ability to handle multiple transactions together let initial_amount_2 = 5_678_901; let balance_change_2 = 678_902; let expected_balance_after_2 = 4_999_999; let boxed_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let setup_holder = make_fingerprint_pair_and_insert_initial_payable_records( + let setup_holder = insert_initial_payable_records_and_return_sent_txs( boxed_conn.as_ref(), - initial_amount_1, - initial_amount_2, - balance_change_1, - balance_change_2, + (initial_amount_1, balance_change_1), + (initial_amount_2, balance_change_2), ); let subject = PayableDaoReal::new(boxed_conn); - let status_1_before_opt = subject.account_status(&setup_holder.wallet_1); - let status_2_before_opt = subject.account_status(&setup_holder.wallet_2); + let wallet_1 = Wallet::from(setup_holder.account_1.pending_payable.receiver_address); + let wallet_2 = Wallet::from(setup_holder.account_2.pending_payable.receiver_address); + let status_1_before_opt = subject.account_status(&wallet_1); + let status_2_before_opt = subject.account_status(&wallet_2); let result = subject.transactions_confirmed(&[ - setup_holder.fingerprint_1.clone(), - setup_holder.fingerprint_2.clone(), + setup_holder.account_1.pending_payable.clone(), + setup_holder.account_2.pending_payable.clone(), ]); assert_eq!(result, Ok(())); + let expected_last_paid_timestamp_1 = + from_unix_timestamp(to_unix_timestamp(setup_holder.account_1.previous_timestamp)); + let expected_last_paid_timestamp_2 = + from_unix_timestamp(to_unix_timestamp(setup_holder.account_2.previous_timestamp)); + // TODO yes these pending_payable_opt values are unsensible now but it will eventually be all cleaned up with GH-662 let expected_status_before_1 = PayableAccount { - wallet: setup_holder.wallet_1.clone(), + wallet: wallet_1.clone(), balance_wei: initial_amount_1, - last_paid_timestamp: setup_holder.previous_timestamp_1, - pending_payable_opt: Some(PendingPayableId::new( - setup_holder.fingerprint_1.rowid, - H256::from_uint(&U256::from(0)), - )), //hash is just garbage + last_paid_timestamp: expected_last_paid_timestamp_1, + pending_payable_opt: None, }; let expected_status_before_2 = PayableAccount { - wallet: setup_holder.wallet_2.clone(), + wallet: wallet_2.clone(), balance_wei: initial_amount_2, - last_paid_timestamp: setup_holder.previous_timestamp_2, - pending_payable_opt: Some(PendingPayableId::new( - setup_holder.fingerprint_2.rowid, - H256::from_uint(&U256::from(0)), - )), //hash is just garbage + last_paid_timestamp: expected_last_paid_timestamp_2, + pending_payable_opt: None, }; let expected_resulting_status_1 = PayableAccount { - wallet: setup_holder.wallet_1.clone(), + wallet: wallet_1.clone(), balance_wei: expected_balance_after_1, - last_paid_timestamp: setup_holder.fingerprint_1.timestamp, + last_paid_timestamp: from_unix_timestamp( + setup_holder.account_1.pending_payable.timestamp, + ), pending_payable_opt: None, }; let expected_resulting_status_2 = PayableAccount { - wallet: setup_holder.wallet_2.clone(), + wallet: wallet_2.clone(), balance_wei: expected_balance_after_2, - last_paid_timestamp: setup_holder.fingerprint_2.timestamp, + last_paid_timestamp: from_unix_timestamp( + setup_holder.account_2.pending_payable.timestamp, + ), pending_payable_opt: None, }; assert_eq!(status_1_before_opt, Some(expected_status_before_1)); assert_eq!(status_2_before_opt, Some(expected_status_before_2)); - let resulting_account_1_opt = subject.account_status(&setup_holder.wallet_1); + let resulting_account_1_opt = subject.account_status(&wallet_1); assert_eq!(resulting_account_1_opt, Some(expected_resulting_status_1)); - let resulting_account_2_opt = subject.account_status(&setup_holder.wallet_2); + let resulting_account_2_opt = subject.account_status(&wallet_2); assert_eq!(resulting_account_2_opt, Some(expected_resulting_status_2)) } @@ -1063,22 +1081,20 @@ mod tests { ); let conn = payable_read_only_conn(&home_dir); let conn_wrapped = Box::new(ConnectionWrapperReal::new(conn)); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - let hash = make_tx_hash(12345); - let rowid = 789; - pending_payable_fingerprint.hash = hash; - pending_payable_fingerprint.rowid = rowid; + let mut confirmed_transaction = make_sent_tx(5); + confirmed_transaction.amount_minor = 12345; + let wallet_address = confirmed_transaction.receiver_address; let subject = PayableDaoReal::new(conn_wrapped); - let result = subject.transactions_confirmed(&[pending_payable_fingerprint]); + let result = subject.transactions_confirmed(&[confirmed_transaction]); assert_eq!( result, - Err(PayableDaoError::RusqliteError( + Err(PayableDaoError::RusqliteError(format!( "Error from invalid update command for payable table and change of -12345 wei to \ - 'pending_payable_rowid = 789' with error 'attempt to write a readonly database'" - .to_string() - )) + 'wallet_address = {:?}' with error 'attempt to write a readonly database'", + wallet_address + ))) ) } @@ -1086,26 +1102,21 @@ mod tests { #[should_panic( expected = "Overflow detected with 340282366920938463463374607431768211455: cannot be converted from u128 to i128" )] - fn transaction_confirmed_works_for_overflow_from_amount_stored_in_pending_payable_fingerprint() - { + fn transaction_confirmed_works_for_overflow_from_sent_tx_record() { let home_dir = ensure_node_home_directory_exists( "payable_dao", - "transaction_confirmed_works_for_overflow_from_amount_stored_in_pending_payable_fingerprint", + "transaction_confirmed_works_for_overflow_from_sent_tx_record", ); let subject = PayableDaoReal::new( DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(), ); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - let hash = make_tx_hash(12345); - let rowid = 789; - pending_payable_fingerprint.hash = hash; - pending_payable_fingerprint.rowid = rowid; - pending_payable_fingerprint.amount = u128::MAX; + let mut sent_tx = make_sent_tx(456); + sent_tx.amount_minor = u128::MAX; //The overflow occurs before we start modifying the payable account so we can have the database empty - let _ = subject.transactions_confirmed(&[pending_payable_fingerprint]); + let _ = subject.transactions_confirmed(&[sent_tx]); } #[test] @@ -1117,38 +1128,37 @@ mod tests { let conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let setup_holder = make_fingerprint_pair_and_insert_initial_payable_records( + let setup_holder = insert_initial_payable_records_and_return_sent_txs( conn.as_ref(), - 1_111_111, - 2_222_222, - 111_111, - 222_222, + (1_111_111, 111_111), + (2_222_222, 222_222), ); + let wallet_1 = Wallet::from(setup_holder.account_1.pending_payable.receiver_address); + let wallet_2 = Wallet::from(setup_holder.account_2.pending_payable.receiver_address); conn.prepare("delete from payable where wallet_address = ?") .unwrap() - .execute(&[&setup_holder.wallet_2]) + .execute(&[&wallet_2.to_string()]) .unwrap(); let subject = PayableDaoReal::new(conn); - let expected_account = PayableAccount { - wallet: setup_holder.wallet_1.clone(), - balance_wei: 1_111_111 - setup_holder.fingerprint_1.amount, - last_paid_timestamp: setup_holder.fingerprint_1.timestamp, - pending_payable_opt: None, - }; - let result = subject - .transactions_confirmed(&[setup_holder.fingerprint_1, setup_holder.fingerprint_2]); + let result = subject.transactions_confirmed(&[ + setup_holder.account_1.pending_payable, + setup_holder.account_2.pending_payable, + ]); + let expected_err_msg = format!( + "Expected 1 row to be changed for the unique key \ + {} but got this count: 0", + wallet_2 + ); assert_eq!( result, - Err(PayableDaoError::RusqliteError( - "Expected 1 row to be changed for the unique key 792 but got this count: 0" - .to_string() - )) + Err(PayableDaoError::RusqliteError(expected_err_msg)) ); - let account_1_opt = subject.account_status(&setup_holder.wallet_1); - assert_eq!(account_1_opt, Some(expected_account)); - let account_2_opt = subject.account_status(&setup_holder.wallet_2); + let expected_resulting_balance_1 = 1_111_111 - 111_111; + let account_1 = subject.account_status(&wallet_1).unwrap(); + assert_eq!(account_1.balance_wei, expected_resulting_balance_1); + let account_2_opt = subject.account_status(&wallet_2); assert_eq!(account_2_opt, None); } diff --git a/node/src/accountant/db_access_objects/pending_payable_dao.rs b/node/src/accountant/db_access_objects/pending_payable_dao.rs index e555fcc9a..414c364d8 100644 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ b/node/src/accountant/db_access_objects/pending_payable_dao.rs @@ -5,7 +5,6 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; @@ -36,7 +35,7 @@ pub struct TransactionHashes { pub trait PendingPayableDao { // Note that the order of the returned results is not guaranteed fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes; - fn return_all_errorless_fingerprints(&self) -> Vec; + // fn return_all_errorless_fingerprints(&self) -> Vec; fn insert_new_fingerprints( &self, hashes_and_amounts: &[HashAndAmount], @@ -87,42 +86,42 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { } } - fn return_all_errorless_fingerprints(&self) -> Vec { - let mut stm = self - .conn - .prepare( - "select rowid, transaction_hash, amount_high_b, amount_low_b, \ - payable_timestamp, attempt from pending_payable where process_error is null", - ) - .expect("Internal error"); - stm.query_map([], |row| { - let rowid: u64 = Self::get_with_expect(row, 0); - let transaction_hash: String = Self::get_with_expect(row, 1); - let amount_high_bytes: i64 = Self::get_with_expect(row, 2); - let amount_low_bytes: i64 = Self::get_with_expect(row, 3); - let timestamp: i64 = Self::get_with_expect(row, 4); - let attempt: u16 = Self::get_with_expect(row, 5); - Ok(PendingPayableFingerprint { - rowid, - timestamp: from_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { - panic!( - "Invalid hash format (\"{}\": {:?}) - database corrupt", - transaction_hash, e - ) - }), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_bytes, - amount_low_bytes, - )), - process_error: None, - }) - }) - .expect("rusqlite failure") - .vigilant_flatten() - .collect() - } + // fn return_all_errorless_fingerprints(&self) -> Vec { + // let mut stm = self + // .conn + // .prepare( + // "select rowid, transaction_hash, amount_high_b, amount_low_b, \ + // payable_timestamp, attempt from pending_payable where process_error is null", + // ) + // .expect("Internal error"); + // stm.query_map([], |row| { + // let rowid: u64 = Self::get_with_expect(row, 0); + // let transaction_hash: String = Self::get_with_expect(row, 1); + // let amount_high_bytes: i64 = Self::get_with_expect(row, 2); + // let amount_low_bytes: i64 = Self::get_with_expect(row, 3); + // let timestamp: i64 = Self::get_with_expect(row, 4); + // let attempt: u16 = Self::get_with_expect(row, 5); + // Ok(SentTx { + // rowid, + // timestamp: from_unix_timestamp(timestamp), + // hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { + // panic!( + // "Invalid hash format (\"{}\": {:?}) - database corrupt", + // transaction_hash, e + // ) + // }), + // attempt, + // amount_minor: checked_conversion::(BigIntDivider::reconstitute( + // amount_high_bytes, + // amount_low_bytes, + // )), + // process_error: None, + // }) + // }) + // .expect("rusqlite failure") + // .vigilant_flatten() + // .collect() + // } fn insert_new_fingerprints( &self, @@ -179,7 +178,7 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { { Ok(x) if x == ids.len() => Ok(()), Ok(num) => panic!( - "deleting fingerprint, expected {} rows to be changed, but the actual number is {}", + "deleting sent tx record, expected {} rows to be changed, but the actual number is {}", ids.len(), num ), @@ -225,21 +224,6 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PendingPayable { - pub recipient_wallet: Wallet, - pub hash: H256, -} - -impl PendingPayable { - pub fn new(recipient_wallet: Wallet, hash: H256) -> Self { - Self { - recipient_wallet, - hash, - } - } -} - #[derive(Debug)] pub struct PendingPayableDaoReal<'a> { conn: Box, @@ -272,12 +256,11 @@ impl PendingPayableDaoFactory for DaoFactoryReal { #[cfg(test)] mod tests { use crate::accountant::checked_conversion; - use crate::accountant::db_access_objects::pending_payable_dao::{ + use crate::accountant::db_access_objects::sent_payable_dao::{ PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, }; use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::test_utils::make_tx_hash; use crate::database::db_initializer::{ @@ -291,660 +274,660 @@ mod tests { use std::time::SystemTime; use web3::types::H256; - #[test] - fn insert_new_fingerprints_happy_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "insert_new_fingerprints_happy_path", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(4546); - let amount_1 = 55556; - let hash_2 = make_tx_hash(6789); - let amount_2 = 44445; - let batch_wide_timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - let _ = subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - - let records = subject.return_all_errorless_fingerprints(); - assert_eq!( - records, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_1.hash, - attempt: 1, - amount: hash_and_amount_1.amount, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_2.hash, - attempt: 1, - amount: hash_and_amount_2.amount, - process_error: None - } - ] - ) - } - - #[test] - fn insert_new_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "insert_new_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let hash = make_tx_hash(45466); - let amount = 55556; - let timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { hash, amount }; - - let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); - - assert_eq!( - result, - Err(PendingPayableDaoError::InsertionFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic(expected = "expected 1 changed rows but got 0")] - fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { - let setup_conn = Connection::open_in_memory().unwrap(); - // injecting a by-plan failing statement into the mocked connection in order to provoke - // a reaction that would've been untestable directly on the table the act is closely coupled with - let statement = { - setup_conn - .execute("create table example (id integer)", []) - .unwrap(); - setup_conn.prepare("select id from example").unwrap() - }; - let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); - let hash_1 = make_tx_hash(4546); - let amount_1 = 55556; - let batch_wide_timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - - let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); - } - - #[test] - fn fingerprints_rowids_when_records_reachable() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_records_reachable", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_unix_timestamp(195_000_000); - // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, - // then complain about its excessive size if supplied in unquoted strings - let hash_1 = - H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") - .unwrap(); - let hash_2 = - H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") - .unwrap(); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 4567, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 6789, - }; - let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; - { - subject - .insert_new_fingerprints(&fingerprints_init_input, timestamp) - .unwrap(); - } - - let result = subject.fingerprints_rowids(&[hash_1, hash_2]); - - let first_expected_pair = &(1, hash_1); - assert!( - result.rowid_results.contains(first_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - first_expected_pair, - result.rowid_results - ); - let second_expected_pair = &(2, hash_2); - assert!( - result.rowid_results.contains(second_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - second_expected_pair, - result.rowid_results - ); - assert_eq!(result.rowid_results.len(), 2); - } - - #[test] - fn fingerprints_rowids_when_nonexistent_records() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_nonexistent_records", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let hash_1 = make_tx_hash(11119); - let hash_2 = make_tx_hash(22229); - let hash_3 = make_tx_hash(33339); - let hash_4 = make_tx_hash(44449); - // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for - // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of - // just an ambiguous 1 - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_2, - amount: 8901234, - }], - SystemTime::now(), - ) - .unwrap(); - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_3, - amount: 1234567, - }], - SystemTime::now(), - ) - .unwrap(); - subject.delete_fingerprints(&[1]).unwrap(); - - let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); - - assert_eq!(result.rowid_results, vec![(2, hash_3),]); - assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); - } - - #[test] - fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let batch_wide_timestamp = from_unix_timestamp(195_000_000); - let hash_1 = make_tx_hash(11119); - let amount_1 = 787; - let hash_2 = make_tx_hash(10000); - let amount_2 = 333; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: None - } - ] - ) - } - - #[test] - fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_unix_timestamp(198_000_000); - let hash = make_tx_hash(10000); - let amount = 333; - let hash_and_amount_1 = HashAndAmount { - hash: make_tx_hash(11119), - amount: 2000, - }; - let hash_and_amount_2 = HashAndAmount { hash, amount }; - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - subject.mark_failures(&[1]).unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![PendingPayableFingerprint { - rowid: 2, - timestamp, - hash, - attempt: 1, - amount, - process_error: None - }] - ) - } - - #[test] - #[should_panic( - expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" - )] - fn return_all_errorless_fingerprints_panics_on_malformed_hash() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_panics_on_malformed_hash", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - wrapped_conn - .prepare("insert into pending_payable \ - (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ - values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") - .unwrap() - .execute([]) - .unwrap(); - } - let subject = PendingPayableDaoReal::new(wrapped_conn); - - let _ = subject.return_all_errorless_fingerprints(); - } - - #[test] - fn delete_fingerprints_happy_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_happy_path", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[ - HashAndAmount { - hash: make_tx_hash(1234), - amount: 1111, - }, - HashAndAmount { - hash: make_tx_hash(2345), - amount: 5555, - }, - HashAndAmount { - hash: make_tx_hash(3456), - amount: 2222, - }, - ], - SystemTime::now(), - ) - .unwrap(); - } - - let result = subject.delete_fingerprints(&[2, 3]); - - assert_eq!(result, Ok(())); - let records_in_the_db = subject.return_all_errorless_fingerprints(); - let record_left_in = &records_in_the_db[0]; - assert_eq!(record_left_in.hash, make_tx_hash(1234)); - assert_eq!(record_left_in.rowid, 1); - assert_eq!(records_in_the_db.len(), 1); - } - - #[test] - fn delete_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let rowid = 45; - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.delete_fingerprints(&[rowid]); - - assert_eq!( - result, - Err(PendingPayableDaoError::RecordDeletion( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "deleting fingerprint, expected 2 rows to be changed, but the actual number is 1" - )] - fn delete_fingerprints_changed_different_number_of_rows_than_expected() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_changed_different_number_of_rows_than_expected", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let rowid_1 = 1; - let rowid_2 = 2; - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: make_tx_hash(666666), - amount: 5555, - }], - SystemTime::now(), - ) - .unwrap(); - } - - let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); - } - - #[test] - fn increment_scan_attempts_works() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(345); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(567); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 1122, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 2233, - }; - let hash_and_amount_3 = HashAndAmount { - hash: hash_3, - amount: 3344, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], - timestamp, - ) - .unwrap(); - } - - let result = subject.increment_scan_attempts(&[2, 3]); - - assert_eq!(result, Ok(())); - let mut all_records = subject.return_all_errorless_fingerprints(); - assert_eq!(all_records.len(), 3); - let record_1 = all_records.remove(0); - assert_eq!(record_1.hash, hash_1); - assert_eq!(record_1.attempt, 1); - let record_2 = all_records.remove(0); - assert_eq!(record_2.hash, hash_2); - assert_eq!(record_2.attempt, 2); - let record_3 = all_records.remove(0); - assert_eq!(record_3.hash, hash_3); - assert_eq!(record_3.attempt, 2); - } - - #[test] - fn increment_scan_attempts_works_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.increment_scan_attempts(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::UpdateFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" - )] - fn increment_scan_attempts_panics_on_unexpected_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_panics_on_unexpected_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.increment_scan_attempts(&[1, 2]); - } - - #[test] - fn mark_failures_works() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_works"); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(555); - let amount_1 = 1234; - let hash_2 = make_tx_hash(666); - let amount_2 = 2345; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - } - - let result = subject.mark_failures(&[2]); - - assert_eq!(result, Ok(())); - let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); - let mut assert_stm = assert_conn - .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") - .unwrap(); - let found_fingerprints = assert_stm - .query_map([], |row| { - let rowid: u64 = row.get(0).unwrap(); - let transaction_hash: String = row.get(1).unwrap(); - let amount_high_b: i64 = row.get(2).unwrap(); - let amount_low_b: i64 = row.get(3).unwrap(); - let timestamp: i64 = row.get(4).unwrap(); - let attempt: u16 = row.get(5).unwrap(); - let process_error: Option = row.get(6).unwrap(); - Ok(PendingPayableFingerprint { - rowid, - timestamp: from_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap(), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_b, - amount_low_b, - )), - process_error, - }) - }) - .unwrap() - .flatten() - .collect::>(); - assert_eq!( - *found_fingerprints, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: Some("ERROR".to_string()) - } - ] - ) - } - - #[test] - fn mark_failures_sad_path() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_sad_path"); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.mark_failures(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::ErrorMarkFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" - )] - fn mark_failures_panics_on_wrong_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "mark_failures_panics_on_wrong_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.mark_failures(&[10, 20]); - } + // #[test] + // fn insert_new_fingerprints_happy_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "insert_new_fingerprints_happy_path", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(4546); + // let amount_1 = 55556; + // let hash_2 = make_tx_hash(6789); + // let amount_2 = 44445; + // let batch_wide_timestamp = from_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // + // let _ = subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2], + // batch_wide_timestamp, + // ) + // .unwrap(); + // + // let records = subject.return_all_errorless_fingerprints(); + // assert_eq!( + // records, + // vec![ + // SentTx { + // rowid: 1, + // timestamp: batch_wide_timestamp, + // hash: hash_and_amount_1.hash, + // attempt: 1, + // amount_minor: hash_and_amount_1.amount, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp: batch_wide_timestamp, + // hash: hash_and_amount_2.hash, + // attempt: 1, + // amount_minor: hash_and_amount_2.amount, + // process_error: None + // } + // ] + // ) + // } + // + // #[test] + // fn insert_new_fingerprints_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "insert_new_fingerprints_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let hash = make_tx_hash(45466); + // let amount = 55556; + // let timestamp = from_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // let hash_and_amount = HashAndAmount { hash, amount }; + // + // let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::InsertionFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic(expected = "expected 1 changed rows but got 0")] + // fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { + // let setup_conn = Connection::open_in_memory().unwrap(); + // // injecting a by-plan failing statement into the mocked connection in order to provoke + // // a reaction that would've been untestable directly on the table the act is closely coupled with + // let statement = { + // setup_conn + // .execute("create table example (id integer)", []) + // .unwrap(); + // setup_conn.prepare("select id from example").unwrap() + // }; + // let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); + // let hash_1 = make_tx_hash(4546); + // let amount_1 = 55556; + // let batch_wide_timestamp = from_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // let hash_and_amount = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // + // let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); + // } + // + // #[test] + // fn fingerprints_rowids_when_records_reachable() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "fingerprints_rowids_when_records_reachable", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let timestamp = from_unix_timestamp(195_000_000); + // // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, + // // then complain about its excessive size if supplied in unquoted strings + // let hash_1 = + // H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") + // .unwrap(); + // let hash_2 = + // H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") + // .unwrap(); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: 4567, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: 6789, + // }; + // let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; + // { + // subject + // .insert_new_fingerprints(&fingerprints_init_input, timestamp) + // .unwrap(); + // } + // + // let result = subject.fingerprints_rowids(&[hash_1, hash_2]); + // + // let first_expected_pair = &(1, hash_1); + // assert!( + // result.rowid_results.contains(first_expected_pair), + // "Returned rowid pairs should have contained {:?} but all it did is {:?}", + // first_expected_pair, + // result.rowid_results + // ); + // let second_expected_pair = &(2, hash_2); + // assert!( + // result.rowid_results.contains(second_expected_pair), + // "Returned rowid pairs should have contained {:?} but all it did is {:?}", + // second_expected_pair, + // result.rowid_results + // ); + // assert_eq!(result.rowid_results.len(), 2); + // } + // + // #[test] + // fn fingerprints_rowids_when_nonexistent_records() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "fingerprints_rowids_when_nonexistent_records", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let hash_1 = make_tx_hash(11119); + // let hash_2 = make_tx_hash(22229); + // let hash_3 = make_tx_hash(33339); + // let hash_4 = make_tx_hash(44449); + // // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for + // // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of + // // just an ambiguous 1 + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: hash_2, + // amount_minor: 8901234, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: hash_3, + // amount_minor: 1234567, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // subject.delete_fingerprints(&[1]).unwrap(); + // + // let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); + // + // assert_eq!(result.rowid_results, vec![(2, hash_3),]); + // assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); + // } + // + // #[test] + // fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let batch_wide_timestamp = from_unix_timestamp(195_000_000); + // let hash_1 = make_tx_hash(11119); + // let amount_1 = 787; + // let hash_2 = make_tx_hash(10000); + // let amount_2 = 333; + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // + // { + // subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2], + // batch_wide_timestamp, + // ) + // .unwrap(); + // } + // + // let result = subject.return_all_errorless_fingerprints(); + // + // assert_eq!( + // result, + // vec![ + // SentTx { + // rowid: 1, + // timestamp: batch_wide_timestamp, + // hash: hash_1, + // attempt: 1, + // amount_minor: amount_1, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp: batch_wide_timestamp, + // hash: hash_2, + // attempt: 1, + // amount_minor: amount_2, + // process_error: None + // } + // ] + // ) + // } + // + // #[test] + // fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let timestamp = from_unix_timestamp(198_000_000); + // let hash = make_tx_hash(10000); + // let amount = 333; + // let hash_and_amount_1 = HashAndAmount { + // hash: make_tx_hash(11119), + // amount_minor: 2000, + // }; + // let hash_and_amount_2 = HashAndAmount { hash, amount }; + // { + // subject + // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) + // .unwrap(); + // subject.mark_failures(&[1]).unwrap(); + // } + // + // let result = subject.return_all_errorless_fingerprints(); + // + // assert_eq!( + // result, + // vec![SentTx { + // rowid: 2, + // timestamp, + // hash, + // attempt: 1, + // amount, + // process_error: None + // }] + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" + // )] + // fn return_all_errorless_fingerprints_panics_on_malformed_hash() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_panics_on_malformed_hash", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // { + // wrapped_conn + // .prepare("insert into pending_payable \ + // (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ + // values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") + // .unwrap() + // .execute([]) + // .unwrap(); + // } + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // + // let _ = subject.return_all_errorless_fingerprints(); + // } + // + // #[test] + // fn delete_fingerprints_happy_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_happy_path", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[ + // HashAndAmount { + // hash: make_tx_hash(1234), + // amount_minor: 1111, + // }, + // HashAndAmount { + // hash: make_tx_hash(2345), + // amount_minor: 5555, + // }, + // HashAndAmount { + // hash: make_tx_hash(3456), + // amount_minor: 2222, + // }, + // ], + // SystemTime::now(), + // ) + // .unwrap(); + // } + // + // let result = subject.delete_fingerprints(&[2, 3]); + // + // assert_eq!(result, Ok(())); + // let records_in_the_db = subject.return_all_errorless_fingerprints(); + // let record_left_in = &records_in_the_db[0]; + // assert_eq!(record_left_in.hash, make_tx_hash(1234)); + // assert_eq!(record_left_in.rowid, 1); + // assert_eq!(records_in_the_db.len(), 1); + // } + // + // #[test] + // fn delete_fingerprints_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let rowid = 45; + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.delete_fingerprints(&[rowid]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::RecordDeletion( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "deleting sent tx record, expected 2 rows to be changed, but the actual number is 1" + // )] + // fn delete_fingerprints_changed_different_number_of_rows_than_expected() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_changed_different_number_of_rows_than_expected", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let rowid_1 = 1; + // let rowid_2 = 2; + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: make_tx_hash(666666), + // amount_minor: 5555, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // } + // + // let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); + // } + // + // #[test] + // fn increment_scan_attempts_works() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_works", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(345); + // let hash_2 = make_tx_hash(456); + // let hash_3 = make_tx_hash(567); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: 1122, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: 2233, + // }; + // let hash_and_amount_3 = HashAndAmount { + // hash: hash_3, + // amount_minor: 3344, + // }; + // let timestamp = from_unix_timestamp(190_000_000); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], + // timestamp, + // ) + // .unwrap(); + // } + // + // let result = subject.increment_scan_attempts(&[2, 3]); + // + // assert_eq!(result, Ok(())); + // let mut all_records = subject.return_all_errorless_fingerprints(); + // assert_eq!(all_records.len(), 3); + // let record_1 = all_records.remove(0); + // assert_eq!(record_1.hash, hash_1); + // assert_eq!(record_1.attempt, 1); + // let record_2 = all_records.remove(0); + // assert_eq!(record_2.hash, hash_2); + // assert_eq!(record_2.attempt, 2); + // let record_3 = all_records.remove(0); + // assert_eq!(record_3.hash, hash_3); + // assert_eq!(record_3.attempt, 2); + // } + // + // #[test] + // fn increment_scan_attempts_works_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_works_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.increment_scan_attempts(&[1]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::UpdateFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" + // )] + // fn increment_scan_attempts_panics_on_unexpected_row_change_count() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_panics_on_unexpected_row_change_count", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // + // let _ = subject.increment_scan_attempts(&[1, 2]); + // } + // + // #[test] + // fn mark_failures_works() { + // let home_dir = + // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_works"); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(555); + // let amount_1 = 1234; + // let hash_2 = make_tx_hash(666); + // let amount_2 = 2345; + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // let timestamp = from_unix_timestamp(190_000_000); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) + // .unwrap(); + // } + // + // let result = subject.mark_failures(&[2]); + // + // assert_eq!(result, Ok(())); + // let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); + // let mut assert_stm = assert_conn + // .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") + // .unwrap(); + // let found_fingerprints = assert_stm + // .query_map([], |row| { + // let rowid: u64 = row.get(0).unwrap(); + // let transaction_hash: String = row.get(1).unwrap(); + // let amount_high_b: i64 = row.get(2).unwrap(); + // let amount_low_b: i64 = row.get(3).unwrap(); + // let timestamp: i64 = row.get(4).unwrap(); + // let attempt: u16 = row.get(5).unwrap(); + // let process_error: Option = row.get(6).unwrap(); + // Ok(SentTx { + // rowid, + // timestamp: from_unix_timestamp(timestamp), + // hash: H256::from_str(&transaction_hash[2..]).unwrap(), + // attempt, + // amount_minor: checked_conversion::(BigIntDivider::reconstitute( + // amount_high_b, + // amount_low_b, + // )), + // process_error, + // }) + // }) + // .unwrap() + // .flatten() + // .collect::>(); + // assert_eq!( + // *found_fingerprints, + // vec![ + // SentTx { + // rowid: 1, + // timestamp, + // hash: hash_1, + // attempt: 1, + // amount_minor: amount_1, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp, + // hash: hash_2, + // attempt: 1, + // amount_minor: amount_2, + // process_error: Some("ERROR".to_string()) + // } + // ] + // ) + // } + // + // #[test] + // fn mark_failures_sad_path() { + // let home_dir = + // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_sad_path"); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.mark_failures(&[1]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::ErrorMarkFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" + // )] + // fn mark_failures_panics_on_wrong_row_change_count() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "mark_failures_panics_on_wrong_row_change_count", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // + // let _ = subject.mark_failures(&[10, 20]); + // } } diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index ad8f52462..9d100c633 100644 --- a/node/src/accountant/db_access_objects/receivable_dao.rs +++ b/node/src/accountant/db_access_objects/receivable_dao.rs @@ -55,7 +55,7 @@ pub trait ReceivableDao { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError>; fn more_money_received( @@ -112,7 +112,7 @@ impl ReceivableDao for ReceivableDaoReal { &self, timestamp: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError> { let main_sql = "insert into receivable (wallet_address, balance_high_b, balance_low_b, last_received_timestamp) values \ (:wallet, :balance_high_b, :balance_low_b, :last_received_timestamp) on conflict (wallet_address) do update set \ @@ -125,7 +125,7 @@ impl ReceivableDao for ReceivableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( diff --git a/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs new file mode 100644 index 000000000..26c7dd5fe --- /dev/null +++ b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; + +impl From<(FailedTx, TxBlock)> for SentTx { + fn from((failed_tx, confirmation_block): (FailedTx, TxBlock)) -> Self { + SentTx { + hash: failed_tx.hash, + receiver_address: failed_tx.receiver_address, + amount_minor: failed_tx.amount_minor, + timestamp: failed_tx.timestamp, + gas_price_minor: failed_tx.gas_price_minor, + nonce: failed_tx.nonce, + status: TxStatus::Confirmed { + block_hash: format!("{:?}", confirmation_block.block_hash), + block_number: confirmation_block.block_number.as_u64(), + detection: Detection::Reclaim, + }, + } + } +} + +impl From<(SentTx, FailureReason)> for FailedTx { + fn from((sent_tx, failure_reason): (SentTx, FailureReason)) -> Self { + FailedTx { + hash: sent_tx.hash, + receiver_address: sent_tx.receiver_address, + amount_minor: sent_tx.amount_minor, + timestamp: sent_tx.timestamp, + gas_price_minor: sent_tx.gas_price_minor, + nonce: sent_tx.nonce, + reason: failure_reason, + status: FailureStatus::RetryRequired, + } + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; + use crate::accountant::db_access_objects::utils::to_unix_timestamp; + use crate::accountant::gwei_to_wei; + use crate::accountant::test_utils::make_transaction_block; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; + use crate::blockchain::errors::validation_status::ValidationStatus; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::make_wallet; + use std::time::{Duration, SystemTime}; + + #[test] + fn sent_tx_record_can_be_converted_from_failed_tx_record() { + let failed_tx = FailedTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + }; + let tx_block = make_transaction_block(789); + + let result = SentTx::from((failed_tx.clone(), tx_block)); + + assert_eq!( + result, + SentTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + status: TxStatus::Confirmed { + block_hash: + "0x000000000000000000000000000000000000000000000000000000003b9acd15" + .to_string(), + block_number: 491169069, + detection: Detection::Reclaim, + }, + } + ); + } + + #[test] + fn conversion_from_sent_tx_and_failure_reason_to_failed_tx_works() { + let sent_tx = SentTx { + hash: make_tx_hash(789), + receiver_address: make_wallet("receiver").address(), + amount_minor: 123_456_789, + timestamp: to_unix_timestamp( + SystemTime::now() + .checked_sub(Duration::from_secs(10_000)) + .unwrap(), + ), + gas_price_minor: gwei_to_wei(424_u64), + nonce: 456_u64.into(), + status: TxStatus::Pending(ValidationStatus::Waiting), + }; + + let result_1 = FailedTx::from((sent_tx.clone(), FailureReason::Reverted)); + let result_2 = FailedTx::from(( + sent_tx.clone(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + )); + + assert_conversion_into_failed_tx(result_1, sent_tx.clone(), FailureReason::Reverted); + assert_conversion_into_failed_tx( + result_2, + sent_tx, + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + ); + } + + fn assert_conversion_into_failed_tx( + result: FailedTx, + original_sent_tx: SentTx, + expected_failure_reason: FailureReason, + ) { + assert_eq!(result.hash, original_sent_tx.hash); + assert_eq!(result.receiver_address, original_sent_tx.receiver_address); + assert_eq!(result.amount_minor, original_sent_tx.amount_minor); + assert_eq!(result.timestamp, original_sent_tx.timestamp); + assert_eq!(result.gas_price_minor, original_sent_tx.gas_price_minor); + assert_eq!(result.nonce, original_sent_tx.nonce); + assert_eq!(result.status, FailureStatus::RetryRequired); + assert_eq!(result.reason, expected_failure_reason); + } +} diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs index 09e293edf..a82bafdce 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -1,19 +1,21 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use std::collections::{HashMap, HashSet}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use ethereum_types::{H256}; -use web3::types::Address; -use masq_lib::utils::ExpectValue; -use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers}; +use crate::accountant::db_access_objects::utils::{ + DaoFactoryReal, TxHash, TxIdentifiers, TxRecordWithHash, +}; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; +use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; +use ethereum_types::H256; use itertools::Itertools; +use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; -use crate::accountant::db_access_objects::failed_payable_dao::ValidationStatus; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use web3::types::Address; #[derive(Debug, PartialEq, Eq)] pub enum SentPayableDaoError { @@ -25,16 +27,22 @@ pub enum SentPayableDaoError { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Tx { +pub struct SentTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub status: TxStatus, } +impl TxRecordWithHash for SentTx { + fn hash(&self) -> TxHash { + self.hash + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TxStatus { Pending(ValidationStatus), @@ -69,26 +77,21 @@ pub enum Detection { Reclaim, } -impl From<&TxConfirmation> for TxStatus { - fn from(tx_confirmation: &TxConfirmation) -> Self { +impl From for TxStatus { + fn from(tx_block: TxBlock) -> Self { TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number) - .expect("block number too big"), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).expect("block number too big"), + detection: Detection::Normal, } } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TxConfirmation { - block_info: TransactionBlock, - detection: Detection, -} - +#[derive(Debug, PartialEq, Eq)] pub enum RetrieveCondition { IsPending, ByHash(Vec), + ByNonce(Vec), } impl Display for RetrieveCondition { @@ -104,19 +107,29 @@ impl Display for RetrieveCondition { comma_joined_stringifiable(tx_hashes, |hash| format!("'{:?}'", hash)) ) } + RetrieveCondition::ByNonce(nonces) => { + write!( + f, + "WHERE nonce IN ({})", + comma_joined_stringifiable(nonces, |nonce| nonce.to_string()) + ) + } } } } pub trait SentPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; - fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError>; - fn retrieve_txs(&self, condition: Option) -> Vec; - fn confirm_tx( + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> Vec; + //TODO potentially atomically + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError>; + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError>; + fn update_statuses( &self, - hash_map: &HashMap, + hash_map: &HashMap, ) -> Result<(), SentPayableDaoError>; - fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; } @@ -156,7 +169,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError> { + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { if txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } @@ -190,8 +203,8 @@ impl SentPayableDao for SentPayableDaoReal<'_> { status \ ) VALUES {}", comma_joined_stringifiable(txs, |tx| { - let amount_checked = checked_conversion::(tx.amount); - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let amount_checked = checked_conversion::(tx.amount_minor); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); let (gas_price_wei_high_b, gas_price_wei_low_b) = BigIntDivider::deconstruct(gas_price_wei_checked); @@ -226,7 +239,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } - fn retrieve_txs(&self, condition_opt: Option) -> Vec { + fn retrieve_txs(&self, condition_opt: Option) -> Vec { let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, status FROM sent_payable" .to_string(); @@ -248,22 +261,22 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Address::from_str(&receiver_address_str[2..]).expect("Failed to parse H160"); let amount_high_b = row.get(2).expectv("amount_high_b"); let amount_low_b = row.get(3).expectv("amount_low_b"); - let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let amount_minor = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; let timestamp = row.get(4).expectv("timestamp"); let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); - let gas_price_wei = + let gas_price_minor = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); let status_str: String = row.get(8).expectv("status"); let status = TxStatus::from_str(&status_str).expect("Failed to parse TxStatus"); - Ok(Tx { + Ok(SentTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, status, }) @@ -273,18 +286,15 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn confirm_tx( - &self, - hash_map: &HashMap, - ) -> Result<(), SentPayableDaoError> { + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { if hash_map.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - for (hash, tx_confirmation) in hash_map { + for (hash, tx_block) in hash_map { let sql = format!( "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", - TxStatus::from(tx_confirmation), + TxStatus::from(*tx_block), hash ); @@ -308,12 +318,12 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Ok(()) } - fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError> { + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { if new_txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - let build_case = |value_fn: fn(&Tx) -> String| { + let build_case = |value_fn: fn(&SentTx) -> String| { new_txs .iter() .map(|tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx))) @@ -323,23 +333,23 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let tx_hash_cases = build_case(|tx| format!("'{:?}'", tx.hash)); let receiver_address_cases = build_case(|tx| format!("'{:?}'", tx.receiver_address)); let amount_high_b_cases = build_case(|tx| { - let amount_checked = checked_conversion::(tx.amount); + let amount_checked = checked_conversion::(tx.amount_minor); let (high, _) = BigIntDivider::deconstruct(amount_checked); high.to_string() }); let amount_low_b_cases = build_case(|tx| { - let amount_checked = checked_conversion::(tx.amount); + let amount_checked = checked_conversion::(tx.amount_minor); let (_, low) = BigIntDivider::deconstruct(amount_checked); low.to_string() }); let timestamp_cases = build_case(|tx| tx.timestamp.to_string()); let gas_price_wei_high_b_cases = build_case(|tx| { - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (high, _) = BigIntDivider::deconstruct(gas_price_wei_checked); high.to_string() }); let gas_price_wei_low_b_cases = build_case(|tx| { - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); low.to_string() }); @@ -391,6 +401,47 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } + fn update_statuses( + &self, + status_updates: &HashMap, + ) -> Result<(), SentPayableDaoError> { + if status_updates.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let case_statements = status_updates + .iter() + .map(|(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status)) + .join(" "); + let tx_hashes = comma_joined_stringifiable(&status_updates.keys().collect_vec(), |hash| { + format!("'{:?}'", hash) + }); + + let sql = format!( + "UPDATE sent_payable \ + SET \ + status = CASE \ + {case_statements} \ + END \ + WHERE tx_hash IN ({tx_hashes})" + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(rows_changed) => { + if rows_changed == status_updates.len() { + Ok(()) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} of {} records had their status updated.", + rows_changed, + status_updates.len(), + ))) + } + } + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { if hashes.is_empty() { return Err(SentPayableDaoError::EmptyInput); @@ -421,30 +472,54 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } +pub trait SentPayableDaoFactory { + fn make(&self) -> Box; +} + +impl SentPayableDaoFactory for DaoFactoryReal { + fn make(&self) -> Box { + Box::new(SentPayableDaoReal::new(self.make_connection())) + } +} + #[cfg(test)] mod tests { - use std::collections::{HashMap, HashSet}; - use std::ops::Add; - use std::str::FromStr; - use std::sync::{Arc, Mutex}; - use std::time::{Duration, UNIX_EPOCH}; - use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, TxConfirmation, TxStatus}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ + ByHash, ByNonce, IsPending, + }; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{ + EmptyInput, PartialExecution, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, + TxStatus, + }; + use crate::accountant::db_access_objects::test_utils::{ + make_read_only_db_connection, TxBuilder, + }; + use crate::accountant::db_access_objects::utils::TxRecordWithHash; + use crate::accountant::test_utils::make_sent_tx; + use crate::blockchain::blockchain_interface::data_structures::TxBlock; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{ + make_block_hash, make_tx_hash, ValidationFailureClockMock, + }; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; use crate::database::test_utils::ConnectionWrapperMock; - use ethereum_types::{ H256, U64}; + use ethereum_types::{H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; - use rusqlite::{Connection}; - use crate::accountant::db_access_objects::failed_payable_dao::{ValidationStatus}; - use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending}; - use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; - use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; - use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, RemoteErrorKind}; - use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; - use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; + use rusqlite::Connection; + use std::collections::{HashMap, HashSet}; + use std::ops::{Add, Sub}; + use std::str::FromStr; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] fn insert_new_records_works() { @@ -530,15 +605,15 @@ mod tests { result, Err(SentPayableDaoError::InvalidInput( "Duplicate hashes found in the input. Input Transactions: \ - [Tx { \ + [SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ nonce: 0, status: Pending(Waiting) }, \ - Tx { \ + SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204020, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204020, gas_price_minor: 0, \ nonce: 0, status: Confirmed { block_hash: \ \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ block_number: 7890123, detection: Reclaim } }]" @@ -657,8 +732,8 @@ mod tests { '0x0000000000000000000000000000000000000000000000000000000123456789', \ '0x0000000000000000000000000000000000000000000000000000000987654321'\ )" - .to_string() ); + assert_eq!(ByNonce(vec![45, 47]).to_string(), "WHERE nonce IN (45, 47)") } #[test] @@ -742,6 +817,35 @@ mod tests { assert_eq!(result, vec![tx1, tx3]); } + #[test] + fn tx_can_be_retrieved_by_nonce() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_nonce"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(make_tx_hash(123)) + .nonce(33) + .build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(456)) + .nonce(34) + .build(); + let tx3 = TxBuilder::default() + .hash(make_tx_hash(789)) + .nonce(35) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(ByNonce(vec![33, 35]))); + + assert_eq!(result, vec![tx1, tx3]); + } + #[test] fn confirm_tx_works() { let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "confirm_tx_works"); @@ -757,26 +861,20 @@ mod tests { let updated_pre_assert_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); let pre_assert_status_tx1 = updated_pre_assert_txs[0].status.clone(); let pre_assert_status_tx2 = updated_pre_assert_txs[1].status.clone(); - let tx_confirmation_1 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(3), - block_number: U64::from(1), - }, - detection: Detection::Normal, + let confirmed_tx_block_1 = TxBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), }; - let tx_confirmation_2 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(4), - block_number: U64::from(2), - }, - detection: Detection::Reclaim, + let confirmed_tx_block_2 = TxBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), }; let hash_map = HashMap::from([ - (tx1.hash, tx_confirmation_1.clone()), - (tx2.hash, tx_confirmation_2.clone()), + (tx1.hash, confirmed_tx_block_1.clone()), + (tx2.hash, confirmed_tx_block_2.clone()), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); assert_eq!(result, Ok(())); @@ -787,9 +885,9 @@ mod tests { assert_eq!( updated_txs[0].status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_1.block_info.block_hash), - block_number: tx_confirmation_1.block_info.block_number.as_u64(), - detection: tx_confirmation_1.detection + block_hash: format!("{:?}", confirmed_tx_block_1.block_hash), + block_number: confirmed_tx_block_1.block_number.as_u64(), + detection: Detection::Normal } ); assert_eq!( @@ -799,9 +897,9 @@ mod tests { assert_eq!( updated_txs[1].status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_2.block_info.block_hash), - block_number: tx_confirmation_2.block_info.block_number.as_u64(), - detection: tx_confirmation_2.detection + block_hash: format!("{:?}", confirmed_tx_block_2.block_hash), + block_number: confirmed_tx_block_2.block_number.as_u64(), + detection: Detection::Normal } ); } @@ -821,7 +919,7 @@ mod tests { subject.insert_new_records(&vec![tx]).unwrap(); let hash_map = HashMap::new(); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); } @@ -843,27 +941,21 @@ mod tests { let hash_map = HashMap::from([ ( existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), }, ), ( non_existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(2), - block_number: U64::from(2), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), }, ), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -885,16 +977,13 @@ mod tests { let hash = make_tx_hash(1); let hash_map = HashMap::from([( hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::default(), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), }, )]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -1007,6 +1096,146 @@ mod tests { ) } + #[test] + fn update_statuses_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "update_statuses_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let timestamp_a = SystemTime::now().sub(Duration::from_millis(11)); + let timestamp_b = SystemTime::now().sub(Duration::from_millis(1234)); + let subject = SentPayableDaoReal::new(wrapped_conn); + let mut tx1 = make_sent_tx(456); + tx1.status = TxStatus::Pending(ValidationStatus::Waiting); + let mut tx2 = make_sent_tx(789); + tx2.status = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ))); + let mut tx3 = make_sent_tx(123); + tx3.status = TxStatus::Pending(ValidationStatus::Waiting); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone()]) + .unwrap(); + let hashmap = HashMap::from([ + ( + tx1.hash, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ))), + ), + ( + tx2.hash, + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + ( + tx3.hash, + TxStatus::Confirmed { + block_hash: + "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + }, + ), + ]); + + let result = subject.update_statuses(&hashmap); + + let updated_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!( + updated_txs[0].status, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a) + ))) + ); + assert_eq!( + updated_txs[1].status, + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable + )), + &ValidationFailureClockMock::default().now_result(timestamp_b) + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable + )), + &ValidationFailureClockReal::default() + ) + )) + ); + assert_eq!( + updated_txs[2].status, + TxStatus::Confirmed { + block_hash: "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + } + ); + assert_eq!(updated_txs.len(), 3) + } + + #[test] + fn update_statuses_handles_empty_input_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_statuses_handles_empty_input_error", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.update_statuses(&HashMap::new()); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn update_statuses_handles_sql_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_statuses_handles_sql_error", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.update_statuses(&HashMap::from([( + make_tx_hash(1), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockReal::default(), + ))), + )])); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ); + } + #[test] fn replace_records_works_as_expected() { let home_dir = ensure_node_home_directory_exists( @@ -1189,6 +1418,7 @@ mod tests { fn tx_status_from_str_works() { let validation_failure_clock = ValidationFailureClockMock::default() .now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); + assert_eq!( TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), TxStatus::Pending(ValidationStatus::Waiting) @@ -1232,22 +1462,29 @@ mod tests { } #[test] - fn tx_status_can_be_converted_from_tx_confirmation() { - let tx_confirmation = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(6), - block_number: 456789_u64.into(), - }, - detection: Detection::Normal, + fn tx_status_can_be_made_from_transaction_block() { + let tx_block = TxBlock { + block_hash: make_block_hash(6), + block_number: 456789_u64.into(), }; assert_eq!( - TxStatus::from(&tx_confirmation), + TxStatus::from(tx_block), TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number).unwrap(), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).unwrap(), + detection: Detection::Normal, } ) } + + #[test] + fn tx_record_with_hash_is_implemented_for_sent_tx() { + let sent_tx = make_sent_tx(1234); + let hash = sent_tx.hash; + + let hash_from_trait = sent_tx.hash(); + + assert_eq!(hash_from_trait, hash); + } } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index a1a2eeb31..e395aa2de 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -2,10 +2,11 @@ #![cfg(test)] use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedTx, FailureReason, FailureStatus, ValidationStatus, + FailedTx, FailureReason, FailureStatus, }; -use crate::accountant::db_access_objects::sent_payable_dao::{Tx, TxStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; @@ -50,13 +51,13 @@ impl TxBuilder { self } - pub fn build(self) -> Tx { - Tx { + pub fn build(self) -> SentTx { + SentTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: self.amount_opt.unwrap_or_default(), timestamp: self.timestamp_opt.unwrap_or_else(current_unix_timestamp), - gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), status: self .status_opt @@ -111,9 +112,9 @@ impl FailedTxBuilder { FailedTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: self.amount_opt.unwrap_or_default(), timestamp: self.timestamp_opt.unwrap_or_default(), - gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), reason: self .reason_opt diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 8fbc875c2..21c9cdc83 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -46,6 +46,10 @@ pub fn from_unix_timestamp(unix_timestamp: i64) -> SystemTime { SystemTime::UNIX_EPOCH + interval } +pub trait TxRecordWithHash { + fn hash(&self) -> TxHash; +} + pub struct DaoFactoryReal { pub data_directory: PathBuf, pub init_config: DbInitializationConfig, diff --git a/node/src/accountant/db_big_integer/big_int_db_processor.rs b/node/src/accountant/db_big_integer/big_int_db_processor.rs index 3ef15278d..c362e3740 100644 --- a/node/src/accountant/db_big_integer/big_int_db_processor.rs +++ b/node/src/accountant/db_big_integer/big_int_db_processor.rs @@ -322,6 +322,7 @@ pub trait DisplayableParamValue: ToSql + Display {} impl DisplayableParamValue for i64 {} impl DisplayableParamValue for &str {} +impl DisplayableParamValue for String {} impl DisplayableParamValue for Wallet {} #[derive(Default)] diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 2ca502557..f54a7dbd6 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -14,10 +14,10 @@ use masq_lib::constants::{SCAN_ERROR, WEIS_IN_GWEI}; use std::cell::{Ref, RefCell}; use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; use crate::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoError}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, SentTx}; use crate::accountant::db_access_objects::utils::{ - remap_payable_accounts, remap_receivable_accounts, CustomQuery, DaoFactoryReal, + remap_payable_accounts, remap_receivable_accounts, CustomQuery, DaoFactoryReal, TxHash, }; use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, @@ -25,13 +25,22 @@ use crate::accountant::financials::visibility_restricted_module::{ use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{StartScanError, Scanners}; -use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableScanResult, Retry, TxHashByTable, +}; +use crate::accountant::scanners::scan_schedulers::{ + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, +}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; +use crate::accountant::scanners::{Scanners, StartScanError}; +use crate::blockchain::blockchain_bridge::{ + BlockMarker, RegisterNewPendingPayables, RetrieveTransactions, +}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, ProcessedPayableFallible, + BlockchainTransaction, ProcessedPayableFallible, StatusReadFromReceiptCheck, }; +use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::bootstrapper::BootstrapperConfig; use crate::database::db_initializer::DbInitializationConfig; use crate::sub_lib::accountant::AccountantSubs; @@ -57,17 +66,17 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, - UiScanRequest, + QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; +use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::ui_gateway::MessageTarget::ClientId; use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; +use std::collections::HashMap; #[cfg(test)] use std::default::Default; use std::fmt::Display; @@ -76,10 +85,6 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; -use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours @@ -89,7 +94,7 @@ pub struct Accountant { earning_wallet: Wallet, payable_dao: Box, receivable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, crashable: bool, scanners: Scanners, scan_schedulers: ScanSchedulers, @@ -136,9 +141,11 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } +pub type TxReceiptResult = Result; + #[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, +pub struct TxReceiptsMessage { + pub results: HashMap, pub response_skeleton_opt: Option, } @@ -230,20 +237,20 @@ impl Handler for Accountant { self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) => self .scan_schedulers .pending_payable .schedule(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::Schedule(scan_type) => unreachable!( "Early stopped pending payable scan was suggested to be followed up \ by the scan for {:?}, which is not supported though", scan_type ), - ScanRescheduleAfterEarlyStop::DoNotSchedule => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the pending payable scan did find results" @@ -267,16 +274,16 @@ impl Handler for Accountant { let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(other_scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::Schedule(other_scan_type) => unreachable!( "Early stopped new payable scan was suggested to be followed up by the scan \ for {:?}, which is not supported though", other_scan_type ), - ScanRescheduleAfterEarlyStop::DoNotSchedule => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the new payable scan did find results" @@ -308,10 +315,10 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: TxReceiptsMessage, ctx: &mut Self::Context) -> Self::Result { let response_skeleton_opt = msg.response_skeleton_opt; match self.scanners.finish_pending_payable_scan(msg, &self.logger) { PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) => { @@ -321,18 +328,30 @@ impl Handler for Accountant { .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - // Externally triggered scan should never be allowed to spark a procedure that - // would bring over payables with fresh nonces. The job's done. + // Non-automatic scan for pending payables is not permitted to spark a payable + // scan bringing over new payables with fresh nonces. The job's done here. } else { self.scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger) } } - PendingPayableScanResult::PaymentRetryRequired => self - .scan_schedulers - .payable - .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + PendingPayableScanResult::PaymentRetryRequired(retry_either) => match retry_either { + Either::Left(Retry::RetryPayments) => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + Either::Left(Retry::RetryTxStatusCheckOnly) => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + Either::Right(node_to_ui_msg) => self + .ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"), + }, }; } } @@ -469,7 +488,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable_fingerprints: Vec, + pub tx_hashes: Vec, pub response_skeleton_opt: Option, } @@ -479,14 +498,14 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); fn handle( &mut self, - msg: PendingPayableFingerprintSeeds, + msg: RegisterNewPendingPayables, _ctx: &mut Self::Context, ) -> Self::Result { - self.handle_new_pending_payable_fingerprints(msg) + self.register_new_pending_sent_tx(msg) } } @@ -519,13 +538,12 @@ impl Accountant { let earning_wallet = config.earning_wallet.clone(); let financial_statistics = Rc::new(RefCell::new(FinancialStatistics::default())); let payable_dao = dao_factories.payable_dao_factory.make(); - let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); + let sent_payable_dao = dao_factories.sent_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); let scan_schedulers = ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), - config.when_pending_too_long_sec, Rc::clone(&financial_statistics), ); @@ -534,7 +552,7 @@ impl Accountant { earning_wallet, payable_dao, receivable_dao, - pending_payable_dao, + sent_payable_dao, scanners, crashable: config.crash_point == CrashPoint::Message, scan_schedulers, @@ -561,8 +579,8 @@ impl Accountant { report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), - init_pending_payable_fingerprints: recipient!(addr, PendingPayableFingerprintSeeds), - report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), + register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), + report_transaction_status: recipient!(addr, TxReceiptsMessage), report_sent_payments: recipient!(addr, SentPayables), scan_errors: recipient!(addr, ScanError), ui_message_sub: recipient!(addr, NodeFromUiMessage), @@ -596,12 +614,12 @@ impl Accountant { byte_rate, payload_size ), - Err(e) => panic!("Recording services provided for {} but has hit fatal database error: {:?}", wallet, e) + Err(e) => panic!("Was recording services provided for {} but hit a fatal database error: {:?}", wallet, e) }; } else { warning!( self.logger, - "Declining to record a receivable against our wallet {} for service we provided", + "Declining to record a receivable against our wallet {} for services we provided", wallet ); } @@ -924,7 +942,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -944,7 +962,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::NewPayables, @@ -978,6 +996,8 @@ impl Accountant { .expect("BlockchainBridge is dead"); } Err(e) => { + // It is thrown away and there is no rescheduling downstream because every error + // happening here on the start resolves into a panic by the current design let _ = self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::RetryPayables, e, @@ -990,7 +1010,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -1003,14 +1023,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: ScanRescheduleAfterEarlyStop = match result { + let hint: ScanReschedulingAfterEarlyStop = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1036,7 +1056,7 @@ impl Accountant { scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error(&self.logger, scanner.into(), is_externally_triggered); @@ -1119,27 +1139,23 @@ impl Accountant { } } - fn handle_new_pending_payable_fingerprints(&self, msg: PendingPayableFingerprintSeeds) { - fn serialize_hashes(fingerprints_data: &[HashAndAmount]) -> String { - comma_joined_stringifiable(fingerprints_data, |hash_and_amount| { - format!("{:?}", hash_and_amount.hash) - }) + fn register_new_pending_sent_tx(&self, msg: RegisterNewPendingPayables) { + fn serialize_hashes(tx_hashes: &[SentTx]) -> String { + comma_joined_stringifiable(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } - match self - .pending_payable_dao - .insert_new_fingerprints(&msg.hashes_and_balances, msg.batch_wide_timestamp) - { + + match self.sent_payable_dao.insert_new_records(&msg.new_sent_txs) { Ok(_) => debug!( self.logger, - "Saved new pending payable fingerprints for: {}", - serialize_hashes(&msg.hashes_and_balances) + "Registered new pending payables for: {}", + serialize_hashes(&msg.new_sent_txs) ), Err(e) => error!( self.logger, - "Failed to process new pending payable fingerprints due to '{:?}', \ - disabling the automated confirmation for all these transactions: {}", - e, - serialize_hashes(&msg.hashes_and_balances) + "Failed to save new pending payable records for {} due to '{:?}' which is integral \ + to the function of the automated tx confirmation", + serialize_hashes(&msg.new_sent_txs), + e ), } } @@ -1149,33 +1165,31 @@ impl Accountant { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingPayable { + pub recipient_wallet: Wallet, + pub hash: TxHash, +} + +impl PendingPayable { + pub fn new(recipient_wallet: Wallet, hash: TxHash) -> Self { + Self { + recipient_wallet, + hash, + } + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct PendingPayableId { pub rowid: u64, - pub hash: H256, + pub hash: TxHash, } impl PendingPayableId { - pub fn new(rowid: u64, hash: H256) -> Self { + pub fn new(rowid: u64, hash: TxHash) -> Self { Self { rowid, hash } } - - fn rowids(ids: &[Self]) -> Vec { - ids.iter().map(|id| id.rowid).collect() - } - - fn serialize_hashes_to_string(ids: &[Self]) -> String { - comma_joined_stringifiable(ids, |id| format!("{:?}", id.hash)) - } -} - -impl From for PendingPayableId { - fn from(pending_payable_fingerprint: PendingPayableFingerprint) -> Self { - Self { - hash: pending_payable_fingerprint.hash, - rowid: pending_payable_fingerprint.rowid, - } - } } pub fn comma_joined_stringifiable(collection: &[T], stringify: F) -> String @@ -1216,33 +1230,58 @@ pub fn wei_to_gwei, S: Display + Copy + Div + From> for Accountant { type Result = (); @@ -1314,7 +1352,8 @@ mod tests { fn new_calls_factories_properly() { let config = make_bc_with_defaults(); let payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let receivable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let banned_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); @@ -1323,11 +1362,14 @@ mod tests { .make_result(PayableDaoMock::new()) // For Accountant .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()); // For PendingPayable Scanner - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() - .make_params(&pending_payable_dao_factory_params_arc) - .make_result(PendingPayableDaoMock::new()) // For Accountant - .make_result(PendingPayableDaoMock::new()) // For Payable Scanner - .make_result(PendingPayableDaoMock::new()); // For PendingPayable Scanner + let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() + .make_params(&sent_payable_dao_factory_params_arc) + .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner + .make_result(SentPayableDaoMock::new()); // For PendingPayable Scanner + let failed_payable_dao_factory = FailedPayableDaoFactoryMock::new() + .make_params(&failed_payable_dao_factory_params_arc) + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(vec![])); // For PendingPayableScanner; let receivable_dao_factory = ReceivableDaoFactoryMock::new() .make_params(&receivable_dao_factory_params_arc) .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1343,7 +1385,8 @@ mod tests { config, DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), @@ -1355,9 +1398,13 @@ mod tests { vec![(), (), ()] ); assert_eq!( - *pending_payable_dao_factory_params_arc.lock().unwrap(), + *sent_payable_dao_factory_params_arc.lock().unwrap(), vec![(), (), ()] ); + assert_eq!( + *failed_payable_dao_factory_params_arc.lock().unwrap(), + vec![()] + ); assert_eq!( *receivable_dao_factory_params_arc.lock().unwrap(), vec![(), ()] @@ -1375,12 +1422,16 @@ mod tests { .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()), // For PendingPayable Scanner ); - let pending_payable_dao_factory = Box::new( - PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) // For Accountant - .make_result(PendingPayableDaoMock::new()) // For Payable Scanner - .make_result(PendingPayableDaoMock::new()), // For PendingPayable Scanner + let sent_payable_dao_factory = Box::new( + SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner + .make_result(SentPayableDaoMock::new()), // For PendingPayable Scanner ); + let failed_payable_dao_factory = Box::new( + FailedPayableDaoFactoryMock::new() + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(vec![])), + ); // For PendingPayableScanner; let receivable_dao_factory = Box::new( ReceivableDaoFactoryMock::new() .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1395,7 +1446,8 @@ mod tests { bootstrapper_config, DaoFactories { payable_dao_factory, - pending_payable_dao_factory, + sent_payable_dao_factory, + failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, config_dao_factory, @@ -1555,14 +1607,12 @@ mod tests { #[test] fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { let config = bc_from_earning_wallet(make_wallet("earning_wallet")); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(1, make_tx_hash(123))], - no_rowid_results: vec![], - }); + let tx_hash = make_tx_hash(123); + let sent_payable_dao = + SentPayableDaoMock::default().get_tx_identifiers_result(hashmap! (tx_hash => 1)); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); let mut subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); @@ -1576,7 +1626,7 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: make_wallet("blah"), - hash: make_tx_hash(123), + hash: tx_hash, })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1836,20 +1886,15 @@ mod tests { receivable_scan_interval: Duration::from_millis(10_000), pending_payable_scan_interval: Duration::from_secs(100), }); - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let sent_tx = make_sent_tx(555); + let tx_hash = sent_tx.hash; + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![sent_tx]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge @@ -1879,7 +1924,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable_fingerprints: vec![fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1898,12 +1943,22 @@ mod tests { let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transaction_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Ok(())); + let mut subject = AccountantBuilder::default().build(); + let mut sent_tx = make_sent_tx(123); + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let sent_payable_cache = + PendingPayableCacheMock::default().get_record_by_hash_result(Some(sent_tx.clone())); + let pending_payable_scanner = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Real( + pending_payable_scanner, + ))); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let ui_gateway = ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); @@ -1919,26 +1974,27 @@ mod tests { Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); let subject_addr = subject.start(); - let tx_fingerprint = make_pending_payable_fingerprint(); - let report_tx_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(777), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(456), - block_number: 78901234.into(), - }), - }), - tx_fingerprint.clone(), + let tx_block = TxBlock { + block_hash: make_tx_hash(456), + block_number: 78901234.into(), + }; + let tx_receipts_msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Succeeded(tx_block), )], response_skeleton_opt, }; - subject_addr.try_send(report_tx_receipts).unwrap(); + subject_addr.try_send(tx_receipts_msg).unwrap(); system.run(); let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transaction_confirmed_params, vec![vec![tx_fingerprint]]); + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, + }; + assert_eq!(*transaction_confirmed_params, vec![vec![sent_tx]]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2111,20 +2167,28 @@ mod tests { #[test] fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( ) { + // TODO now only GH-605 logic is missing let response_skeleton_opt = Some(ResponseSkeleton { client_id: 4555, context_id: 5566, }); - // TODO when we have more logic in place with the other cards taken in, we'll need to configure these - // accordingly + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]) - .mark_failures_result(Ok(())); + let sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let sent_payable_dao = SentPayableDaoMock::default() + .retrieve_txs_result(vec![sent_tx.clone()]) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); subject.scan_schedulers.automatic_scans_enabled = false; let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -2139,14 +2203,10 @@ mod tests { let system = System::new("test"); let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, - ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(234), - status: TxStatus::Failed - }), - make_pending_payable_fingerprint() - )], + TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Reverted + ),], response_skeleton_opt }, &subject_addr @@ -2179,6 +2239,11 @@ mod tests { subject_addr.try_send(pending_payable_request).unwrap(); system.run(); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); + assert_eq!(*insert_new_records_params, vec![vec![expected_failed_tx]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash]]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2717,7 +2782,7 @@ mod tests { let system = System::new(test_name); let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); - let pp_fingerprint = make_pending_payable_fingerprint(); + let tx_hash = make_tx_hash(456); let payable_scanner = ScannerMock::new() .scan_started_at_result(None) .scan_started_at_result(None) @@ -2741,11 +2806,13 @@ mod tests { .scan_started_at_result(None) .start_scan_params(&scan_params.pending_payable_start_scan) .start_scan_result(Ok(RequestTransactionReceipts { - pending_payable_fingerprints: vec![pp_fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, })) .finish_scan_params(&scan_params.pending_payable_finish_scan) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryPayments), + )); let receivable_scanner = ScannerMock::new() .scan_started_at_result(None) .start_scan_params(&scan_params.receivable_start_scan) @@ -2762,13 +2829,9 @@ mod tests { let (peer_actors, addresses) = peer_actors_builder().build_and_provide_addresses(); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); - let expected_report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(789), - status: TxStatus::Failed, - }), - make_pending_payable_fingerprint(), + let expected_tx_receipts_msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok( + StatusReadFromReceiptCheck::Reverted, )], response_skeleton_opt: None, }; @@ -2781,7 +2844,7 @@ mod tests { }; let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, - expected_report_transaction_receipts.clone(), + expected_tx_receipts_msg.clone(), &subject_addr ); let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( @@ -2810,7 +2873,7 @@ mod tests { &scan_params, ¬ify_and_notify_later_params.pending_payables_notify_later, pending_payable_expected_notify_later_interval, - expected_report_transaction_receipts, + expected_tx_receipts_msg, before, after, ); @@ -2828,9 +2891,9 @@ mod tests { ¬ify_and_notify_later_params.receivables_notify_later, receivable_scan_interval, ); - // Given the assertions prove that the pending payable scanner has run multiple times - // before the new payable scanner started or was scheduled, the front position belongs to - // the one first mentioned, no doubts. + // Since the assertions proved that the pending payable scanner had run multiple times + // before the new payable scanner started or was scheduled, the front position definitely + // belonged to the one first mentioned. } #[derive(Default)] @@ -2840,10 +2903,9 @@ mod tests { payable_finish_scan: Arc>>, pending_payable_start_scan: Arc, Logger, String)>>>, - pending_payable_finish_scan: Arc>>, + pending_payable_finish_scan: Arc>>, receivable_start_scan: Arc, Logger, String)>>>, - // receivable_finish_scan ... not needed } #[derive(Default)] @@ -2862,7 +2924,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -2921,7 +2983,7 @@ mod tests { payable_scanner: ScannerMock, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -2975,7 +3037,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -3048,7 +3110,7 @@ mod tests { Mutex>, >, pending_payable_expected_notify_later_interval: Duration, - expected_report_tx_receipts_msg: ReportTransactionReceipts, + expected_tx_receipts_msg: TxReceiptsMessage, act_started_at: SystemTime, act_finished_at: SystemTime, ) { @@ -3061,12 +3123,9 @@ mod tests { assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); let mut pending_payable_finish_scan_params = scan_params.pending_payable_finish_scan.lock().unwrap(); - let (actual_report_tx_receipts_msg, pp_finish_scan_logger) = + let (actual_tx_receipts_msg, pp_finish_scan_logger) = pending_payable_finish_scan_params.remove(0); - assert_eq!( - actual_report_tx_receipts_msg, - expected_report_tx_receipts_msg - ); + assert_eq!(actual_tx_receipts_msg, expected_tx_receipts_msg); assert_using_the_same_logger(&pp_finish_scan_logger, test_name, Some("pp finish scan")); let scan_for_pending_payables_notify_later_params = scan_for_pending_payables_notify_later_params_arc @@ -3287,11 +3346,13 @@ mod tests { #[test] fn initial_pending_payable_scan_if_some_payables_found() { - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let sent_payable_dao = + SentPayableDaoMock::default().retrieve_txs_result(vec![make_sent_tx(789)]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let system = System::new("test"); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -3304,7 +3365,7 @@ mod tests { System::current().stop(); system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, ScanRescheduleAfterEarlyStop::DoNotSchedule); + assert_eq!(hint, ScanReschedulingAfterEarlyStop::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); @@ -3313,11 +3374,12 @@ mod tests { #[test] fn initial_pending_payable_scan_if_no_payables_found() { - let pending_payable_dao = - PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let flag_before = subject.scanners.initial_pending_payable_scan(); @@ -3326,7 +3388,7 @@ mod tests { let flag_after = subject.scanners.initial_pending_payable_scan(); assert_eq!( hint, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); assert_eq!(flag_before, true); assert_eq!(flag_after, false); @@ -3493,6 +3555,7 @@ mod tests { response_skeleton_opt: None, }; let transaction_hash = make_tx_hash(789); + let tx_hash = make_tx_hash(456); let creditor_wallet = make_wallet("blah"); let counter_msg_2 = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( @@ -3500,23 +3563,16 @@ mod tests { )]), response_skeleton_opt: None, }; - let tx_receipt = TxReceipt { - transaction_hash, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(369369), - block_number: 4444444444u64.into(), - }), - }; - let pending_payable_fingerprint = make_pending_payable_fingerprint(); - let counter_msg_3 = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(tx_receipt), - pending_payable_fingerprint.clone(), - )], + let tx_status = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), + }); + let counter_msg_3 = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], response_skeleton_opt: None, }; let request_transaction_receipts_msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![pending_payable_fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, }; let qualified_payables_msg = QualifiedPayablesMessage { @@ -3853,7 +3909,7 @@ mod tests { system.run(); assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); @@ -3942,7 +3998,7 @@ mod tests { fn start_scan_error_in_new_payables_and_unexpected_reaction_by_receivable_scan_scheduling() { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() - .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4051,41 +4107,40 @@ mod tests { } #[test] - fn scan_for_pending_payables_finds_still_pending_payables() { + fn scan_for_pending_payables_finds_various_payables() { init_test_logging(); + let test_name = "scan_for_pending_payables_finds_various_payables"; + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 2, - amount: 7999, - process_error: None, + let tx_hash_1 = make_tx_hash(456); + let tx_hash_2 = make_tx_hash(789); + let tx_hash_3 = make_tx_hash(123); + let expected_composed_msg_for_blockchain_bridge = RequestTransactionReceipts { + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + TxHashByTable::FailedPayable(tx_hash_3), + ], + response_skeleton_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![ - payable_fingerprint_1.clone(), - payable_fingerprint_2.clone(), - ]); - let config = bc_from_earning_wallet(make_wallet("mine")); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Ok(expected_composed_msg_for_blockchain_bridge.clone())); + let consuming_wallet = make_wallet("consuming"); let system = System::new("pending payable scan"); let mut subject = AccountantBuilder::default() - .consuming_wallet(make_paying_wallet(b"consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) + .logger(Logger::new(test_name)) .build(); - + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); @@ -4095,19 +4150,24 @@ mod tests { }) .unwrap(); + let before = SystemTime::now(); system.run(); + let after = SystemTime::now(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(before <= timestamp && timestamp <= after); + assert_eq!(response_skeleton_opt, None); + assert!( + start_scan_params.is_empty(), + "Should be empty but {:?}", + start_scan_params + ); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let received_msg = blockchain_bridge_recording.get_record::(0); - assert_eq!( - received_msg, - &RequestTransactionReceipts { - pending_payable_fingerprints: vec![payable_fingerprint_1, payable_fingerprint_2], - response_skeleton_opt: None, - } - ); + assert_eq!(received_msg, &expected_composed_msg_for_blockchain_bridge); assert_eq!(blockchain_bridge_recording.len(), 1); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); } #[test] @@ -4163,7 +4223,7 @@ mod tests { { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() - .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4267,7 +4327,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", consuming_wallet, )); } @@ -4312,7 +4372,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", earning_wallet, )); } @@ -4404,7 +4464,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", consuming_wallet )); } @@ -4449,7 +4509,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", earning_wallet, )); } @@ -4718,8 +4778,8 @@ mod tests { #[test] #[should_panic( - expected = "Recording services provided for 0x000000000000000000000000000000626f6f6761 \ - but has hit fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" + expected = "Was recording services provided for 0x000000000000000000000000000000626f6f6761 \ + but hit a fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" )] fn record_service_provided_panics_on_fatal_errors() { init_test_logging(); @@ -4810,27 +4870,19 @@ mod tests { #[test] fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let expected_wallet = make_wallet("paying_you"); let expected_hash = H256::from("transaction_hash".keccak256()); let expected_rowid = 45623; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(expected_rowid, expected_hash)], - no_rowid_results: vec![], - }); - let payable_dao = PayableDaoMock::new() - .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) - .mark_pending_payables_rowids_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result(hashmap! (expected_hash => expected_rowid)); let system = System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) - .payable_daos(vec![ForPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .build(); let pending_payable_interval = Duration::from_millis(55); subject.scan_schedulers.pending_payable.interval = pending_payable_interval; @@ -4851,14 +4903,8 @@ mod tests { System::current().stop(); system.run(); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); - assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); - let mark_pending_payables_rowids_params = - mark_pending_payables_rowids_params_arc.lock().unwrap(); - assert_eq!( - *mark_pending_payables_rowids_params, - vec![vec![(expected_wallet, expected_rowid)]] - ); + let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); + assert_eq!(*get_tx_identifiers_params, vec![hashset!(expected_hash)]); let pending_payable_notify_later_params = pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -4908,7 +4954,7 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "booga".to_string(), - hashes: vec![make_tx_hash(456)], + hashes: hashset![make_tx_hash(456)], }), response_skeleton_opt: None, }; @@ -4945,17 +4991,33 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryPayments), + )); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.retry_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); let system = System::new(test_name); - let (mut msg, _) = - make_report_transaction_receipts_msg(vec![TxStatus::Pending, TxStatus::Failed]); + let (mut msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Pending, + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Reverted, + }, + ]); let response_skeleton_opt = Some(ResponseSkeleton { client_id: 45, context_id: 7, @@ -4981,21 +5043,146 @@ mod tests { } #[test] - fn accountant_confirms_payable_txs_and_schedules_the_new_payable_scanner_timely() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + fn accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed() { + init_test_logging(); + let test_name = + "accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryTxStatusCheckOnly), + )); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let interval = Duration::from_secs(20); + subject.scan_schedulers.pending_payable.interval = interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), + ); + let system = System::new(test_name); + let msg = TxReceiptsMessage { + results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: None, + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + System::current().stop(); + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payable_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + interval + )] + ); + assert_using_the_same_logger(&logger, test_name, None) + } + + #[test] + fn accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected() + { + init_test_logging(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let test_name = + "accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + subject.ui_message_sub_opt = Some(ui_gateway.start().recipient()); + let response_skeleton = ResponseSkeleton { + client_id: 123, + context_id: 333, + }; + let node_to_ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(123), + body: UiScanResponse {}.tmb(333), + }; + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Right(node_to_ui_msg.clone()), + )); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let system = System::new(test_name); + + let msg = TxReceiptsMessage { + results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: Some(response_skeleton), + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let captured_msg = ui_gateway_recording.get_record::(0); + assert_eq!(captured_msg, &node_to_ui_msg); + assert_using_the_same_logger(&logger, test_name, None) + } + + #[test] + fn accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely() { + init_test_logging(); + let test_name = + "accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); let system = System::new("new_payable_scanner_timely"); let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(3)) .unwrap(); @@ -5019,23 +5206,36 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(123), - block_number: U64::from(100), - }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(555)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); System::current().stop(); system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); let (_, last_new_payable_timestamp_actual, scan_interval_actual) = compute_interval_params.remove(0); @@ -5060,19 +5260,24 @@ mod tests { #[test] fn accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + init_test_logging(); + let test_name = + "accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(8)) .unwrap(); @@ -5094,25 +5299,34 @@ mod tests { ); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let tx_block_1 = make_transaction_block(4567); + let tx_block_2 = make_transaction_block(1234); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(123), - block_number: U64::from(100), - }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_1), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_2), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); - let system = System::new("new_payable_scanner_asap"); + let system = System::new(test_name); System::current().stop(); system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); let (_, last_new_payable_timestamp_actual, scan_interval_actual) = compute_interval_params.remove(0); @@ -5129,20 +5343,23 @@ mod tests { new_payable_notify_later ); let new_payable_notify = new_payable_notify_arc.lock().unwrap(); - assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]) + assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]); } #[test] fn scheduler_for_new_payables_operates_with_proper_now_timestamp() { let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let system = System::new("scheduler_for_new_payables_operates_with_proper_now_timestamp"); + let test_name = "scheduler_for_new_payables_operates_with_proper_now_timestamp"; let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_millis(3500)) .unwrap(); @@ -5158,17 +5375,15 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); + let system = System::new(test_name); let subject_addr = subject.start(); - let (msg, _) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { + let (msg, _) = make_tx_receipts_msg(vec![SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { block_hash: make_tx_hash(123), block_number: U64::from(100), }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), - ]); + }]); subject_addr.try_send(msg).unwrap(); @@ -5198,126 +5413,147 @@ mod tests { ); } - fn make_report_transaction_receipts_msg( - status_txs: Vec, - ) -> (ReportTransactionReceipts, Vec) { - let (receipt_result_fingerprint_pairs, fingerprints): (Vec<_>, Vec<_>) = status_txs - .into_iter() - .enumerate() - .map(|(idx, status)| { - let transaction_hash = make_tx_hash(idx as u32); - let transaction_receipt_result = TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash, - status, - }); - let fingerprint = PendingPayableFingerprint { - rowid: idx as u64, - timestamp: from_unix_timestamp(1_000_000_000 * idx as i64), - hash: transaction_hash, - attempt: 2, - amount: 1_000_000 * idx as u128 * idx as u128, - process_error: None, - }; - ( - (transaction_receipt_result, fingerprint.clone()), - fingerprint, - ) - }) - .unzip(); + pub struct SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + } + + fn make_tx_receipts_msg( + seeds: Vec, + ) -> (TxReceiptsMessage, Vec) { + let (tx_receipt_results, tx_record_vec) = seeds.into_iter().enumerate().fold( + (hashmap![], vec![]), + |(mut tx_receipt_results, mut record_by_table_vec), (idx, seed_params)| { + let tx_hash = seed_params.tx_hash; + let status = seed_params.status; + let (key, value, record) = + make_receipt_check_result_and_record(tx_hash, status, idx as u64); + tx_receipt_results.insert(key, value); + record_by_table_vec.push(record); + (tx_receipt_results, record_by_table_vec) + }, + ); - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: receipt_result_fingerprint_pairs, + let msg = TxReceiptsMessage { + results: tx_receipt_results, response_skeleton_opt: None, }; - (msg, fingerprints) + (msg, tx_record_vec) + } + + fn make_receipt_check_result_and_record( + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + idx: u64, + ) -> (TxHashByTable, TxReceiptResult, TxByTable) { + match tx_hash { + TxHashByTable::SentPayable(hash) => { + let mut sent_tx = make_sent_tx(1 + idx); + sent_tx.hash = hash; + + if let StatusReadFromReceiptCheck::Succeeded(block) = &status { + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + } + } + + let result = Ok(status); + let record_by_table = TxByTable::SentPayable(sent_tx); + (tx_hash, result, record_by_table) + } + TxHashByTable::FailedPayable(hash) => { + let mut failed_tx = make_failed_tx(1 + idx); + failed_tx.hash = hash; + + let result = Ok(status); + let record_by_table = TxByTable::FailedPayable(failed_tx); + (tx_hash, result, record_by_table) + } + } } #[test] - fn accountant_handles_inserting_new_fingerprints() { + fn accountant_handles_registering_new_pending_payables() { init_test_logging(); - let insert_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .insert_fingerprints_params(&insert_fingerprint_params_arc) - .insert_fingerprints_result(Ok(())); + let test_name = "accountant_handles_registering_new_pending_payables"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); let subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForAccountantBody(pending_payable_dao)]) + .sent_payable_daos(vec![ForAccountantBody(sent_payable_dao)]) + .logger(Logger::new(test_name)) .build(); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); - let timestamp = SystemTime::now(); + let mut sent_tx_1 = make_sent_tx(456); let hash_1 = make_tx_hash(0x6c81c); - let amount_1 = 12345; + sent_tx_1.hash = hash_1; + let mut sent_tx_2 = make_sent_tx(789); let hash_2 = make_tx_hash(0x1b207); - let amount_2 = 87654; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - let init_params = vec![hash_and_amount_1, hash_and_amount_2]; - let init_fingerprints_msg = PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: init_params.clone(), - }; + sent_tx_2.hash = hash_2; + let new_sent_txs = vec![sent_tx_1.clone(), sent_tx_2.clone()]; + let msg = RegisterNewPendingPayables { new_sent_txs }; let _ = accountant_subs - .init_pending_payable_fingerprints - .try_send(init_fingerprints_msg) + .register_new_pending_payables + .try_send(msg) .unwrap(); - let system = System::new("ordering payment fingerprint test"); + let system = System::new("ordering payment sent tx record test"); System::current().stop(); assert_eq!(system.run(), 0); - let insert_fingerprint_params = insert_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *insert_fingerprint_params, - vec![(vec![hash_and_amount_1, hash_and_amount_2], timestamp)] - ); - TestLogHandler::new().exists_log_containing( - "DEBUG: Accountant: Saved new pending payable fingerprints for: \ - 0x000000000000000000000000000000000000000000000000000000000006c81c, 0x000000000000000000000000000000000000000000000000000000000001b207", - ); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Registered new pending payables for: \ + 0x000000000000000000000000000000000000000000000000000000000006c81c, \ + 0x000000000000000000000000000000000000000000000000000000000001b207", + )); } #[test] - fn payable_fingerprint_insertion_clearly_failed_and_we_log_it_at_least() { - //despite it doesn't end so here this event would be a cause of a later panic + fn sent_payable_insertion_clearly_failed_and_we_log_at_least() { + // Even though it's factually a filed db operation, which is treated by an instant panic + // due to the broken db reliance, this is an exception. We give out some time to complete + // the actual paying and panic soon after when we figure out, from a different place + // that some sent tx records are missing. This should eventually be eliminated by GH-655 init_test_logging(); - let insert_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .insert_fingerprints_params(&insert_fingerprint_params_arc) - .insert_fingerprints_result(Err(PendingPayableDaoError::InsertionFailed( + let test_name = "sent_payable_insertion_clearly_failed_and_we_log_at_least"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Err(SentPayableDaoError::SqlExecutionFailed( "Crashed".to_string(), ))); - let amount = 2345; - let transaction_hash = make_tx_hash(0x1c8); - let hash_and_amount = HashAndAmount { - hash: transaction_hash, - amount, - }; + let tx_hash_1 = make_tx_hash(0x1c8); + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.hash = tx_hash_1; + let tx_hash_2 = make_tx_hash(0x1b2); + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.hash = tx_hash_2; let subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForAccountantBody(pending_payable_dao)]) + .sent_payable_daos(vec![ForAccountantBody(sent_payable_dao)]) + .logger(Logger::new(test_name)) .build(); - let timestamp = SystemTime::now(); - let report_new_fingerprints = PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: vec![hash_and_amount], + let msg = RegisterNewPendingPayables { + new_sent_txs: vec![sent_tx_1.clone(), sent_tx_2.clone()], }; - let _ = subject.handle_new_pending_payable_fingerprints(report_new_fingerprints); + let _ = subject.register_new_pending_sent_tx(msg); - let insert_fingerprint_params = insert_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *insert_fingerprint_params, - vec![(vec![hash_and_amount], timestamp)] - ); - TestLogHandler::new().exists_log_containing("ERROR: Accountant: Failed to process \ - new pending payable fingerprints due to 'InsertionFailed(\"Crashed\")', disabling the automated \ - confirmation for all these transactions: 0x00000000000000000000000000000000000000000000000000000000000001c8"); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Failed to save new pending payable records for \ + 0x00000000000000000000000000000000000000000000000000000000000001c8, \ + 0x00000000000000000000000000000000000000000000000000000000000001b2 \ + due to 'SqlExecutionFailed(\"Crashed\")' which is integral to the function \ + of the automated tx confirmation" + )); } const EXAMPLE_RESPONSE_SKELETON: ResponseSkeleton = ResponseSkeleton { @@ -5329,7 +5565,7 @@ mod tests { #[test] fn handling_scan_error_for_externally_triggered_payables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_payables", ScanError { scan_type: ScanType::Payables, @@ -5341,19 +5577,21 @@ mod tests { #[test] fn handling_scan_error_for_externally_triggered_pending_payables() { - assert_scan_error_is_handled_properly( + let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); + test_scan_error_is_handled_properly_more_specifically( "handling_scan_error_for_externally_triggered_pending_payables", ScanError { scan_type: ScanType::PendingPayables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, + Some(additional_test_setup_and_assertions), ); } #[test] fn handling_scan_error_for_externally_triggered_receivables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_receivables", ScanError { scan_type: ScanType::Receivables, @@ -5365,7 +5603,7 @@ mod tests { #[test] fn handling_scan_error_for_internally_triggered_payables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_payables", ScanError { scan_type: ScanType::Payables, @@ -5377,19 +5615,21 @@ mod tests { #[test] fn handling_scan_error_for_internally_triggered_pending_payables() { - assert_scan_error_is_handled_properly( + let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); + test_scan_error_is_handled_properly_more_specifically( "handling_scan_error_for_internally_triggered_pending_payables", ScanError { scan_type: ScanType::PendingPayables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + Some(additional_test_setup_and_assertions), ); } #[test] fn handling_scan_error_for_internally_triggered_receivables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_receivables", ScanError { scan_type: ScanType::Receivables, @@ -5399,6 +5639,34 @@ mod tests { ); } + fn prepare_setup_and_assertion_fns() -> (Box, Box) { + let ensure_empty_cache_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_sent_tx_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_failed_tx_params_arc); + let scanner = PendingPayableScannerBuilder::new() + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + ( + Box::new(|scanners: &mut Scanners| { + scanners.replace_scanner(ScannerReplacement::PendingPayable( + ReplacementType::Real(scanner), + )); + }) as Box, + Box::new(move || { + let ensure_empty_cache_sent_tx_params = + ensure_empty_cache_sent_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_sent_tx_params, vec![()]); + let ensure_empty_cache_failed_tx_params = + ensure_empty_cache_failed_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_failed_tx_params, vec![()]); + }) as Box, + ) + } + #[test] fn financials_request_with_nothing_to_respond_to_is_refused() { let system = System::new("test"); @@ -6144,15 +6412,32 @@ mod tests { let _: u64 = wei_to_gwei(u128::MAX); } - fn assert_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + fn test_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + test_scan_error_is_handled_properly_more_specifically(test_name, message, None) + } + fn test_scan_error_is_handled_properly_more_specifically( + test_name: &str, + message: ScanError, + additional_assertion_opt: Option<(Box, Box)>, + ) { init_test_logging(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); - subject - .scanners - .reset_scan_started(message.scan_type, MarkScanner::Started(SystemTime::now())); + let (adjust_scanner, run_additional_assertion) = match additional_assertion_opt { + Some(two_functions) => two_functions, + None => ( + Box::new(|scanners: &mut Scanners| { + scanners.reset_scan_started( + message.scan_type, + MarkScanner::Started(SystemTime::now()), + ) + }) as Box, + Box::new(|| ()) as Box, + ), + }; + adjust_scanner(&mut subject.scanners); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -6209,6 +6494,7 @@ mod tests { )); } } + run_additional_assertion(); } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index d49cf3efc..71ff0f62c 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -8,16 +8,15 @@ pub mod scanners_utils; pub mod test_utils; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; -use crate::accountant::db_access_objects::pending_payable_dao::{PendingPayable, PendingPayableDao}; use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::{ScanError, ScanForPendingPayables, ScanForRetryPayables}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_sent_tx_record, investigate_debt_extremes, payables_debug_summary, separate_errors, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMissingInDb}; +use crate::accountant::{PendingPayable, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, + TxReceiptsMessage, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; @@ -41,7 +40,8 @@ use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use variant_count::VariantCount; -use web3::types::H256; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao}; +use crate::accountant::db_access_objects::utils::{TxHash}; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; @@ -59,7 +59,7 @@ pub struct Scanners { dyn PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, >, @@ -77,21 +77,20 @@ impl Scanners { pub fn new( dao_factories: DaoFactories, payment_thresholds: Rc, - when_pending_too_long_sec: u64, financial_statistics: Rc>, ) -> Self { let payable = Box::new(PayableScanner::new( dao_factories.payable_dao_factory.make(), - dao_factories.pending_payable_dao_factory.make(), + dao_factories.sent_payable_dao_factory.make(), Rc::clone(&payment_thresholds), Box::new(PaymentAdjusterReal::new()), )); let pending_payable = Box::new(PendingPayableScanner::new( dao_factories.payable_dao_factory.make(), - dao_factories.pending_payable_dao_factory.make(), + dao_factories.sent_payable_dao_factory.make(), + dao_factories.failed_payable_dao_factory.make(), Rc::clone(&payment_thresholds), - when_pending_too_long_sec, Rc::clone(&financial_statistics), )); @@ -215,6 +214,7 @@ impl Scanners { } (None, None) => (), } + self.pending_payable .start_scan(wallet, timestamp, response_skeleton_opt, logger) } @@ -255,7 +255,7 @@ impl Scanners { pub fn finish_pending_payable_scan( &mut self, - msg: ReportTransactionReceipts, + msg: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { self.pending_payable.finish_scan(msg, logger) @@ -275,6 +275,7 @@ impl Scanners { self.payable.mark_as_ended(logger); } ScanType::PendingPayables => { + self.empty_caches(logger); self.pending_payable.mark_as_ended(logger); } ScanType::Receivables => { @@ -283,6 +284,20 @@ impl Scanners { }; } + fn empty_caches(&mut self, logger: &Logger) { + let pending_payable_scanner = self + .pending_payable + .as_any_mut() + .downcast_mut::() + .expect("mismatched types"); + pending_payable_scanner + .current_sent_payables + .ensure_empty_cache(logger); + pending_payable_scanner + .yet_unproven_failed_payables + .ensure_empty_cache(logger); + } + pub fn try_skipping_payable_adjustment( &self, msg: BlockchainAgentWithContextMessage, @@ -308,7 +323,7 @@ impl Scanners { } // This is a helper function reducing a boilerplate of complex trait resolving where - // the compiler requires to specify which trigger message distinguish the scan to run. + // the compiler requires to specify which trigger message distinguishes the scan to run. // The payable scanner offers two modes through doubled implementations of StartableScanner // which uses the trigger message type as the only distinction between them. fn start_correct_payable_scanner<'a, TriggerMessage>( @@ -428,7 +443,7 @@ impl ScannerCommon { None => { error!( logger, - "Called scan_finished() for {:?} scanner but timestamp was not found", + "Called scan_finished() for {:?} scanner but could not find any timestamp", scan_type ); } @@ -461,7 +476,7 @@ pub struct PayableScanner { pub payable_threshold_gauge: Box, pub common: ScannerCommon, pub payable_dao: Box, - pub pending_payable_dao: Box, + pub sent_payable_dao: Box, pub payment_adjuster: Box, } @@ -536,8 +551,9 @@ impl Scanner for PayableScanner { ); if !sent_payables.is_empty() { - self.mark_pending_payable(&sent_payables, logger); + self.check_on_missing_sent_tx_records(&sent_payables); } + self.handle_sent_payable_errors(err_opt, logger); self.mark_as_ended(logger); @@ -600,14 +616,14 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { impl PayableScanner { pub fn new( payable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, payment_thresholds: Rc, payment_adjuster: Box, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, - pending_payable_dao, + sent_payable_dao, payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), payment_adjuster, } @@ -675,169 +691,200 @@ impl PayableScanner { } } - fn separate_existent_and_nonexistent_fingerprints<'a>( - &'a self, - sent_payables: &[&'a PendingPayable], - ) -> (Vec, Vec) { - let hashes = sent_payables + fn check_for_missing_records( + &self, + just_baked_sent_payables: &[&PendingPayable], + ) -> Vec { + let actual_sent_payables_len = just_baked_sent_payables.len(); + let hashset_with_hashes_to_eliminate_duplicates = just_baked_sent_payables .iter() .map(|pending_payable| pending_payable.hash) - .collect::>(); - let mut sent_payables_hashmap = sent_payables - .iter() - .map(|payable| (payable.hash, &payable.recipient_wallet)) - .collect::>(); - - let transaction_hashes = self.pending_payable_dao.fingerprints_rowids(&hashes); - let mut hashes_from_db = transaction_hashes - .rowid_results - .iter() - .map(|(_rowid, hash)| *hash) - .collect::>(); - for hash in &transaction_hashes.no_rowid_results { - hashes_from_db.insert(*hash); - } - let sent_payables_hashes = hashes.iter().copied().collect::>(); + .collect::>(); - if !Self::is_symmetrical(sent_payables_hashes, hashes_from_db) { + if hashset_with_hashes_to_eliminate_duplicates.len() != actual_sent_payables_len { panic!( - "Inconsistency in two maps, they cannot be matched by hashes. Data set directly \ - sent from BlockchainBridge: {:?}, set derived from the DB: {:?}", - sent_payables, transaction_hashes - ) + "Found duplicates in the recent sent txs: {:?}", + just_baked_sent_payables + ); } - let pending_payables_with_rowid = transaction_hashes - .rowid_results - .into_iter() - .map(|(rowid, hash)| { - let wallet = sent_payables_hashmap - .remove(&hash) - .expect("expect transaction hash, but it disappear"); - PendingPayableMetadata::new(wallet, hash, Some(rowid)) - }) - .collect_vec(); - let pending_payables_without_rowid = transaction_hashes - .no_rowid_results + let transaction_hashes_and_rowids_from_db = self + .sent_payable_dao + .get_tx_identifiers(&hashset_with_hashes_to_eliminate_duplicates); + let hashes_from_db = transaction_hashes_and_rowids_from_db + .keys() + .copied() + .collect::>(); + + let missing_sent_payables_hashes: Vec = hashset_with_hashes_to_eliminate_duplicates + .difference(&hashes_from_db) + .copied() + .collect(); + + let mut sent_payables_hashmap = just_baked_sent_payables + .iter() + .map(|payable| (payable.hash, &payable.recipient_wallet)) + .collect::>(); + missing_sent_payables_hashes .into_iter() .map(|hash| { - let wallet = sent_payables_hashmap + let wallet_address = sent_payables_hashmap .remove(&hash) - .expect("expect transaction hash, but it disappear"); - PendingPayableMetadata::new(wallet, hash, None) + .expectv("wallet") + .address(); + PendingPayableMissingInDb::new(wallet_address, hash) }) - .collect_vec(); - - (pending_payables_with_rowid, pending_payables_without_rowid) + .collect() } - fn is_symmetrical( - sent_payables_hashes: HashSet, - fingerptint_hashes: HashSet, - ) -> bool { - sent_payables_hashes == fingerptint_hashes - } - - fn mark_pending_payable(&self, sent_payments: &[&PendingPayable], logger: &Logger) { - fn missing_fingerprints_msg(nonexistent: &[PendingPayableMetadata]) -> String { + fn check_on_missing_sent_tx_records(&self, sent_payments: &[&PendingPayable]) { + fn missing_record_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { format!( - "Expected pending payable fingerprints for {} were not found; system unreliable", - comma_joined_stringifiable(nonexistent, |pp_triple| format!( - "(tx: {:?}, to wallet: {})", - pp_triple.hash, pp_triple.recipient + "Expected sent-payable records for {} were not found. The system has become unreliable", + comma_joined_stringifiable(nonexistent, |missing_sent_tx_ids| format!( + "(tx: {:?}, to wallet: {:?})", + missing_sent_tx_ids.hash, missing_sent_tx_ids.recipient )) ) } - fn ready_data_for_supply<'a>( - existent: &'a [PendingPayableMetadata], - ) -> Vec<(&'a Wallet, u64)> { - existent - .iter() - .map(|pp_triple| (pp_triple.recipient, pp_triple.rowid_opt.expectv("rowid"))) - .collect() - } - let (existent, nonexistent) = - self.separate_existent_and_nonexistent_fingerprints(sent_payments); - let mark_pp_input_data = ready_data_for_supply(&existent); - if !mark_pp_input_data.is_empty() { - if let Err(e) = self - .payable_dao - .as_ref() - .mark_pending_payables_rowids(&mark_pp_input_data) - { - mark_pending_payable_fatal_error( - sent_payments, - &nonexistent, - e, - missing_fingerprints_msg, - logger, - ) - } - debug!( - logger, - "Payables {} marked as pending in the payable table", - comma_joined_stringifiable(sent_payments, |pending_p| format!( - "{:?}", - pending_p.hash - )) - ) - } - if !nonexistent.is_empty() { - panic!("{}", missing_fingerprints_msg(&nonexistent)) + let missing_sent_tx_records = self.check_for_missing_records(sent_payments); + if !missing_sent_tx_records.is_empty() { + panic!("{}", missing_record_msg(&missing_sent_tx_records)) } } + // TODO this has become dead (GH-662) + #[allow(dead_code)] + fn mark_pending_payable(&self, _sent_payments: &[&PendingPayable], _logger: &Logger) { + todo!("remove me when the time comes") + // fn missing_fingerprints_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { + // format!( + // "Expected pending payable fingerprints for {} were not found; system unreliable", + // comma_joined_stringifiable(nonexistent, |pp_triple| format!( + // "(tx: {:?}, to wallet: {})", + // pp_triple.hash, pp_triple.recipient + // )) + // ) + // } + // fn ready_data_for_supply<'a>( + // existent: &'a [PendingPayableMissingInDb], + // ) -> Vec<(&'a Wallet, u64)> { + // existent + // .iter() + // .map(|pp_triple| (pp_triple.recipient, pp_triple.rowid_opt.expectv("rowid"))) + // .collect() + // } + // + // // TODO eventually should be taken over by GH-655 + // let missing_sent_tx_records = + // self.check_for_missing_records(sent_payments); + // + // if !existent.is_empty() { + // if let Err(e) = self + // .payable_dao + // .as_ref() + // .mark_pending_payables_rowids(&existent) + // { + // mark_pending_payable_fatal_error( + // sent_payments, + // &nonexistent, + // e, + // missing_fingerprints_msg, + // logger, + // ) + // } + // debug!( + // logger, + // "Payables {} marked as pending in the payable table", + // comma_joined_stringifiable(sent_payments, |pending_p| format!( + // "{:?}", + // pending_p.hash + // )) + // ) + // } + // if !missing_sent_tx_records.is_empty() { + // panic!("{}", missing_fingerprints_msg(&missing_sent_tx_records)) + // } + } + fn handle_sent_payable_errors( &self, err_opt: Option, logger: &Logger, ) { - if let Some(err) = err_opt { + fn decide_on_tx_error_handling( + err: &PayableTransactingErrorEnum, + ) -> Option<&HashSet> { match err { LocallyCausedError(PayableTransactionError::Sending { hashes, .. }) - | RemotelyCausedErrors(hashes) => { - self.discard_failed_transactions_with_possible_fingerprints(hashes, logger) - } - non_fatal => - debug!( - logger, - "Ignoring a non-fatal error on our end from before the transactions are hashed: {:?}", - non_fatal - ) + | RemotelyCausedErrors(hashes) => Some(hashes), + _ => None, + } + } + + if let Some(err) = err_opt { + if let Some(hashes) = decide_on_tx_error_handling(&err) { + self.discard_failed_transactions_with_possible_sent_tx_records(hashes, logger) + } else { + debug!( + logger, + "A non-fatal error {:?} will be ignored as it is from before any tx could \ + even be hashed", + err + ) } } } - fn discard_failed_transactions_with_possible_fingerprints( + fn discard_failed_transactions_with_possible_sent_tx_records( &self, - hashes_of_failed: Vec, + hashes_of_failed: &HashSet, logger: &Logger, ) { - fn serialize_hashes(hashes: &[H256]) -> String { + fn serialize_hashes(hashes: &[TxHash]) -> String { comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) } - let existent_and_nonexistent = self - .pending_payable_dao - .fingerprints_rowids(&hashes_of_failed); - let missing_fgp_err_msg_opt = err_msg_for_failure_with_expected_but_missing_fingerprints( - existent_and_nonexistent.no_rowid_results, + + let existent_sent_tx_in_db = self.sent_payable_dao.get_tx_identifiers(&hashes_of_failed); + + let hashes_of_missing_sent_tx = hashes_of_failed + .difference( + &existent_sent_tx_in_db + .keys() + .copied() + .collect::>(), + ) + .copied() + .sorted() + .collect(); + + let missing_fgp_err_msg_opt = err_msg_for_failure_with_expected_but_missing_sent_tx_record( + hashes_of_missing_sent_tx, serialize_hashes, ); - if !existent_and_nonexistent.rowid_results.is_empty() { - let (ids, hashes) = separate_rowids_and_hashes(existent_and_nonexistent.rowid_results); + + if !existent_sent_tx_in_db.is_empty() { + let hashes = existent_sent_tx_in_db + .keys() + .copied() + .sorted() + .collect_vec(); warning!( logger, - "Deleting fingerprints for failed transactions {}", + "Deleting sent payable records for {}", serialize_hashes(&hashes) ); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&ids) { + if let Err(e) = self + .sent_payable_dao + .delete_records(&existent_sent_tx_in_db.keys().copied().collect()) + { if let Some(msg) = missing_fgp_err_msg_opt { error!(logger, "{}", msg) }; panic!( - "Database corrupt: payable fingerprint deletion for transactions {} \ - failed due to {:?}", + "Database corrupt: sent payable record deletion for txs {} failed \ + due to {:?}", serialize_hashes(&hashes), e ) @@ -968,51 +1015,84 @@ impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScann #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; - use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayable, PendingPayableDaoError, TransactionHashes, + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, SentPayableDaoError, SentTx, TxStatus, }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; - use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables}; - use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, + UnpricedQualifiedPayables, + }; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, Retry, + TxHashByTable, + }; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + OperationOutcome, PayableScanResult, + }; + use crate::accountant::scanners::test_utils::{ + assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, + PendingPayableCacheMock, ReplacementType, ScannerReplacement, + }; + use crate::accountant::scanners::{ + ManulTriggerError, PayableScanner, PendingPayableScanner, ReceivableScanner, Scanner, + ScannerCommon, Scanners, StartScanError, StartableScanner, + }; + use crate::accountant::test_utils::{ + make_custom_payment_thresholds, make_failed_tx, make_payable_account, + make_qualified_and_unqualified_payables, make_receivable_account, make_sent_tx, + BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, + FailedPayableDaoMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, + PayableThresholdsGaugeMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, + ReceivableDaoMock, ReceivableScannerBuilder, SentPayableDaoFactoryMock, SentPayableDaoMock, + }; + use crate::accountant::{ + gwei_to_wei, PendingPayable, ReceivedPayments, RequestTransactionReceipts, ScanError, + ScanForRetryPayables, SentPayables, TxReceiptsMessage, + }; + use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ BlockchainTransaction, ProcessedPayableFallible, RpcPayableFailure, + StatusReadFromReceiptCheck, TxBlock, }; - use crate::blockchain::test_utils::make_tx_hash; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, RemoteError, RemoteErrorKind, + }; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::mocks::ConfigDaoMock; use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, - DEFAULT_PAYMENT_THRESHOLDS, + DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::{Message, System}; use ethereum_types::U64; + use itertools::Either; use masq_lib::logger::Logger; + use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::{Regex}; + use masq_lib::ui_gateway::NodeToUiMessage; + use regex::Regex; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; + use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use web3::types::{H256}; use web3::Error; - use masq_lib::messages::ScanType; - use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; - use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; impl Scanners { pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { @@ -1096,18 +1176,19 @@ mod tests { let payable_dao_factory = PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()); - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()); - let receivable_dao = ReceivableDaoMock::new(); - let receivable_dao_factory = ReceivableDaoFactoryMock::new().make_result(receivable_dao); + let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()); + let failed_payable_dao_factory = + FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new()); + let receivable_dao_factory = + ReceivableDaoFactoryMock::new().make_result(ReceivableDaoMock::new()); let banned_dao_factory = BannedDaoFactoryMock::new().make_result(BannedDaoMock::new()); let set_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_mock = ConfigDaoMock::new() .set_params(&set_params_arc) .set_result(Ok(())); let config_dao_factory = ConfigDaoFactoryMock::new().make_result(config_dao_mock); - let when_pending_too_long_sec = 1234; let financial_statistics = FinancialStatistics { total_paid_payable_wei: 1, total_paid_receivable_wei: 2, @@ -1119,13 +1200,13 @@ mod tests { let mut scanners = Scanners::new( DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), }, Rc::clone(&payment_thresholds_rc), - when_pending_too_long_sec, Rc::new(RefCell::new(financial_statistics.clone())), ); @@ -1136,8 +1217,8 @@ mod tests { .unwrap(); let pending_payable_scanner = scanners .pending_payable - .as_any() - .downcast_ref::() + .as_any_mut() + .downcast_mut::() .unwrap(); let receivable_scanner = scanners .receivable @@ -1151,10 +1232,6 @@ mod tests { assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!(scanners.aware_of_unresolved_pending_payable, false); assert_eq!(scanners.initial_pending_payable_scan, true); - assert_eq!( - pending_payable_scanner.when_pending_too_long_sec, - when_pending_too_long_sec - ); assert_eq!( *pending_payable_scanner.financial_statistics.borrow(), financial_statistics @@ -1167,6 +1244,19 @@ mod tests { pending_payable_scanner.common.initiated_at_opt.is_some(), false ); + let dumped_records = pending_payable_scanner + .yet_unproven_failed_payables + .dump_cache(); + assert!( + dumped_records.is_empty(), + "There should be no yet unproven failures but found {:?}.", + dumped_records + ); + assert_eq!( + receivable_scanner.common.payment_thresholds.as_ref(), + &payment_thresholds + ); + assert_eq!(receivable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!( *receivable_scanner.financial_statistics.borrow(), financial_statistics @@ -1472,9 +1562,9 @@ mod tests { fn payable_scanner_handles_sent_payable_message() { init_test_logging(); let test_name = "payable_scanner_handles_sent_payable_message"; - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let mark_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let correct_payable_hash_1 = make_tx_hash(0x6f); let correct_payable_rowid_1 = 125; let correct_payable_wallet_1 = make_wallet("tralala"); @@ -1485,7 +1575,7 @@ mod tests { let failure_payable_wallet_2 = make_wallet("hihihi"); let failure_payable_2 = RpcPayableFailure { rpc_error: Error::InvalidResponse( - "Learn how to write before you send your garbage!".to_string(), + "Ged rid of your illiteracy before you send your garbage!".to_string(), ), recipient_wallet: failure_payable_wallet_2, hash: failure_payable_hash_2, @@ -1495,28 +1585,21 @@ mod tests { let correct_payable_wallet_3 = make_wallet("booga"); let correct_pending_payable_3 = PendingPayable::new(correct_payable_wallet_3.clone(), correct_payable_hash_3); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (correct_payable_rowid_3, correct_payable_hash_3), - (correct_payable_rowid_1, correct_payable_hash_1), - ], - no_rowid_results: vec![], - }) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(failure_payable_rowid_2, failure_payable_hash_2)], - no_rowid_results: vec![], - }) - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result(hashmap!(correct_payable_hash_3 => correct_payable_rowid_3, + correct_payable_hash_1 => correct_payable_rowid_1, + )) + .get_tx_identifiers_result(hashmap!(failure_payable_hash_2 => failure_payable_rowid_2)) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); let payable_dao = PayableDaoMock::new() .mark_pending_payables_rowids_params(&mark_pending_payables_params_arc) .mark_pending_payables_rowids_result(Ok(())) .mark_pending_payables_rowids_result(Ok(())); let mut payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let logger = Logger::new(test_name); let sent_payable = SentPayables { @@ -1547,41 +1630,30 @@ mod tests { assert_eq!(is_scan_running, false); assert_eq!(aware_of_unresolved_pending_payable_before, false); assert_eq!(aware_of_unresolved_pending_payable_after, true); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); + let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); assert_eq!( - *fingerprints_rowids_params, + *get_tx_identifiers_params, vec![ - vec![correct_payable_hash_1, correct_payable_hash_3], - vec![failure_payable_hash_2] + hashset![correct_payable_hash_1, correct_payable_hash_3], + hashset![failure_payable_hash_2] ] ); - let mark_pending_payables_params = mark_pending_payables_params_arc.lock().unwrap(); - assert_eq!( - *mark_pending_payables_params, - vec![vec![ - (correct_payable_wallet_3, correct_payable_rowid_3), - (correct_payable_wallet_1, correct_payable_rowid_1), - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); + let delete_records_params = delete_records_params_arc.lock().unwrap(); assert_eq!( - *delete_fingerprints_params, - vec![vec![failure_payable_rowid_2]] + *delete_records_params, + vec![hashset![failure_payable_hash_2]] ); let log_handler = TestLogHandler::new(); log_handler.assert_logs_contain_in_order(vec![ &format!( - "WARN: {test_name}: Remote transaction failure: 'Got invalid response: Learn how to write before you send your garbage!' \ - for payment to 0x0000000000000000000000000000686968696869 and transaction hash \ - 0x00000000000000000000000000000000000000000000000000000000000000de. Please check your blockchain service URL configuration" + "WARN: {test_name}: Remote sent payable failure 'Got invalid response: Ged rid of \ + your illiteracy before you send your garbage!' \ + for wallet 0x0000000000000000000000000000686968696869 and tx hash \ + 0x00000000000000000000000000000000000000000000000000000000000000de" ), &format!("DEBUG: {test_name}: Got 2 properly sent payables of 3 attempts"), &format!( - "DEBUG: {test_name}: Payables 0x000000000000000000000000000000000000000000000000000000000000006f, \ - 0x000000000000000000000000000000000000000000000000000000000000014d marked as pending in the payable table" - ), - &format!( - "WARN: {test_name}: Deleting fingerprints for failed transactions \ + "WARN: {test_name}: Deleting sent payable records for \ 0x00000000000000000000000000000000000000000000000000000000000000de" ), ]); @@ -1590,44 +1662,86 @@ mod tests { )); } + #[test] + fn no_missing_records() { + let wallet_1 = make_wallet("abc"); + let hash_1 = make_tx_hash(123); + let wallet_2 = make_wallet("def"); + let hash_2 = make_tx_hash(345); + let wallet_3 = make_wallet("ghi"); + let hash_3 = make_tx_hash(546); + let wallet_4 = make_wallet("jkl"); + let hash_4 = make_tx_hash(678); + let pending_payables_owned = vec![ + PendingPayable::new(wallet_1.clone(), hash_1), + PendingPayable::new(wallet_2.clone(), hash_2), + PendingPayable::new(wallet_3.clone(), hash_3), + PendingPayable::new(wallet_4.clone(), hash_4), + ]; + let pending_payables_ref = pending_payables_owned + .iter() + .collect::>(); + let sent_payable_dao = SentPayableDaoMock::new().get_tx_identifiers_result( + hashmap!(hash_4 => 4, hash_1 => 1, hash_3 => 3, hash_2 => 2), + ); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .build(); + + let missing_records = subject.check_for_missing_records(&pending_payables_ref); + + assert!( + missing_records.is_empty(), + "We thought the vec would be empty but contained: {:?}", + missing_records + ); + } + #[test] #[should_panic( - expected = "Expected pending payable fingerprints for (tx: 0x0000000000000000000000000000000000000000000000000000000000000315, \ - to wallet: 0x000000000000000000000000000000626f6f6761), (tx: 0x000000000000000000000000000000000000000000000000000000000000007b, \ - to wallet: 0x00000000000000000000000000000061676f6f62) were not found; system unreliable" + expected = "Found duplicates in the recent sent txs: [PendingPayable { recipient_wallet: \ + Wallet { kind: Address(0x0000000000000000000000000000000000616263) }, hash: \ + 0x000000000000000000000000000000000000000000000000000000000000007b }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000646566) }, \ + hash: 0x00000000000000000000000000000000000000000000000000000000000001c8 }, \ + PendingPayable { recipient_wallet: Wallet { kind: \ + Address(0x0000000000000000000000000000000000676869) }, hash: \ + 0x00000000000000000000000000000000000000000000000000000000000001c8 }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x00000000000000000000000000000000006a6b6c) }, \ + hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }]" )] - fn payable_scanner_panics_when_fingerprints_for_correct_payments_not_found() { - let hash_1 = make_tx_hash(0x315); - let payment_1 = PendingPayable::new(make_wallet("booga"), hash_1); - let hash_2 = make_tx_hash(0x7b); - let payment_2 = PendingPayable::new(make_wallet("agoob"), hash_2); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![], - no_rowid_results: vec![hash_1, hash_2], - }); - let payable_dao = PayableDaoMock::new(); - let mut subject = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + fn just_baked_pending_payables_contain_duplicates() { + let hash_1 = make_tx_hash(123); + let hash_2 = make_tx_hash(456); + let hash_3 = make_tx_hash(789); + let pending_payables = vec![ + PendingPayable::new(make_wallet("abc"), hash_1), + PendingPayable::new(make_wallet("def"), hash_2), + PendingPayable::new(make_wallet("ghi"), hash_2), + PendingPayable::new(make_wallet("jkl"), hash_3), + ]; + let pending_payables_ref = pending_payables.iter().collect::>(); + let sent_payable_dao = SentPayableDaoMock::new() + .get_tx_identifiers_result(hashmap!(hash_1 => 1, hash_2 => 3, hash_3 => 5)); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) .build(); - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(payment_1), - ProcessedPayableFallible::Correct(payment_2), - ]), - response_skeleton_opt: None, - }; - let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + subject.check_for_missing_records(&pending_payables_ref); } - fn assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name: &str, - pending_payable_dao: PendingPayableDaoMock, - hash_1: H256, - hash_2: H256, - ) { + #[test] + #[should_panic(expected = "Expected sent-payable records for \ + (tx: 0x00000000000000000000000000000000000000000000000000000000000000f8, \ + to wallet: 0x00000000000000000000000000626c6168323232) \ + were not found. The system has become unreliable")] + fn payable_scanner_found_out_nonexistent_sent_tx_records() { + init_test_logging(); + let test_name = "payable_scanner_found_out_nonexistent_sent_tx_records"; + let hash_1 = make_tx_hash(0xff); + let hash_2 = make_tx_hash(0xf8); + let sent_payable_dao = + SentPayableDaoMock::default().get_tx_identifiers_result(hashmap!(hash_1 => 7881)); let payable_1 = PendingPayable::new(make_wallet("blah111"), hash_1); let payable_2 = PendingPayable::new(make_wallet("blah222"), hash_2); let payable_dao = PayableDaoMock::new().mark_pending_payables_rowids_result(Err( @@ -1635,7 +1749,7 @@ mod tests { )); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let sent_payables = SentPayables { payment_procedure_result: Ok(vec![ @@ -1645,99 +1759,36 @@ mod tests { response_skeleton_opt: None, }; - let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { - subject.finish_scan(sent_payables, &Logger::new(test_name)) - })); - - let caught_panic = caught_panic_in_err.unwrap_err(); - let panic_msg = caught_panic.downcast_ref::().unwrap(); - assert_eq!( - panic_msg, - "Unable to create a mark in the payable table for wallets 0x00000000000\ - 000000000000000626c6168313131, 0x00000000000000000000000000626c6168323232 due to \ - SignConversion(9999999999999)" - ); - } - - #[test] - fn payable_scanner_mark_pending_payable_only_panics_all_fingerprints_found() { - init_test_logging(); - let test_name = "payable_scanner_mark_pending_payable_only_panics_all_fingerprints_found"; - let hash_1 = make_tx_hash(248); - let hash_2 = make_tx_hash(139); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(7879, hash_1), (7881, hash_2)], - no_rowid_results: vec![], - }); - - assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name, - pending_payable_dao, - hash_1, - hash_2, - ); - - // Missing fingerprints, being an additional issue, would provoke an error log, but not here. - TestLogHandler::new().exists_no_log_containing(&format!("ERROR: {test_name}:")); + subject.finish_scan(sent_payables, &Logger::new(test_name)); } #[test] - fn payable_scanner_mark_pending_payable_panics_nonexistent_fingerprints_also_found() { + fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist() { init_test_logging(); let test_name = - "payable_scanner_mark_pending_payable_panics_nonexistent_fingerprints_also_found"; - let hash_1 = make_tx_hash(0xff); - let hash_2 = make_tx_hash(0xf8); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(7881, hash_1)], - no_rowid_results: vec![hash_2], - }); - - assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name, - pending_payable_dao, - hash_1, - hash_2, - ); - - TestLogHandler::new().exists_log_containing(&format!("ERROR: {test_name}: Expected pending payable \ - fingerprints for (tx: 0x00000000000000000000000000000000000000000000000000000000000000f8, to wallet: \ - 0x00000000000000000000000000626c6168323232) were not found; system unreliable")); - } - - #[test] - fn payable_scanner_is_facing_failed_transactions_and_their_fingerprints_exist() { - init_test_logging(); - let test_name = - "payable_scanner_is_facing_failed_transactions_and_their_fingerprints_exist"; - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + "payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist"; + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let hash_tx_1 = make_tx_hash(0x15b3); let hash_tx_2 = make_tx_hash(0x3039); - let first_fingerprint_rowid = 3; - let second_fingerprint_rowid = 5; + let first_sent_tx_rowid = 3; + let second_sent_tx_rowid = 5; let system = System::new(test_name); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (first_fingerprint_rowid, hash_tx_1), - (second_fingerprint_rowid, hash_tx_2), - ], - no_rowid_results: vec![], - }) - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result( + hashmap!(hash_tx_1 => first_sent_tx_rowid, hash_tx_2 => second_sent_tx_rowid), + ) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); let payable_scanner = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let logger = Logger::new(test_name); let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "Attempt failed".to_string(), - hashes: vec![hash_tx_1, hash_tx_2], + hashes: hashset![hash_tx_1, hash_tx_2], }), response_skeleton_opt: None, }; @@ -1760,26 +1811,25 @@ mod tests { ); assert_eq!(aware_of_unresolved_pending_payable_before, false); assert_eq!(aware_of_unresolved_pending_payable_after, false); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); - assert_eq!( - *fingerprints_rowids_params, - vec![vec![hash_tx_1, hash_tx_2]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *delete_fingerprints_params, - vec![vec![first_fingerprint_rowid, second_fingerprint_rowid]] - ); + let sent_tx_rowids_params = get_tx_identifiers_params_arc.lock().unwrap(); + assert_eq!(*sent_tx_rowids_params, vec![hashset!(hash_tx_1, hash_tx_2)]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset!(hash_tx_1, hash_tx_2)]); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!("WARN: {test_name}: \ - Any persisted data from failed process will be deleted. Caused by: Sending phase: \"Attempt failed\". \ - Signed and hashed transactions: 0x000000000000000000000000000000000000000000000000000\ - 00000000015b3, 0x0000000000000000000000000000000000000000000000000000000000003039")); - log_handler.exists_log_containing( - &format!("WARN: {test_name}: \ - Deleting fingerprints for failed transactions 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Any persisted data from the failed process will be deleted. Caused by: Sending phase: \ + \"Attempt failed\". \ + Signed and hashed txs: \ + 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + 0x0000000000000000000000000000000000000000000000000000000000003039" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Deleting sent payable records for \ + 0x00000000000000000000000000000000000000000000000000000000000015b3, \ 0x0000000000000000000000000000000000000000000000000000000000003039", - )); + )); // we haven't supplied any result for mark_pending_payable() and so it's proved uncalled } @@ -1809,15 +1859,15 @@ mod tests { "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" )); log_handler.exists_log_containing(&format!( - "DEBUG: {test_name}: Ignoring a non-fatal error on our end from before \ - the transactions are hashed: LocallyCausedError(Signing(\"Some error\"))" + "DEBUG: {test_name}: A non-fatal error LocallyCausedError(Signing(\"Some error\")) \ + will be ignored as it is from before any tx could even be hashed" )); } #[test] - fn payable_scanner_finds_fingerprints_for_failed_payments_but_panics_at_their_deletion() { + fn payable_scanner_finds_sent_tx_record_for_failed_payments_but_panics_at_their_deletion() { let test_name = - "payable_scanner_finds_fingerprints_for_failed_payments_but_panics_at_their_deletion"; + "payable_scanner_finds_sent_tx_record_for_failed_payments_but_panics_at_their_deletion"; let rowid_1 = 4; let hash_1 = make_tx_hash(0x7b); let rowid_2 = 6; @@ -1825,20 +1875,17 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "blah".to_string(), - hashes: vec![hash_1, hash_2], + hashes: hashset![hash_1, hash_2], }), response_skeleton_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(rowid_1, hash_1), (rowid_2, hash_2)], - no_rowid_results: vec![], - }) - .delete_fingerprints_result(Err(PendingPayableDaoError::RecordDeletion( - "Gosh, I overslept without an alarm set".to_string(), + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(hash_1 => rowid_1, hash_2 => rowid_2)) + .delete_records_result(Err(SentPayableDaoError::SqlExecutionFailed( + "I overslept since my brain thinks the alarm is just a lullaby".to_string(), ))); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { @@ -1849,37 +1896,34 @@ mod tests { let panic_msg = caught_panic.downcast_ref::().unwrap(); assert_eq!( panic_msg, - "Database corrupt: payable fingerprint deletion for transactions \ + "Database corrupt: sent payable record deletion for txs \ 0x000000000000000000000000000000000000000000000000000000000000007b, 0x00000000000000000000\ - 00000000000000000000000000000000000000000315 failed due to RecordDeletion(\"Gosh, I overslept \ - without an alarm set\")"); + 00000000000000000000000000000000000000000315 failed due to SqlExecutionFailed(\"I overslept \ + since my brain thinks the alarm is just a lullaby\")"); let log_handler = TestLogHandler::new(); - // There is a possible situation when we stumble over missing fingerprints, so we log it. + // There's a possibility that we stumble over missing sent tx records, so we log it. // Here we don't and so any ERROR log shouldn't turn up log_handler.exists_no_log_containing(&format!("ERROR: {}", test_name)) } #[test] - fn payable_scanner_panics_for_missing_fingerprints_but_deletion_of_some_works() { + fn payable_scanner_panics_for_missing_sent_tx_records_but_deletion_of_some_works() { init_test_logging(); let test_name = - "payable_scanner_panics_for_missing_fingerprints_but_deletion_of_some_works"; + "payable_scanner_panics_for_missing_sent_tx_records_but_deletion_of_some_works"; let hash_1 = make_tx_hash(0x1b669); let hash_2 = make_tx_hash(0x3039); let hash_3 = make_tx_hash(0x223d); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(333, hash_1)], - no_rowid_results: vec![hash_2, hash_3], - }) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(hash_1 => 333)) + .delete_records_result(Ok(())); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "SQLite migraine".to_string(), - hashes: vec![hash_1, hash_2, hash_3], + hashes: hashset![hash_1, hash_2, hash_3], }), response_skeleton_opt: None, }; @@ -1890,41 +1934,42 @@ mod tests { let caught_panic = caught_panic_in_err.unwrap_err(); let panic_msg = caught_panic.downcast_ref::().unwrap(); - assert_eq!(panic_msg, "Ran into failed transactions 0x0000000000000000000000000000000000\ - 000000000000000000000000003039, 0x000000000000000000000000000000000000000000000000000000000000223d \ - with missing fingerprints. System no longer reliable"); + assert_eq!( + panic_msg, + "Ran into failed payables \ + 0x000000000000000000000000000000000000000000000000000000000000223d, \ + 0x0000000000000000000000000000000000000000000000000000000000003039 \ + with missing records. The system has become unreliable" + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - &format!("WARN: {test_name}: Any persisted data from failed process will be deleted. Caused by: \ - Sending phase: \"SQLite migraine\". Signed and hashed transactions: \ - 0x000000000000000000000000000000000000000000000000000000000001b669, \ - 0x0000000000000000000000000000000000000000000000000000000000003039, \ - 0x000000000000000000000000000000000000000000000000000000000000223d")); log_handler.exists_log_containing(&format!( - "WARN: {test_name}: Deleting fingerprints for failed transactions {:?}", + "WARN: {test_name}: Any persisted data from the failed process will \ + be deleted. Caused by: Sending phase: \"SQLite migraine\". Signed and hashed txs: \ + 0x000000000000000000000000000000000000000000000000000000000000223d, \ + 0x0000000000000000000000000000000000000000000000000000000000003039, \ + 0x000000000000000000000000000000000000000000000000000000000001b669" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Deleting sent payable records for {:?}", hash_1 )); } #[test] - fn payable_scanner_for_failed_rpcs_one_fingerprint_missing_and_deletion_of_the_other_one_fails() - { - // Two fatal failures at once, missing fingerprints and fingerprint deletion error are both - // legitimate reasons for panic + fn payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails() { + // Two fatal failures at once, missing sent tx records and another record deletion error + // are both legitimate reasons for panic init_test_logging(); - let test_name = "payable_scanner_for_failed_rpcs_one_fingerprint_missing_and_deletion_of_the_other_one_fails"; + let test_name = "payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails"; let existent_record_hash = make_tx_hash(0xb26e); let nonexistent_record_hash = make_tx_hash(0x4d2); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(45, existent_record_hash)], - no_rowid_results: vec![nonexistent_record_hash], - }) - .delete_fingerprints_result(Err(PendingPayableDaoError::RecordDeletion( + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(existent_record_hash => 45)) + .delete_records_result(Err(SentPayableDaoError::SqlExecutionFailed( "Another failure. Really???".to_string(), ))); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let failed_payment_1 = RpcPayableFailure { rpc_error: Error::Unreachable, @@ -1952,20 +1997,33 @@ mod tests { let panic_msg = caught_panic.downcast_ref::().unwrap(); assert_eq!( panic_msg, - "Database corrupt: payable fingerprint deletion for transactions 0x00000000000000000000000\ - 0000000000000000000000000000000000000b26e failed due to RecordDeletion(\"Another failure. Really???\")"); + "Database corrupt: sent payable record deletion for txs \ + 0x000000000000000000000000000000000000000000000000000000000000b26e failed due to \ + SqlExecutionFailed(\"Another failure. Really???\")" + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!("WARN: {test_name}: Remote transaction failure: 'Server is unreachable' \ - for payment to 0x0000000000000000000000000000000000616263 and transaction hash 0x00000000000000000000000\ - 0000000000000000000000000000000000000b26e. Please check your blockchain service URL configuration.")); - log_handler.exists_log_containing(&format!("WARN: {test_name}: Remote transaction failure: 'Internal Web3 error' \ - for payment to 0x0000000000000000000000000000000000646566 and transaction hash 0x000000000000000000000000\ - 00000000000000000000000000000000000004d2. Please check your blockchain service URL configuration.")); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Remote sent payable \ + failure 'Server is unreachable' for wallet 0x0000000000000000000000000000000000616263 \ + and tx hash 0x000000000000000000000000000000000000000000000000000000000000b26e" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Remote sent payable \ + failure 'Internal Web3 error' for wallet 0x0000000000000000000000000000000000646566 \ + and tx hash 0x00000000000000000000000000000000000000000000000000000000000004d2" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Please check your blockchain service URL configuration due to detected remote failures" + )); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of 2 attempts" )); - log_handler.exists_log_containing(&format!("ERROR: {test_name}: Ran into failed transactions 0x0000000000000000\ - 0000000000000000000000000000000000000000000004d2 with missing fingerprints. System no longer reliable")); + log_handler.exists_log_containing(&format!( + "ERROR: {test_name}: Ran into failed \ + payables 0x00000000000000000000000000000000000000000000000000000000000004d2 with missing \ + records. The system has become unreliable" + )); } #[test] @@ -2029,7 +2087,7 @@ mod tests { gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) )] ) - //no other method was called (absence of panic) and that means we returned early + //no other method was called (absence of panic), and that means we returned early } #[test] @@ -2154,9 +2212,9 @@ mod tests { assert_eq!(result, vec![qualified_payable]); TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {}: Paying qualified debts:\n999,999,999,000,000,\ - 000 wei owed for \\d+ sec exceeds threshold: 500,000,000,000,000,000 wei; creditor: \ - 0x0000000000000000000000000077616c6c657430", + "DEBUG: {}: Paying qualified debts:\n\ + 999,999,999,000,000,000 wei owed for \\d+ sec exceeds the threshold \ + 500,000,000,000,000,000 wei for creditor 0x0000000000000000000000000077616c6c657430", test_name )); } @@ -2194,28 +2252,18 @@ mod tests { let test_name = "pending_payable_scanner_can_initiate_a_scan"; let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 1, - amount: 7999, - process_error: None, - }; - let fingerprints = vec![payable_fingerprint_1, payable_fingerprint_2]; - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(fingerprints.clone()); + let sent_tx = make_sent_tx(456); + let sent_tx_hash = sent_tx.hash; + let failed_tx = make_failed_tx(789); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![sent_tx.clone()]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(vec![failed_tx.clone()]); let mut subject = make_dull_subject(); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -2231,21 +2279,21 @@ mod tests { true, ); - let no_of_pending_payables = fingerprints.len(); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, Ok(RequestTransactionReceipts { - pending_payable_fingerprints: fingerprints, + tx_hashes: vec![ + TxHashByTable::SentPayable(sent_tx_hash), + TxHashByTable::FailedPayable(failed_tx.hash) + ], response_skeleton_opt: None }) ); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!("INFO: {test_name}: Scanning for pending payable"), - &format!( - "DEBUG: {test_name}: Found {no_of_pending_payables} pending payables to process" - ), + &format!("DEBUG: {test_name}: Found 1 pending payables and 1 unfinalized failures to process"), ]) } @@ -2254,10 +2302,15 @@ mod tests { let now = SystemTime::now(); let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = make_dull_subject(); - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let sent_payable_dao = + SentPayableDaoMock::new().retrieve_txs_result(vec![make_sent_tx(123)]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(vec![make_failed_tx(456)]); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -2400,24 +2453,6 @@ mod tests { ); } - #[test] - fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { - let now = SystemTime::now(); - let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let pending_payable_dao = - PendingPayableDaoMock::new().return_all_errorless_fingerprints_result(vec![]); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - let result = - pending_payable_scanner.start_scan(&consuming_wallet, now, None, &Logger::new("test")); - - let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); - assert_eq!(result, Err(StartScanError::NothingToProcess)); - assert_eq!(is_scan_running, false); - } - #[test] fn check_general_conditions_for_pending_payable_scan_if_it_is_initial_pending_payable_scan() { let mut subject = make_dull_subject(); @@ -2430,60 +2465,110 @@ mod tests { } #[test] - fn pending_payable_scanner_handles_report_transaction_receipts_message() { + fn pending_payable_scanner_handles_tx_receipts_message() { + // Note: the choice of those hashes isn't random; I tried to make sure I will know the order, + // in which these records will be processed, because they are in an ordered map. + // It is important because otherwise preparation of results with the mocks would become + // chaotic, as long as you care about the exact receiver of the mock call among these records init_test_logging(); - let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message"; + let test_name = "pending_payable_scanner_handles_tx_receipts_message"; + // Normal confirmation let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); + // FailedTx reclaim + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + // New tx failure + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + // Validation failures + let update_statuses_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_failed_payable_params_arc = Arc::new(Mutex::new(vec![])); + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_millis(12)); + let timestamp_c = SystemTime::now().sub(Duration::from_millis(1234)); let payable_dao = PayableDaoMock::new() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::new().delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::new() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .update_statuses_params(&update_statuses_pending_payable_params_arc) + .update_statuses_result(Ok(())) + .replace_records_result(Ok(())) + .delete_records_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::new() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())) + .update_statuses_params(&update_statuses_failed_payable_params_arc) + .update_statuses_result(Ok(())) + .delete_records_result(Ok(())); + let tx_hash_1 = make_tx_hash(0x111); + let mut sent_tx_1 = make_sent_tx(123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(333), + block_number: U64::from(1234), + }; + let tx_status_1 = StatusReadFromReceiptCheck::Succeeded(tx_block_1); + let tx_hash_2 = make_tx_hash(0x222); + let mut failed_tx_2 = make_failed_tx(789); + failed_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(222), + block_number: U64::from(2345), + }; + let tx_status_2 = StatusReadFromReceiptCheck::Succeeded(tx_block_2); + let tx_hash_3 = make_tx_hash(0x333); + let mut sent_tx_3 = make_sent_tx(456); + sent_tx_3.hash = tx_hash_3; + let tx_status_3 = StatusReadFromReceiptCheck::Pending; + let tx_hash_4 = make_tx_hash(0x444); + let mut sent_tx_4 = make_sent_tx(4567); + sent_tx_4.hash = tx_hash_4; + sent_tx_4.status = TxStatus::Pending(ValidationStatus::Waiting); + let tx_receipt_rpc_error_4 = AppRpcError::Remote(RemoteError::Unreachable); + let tx_hash_5 = make_tx_hash(0x555); + let mut failed_tx_5 = make_failed_tx(888); + failed_tx_5.hash = tx_hash_5; + failed_tx_5.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ))); + let tx_receipt_rpc_error_5 = + AppRpcError::Remote(RemoteError::InvalidResponse("game over".to_string())); + let tx_hash_6 = make_tx_hash(0x666); + let mut sent_tx_6 = make_sent_tx(789); + sent_tx_6.hash = tx_hash_6; + let tx_status_6 = StatusReadFromReceiptCheck::Reverted; + let sent_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(sent_tx_1.clone())) + .get_record_by_hash_result(Some(sent_tx_3.clone())) + .get_record_by_hash_result(Some(sent_tx_4)) + .get_record_by_hash_result(Some(sent_tx_6.clone())); + let failed_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(failed_tx_2.clone())) + .get_record_by_hash_result(Some(failed_tx_5)); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b); let mut pending_payable_scanner = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .validation_failure_clock(Box::new(validation_failure_clock)) .build(); - let transaction_hash_1 = make_tx_hash(4545); - let transaction_receipt_1 = TxReceipt { - transaction_hash: transaction_hash_1, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(1234), - }), - }; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, - }; - let transaction_hash_2 = make_tx_hash(1234); - let transaction_receipt_2 = TxReceipt { - transaction_hash: transaction_hash_2, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(2345), - }), - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 10, - timestamp: from_unix_timestamp(199_780_000), - hash: transaction_hash_2, - attempt: 15, - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_1), - fingerprint_1.clone(), - ), - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_2), - fingerprint_2.clone(), - ), + let msg = TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(tx_hash_1) => Ok(tx_status_1), + TxHashByTable::FailedPayable(tx_hash_2) => Ok(tx_status_2), + TxHashByTable::SentPayable(tx_hash_3) => Ok(tx_status_3), + TxHashByTable::SentPayable(tx_hash_4) => Err(tx_receipt_rpc_error_4), + TxHashByTable::FailedPayable(tx_hash_5) => Err(tx_receipt_rpc_error_5), + TxHashByTable::SentPayable(tx_hash_6) => Ok(tx_status_6), ], response_skeleton_opt: None, }; @@ -2493,54 +2578,75 @@ mod tests { let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); assert_eq!( result, - PendingPayableScanResult::NoPendingPayablesLeft(None) + PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) ); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + assert_eq!(*transactions_confirmed_params, vec![vec![sent_tx_1]]); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); + assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); + let sent_tx_2 = SentTx::from((failed_tx_2, tx_block_2)); + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failure_for_tx_3 = FailedTx::from((sent_tx_3, FailureReason::PendingTooLong)); + let expected_failure_for_tx_6 = FailedTx::from((sent_tx_6, FailureReason::Reverted)); assert_eq!( - *transactions_confirmed_params, - vec![vec![fingerprint_1, fingerprint_2]] + *insert_new_records_params, + vec![vec![expected_failure_for_tx_3, expected_failure_for_tx_6]] + ); + let update_statuses_pending_payable_params = + update_statuses_pending_payable_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_pending_payable_params, + vec![ + hashmap!(tx_hash_4 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_a))))) + ] + ); + let update_statuses_failed_payable_params = + update_statuses_failed_payable_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_failed_payable_params, + vec![ + hashmap!(tx_hash_5 => FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_c)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_b))))) + ] ); assert_eq!(subject.scan_started_at(ScanType::PendingPayables), None); - TestLogHandler::new().assert_logs_match_in_order(vec![ - &format!( - "INFO: {}: Transactions {:?}, {:?} completed their confirmation process succeeding", - test_name, transaction_hash_1, transaction_hash_2 - ), - &format!("INFO: {test_name}: The PendingPayables scan ended in \\d+ms."), - ]); + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Processing receipts for 6 txs" + )); + test_log_handler.exists_log_containing(&format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000000000000000000000000000000000000000000000000000000444): Remote(Unreachable). Will retry receipt retrieval next cycle")); + test_log_handler.exists_log_containing(&format!("WARN: {test_name}: Failed to retrieve tx receipt for FailedPayable(0x0000000000000000000000000000000000000000000000000000000000000555): Remote(InvalidResponse(\"game over\")). Will retry receipt retrieval next cycle")); + test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000222 (block 2345) as confirmed on-chain")); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000111 (block 1234) was confirmed", + )); + test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000333, 0x0000000000000000000000000000000000000000000000000000000000000666 were processed in the db")); } #[test] + #[should_panic( + expected = "We should never receive an empty list of results. Even receipts that could not \ + be retrieved can be interpreted" + )] fn pending_payable_scanner_handles_empty_report_transaction_receipts_message() { - init_test_logging(); - let test_name = - "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], + let msg = TxReceiptsMessage { + results: hashmap![], response_skeleton_opt: None, }; pending_payable_scanner.mark_as_started(SystemTime::now()); let mut subject = make_dull_subject(); subject.pending_payable = Box::new(pending_payable_scanner); - let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - - let is_scan_running = subject.scan_started_at(ScanType::PendingPayables).is_some(); - assert_eq!( - result, - PendingPayableScanResult::NoPendingPayablesLeft(None) - ); - assert_eq!(is_scan_running, false); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!( - "WARN: {test_name}: No transaction receipts found." - )); - tlh.exists_log_matching(&format!( - "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." - )); + let _ = subject.finish_pending_payable_scan(msg, &Logger::new("test")); } #[test] @@ -2726,8 +2832,10 @@ mod tests { } #[test] - #[should_panic(expected = "Attempt to set new start block to 6709 failed due to: \ - UninterpretableValue(\"Illiterate database manager\")")] + #[should_panic( + expected = "Attempt to advance the start block to 6709 failed due to: \ + UninterpretableValue(\"Illiterate database manager\")" + )] fn no_transactions_received_but_start_block_setting_fails() { init_test_logging(); let test_name = "no_transactions_received_but_start_block_setting_fails"; @@ -2972,7 +3080,7 @@ mod tests { subject.signal_scanner_completion(ScanType::Receivables, SystemTime::now(), &logger); TestLogHandler::new().exists_log_containing(&format!( - "ERROR: {test_name}: Called scan_finished() for Receivables scanner but timestamp was not found" + "ERROR: {test_name}: Called scan_finished() for Receivables scanner but could not find any timestamp" )); } @@ -3023,7 +3131,7 @@ mod tests { &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PendingPayableScannerBuilder::new().build(), "PendingPayables", test_name, diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index cfb874f19..f501a7be2 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -1,37 +1,67 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod test_utils; +mod tx_receipt_interpreter; pub mod utils; -use std::cell::RefCell; -use std::rc::Rc; -use std::time::SystemTime; -use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; -use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; -use crate::accountant::db_access_objects::payable_dao::PayableDao; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; -use crate::accountant::{comma_joined_stringifiable, PendingPayableId, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables}; -use crate::accountant::scanners::{PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner}; -use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedTx, FailureRetrieveCondition, FailureStatus, +}; +use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDao, SentPayableDaoError, SentTx, TxStatus, +}; +use crate::accountant::db_access_objects::utils::{TxHash, TxRecordWithHash}; +use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, MismatchReport, PendingPayableCache, PendingPayableScanResult, + PresortedTxFailure, ReceiptScanReport, RecheckRequiringFailures, Retry, TxByTable, + TxCaseToBeInterpreted, TxHashByTable, UpdatableValidationStatus, +}; +use crate::accountant::scanners::{ + PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, +}; +use crate::accountant::{ + comma_joined_stringifiable, RequestTransactionReceipts, ResponseSkeleton, + ScanForPendingPayables, TxReceiptResult, TxReceiptsMessage, +}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; +use crate::blockchain::errors::validation_status::{ + ValidationFailureClock, ValidationFailureClockReal, +}; use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; use crate::sub_lib::wallet::Wallet; use crate::time_marking_methods; +use itertools::{Either, Itertools}; +use masq_lib::logger::Logger; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::rc::Rc; +use std::str::FromStr; +use std::time::SystemTime; +use thousands::Separable; +use web3::types::H256; pub struct PendingPayableScanner { pub common: ScannerCommon, pub payable_dao: Box, - pub pending_payable_dao: Box, - pub when_pending_too_long_sec: u64, + pub sent_payable_dao: Box, + pub failed_payable_dao: Box, pub financial_statistics: Rc>, + pub current_sent_payables: Box>, + pub yet_unproven_failed_payables: Box>, + pub clock: Box, } impl PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, > for PendingPayableScanner { @@ -49,756 +79,1975 @@ impl StartableScanner ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); - match filtered_pending_payable.is_empty() { - true => { - self.mark_as_ended(logger); - Err(StartScanError::NothingToProcess) - } - false => { - debug!( - logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - Ok(RequestTransactionReceipts { - pending_payable_fingerprints: filtered_pending_payable, - response_skeleton_opt, - }) - } + + let pending_tx_hashes_opt = self.handle_pending_payables(); + let failure_hashes_opt = self.handle_unproven_failures(); + + if pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() { + self.mark_as_ended(logger); + return Err(StartScanError::NothingToProcess); } + + Self::log_records_found_for_receipt_check( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + logger, + ); + + let all_hashes = pending_tx_hashes_opt + .unwrap_or_default() + .into_iter() + .chain(failure_hashes_opt.unwrap_or_default()) + .collect_vec(); + + Ok(RequestTransactionReceipts { + tx_hashes: all_hashes, + response_skeleton_opt, + }) } } -impl Scanner for PendingPayableScanner { +impl Scanner for PendingPayableScanner { fn finish_scan( &mut self, - message: ReportTransactionReceipts, + message: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { let response_skeleton_opt = message.response_skeleton_opt; - let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { - true => { - warning!(logger, "No transaction receipts found."); - todo!("This requires the payment retry. GH-631 must be completed first"); - } - false => { - debug!( - logger, - "Processing receipts for {} transactions", - message.fingerprints_with_receipts.len() - ); - let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - let requires_payment_retry = - self.process_transactions_by_reported_state(scan_report, logger); + let scan_report = self.interpret_tx_receipts(message, logger); - self.mark_as_ended(logger); + let retry_opt = scan_report.requires_payments_retry(); - requires_payment_retry - } - }; + self.process_txs_by_state(scan_report, logger); - if requires_payment_retry { - PendingPayableScanResult::PaymentRetryRequired - } else { - let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }); - PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) - } + self.mark_as_ended(logger); + + Self::compose_scan_result(retry_opt, response_skeleton_opt) } time_marking_methods!(PendingPayables); as_any_ref_in_trait_impl!(); + + as_any_mut_in_trait_impl!(); } impl PendingPayableScanner { pub fn new( payable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, + failed_payable_dao: Box, payment_thresholds: Rc, - when_pending_too_long_sec: u64, financial_statistics: Rc>, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, - pending_payable_dao, - when_pending_too_long_sec, + sent_payable_dao, + failed_payable_dao, financial_statistics, + current_sent_payables: Box::new(CurrentPendingPayables::default()), + yet_unproven_failed_payables: Box::new(RecheckRequiringFailures::default()), + clock: Box::new(ValidationFailureClockReal::default()), } } - fn handle_receipts_for_pending_transactions( - &self, - msg: ReportTransactionReceipts, + fn handle_pending_payables(&mut self) -> Option> { + let pending_txs = self + .sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::IsPending)); + + if pending_txs.is_empty() { + return None; + } + + let pending_tx_hashes = Self::get_wrapped_hashes(&pending_txs, TxHashByTable::SentPayable); + self.current_sent_payables.load_cache(pending_txs); + Some(pending_tx_hashes) + } + + fn handle_unproven_failures(&mut self) -> Option> { + let failures = self + .failed_payable_dao + .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)); + + if failures.is_empty() { + return None; + } + + let failure_hashes = Self::get_wrapped_hashes(&failures, TxHashByTable::FailedPayable); + self.yet_unproven_failed_payables.load_cache(failures); + Some(failure_hashes) + } + + fn get_wrapped_hashes( + records: &[Record], + wrap_the_hash: fn(TxHash) -> TxHashByTable, + ) -> Vec + where + Record: TxRecordWithHash, + { + records + .iter() + .map(|record| wrap_the_hash(record.hash())) + .collect_vec() + } + + fn emptiness_check(&self, msg: &TxReceiptsMessage) { + if msg.results.is_empty() { + panic!( + "We should never receive an empty list of results. \ + Even receipts that could not be retrieved can be interpreted" + ) + } + } + + fn compose_scan_result( + retry_opt: Option, + response_skeleton_opt: Option, + ) -> PendingPayableScanResult { + if let Some(retry) = retry_opt { + if let Some(response_skeleton) = response_skeleton_opt { + let ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }; + PendingPayableScanResult::PaymentRetryRequired(Either::Right(ui_msg)) + } else { + PendingPayableScanResult::PaymentRetryRequired(Either::Left(retry)) + } + } else { + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) + } + } + + fn interpret_tx_receipts( + &mut self, + msg: TxReceiptsMessage, + logger: &Logger, + ) -> ReceiptScanReport { + self.emptiness_check(&msg); + + debug!(logger, "Processing receipts for {} txs", msg.results.len()); + + let interpretable_data = self.prepare_cases_to_interpret(msg, logger); + TxReceiptInterpreter::default().compose_receipt_scan_report( + interpretable_data, + &self, + logger, + ) + } + + fn prepare_cases_to_interpret( + &mut self, + msg: TxReceiptsMessage, logger: &Logger, - ) -> PendingPayableScanReport { - let scan_report = PendingPayableScanReport::default(); - msg.fingerprints_with_receipts.into_iter().fold( - scan_report, - |scan_report_so_far, (receipt_result, fingerprint)| match receipt_result { - TransactionReceiptResult::RpcResponse(tx_receipt) => match tx_receipt.status { - TxStatus::Pending => handle_none_receipt( - scan_report_so_far, - fingerprint, - "none was given", - logger, - ), - TxStatus::Failed => { - handle_status_with_failure(scan_report_so_far, fingerprint, logger) + ) -> Vec { + let init: Either, MismatchReport> = Either::Left(vec![]); + let either = msg + .results + .into_iter() + // This must be in for predictability in tests + .sorted_by_key(|(hash_by_table, _)| hash_by_table.hash()) + .fold( + init, + |acc, (tx_hash_by_table, tx_receipt_result)| match acc { + Either::Left(cases) => { + self.resolve_real_query(cases, tx_receipt_result, tx_hash_by_table) } - TxStatus::Succeeded(_) => { - handle_status_with_success(scan_report_so_far, fingerprint, logger) + Either::Right(mut mismatch_report) => { + mismatch_report.remaining_hashes.push(tx_hash_by_table); + Either::Right(mismatch_report) } }, - TransactionReceiptResult::LocalError(e) => handle_none_receipt( - scan_report_so_far, - fingerprint, - &format!("failed due to {}", e), - logger, - ), - }, + ); + + let cases = match either { + Either::Left(cases) => cases, + Either::Right(mismatch_report) => self.panic_dump(mismatch_report), + }; + + self.current_sent_payables.ensure_empty_cache(logger); + self.yet_unproven_failed_payables.ensure_empty_cache(logger); + + cases + } + + fn resolve_real_query( + &mut self, + mut cases: Vec, + receipt_result: TxReceiptResult, + looked_up_hash: TxHashByTable, + ) -> Either, MismatchReport> { + match looked_up_hash { + TxHashByTable::SentPayable(tx_hash) => { + match self.current_sent_payables.get_record_by_hash(tx_hash) { + Some(sent_tx) => { + cases.push(TxCaseToBeInterpreted::new( + TxByTable::SentPayable(sent_tx), + receipt_result, + )); + Either::Left(cases) + } + None => Either::Right(MismatchReport { + noticed_with: looked_up_hash, + remaining_hashes: vec![], + }), + } + } + TxHashByTable::FailedPayable(tx_hash) => { + match self + .yet_unproven_failed_payables + .get_record_by_hash(tx_hash) + { + Some(failed_tx) => { + cases.push(TxCaseToBeInterpreted::new( + TxByTable::FailedPayable(failed_tx), + receipt_result, + )); + Either::Left(cases) + } + None => Either::Right(MismatchReport { + noticed_with: looked_up_hash, + remaining_hashes: vec![], + }), + } + } + } + } + + fn panic_dump(&mut self, mismatch_report: MismatchReport) -> ! { + fn rearrange(hashmap: HashMap) -> Vec { + hashmap + .into_iter() + .sorted_by_key(|(tx_hash, _)| *tx_hash) + .map(|(_, record)| record) + .collect_vec() + } + + panic!( + "Looking up '{:?}' in the cache, the record could not be found. Dumping \ + the remaining values. Pending payables: {:?}. Unproven failures: {:?}. \ + Hashes yet not looked up: {:?}.", + mismatch_report.noticed_with, + rearrange(self.current_sent_payables.dump_cache()), + rearrange(self.yet_unproven_failed_payables.dump_cache()), + mismatch_report.remaining_hashes ) } - fn process_transactions_by_reported_state( + fn process_txs_by_state(&mut self, scan_report: ReceiptScanReport, logger: &Logger) { + self.handle_confirmed_transactions(scan_report.confirmations, logger); + self.handle_failed_transactions(scan_report.failures, logger); + } + + fn handle_confirmed_transactions( &mut self, - scan_report: PendingPayableScanReport, + confirmed_txs: DetectedConfirmations, logger: &Logger, - ) -> bool { - let requires_payments_retry = scan_report.requires_payments_retry(); + ) { + self.handle_tx_failure_reclaims(confirmed_txs.reclaims, logger); + self.handle_normal_confirmations(confirmed_txs.normal_confirmations, logger); + } + + fn handle_tx_failure_reclaims(&mut self, reclaimed: Vec, logger: &Logger) { + if reclaimed.is_empty() { + return; + } + + let hashes_and_blocks = Self::collect_and_sort_hashes_and_blocks(&reclaimed); + + self.replace_sent_tx_records(&reclaimed, &hashes_and_blocks, logger); + + self.delete_failed_tx_records(&hashes_and_blocks, logger); + + self.add_to_the_total_of_paid_payable(&reclaimed, logger) + } + + fn isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> HashSet { + reclaimed.iter().map(|(tx_hash, _)| *tx_hash).collect() + } + + fn collect_and_sort_hashes_and_blocks(sent_txs: &[SentTx]) -> Vec<(TxHash, TxBlock)> { + Self::collect_hashes_and_blocks(sent_txs) + .into_iter() + .sorted() + .collect_vec() + } - self.confirm_transactions(scan_report.confirmed, logger); - self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger); + fn collect_hashes_and_blocks(reclaimed: &[SentTx]) -> HashMap { + reclaimed + .iter() + .map(|reclaim| { + let tx_block = if let TxStatus::Confirmed { block_hash, block_number, .. } = + &reclaim.status + { + TxBlock{ + block_hash: H256::from_str(&block_hash[2..]).expect("Failed to construct hash from str"), + block_number: (*block_number).into() + } + } else { + panic!( + "Processing a reclaim for tx {:?} which isn't filled with the confirmation details", + reclaim.hash + ) + }; + (reclaim.hash, tx_block) + }) + .collect() + } - requires_payments_retry + fn replace_sent_tx_records( + &self, + sent_txs_to_reclaim: &[SentTx], + hashes_and_blocks: &[(TxHash, TxBlock)], + logger: &Logger, + ) { + match self.sent_payable_dao.replace_records(sent_txs_to_reclaim) { + Ok(_) => { + debug!(logger, "Replaced records for txs being reclaimed") + } + Err(e) => { + panic!( + "Unable to proceed in a reclaim as the replacement of sent tx records \ + {} failed due to: {:?}", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + format!("{:?}", tx_hash) + }), + e + ) + } + } } - fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.increment_scan_attempts(&rowids) { - Ok(_) => trace!( + fn delete_failed_tx_records(&self, hashes_and_blocks: &[(TxHash, TxBlock)], logger: &Logger) { + let hashes = Self::isolate_hashes(hashes_and_blocks); + match self.failed_payable_dao.delete_records(&hashes) { + Ok(_) => { + info!( logger, - "Updated records for rowids: {} ", - comma_joined_stringifiable(&rowids, |id| id.to_string()) - ), - Err(e) => panic!( - "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", - PendingPayableId::serialize_hashes_to_string(&ids), + "Reclaimed txs {} as confirmed on-chain", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, tx_block)| { + format!("{:?} (block {})", tx_hash, tx_block.block_number) + }) + ) + } + Err(e) => { + panic!( + "Unable to delete failed tx records {} to finish the reclaims due to: {:?}", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + format!("{:?}", tx_hash) + }), e - ), + ) + } + } + } + + fn handle_normal_confirmations(&mut self, confirmed_txs: Vec, logger: &Logger) { + if confirmed_txs.is_empty() { + return; + } + + self.confirm_transactions(&confirmed_txs); + + self.update_tx_blocks(&confirmed_txs, logger); + + self.add_to_the_total_of_paid_payable(&confirmed_txs, logger); + } + + fn confirm_transactions(&self, confirmed_sent_txs: &[SentTx]) { + if let Err(e) = self.payable_dao.transactions_confirmed(confirmed_sent_txs) { + Self::transaction_confirmed_panic(confirmed_sent_txs, e); + } + } + + fn update_tx_blocks(&self, confirmed_sent_txs: &[SentTx], logger: &Logger) { + let tx_confirmations = Self::collect_hashes_and_blocks(confirmed_sent_txs); + + if let Err(e) = self.sent_payable_dao.confirm_txs(&tx_confirmations) { + Self::update_tx_blocks_panic(&tx_confirmations, e); + } else { + Self::log_tx_success(logger, &tx_confirmations); + } + } + + fn log_tx_success(logger: &Logger, tx_hashes_and_tx_blocks: &HashMap) { + logger.info(|| { + let pretty_pairs = tx_hashes_and_tx_blocks + .iter() + .sorted() + .map(|(hash, tx_confirmation)| { + format!("{:?} (block {})", hash, tx_confirmation.block_number) + }) + .join(", "); + match tx_hashes_and_tx_blocks.len() { + 1 => format!("Tx {} was confirmed", pretty_pairs), + _ => format!("Txs {} were confirmed", pretty_pairs), + } + }); + } + + fn transaction_confirmed_panic(confirmed_txs: &[SentTx], e: PayableDaoError) -> ! { + panic!( + "Unable to complete the tx confirmation by the adjustment of the payable accounts \ + {} due to: {:?}", + comma_joined_stringifiable( + &confirmed_txs + .iter() + .map(|tx| tx.receiver_address) + .collect_vec(), + |wallet| format!("{:?}", wallet) + ), + e + ) + } + fn update_tx_blocks_panic( + tx_hashes_and_tx_blocks: &HashMap, + e: SentPayableDaoError, + ) -> ! { + panic!( + "Unable to update sent payable records {} by their tx blocks due to: {:?}", + comma_joined_stringifiable( + &tx_hashes_and_tx_blocks.keys().sorted().collect_vec(), + |tx_hash| format!("{:?}", tx_hash) + ), + e + ) + } + + fn add_to_the_total_of_paid_payable(&mut self, confirmed_payments: &[SentTx], logger: &Logger) { + let to_be_added: u128 = confirmed_payments + .iter() + .map(|sent_tx| sent_tx.amount_minor) + .sum(); + + let total_paid_payable = &mut self + .financial_statistics + .borrow_mut() + .total_paid_payable_wei; + + *total_paid_payable += to_be_added; + + debug!( + logger, + "The total paid payables increased by {} to {} wei", + to_be_added.separate_with_commas(), + total_paid_payable.separate_with_commas() + ); + } + + fn handle_failed_transactions(&self, failures: DetectedFailures, logger: &Logger) { + self.handle_tx_failures(failures.tx_failures, logger); + self.handle_rpc_failures(failures.tx_receipt_rpc_failures, logger); + } + + fn handle_tx_failures(&self, failures: Vec, logger: &Logger) { + #[derive(Default)] + struct GroupedFailures { + new_failures: Vec, + rechecks_completed: Vec, + } + + let grouped_failures = + failures + .into_iter() + .fold(GroupedFailures::default(), |mut acc, failure| { + match failure { + PresortedTxFailure::NewEntry(failed_tx) => { + acc.new_failures.push(failed_tx); + } + PresortedTxFailure::RecheckCompleted(tx_hash) => { + acc.rechecks_completed.push(tx_hash); + } + } + acc + }); + + self.add_new_failures(grouped_failures.new_failures, logger); + self.finalize_unproven_failures(grouped_failures.rechecks_completed, logger); + } + + fn add_new_failures(&self, new_failures: Vec, logger: &Logger) { + fn prepare_hashset(failures: &[FailedTx]) -> HashSet { + failures.iter().map(|failure| failure.hash).collect() + } + fn log_procedure_finished(logger: &Logger, new_failures: &[FailedTx]) { + info!( + logger, + "Failed txs {} were processed in the db", + comma_joined_stringifiable(new_failures, |failure| format!("{:?}", failure.hash)) + ) + } + + if new_failures.is_empty() { + return; + } + + if let Err(e) = self.failed_payable_dao.insert_new_records(&new_failures) { + panic!( + "Unable to persist failed txs {} due to: {:?}", + comma_joined_stringifiable(&new_failures, |failure| format!("{:?}", failure.hash)), + e + ) + } + + match self + .sent_payable_dao + .delete_records(&prepare_hashset(&new_failures)) + { + Ok(_) => { + log_procedure_finished(logger, &new_failures); + } + Err(e) => { + panic!( + "Unable to purge sent payable records for failed txs {} due to: {:?}", + comma_joined_stringifiable(&new_failures, |failure| format!( + "{:?}", + failure.hash + )), + e + ) } } } - fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - //TODO this function is imperfect. It waits for GH-663 - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.mark_failures(&rowids) { - Ok(_) => warning!( + fn finalize_unproven_failures(&self, rechecks_completed: Vec, logger: &Logger) { + fn prepare_hashmap(rechecks_completed: &[TxHash]) -> HashMap { + rechecks_completed + .iter() + .map(|tx_hash| (tx_hash.clone(), FailureStatus::Concluded)) + .collect() + } + + if rechecks_completed.is_empty() { + return; + } + + match self + .failed_payable_dao + .update_statuses(&prepare_hashmap(&rechecks_completed)) + { + Ok(_) => { + debug!( logger, - "Broken transactions {} marked as an error. You should take over the care \ - of those to make sure your debts are going to be settled properly. At the moment, \ - there is no automated process fixing that without your assistance", - PendingPayableId::serialize_hashes_to_string(&ids) - ), - Err(e) => panic!( - "Unsuccessful attempt for transactions {} \ - to mark fatal error at payable fingerprint due to {:?}; database unreliable", - PendingPayableId::serialize_hashes_to_string(&ids), + "Concluded failures that had required rechecks: {}.", + comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( + "{:?}", + tx_hash + )) + ); + } + Err(e) => { + panic!( + "Unable to conclude rechecks for failed txs {} due to: {:?}", + comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( + "{:?}", + tx_hash + )), e - ), + ) } } } - fn confirm_transactions( - &mut self, - fingerprints: Vec, + fn handle_rpc_failures(&self, failures: Vec, logger: &Logger) { + if failures.is_empty() { + return; + } + + let (sent_payable_failures, failed_payable_failures): ( + Vec>, + Vec>, + ) = failures.into_iter().partition_map(|failure| match failure { + FailedValidationByTable::SentPayable(failed_validation) => { + Either::Left(failed_validation) + } + FailedValidationByTable::FailedPayable(failed_validation) => { + Either::Right(failed_validation) + } + }); + + self.update_validation_status_for_sent_txs(sent_payable_failures, logger); + + self.update_validation_status_for_failed_txs(failed_payable_failures, logger); + } + + fn update_validation_status_for_sent_txs( + &self, + sent_payable_failures: Vec>, logger: &Logger, ) { - fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { - comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) + if !sent_payable_failures.is_empty() { + let updatable = + Self::prepare_statuses_for_update(&sent_payable_failures, &*self.clock, logger); + if !updatable.is_empty() { + match self.sent_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Pending-tx statuses were processed in the db for validation failure \ + of txs {}", + comma_joined_stringifiable(&sent_payable_failures, |failure| { + format!("{:?}", failure.tx_hash) + }) + ) + } + Err(e) => { + panic!( + "Unable to update pending-tx statuses for validation failures '{:?}' \ + due to: {:?}", + sent_payable_failures, e + ) + } + } + } } + } - if !fingerprints.is_empty() { - if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { - panic!( - "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ - records due to {:?}", serialize_hashes(&fingerprints), e - ) - } else { - self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); - let rowids = fingerprints - .iter() - .map(|fingerprint| fingerprint.rowid) - .collect::>(); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { - panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", - serialize_hashes(&fingerprints), e) - } else { - info!( - logger, - "Transactions {} completed their confirmation process succeeding", - serialize_hashes(&fingerprints) - ) + fn update_validation_status_for_failed_txs( + &self, + failed_txs_validation_failures: Vec>, + logger: &Logger, + ) { + if !failed_txs_validation_failures.is_empty() { + let updatable = Self::prepare_statuses_for_update( + &failed_txs_validation_failures, + &*self.clock, + logger, + ); + if !updatable.is_empty() { + match self.failed_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Failed-tx statuses were processed in the db for validation failure \ + of txs {}", + comma_joined_stringifiable( + &failed_txs_validation_failures, + |failure| { format!("{:?}", failure.tx_hash) } + ) + ) + } + Err(e) => { + panic!( + "Unable to update failed-tx statuses for validation failures '{:?}' \ + due to: {:?}", + failed_txs_validation_failures, e + ) + } } } } } - fn add_to_the_total_of_paid_payable( - &mut self, - fingerprints: &[PendingPayableFingerprint], - serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, + fn prepare_statuses_for_update( + failures: &[FailedValidation], + clock: &dyn ValidationFailureClock, + logger: &Logger, + ) -> HashMap { + failures + .iter() + .flat_map(|failure| { + failure + .new_status(clock) + .map(|tx_status| (failure.tx_hash, tx_status)) + .or_else(|| { + debug!( + logger, + "{}", + PendingPayableScanner::status_not_updatable_log_msg( + &failure.current_status + ) + ); + None + }) + }) + .collect() + } + + fn status_not_updatable_log_msg(status: &dyn Display) -> String { + format!( + "Handling a validation failure, but the status {} cannot be updated.", + status + ) + } + + fn log_records_found_for_receipt_check( + pending_tx_hashes_opt: Option<&Vec>, + failure_hashes_opt: Option<&Vec>, logger: &Logger, ) { - fingerprints.iter().for_each(|fingerprint| { - self.financial_statistics - .borrow_mut() - .total_paid_payable_wei += fingerprint.amount - }); + fn resolve_optional_vec(vec_opt: Option<&Vec>) -> usize { + vec_opt.map(|hashes| hashes.len()).unwrap_or_default() + } + debug!( logger, - "Confirmation of transactions {}; record for total paid payable was modified", - serialize_hashes(fingerprints) + "Found {} pending payables and {} unfinalized failures to process", + resolve_optional_vec(pending_tx_hashes_opt), + resolve_optional_vec(failure_hashes_opt) ); } } #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDaoError, FailureStatus, + }; + use crate::accountant::db_access_objects::payable_dao::PayableDaoError; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, SentPayableDaoError, TxStatus, + }; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PendingPayableScanResult, PresortedTxFailure, + RecheckRequiringFailures, Retry, TxHashByTable, + }; + use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; + use crate::accountant::scanners::test_utils::PendingPayableCacheMock; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner}; + use crate::accountant::test_utils::{ + make_failed_tx, make_sent_tx, make_transaction_block, FailedPayableDaoMock, PayableDaoMock, + PendingPayableScannerBuilder, SentPayableDaoMock, + }; + use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; + use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, + }; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteErrorKind, + }; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; + use crate::test_utils::{make_paying_wallet, make_wallet}; + use itertools::{Either, Itertools}; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use regex::Regex; + use std::collections::HashMap; use std::ops::Sub; + use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use ethereum_types::{H256, U64}; - use regex::Regex; - use web3::types::TransactionReceipt; - use masq_lib::logger::Logger; - use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use crate::accountant::{PendingPayableId, ReportTransactionReceipts, DEFAULT_PENDING_TOO_LONG_SEC}; - use crate::accountant::db_access_objects::payable_dao::PayableDaoError; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoError; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::test_utils::{make_pending_payable_fingerprint, PayableDaoMock, PendingPayableDaoMock, PendingPayableScannerBuilder}; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxReceipt, TxStatus}; - use crate::blockchain::test_utils::make_tx_hash; - - fn assert_interpreting_none_status_for_pending_payable( - test_name: &str, - when_pending_too_long_sec: u64, - pending_payable_age_sec: u64, - rowid: u64, - hash: H256, - ) -> PendingPayableScanReport { - init_test_logging(); - let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: when_sent, - hash, - attempt: 1, - amount: 123, - process_error: None, - }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - - handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) - } - fn assert_log_msg_and_elapsed_time_in_log_makes_sense( - expected_msg: &str, - elapsed_after: u64, - capture_regex: &str, - ) { - let log_handler = TestLogHandler::default(); - let log_idx = log_handler.exists_log_matching(expected_msg); - let log = log_handler.get_log_at(log_idx); - let capture = captures_for_regex_time_in_sec(&log, capture_regex); - assert!(capture <= elapsed_after) - } + #[test] + fn start_scan_fills_in_caches_and_returns_msg() { + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(890); + let failed_tx_hash_2 = failed_tx_2.hash; + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_result(vec![sent_tx_1.clone(), sent_tx_2.clone()]); + let failed_payable_dao = FailedPayableDaoMock::new() + .retrieve_txs_result(vec![failed_tx_1.clone(), failed_tx_2.clone()]); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) + .build(); + let logger = Logger::new("start_scan_fills_in_caches_and_returns_msg"); + let pending_payable_cache_before = subject.current_sent_payables.dump_cache(); + let failed_payable_cache_before = subject.yet_unproven_failed_payables.dump_cache(); - fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { - let capture_regex = Regex::new(capture_regex).unwrap(); - let time_str = capture_regex - .captures(stack) - .unwrap() - .get(1) - .unwrap() - .as_str(); - time_str.parse().unwrap() - } + let result = subject.start_scan(&make_wallet("bluh"), SystemTime::now(), None, &logger); - fn elapsed_since_secs_back(sec: u64) -> u64 { - SystemTime::now() - .sub(Duration::from_secs(sec)) - .elapsed() - .unwrap() - .as_secs() + assert_eq!( + result, + Ok(RequestTransactionReceipts { + tx_hashes: vec![ + TxHashByTable::SentPayable(sent_tx_hash_1), + TxHashByTable::SentPayable(sent_tx_hash_2), + TxHashByTable::FailedPayable(failed_tx_hash_1), + TxHashByTable::FailedPayable(failed_tx_hash_2) + ], + response_skeleton_opt: None + }) + ); + assert!( + pending_payable_cache_before.is_empty(), + "Should have been empty but {:?}", + pending_payable_cache_before + ); + assert!( + failed_payable_cache_before.is_empty(), + "Should have been empty but {:?}", + failed_payable_cache_before + ); + let pending_payable_cache_after = subject.current_sent_payables.dump_cache(); + let failed_payable_cache_after = subject.yet_unproven_failed_payables.dump_cache(); + assert_eq!( + pending_payable_cache_after, + hashmap!(sent_tx_hash_1 => sent_tx_1, sent_tx_hash_2 => sent_tx_2) + ); + assert_eq!( + failed_payable_cache_after, + hashmap!(failed_tx_hash_1 => failed_tx_1, failed_tx_hash_2 => failed_tx_2) + ); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() - { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; - let hash = make_tx_hash(0x237); - let rowid = 466; + fn finish_scan_operates_caches_and_clears_them_after_use() { + let get_record_by_hash_failed_payable_cache_params_arc = Arc::new(Mutex::new(vec![])); + let get_record_by_hash_sent_payable_cache_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_payable_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_sent_payable_params_arc = Arc::new(Mutex::new(vec![])); + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(890); + let failed_tx_hash_2 = failed_tx_2.hash; + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::new() + .confirm_tx_result(Ok(())) + .replace_records_result(Ok(())) + .delete_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::new() + .insert_new_records_result(Ok(())) + .delete_records_result(Ok(())); + let sent_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_params(&get_record_by_hash_sent_payable_cache_params_arc) + .get_record_by_hash_result(Some(sent_tx_1.clone())) + .get_record_by_hash_result(Some(sent_tx_2)) + .ensure_empty_cache_params(&ensure_empty_cache_sent_payable_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_params(&get_record_by_hash_failed_payable_cache_params_arc) + .get_record_by_hash_result(Some(failed_tx_1)) + .get_record_by_hash_result(Some(failed_tx_2)) + .ensure_empty_cache_params(&ensure_empty_cache_failed_payable_params_arc); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + let logger = Logger::new("test"); + let confirmed_tx_block_sent_tx = make_transaction_block(901); + let confirmed_tx_block_failed_tx = make_transaction_block(902); + let msg = TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_sent_tx)), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_failed_tx)) + ], + response_skeleton_opt: None, + }; - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - DEFAULT_PENDING_TOO_LONG_SEC + 1, - rowid, - hash, - ); + let result = subject.finish_scan(msg, &logger); - let elapsed_after = elapsed_since_secs_back(DEFAULT_PENDING_TOO_LONG_SEC + 1); assert_eq!( result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(rowid, hash)], - confirmed: vec![] - } + PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) + ); + let get_record_by_hash_failed_payable_cache_params = + get_record_by_hash_failed_payable_cache_params_arc + .lock() + .unwrap(); + assert_eq!( + *get_record_by_hash_failed_payable_cache_params, + vec![failed_tx_hash_1, failed_tx_hash_2] + ); + let get_record_by_hash_sent_payable_cache_params = + get_record_by_hash_sent_payable_cache_params_arc + .lock() + .unwrap(); + assert_eq!( + *get_record_by_hash_sent_payable_cache_params, + vec![sent_tx_hash_1, sent_tx_hash_2] ); - let capture_regex = "(\\d+){2}sec"; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "ERROR: {}: Pending transaction 0x00000000000000000000000000000000000000\ - 00000000000000000000000237 has exceeded the maximum pending time \\({}sec\\) with the age \ - \\d+sec and the confirmation process is going to be aborted now at the final attempt 1; manual \ - resolution is required from the user to complete the transaction" - , test_name, DEFAULT_PENDING_TOO_LONG_SEC, ), elapsed_after, capture_regex) + let pending_payable_ensure_empty_cache_params = + ensure_empty_cache_sent_payable_params_arc.lock().unwrap(); + assert_eq!(*pending_payable_ensure_empty_cache_params, vec![()]); + let failed_payable_ensure_empty_cache_params = + ensure_empty_cache_failed_payable_params_arc.lock().unwrap(); + assert_eq!(*failed_payable_ensure_empty_cache_params, vec![()]); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; - let hash = make_tx_hash(0x7b); - let rowid = 333; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; + fn finish_scan_with_missing_records_inside_caches_noticed_on_missing_sent_tx() { + // Note: the ordering of the hashes matters in this test + let sent_tx_hash_1 = make_tx_hash(0x123); + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.hash = sent_tx_hash_1; + let sent_tx_hash_2 = make_tx_hash(0x876); + let failed_tx_hash_1 = make_tx_hash(0x987); + let mut failed_tx_1 = make_failed_tx(567); + failed_tx_1.hash = failed_tx_hash_1; + let failed_tx_hash_2 = make_tx_hash(0x789); + let mut failed_tx_2 = make_failed_tx(890); + failed_tx_2.hash = failed_tx_hash_2; + let mut pending_payable_cache = CurrentPendingPayables::default(); + pending_payable_cache.load_cache(vec![sent_tx_1]); + let mut failed_payable_cache = RecheckRequiringFailures::default(); + failed_payable_cache.load_cache(vec![failed_tx_1, failed_tx_2]); + let mut subject = PendingPayableScannerBuilder::new().build(); + subject.current_sent_payables = Box::new(pending_payable_cache); + subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + let logger = Logger::new("test"); + let msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok( + StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, + }; - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let regex_str_in_pieces = vec![ + r#"Looking up 'SentPayable\(0x0000000000000000000000000000000000000000000000000000000000000876\)'"#, + r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, + r#" Unproven failures: \[FailedTx \{ hash:"#, + r#" 0x0000000000000000000000000000000000000000000000000000000000000987, receiver_address:"#, + r#" 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \d*,"#, + r#" gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: RetryRequired \}\]."#, + r#" Hashes yet not looked up: \[FailedPayable\(0x000000000000000000000000000000000000000"#, + r#"0000000000000000000000987\)\]"#, + ]; + let regex_str = regex_str_in_pieces.join(""); + let expected_msg_regex = Regex::new(®ex_str).unwrap(); + assert!( + expected_msg_regex.is_match(panic_msg), + "Expected string that matches this regex '{}' but it couldn't with '{}'", + regex_str, + panic_msg ); + } - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } + #[test] + fn finish_scan_with_missing_records_inside_caches_noticed_on_missing_failed_tx() { + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_hash_2 = make_tx_hash(901); + let mut pending_payable_cache = CurrentPendingPayables::default(); + pending_payable_cache.load_cache(vec![sent_tx_1, sent_tx_2]); + let mut failed_payable_cache = RecheckRequiringFailures::default(); + failed_payable_cache.load_cache(vec![failed_tx_1]); + let mut subject = PendingPayableScannerBuilder::new().build(); + subject.current_sent_payables = Box::new(pending_payable_cache); + subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + let logger = Logger::new("test"); + let msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, + }; + + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let regex_str_in_pieces = vec![ + r#"Looking up 'FailedPayable\(0x0000000000000000000000000000000000000000000000000000000000000385\)'"#, + r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, + r#" Unproven failures: \[\]. Hashes yet not looked up: \[\]."#, + ]; + let regex_str = regex_str_in_pieces.join(""); + let expected_msg_regex = Regex::new(®ex_str).unwrap(); + assert!( + expected_msg_regex.is_match(panic_msg), + "Expected string that matches this regex '{}' but it couldn't with '{}'", + regex_str, + panic_msg ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 00000000000007b couldn't be confirmed at attempt 1 at \\d+ms after its sending"), elapsed_after_ms, capture_regex); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; - let hash = make_tx_hash(0x237); - let rowid = 466; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; + fn throws_an_error_when_no_records_to_process_were_found() { + let now = SystemTime::now(); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(vec![]); + let mut subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, - ); + let result = subject.start_scan(&consuming_wallet, now, None, &Logger::new("test")); - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 000000000000237 couldn't be confirmed at attempt 1 at \\d+ms after its sending", - ), elapsed_after_ms, capture_regex); + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(result, Err(StartScanError::NothingToProcess)); + assert_eq!(is_scan_running, false); + } + + #[test] + fn handle_failed_transactions_does_nothing_if_no_failure_detected() { + let subject = PendingPayableScannerBuilder::new().build(); + let detected_failures = DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")) + + // Mocked pending payable DAO without prepared results didn't panic which means none of its + // methods was used in this test } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { + fn handle_failed_transactions_can_process_standard_tx_failures() { init_test_logging(); - let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = make_tx_hash(0xd7); - let fingerprint = PendingPayableFingerprint { - rowid: 777777, - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt: 5, - amount: 2222, - process_error: None, + let test_name = "handle_failed_transactions_can_process_standard_tx_failures"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1.clone()), + PresortedTxFailure::NewEntry(failed_tx_2.clone()), + ], + tx_receipt_rpc_failures: vec![], }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - let result = handle_status_with_failure(scan_report, fingerprint, &logger); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(777777, hash,)], - confirmed: vec![] - } + *insert_new_records_params, + vec![vec![failed_tx_1, failed_tx_2]] ); - TestLogHandler::new().exists_log_matching(&format!( - "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ - 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ - 1500\\d\\dms from the sending" + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![hash_1, hash_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 were processed in the db" )); } #[test] - fn handle_pending_txs_with_receipts_handles_none_for_receipt() { + fn handle_failed_transactions_can_process_receipt_retrieval_rpc_failures() { init_test_logging(); - let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; - let subject = PendingPayableScannerBuilder::new().build(); - let rowid = 455; - let hash = make_tx_hash(0x913); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt: 3, - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }), - fingerprint.clone(), - )], - response_skeleton_opt: None, + let test_name = "handle_failed_transactions_can_process_receipt_retrieval_rpc_failures"; + let retrieve_failed_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let retrieve_sent_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let hash_3 = make_tx_hash(0x987); + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(1)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(2)); + let timestamp_d = SystemTime::now().sub(Duration::from_secs(3)); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + failed_tx_1.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + failed_tx_2.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ))); + let failed_payable_dao = FailedPayableDaoMock::default() + .retrieve_txs_params(&retrieve_failed_txs_params_arc) + .retrieve_txs_result(vec![failed_tx_1, failed_tx_2]) + .update_statuses_params(&update_statuses_failed_tx_params_arc) + .update_statuses_result(Ok(())); + let mut sent_tx = make_sent_tx(789); + sent_tx.hash = hash_3; + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let sent_payable_dao = SentPayableDaoMock::default() + .retrieve_txs_params(&retrieve_sent_txs_params_arc) + .retrieve_txs_result(vec![sent_tx.clone()]) + .update_statuses_params(&update_statuses_sent_tx_params_arc) + .update_statuses_result(Ok(())); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b) + .now_result(timestamp_c); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new(validation_failure_clock)) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + )), + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_d), + ), + )), + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_3, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Pending(ValidationStatus::Waiting), + )), + ], }; - let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let update_statuses_sent_tx_params = update_statuses_sent_tx_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } + *update_statuses_sent_tx_params, + vec![ + hashmap![hash_3 => TxStatus::Pending(ValidationStatus::Reattempting (PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_a))))] + ] + ); + let mut update_statuses_failed_tx_params = + update_statuses_failed_tx_params_arc.lock().unwrap(); + let actual_params = update_statuses_failed_tx_params + .remove(0) + .into_iter() + .sorted_by_key(|(key, _)| *key) + .collect::>(); + let expected_params = hashmap!( + hash_1 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_b))) + ), + hash_2 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp_d)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockReal::default()))) + ).into_iter().sorted_by_key(|(key,_)|*key).collect::>(); + assert_eq!(actual_params, expected_params); + assert!( + update_statuses_failed_tx_params.is_empty(), + "Should be empty but: {:?}", + update_statuses_sent_tx_params ); - TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {test_name}: Interpreting a receipt for transaction \ - 0x0000000000000000000000000000000000000000000000000000000000000913 \ - but none was given; attempt 3, 100\\d\\dms since sending" + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Pending-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000987" + )); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Failed-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654" )); + let expectedly_missing_log_msg_fragment = "Handling a validation failure, but the status"; + let otherwise_possible_log_msg = + PendingPayableScanner::status_not_updatable_log_msg(&"Something"); + assert!( + otherwise_possible_log_msg.contains(expectedly_missing_log_msg_fragment), + "We expected to select a true log fragment '{}', but it is not included in '{}'", + expectedly_missing_log_msg_fragment, + otherwise_possible_log_msg + ); + test_log_handler.exists_no_log_containing(&format!( + "DEBUG: {test_name}: {}", + expectedly_missing_log_msg_fragment + )) } #[test] - fn increment_scan_attempts_happy_path() { - let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let hash_1 = make_tx_hash(444888); - let rowid_1 = 3456; - let hash_2 = make_tx_hash(444888); - let rowid_2 = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) - .increment_scan_attempts_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); - let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); + fn handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated() { + init_test_logging(); + let test_name = "handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated"; + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let subject = PendingPayableScannerBuilder::new().build(); - let _ = subject.update_remaining_fingerprints( - vec![transaction_id_1, transaction_id_2], - &Logger::new("test"), + subject.handle_rpc_failures( + vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RetryRequired, + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Confirmed { + block_hash: "abc".to_string(), + block_number: 0, + detection: Detection::Normal, + }, + )), + ], + &Logger::new(test_name), ); - let update_remaining_fingerprints_params = - update_remaining_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *update_remaining_fingerprints_params, - vec![vec![rowid_1, rowid_2]] - ) + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_no_log_containing(&format!("INFO: {test_name}: ")); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \ + {{\"Confirmed\":{{\"block_hash\":\"abc\",\"block_number\":0,\"detection\":\"Normal\"}}}} \ + cannot be updated.", + )); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \"RetryRequired\" \ + cannot be updated." + )); + // It didn't panic, which means none of the DAO methods was called because the DAOs are + // mocked in this test } #[test] #[should_panic( - expected = "Failure on incrementing scan attempts for fingerprints of \ - 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ - due to UpdateFailed(\"yeah, bad\")" + expected = "Unable to update pending-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"bluh\")" )] - fn increment_scan_attempts_sad_path() { - let hash = make_tx_hash(0x6c9d8); - let rowid = 3456; - let pending_payable_dao = - PendingPayableDaoMock::default().increment_scan_attempts_result(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); + fn update_validation_status_for_sent_txs_panics_on_update_statuses() { + let failed_validation = FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ); + let sent_payable_dao = SentPayableDaoMock::default() + .update_statuses_result(Err(SentPayableDaoError::InvalidInput("bluh".to_string()))); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) .build(); - let logger = Logger::new("test"); - let transaction_id = PendingPayableId::new(rowid, hash); - let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); + let _ = subject + .update_validation_status_for_sent_txs(vec![failed_validation], &Logger::new("test")); } #[test] - fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { - let subject = PendingPayableScannerBuilder::new().build(); + #[should_panic( + expected = "Unable to update failed-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"bluh\")" + )] + fn update_validation_status_for_failed_txs_panics_on_update_statuses() { + let failed_validation = FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ); + let failed_payable_dao = FailedPayableDaoMock::default() + .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("bluh".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) + .build(); + + let _ = subject + .update_validation_status_for_failed_txs(vec![failed_validation], &Logger::new("test")); + } + + #[test] + fn handle_failed_transactions_can_process_mixed_failures() { + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let update_status_params_arc = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let timestamp = SystemTime::now(); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = tx_hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = tx_hash_2; + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .update_statuses_params(&update_status_params_arc) + .update_statuses_result(Ok(())) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new( + ValidationFailureClockMock::default().now_result(timestamp), + )) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(failed_tx_1.clone())], + tx_receipt_rpc_failures: vec![FailedValidationByTable::SentPayable( + FailedValidation::new( + tx_hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ), + )], + }; - subject.update_remaining_fingerprints(vec![], &Logger::new("test")) + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![failed_tx_1]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_1]]); + let update_statuses_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_params, + vec![ + hashmap!(tx_hash_2 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp))))) + ] + ); } #[test] - fn cancel_failed_transactions_works() { - init_test_logging(); - let test_name = "cancel_failed_transactions_works"; - let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failures_params(&mark_failures_params_arc) - .mark_failures_result(Ok(())); + #[should_panic(expected = "Unable to persist failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: NoChange")] + fn handle_failed_transactions_panics_when_it_fails_to_insert_failed_tx_record() { + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_result(Err(FailedPayableDaoError::NoChange)); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); - let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + } - let mark_failures_params = mark_failures_params_arc.lock().unwrap(); - assert_eq!(*mark_failures_params, vec![vec![2, 3]]); - TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ - 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ - the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ - process fixing that without your assistance", - )); + #[test] + #[should_panic(expected = "Unable to purge sent payable records for failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: \ + InvalidInput(\"Booga\")")] + fn handle_failed_transactions_panics_when_it_fails_to_delete_obsolete_sent_tx_records() { + let failed_payable_dao = FailedPayableDaoMock::default().insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_result(Err(SentPayableDaoError::InvalidInput("Booga".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - #[should_panic( - expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ - 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ - 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ - database unreliable" - )] - fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { - let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), + fn handle_failed_transactions_can_conclude_rechecked_failures() { + let update_status_params_arc = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = tx_hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = tx_hash_2; + let failed_payable_dao = FailedPayableDaoMock::default() + .update_statuses_params(&update_status_params_arc) + .update_statuses_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + + let update_status_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_status_params, + vec![ + hashmap!(tx_hash_1 => FailureStatus::Concluded, tx_hash_2 => FailureStatus::Concluded), + ] + ); + } + + #[test] + #[should_panic(expected = "Unable to conclude rechecks for failed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 due to: \ + InvalidInput(\"Booga\")")] + fn concluding_rechecks_fails_on_updating_statuses() { + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let failed_payable_dao = FailedPayableDaoMock::default().update_statuses_result(Err( + FailedPayableDaoError::InvalidInput("Booga".to_string()), )); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); - let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); - let transaction_ids = vec![transaction_id_1, transaction_id_2]; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { - let subject = PendingPayableScannerBuilder::new().build(); + fn handle_confirmed_transactions_does_nothing_if_no_confirmation_found_on_the_blockchain() { + let mut subject = PendingPayableScannerBuilder::new().build(); + + subject + .handle_confirmed_transactions(DetectedConfirmations::default(), &Logger::new("test")) + + // Mocked payable DAO without prepared results didn't panic, which means none of its methods + // was used in this test + } + + #[test] + fn handles_failure_reclaims_alone() { + init_test_logging(); + let test_name = "handles_failure_reclaims_alone"; + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.cancel_failed_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &logger, + ); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) as confirmed on-chain", + )); } #[test] #[should_panic( - expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ - 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ - 0000000000021a of verified transactions due to RecordDeletion(\"the database \ - is fooling around with us\")" + expected = "Unable to proceed in a reclaim as the replacement of sent tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + failed due to: NoChange" )] - fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { - let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Err( - PendingPayableDaoError::RecordDeletion( - "the database is fooling around with us".to_string(), - ), - )); + fn failure_reclaim_fails_on_replace_sent_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_result(Err(SentPayableDaoError::NoChange)); let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.rowid = 1; - fingerprint_1.hash = make_tx_hash(0x315); - let mut fingerprint_2 = make_pending_payable_fingerprint(); - fingerprint_2.rowid = 1; - fingerprint_2.hash = make_tx_hash(0x21a); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { - let mut subject = PendingPayableScannerBuilder::new().build(); + #[should_panic(expected = "Unable to delete failed tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + to finish the reclaims due to: EmptyInput")] + fn failure_reclaim_fails_on_delete_failed_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default().replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_result(Err(FailedPayableDaoError::EmptyInput)); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); + } - //mocked payable DAO didn't panic which means we skipped the actual process + #[test] + #[should_panic( + expected = "Processing a reclaim for tx 0x0000000000000000000000000000000000000000000000000\ + 000000000000123 which isn't filled with the confirmation details" + )] + fn handle_failure_reclaim_meets_a_record_without_confirmation_details() { + let mut subject = PendingPayableScannerBuilder::new().build(); + let tx_hash = make_tx_hash(0x123); + let mut sent_tx = make_sent_tx(123_123); + sent_tx.hash = tx_hash; + // Here, it should be confirmed already in this status + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_works() { + fn handles_normal_confirmations_alone() { init_test_logging(); + let test_name = "handles_normal_confirmations_alone"; let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())); + let logger = Logger::new(test_name); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let rowid_1 = 2; - let rowid_2 = 5; - let pending_payable_fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: from_unix_timestamp(199_000_000), - hash: make_tx_hash(0x123), - attempt: 1, - amount: 4567, - process_error: None, - }; - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: from_unix_timestamp(200_000_000), - hash: make_tx_hash(0x567), - attempt: 1, - amount: 5555, - process_error: None, - }; - - subject.confirm_transactions( - vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), - ], - &Logger::new("confirm_transactions_works"), + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_2.block_hash), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone(), sent_tx_2.clone()], + reclaims: vec![], + }, + &logger, ); - let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!( + *transactions_confirmed_params, + vec![vec![sent_tx_1, sent_tx_2]] + ); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); assert_eq!( - *confirm_transactions_params, - vec![vec![ - pending_payable_fingerprint_1, - pending_payable_fingerprint_2 - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); + *confirm_tx_params, + vec![hashmap![tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2]] + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "DEBUG: confirm_transactions_works: \ - Confirmation of transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567; \ - record for total paid payable was modified", + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) were confirmed", + )); + } + + #[test] + fn mixed_tx_confirmations_work() { + init_test_logging(); + let test_name = "mixed_tx_confirmations_work"; + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transactions_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x913); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(567_567); + sent_tx_2.hash = tx_hash_2; + let tx_block_3 = TxBlock { + block_hash: make_block_hash(78), + block_number: 7_898_989_878_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_3.block_hash), + block_number: tx_block_3.block_number.as_u64(), + detection: Detection::Reclaim, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone()], + reclaims: vec![sent_tx_2.clone()], + }, + &logger, ); - log_handler.exists_log_containing( - "INFO: confirm_transactions_works: \ - Transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567 \ - completed their confirmation process succeeding", + + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![vec![sent_tx_1]]); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); + assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000913 (block 7898989878) \ + as confirmed on-chain", + )); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878) was confirmed", + )); + } + + #[test] + #[should_panic( + expected = "Unable to update sent payable records 0x000000000000000000000000000000000000000\ + 000000000000000000000021a, 0x0000000000000000000000000000000000000000000000000000000000000315 \ + by their tx blocks due to: SqlExecutionFailed(\"The database manager is \ + a funny guy, he's fooling around with us\")" + )] + fn handle_confirmed_transactions_panics_while_updating_sent_payable_records_with_the_tx_blocks() + { + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Err( + SentPayableDaoError::SqlExecutionFailed( + "The database manager is a funny guy, he's fooling around with us".to_string(), + ), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let mut sent_tx_1 = make_sent_tx(456); + let block = make_transaction_block(678); + sent_tx_1.hash = make_tx_hash(0x315); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.hash = make_tx_hash(0x21a); + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![], + }, + &Logger::new("test"), ); } #[test] #[should_panic( - expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ - 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ - (\"record change not successful\")" + expected = "Unable to complete the tx confirmation by the adjustment of the payable accounts \ + 0x000000000000000000000077616c6c6574343536 due to: \ + RusqliteError(\"record change not successful\")" )] - fn confirm_transactions_panics_on_unchecking_payable_table() { + fn handle_confirmed_transactions_panics_on_unchecking_payable_table() { let hash = make_tx_hash(0x315); - let rowid = 3; let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( PayableDaoError::RusqliteError("record change not successful".to_string()), )); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let mut fingerprint = make_pending_payable_fingerprint(); - fingerprint.rowid = rowid; - fingerprint.hash = hash; + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = hash; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx], + reclaims: vec![], + }, + &Logger::new("test"), + ); + } + + #[test] + fn log_tx_success_is_agnostic_to_singular_or_plural_form() { + init_test_logging(); + let test_name = "log_tx_success_is_agnostic_to_singular_or_plural_form"; + let plural_case_name = format!("{}_testing_plural_case", test_name); + let singular_case_name = format!("{}_testing_singular_case", test_name); + let logger_plural = Logger::new(&plural_case_name); + let logger_singular = Logger::new(&singular_case_name); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut tx_block_1 = make_transaction_block(456); + tx_block_1.block_number = 1_234_501_u64.into(); + let mut tx_block_2 = make_transaction_block(789); + tx_block_2.block_number = 1_234_502_u64.into(); + let mut tx_hashes_and_blocks = hashmap!(tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2); + + PendingPayableScanner::log_tx_success(&logger_plural, &tx_hashes_and_blocks); + + tx_hashes_and_blocks.remove(&tx_hash_2); + + PendingPayableScanner::log_tx_success(&logger_singular, &tx_hashes_and_blocks); - subject.confirm_transactions(vec![fingerprint], &Logger::new("test")); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {plural_case_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 1234502) were confirmed", + )); + log_handler.exists_log_containing(&format!( + "INFO: {singular_case_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501) was confirmed", + )); } #[test] fn total_paid_payable_rises_with_each_bill_paid() { + init_test_logging(); let test_name = "total_paid_payable_rises_with_each_bill_paid"; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(189_999_888), - hash: make_tx_hash(56789), - attempt: 1, - amount: 5478, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 6, - timestamp: from_unix_timestamp(200_000_011), - hash: make_tx_hash(33333), - attempt: 1, - amount: 6543, - process_error: None, + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.amount_minor = 5478; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: 89898, + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.amount_minor = 3344; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(234)), + block_number: 66312, + detection: Detection::Normal, + }; + let mut sent_tx_3 = make_sent_tx(789); + sent_tx_3.amount_minor = 6543; + sent_tx_3.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(321)), + block_number: 67676, + detection: Detection::Reclaim, }; let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_result(Ok(())) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default().delete_records_result(Ok(())); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let mut financial_statistics = subject.financial_statistics.borrow().clone(); financial_statistics.total_paid_payable_wei += 1111; subject.financial_statistics.replace(financial_statistics); - subject.confirm_transactions( - vec![fingerprint_1.clone(), fingerprint_2.clone()], + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![sent_tx_3], + }, &Logger::new(test_name), ); let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; - assert_eq!(total_paid_payable, 1111 + 5478 + 6543); + assert_eq!(total_paid_payable, 1111 + 5478 + 3344 + 6543); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!("DEBUG: {test_name}: The total paid payables increased by 6,543 to 7,654 wei"), + &format!( + "DEBUG: {test_name}: The total paid payables increased by 8,822 to 16,476 wei" + ), + ]); } } diff --git a/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs new file mode 100644 index 000000000..473fd28cb --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs @@ -0,0 +1,23 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::validation_status::ValidationFailureClock; +use std::cell::RefCell; +use std::time::SystemTime; + +#[derive(Default)] +pub struct ValidationFailureClockMock { + now_results: RefCell>, +} + +impl ValidationFailureClock for ValidationFailureClockMock { + fn now(&self) -> SystemTime { + self.now_results.borrow_mut().remove(0) + } +} + +impl ValidationFailureClockMock { + pub fn now_result(self, result: SystemTime) -> Self { + self.now_results.borrow_mut().push(result); + self + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs new file mode 100644 index 000000000..4bf96bf1e --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -0,0 +1,706 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentTx, TxStatus, +}; +use crate::accountant::db_access_objects::utils::from_unix_timestamp; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + ConfirmationType, FailedValidation, FailedValidationByTable, ReceiptScanReport, TxByTable, + TxCaseToBeInterpreted, TxHashByTable, +}; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, +}; +use crate::blockchain::errors::internal_errors::InternalErrorKind; +use crate::blockchain::errors::rpc_errors::AppRpcError; +use crate::blockchain::errors::BlockchainErrorKind; +use itertools::Either; +use masq_lib::logger::Logger; +use std::time::SystemTime; +use thousands::Separable; + +#[derive(Default)] +pub struct TxReceiptInterpreter {} + +impl TxReceiptInterpreter { + pub fn compose_receipt_scan_report( + &self, + tx_cases: Vec, + pending_payable_scanner: &PendingPayableScanner, + logger: &Logger, + ) -> ReceiptScanReport { + let scan_report = ReceiptScanReport::default(); + tx_cases + .into_iter() + .fold(scan_report, |scan_report_so_far, tx_case| { + match tx_case.tx_receipt_result { + Ok(tx_status) => match tx_status { + StatusReadFromReceiptCheck::Succeeded(tx_block) => { + Self::handle_tx_confirmation( + scan_report_so_far, + tx_case.tx_by_table, + tx_block, + logger, + ) + } + StatusReadFromReceiptCheck::Reverted => Self::handle_reverted_tx( + scan_report_so_far, + tx_case.tx_by_table, + logger, + ), + StatusReadFromReceiptCheck::Pending => Self::handle_still_pending_tx( + scan_report_so_far, + tx_case.tx_by_table, + &*pending_payable_scanner.sent_payable_dao, + logger, + ), + }, + Err(e) => { + Self::handle_rpc_failure(scan_report_so_far, tx_case.tx_by_table, e, logger) + } + } + }) + } + + fn handle_still_pending_tx( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + sent_payable_dao: &dyn SentPayableDao, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + info!( + logger, + "Tx {:?} not confirmed within {} ms. Will resubmit with higher gas price", + sent_tx.hash, + Self::elapsed_in_ms(from_unix_timestamp(sent_tx.timestamp)) + .separate_with_commas() + ); + let failed_tx = FailedTx::from((sent_tx, FailureReason::PendingTooLong)); + scan_report.register_new_failure(failed_tx); + } + TxByTable::FailedPayable(failed_tx) => { + if failed_tx.reason != FailureReason::PendingTooLong { + unreachable!( + "Transaction is both pending and failed (failure reason: '{:?}'). Should be \ + possible only with the reason 'PendingTooLong'", + failed_tx.reason + ) + } + let replacement_tx = sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::ByNonce(vec![failed_tx.nonce]))); + let replacement_tx_hash = replacement_tx + .get(0) + .unwrap_or_else(|| { + panic!( + "Attempted to display a replacement tx for {:?} but couldn't find \ + one in the database", + failed_tx.hash + ) + }) + .hash; + warning!( + logger, + "Failed tx {:?} on a recheck was found pending on its receipt unexpectedly. \ + It was supposed to be replaced by {:?}", + failed_tx.hash, + replacement_tx_hash + ); + scan_report.register_rpc_failure(FailedValidationByTable::FailedPayable( + FailedValidation::new( + failed_tx.hash, + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + failed_tx.status, + ), + )) + } + } + scan_report + } + + fn elapsed_in_ms(timestamp: SystemTime) -> u128 { + timestamp + .elapsed() + .expect("time calculation for elapsed failed") + .as_millis() + } + + fn handle_tx_confirmation( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + tx_block: TxBlock, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + info!( + logger, + "Pending tx {:?} was confirmed on-chain", sent_tx.hash, + ); + + let completed_sent_tx = SentTx { + status: TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, + }, + ..sent_tx + }; + scan_report.register_confirmed_tx(completed_sent_tx, ConfirmationType::Normal); + } + TxByTable::FailedPayable(failed_tx) => { + info!( + logger, + "Failed tx {:?} was later confirmed on-chain and will be reclaimed", + failed_tx.hash + ); + + let sent_tx = SentTx::from((failed_tx, tx_block)); + scan_report.register_confirmed_tx(sent_tx, ConfirmationType::Reclaim); + } + } + scan_report + } + + //TODO: failures handling might need enhancement suggested by GH-693 + fn handle_reverted_tx( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + let failure_reason = FailureReason::Reverted; + let failed_tx = FailedTx::from((sent_tx, failure_reason)); + + warning!(logger, "Pending tx {:?} was reverted", failed_tx.hash,); + + scan_report.register_new_failure(failed_tx); + } + TxByTable::FailedPayable(failed_tx) => { + debug!( + logger, + "Reverted tx {:?} on a recheck after {}. Status will be changed to \"Concluded\"", + failed_tx.hash, + failed_tx.reason, + ); + + scan_report.register_finalization_of_unproven_failure(failed_tx.hash); + } + } + scan_report + } + + fn handle_rpc_failure( + mut scan_report: ReceiptScanReport, + tx_by_table: TxByTable, + rpc_error: AppRpcError, + logger: &Logger, + ) -> ReceiptScanReport { + warning!( + logger, + "Failed to retrieve tx receipt for {:?}: {:?}. Will retry receipt retrieval next cycle", + TxHashByTable::from(&tx_by_table), + rpc_error + ); + let hash = tx_by_table.hash(); + let validation_status_update = match tx_by_table { + TxByTable::SentPayable(sent_tx) => { + FailedValidationByTable::new(hash, rpc_error, Either::Left(sent_tx.status)) + } + TxByTable::FailedPayable(failed_tx) => { + FailedValidationByTable::new(hash, rpc_error, Either::Right(failed_tx.status)) + } + }; + scan_report.register_rpc_failure(validation_status_update); + scan_report + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentTx, TxStatus, + }; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + DetectedConfirmations, DetectedFailures, FailedValidation, FailedValidationByTable, + PresortedTxFailure, ReceiptScanReport, TxByTable, + }; + use crate::accountant::test_utils::{ + make_failed_tx, make_sent_tx, make_transaction_block, SentPayableDaoMock, + }; + use crate::blockchain::errors::internal_errors::InternalErrorKind; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, + }; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::unshared_test_utils::capture_digits_with_separators_from_str; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; + + #[test] + fn interprets_receipt_for_pending_tx_if_it_is_a_success() { + init_test_logging(); + let test_name = "interprets_tx_receipt_if_it_is_a_success"; + let hash = make_tx_hash(0xcdef); + let mut sent_tx = make_sent_tx(2244); + sent_tx.hash = hash; + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let tx_block = make_transaction_block(1234); + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_tx_confirmation( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + tx_block, + &logger, + ); + + let mut updated_tx = sent_tx; + updated_tx.status = TxStatus::Confirmed { + block_hash: "0x000000000000000000000000000000000000000000000000000000003b9aced2" + .to_string(), + block_number: 1879080904, + detection: Detection::Normal, + }; + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures::default(), + confirmations: DetectedConfirmations { + normal_confirmations: vec![updated_tx], + reclaims: vec![] + } + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ + 00000cdef was confirmed on-chain", + )); + } + + #[test] + fn interprets_receipt_for_failed_tx_being_rechecked_when_it_is_a_success() { + init_test_logging(); + let test_name = "interprets_receipt_for_failed_tx_being_rechecked_when_it_is_a_success"; + let hash = make_tx_hash(0xcdef); + let mut failed_tx = make_failed_tx(2244); + failed_tx.hash = hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let tx_block = make_transaction_block(1234); + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_tx_confirmation( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + tx_block, + &logger, + ); + + let sent_tx = SentTx::from((failed_tx, tx_block)); + assert!( + matches!( + sent_tx.status, + TxStatus::Confirmed { + detection: Detection::Reclaim, + .. + } + ), + "We expected reclaimed tx, but it says: {:?}", + sent_tx + ); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures::default(), + confirmations: DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx] + } + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ + 00000cdef was later confirmed on-chain and will be reclaimed", + )); + } + + #[test] + fn interprets_tx_receipt_for_pending_tx_when_tx_status_says_reverted() { + init_test_logging(); + let test_name = "interprets_tx_receipt_for_pending_tx_when_tx_status_says_reverted"; + let hash = make_tx_hash(0xabc); + let mut sent_tx = make_sent_tx(2244); + sent_tx.hash = hash; + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_reverted_tx( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + &logger, + ); + + let failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(failed_tx)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ + 000000abc was reverted", + )); + } + + #[test] + fn interprets_tx_receipt_for_failed_tx_when_newly_fetched_tx_status_says_reverted() { + init_test_logging(); + let test_name = "interprets_tx_receipt_for_failed_tx_when_tx_status_reveals_failure"; + let tx_hash = make_tx_hash(0xabc); + let mut failed_tx = make_failed_tx(2244); + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_reverted_tx( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + &logger, + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::RecheckCompleted(tx_hash)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Reverted tx 0x000000000000000000000000000000000000000000000000000000\ + 0000000abc on a recheck after \"PendingTooLong\". Status will be changed to \"Concluded\"", + )); + } + + #[test] + fn interprets_tx_receipt_for_pending_payable_if_the_tx_keeps_pending() { + init_test_logging(); + let retrieve_txs_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "interprets_tx_receipt_for_pending_payable_if_the_tx_keeps_pending"; + let newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_params(&retrieve_txs_params_arc) + .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + let hash = make_tx_hash(0x913); + let sent_tx_timestamp = to_unix_timestamp( + SystemTime::now() + .checked_sub(Duration::from_secs(120)) + .unwrap(), + ); + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = hash; + sent_tx.timestamp = sent_tx_timestamp; + let scan_report = ReceiptScanReport::default(); + let before = SystemTime::now(); + + let result = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + &sent_payable_dao, + &Logger::new(test_name), + ); + + let after = SystemTime::now(); + let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::PendingTooLong)); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(expected_failed_tx)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + let log_handler = TestLogHandler::new(); + let log_idx = log_handler.exists_log_matching(&format!( + "INFO: {test_name}: Tx \ + 0x0000000000000000000000000000000000000000000000000000000000000913 not confirmed within \ + \\d{{1,3}}(,\\d{{3}})* ms. Will resubmit with higher gas price" + )); + let log_msg = log_handler.get_log_at(log_idx); + let str_elapsed_ms = capture_digits_with_separators_from_str(&log_msg, 3, ','); + let elapsed_ms = str_elapsed_ms[0].replace(",", "").parse::().unwrap(); + let elapsed_ms_when_before = before + .duration_since(from_unix_timestamp(sent_tx_timestamp)) + .unwrap() + .as_millis(); + let elapsed_ms_when_after = after + .duration_since(from_unix_timestamp(sent_tx_timestamp)) + .unwrap() + .as_millis(); + assert!( + elapsed_ms_when_before <= elapsed_ms && elapsed_ms <= elapsed_ms_when_after, + "we expected the elapsed time {} ms to be between {} and {}.", + elapsed_ms, + elapsed_ms_when_before, + elapsed_ms_when_after + ); + } + + #[test] + fn interprets_tx_receipt_for_supposedly_failed_tx_if_the_tx_keeps_pending() { + init_test_logging(); + let retrieve_txs_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "interprets_tx_receipt_for_supposedly_failed_tx_if_the_tx_keeps_pending"; + let mut newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + newer_sent_tx_for_older_failed_tx.hash = make_tx_hash(0x7c6); + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_params(&retrieve_txs_params_arc) + .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + let tx_hash = make_tx_hash(0x913); + let mut failed_tx = make_failed_tx(789); + let failed_tx_nonce = failed_tx.nonce; + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + &sent_payable_dao, + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::Internal( + InternalErrorKind::PendingTooLongNotReplaced + ), + FailureStatus::RecheckRequired(ValidationStatus::Waiting) + ) + )] + }, + confirmations: DetectedConfirmations::default() + } + ); + let retrieve_txs_params = retrieve_txs_params_arc.lock().unwrap(); + assert_eq!( + *retrieve_txs_params, + vec![Some(RetrieveCondition::ByNonce(vec![failed_tx_nonce]))] + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ + 000000913 on a recheck was found pending on its receipt unexpectedly. It was supposed \ + to be replaced by 0x00000000000000000000000000000000000000000000000000000000000007c6" + )); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Transaction is both pending \ + and failed (failure reason: 'Reverted'). Should be possible only with the reason 'PendingTooLong'" + )] + fn interprets_failed_tx_recheck_as_still_pending_while_the_failure_reason_wasnt_pending_too_long( + ) { + let mut newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + newer_sent_tx_for_older_failed_tx.hash = make_tx_hash(0x7c6); + let sent_payable_dao = SentPayableDaoMock::new(); + let tx_hash = make_tx_hash(0x913); + let mut failed_tx = make_failed_tx(789); + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::Reverted; + let scan_report = ReceiptScanReport::default(); + + let _ = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(failed_tx), + &sent_payable_dao, + &Logger::new("test"), + ); + } + + #[test] + #[should_panic( + expected = "Attempted to display a replacement tx for 0x000000000000000000000000000\ + 00000000000000000000000000000000001c8 but couldn't find one in the database" + )] + fn handle_still_pending_tx_if_unexpected_behavior_due_to_already_failed_tx_and_db_retrieval_fails( + ) { + let scan_report = ReceiptScanReport::default(); + let still_pending_tx = make_failed_tx(456); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + + let _ = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(still_pending_tx), + &sent_payable_dao, + &Logger::new("test"), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_pending_payable_in_first_attempt() { + let test_name = + "interprets_failed_retrieval_of_receipt_for_pending_payable_in_first_attempt"; + + test_failed_retrieval_of_receipt_for_pending_payable( + test_name, + TxStatus::Pending(ValidationStatus::Waiting), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_pending_payable_as_reattempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_pending_payable_as_reattempt"; + + test_failed_retrieval_of_receipt_for_pending_payable( + test_name, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockReal::default(), + ))), + ); + } + + fn test_failed_retrieval_of_receipt_for_pending_payable( + test_name: &str, + current_tx_status: TxStatus, + ) { + init_test_logging(); + let tx_hash = make_tx_hash(913); + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = tx_hash; + sent_tx.status = current_tx_status.clone(); + let rpc_error = AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_rpc_failure( + scan_report, + TxByTable::SentPayable(sent_tx), + rpc_error.clone(), + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::SentPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&rpc_error).into()), + current_tx_status + ) + ),] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing( + &format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000\ + 000000000000000000000000000000000000000000000000000391): Remote(InvalidResponse(\"bluh\")). \ + Will retry receipt retrieval next cycle")); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_failed_tx_in_first_attempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_failed_tx_in_first_attempt"; + + test_failed_retrieval_of_receipt_for_failed_tx( + test_name, + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_failed_tx_as_reattempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_failed_tx_as_reattempt"; + + test_failed_retrieval_of_receipt_for_failed_tx( + test_name, + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockReal::default(), + ))), + ); + } + + fn test_failed_retrieval_of_receipt_for_failed_tx( + test_name: &str, + current_failure_status: FailureStatus, + ) { + init_test_logging(); + let tx_hash = make_tx_hash(914); + let mut failed_tx = make_failed_tx(456); + failed_tx.hash = tx_hash; + failed_tx.status = current_failure_status.clone(); + let rpc_error = AppRpcError::Local(LocalError::Internal); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_rpc_failure( + scan_report, + TxByTable::FailedPayable(failed_tx), + rpc_error.clone(), + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&rpc_error).into()), + current_failure_status + ) + )] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Failed to retrieve tx receipt for FailedPayable(0x0000000000\ + 000000000000000000000000000000000000000000000000000392): Local(Internal). \ + Will retry receipt retrieval next cycle" + )); + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index f277a1c91..d08808d75 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -1,191 +1,1160 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::PendingPayableId; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::TxReceiptResult; +use crate::blockchain::errors::rpc_errors::AppRpcError; +use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClock, ValidationStatus, +}; +use crate::blockchain::errors::BlockchainErrorKind; +use itertools::Either; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; -use std::time::SystemTime; +use std::collections::HashMap; #[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct PendingPayableScanReport { - pub still_pending: Vec, - pub failures: Vec, - pub confirmed: Vec, +pub struct ReceiptScanReport { + pub failures: DetectedFailures, + pub confirmations: DetectedConfirmations, } -impl PendingPayableScanReport { - pub fn requires_payments_retry(&self) -> bool { - todo!("complete my within GH-642") +impl ReceiptScanReport { + pub fn requires_payments_retry(&self) -> Option { + match ( + self.failures.requires_retry(), + self.confirmations.is_empty(), + ) { + (None, true) => unreachable!("reading tx receipts gave no results, but always should"), + (None, _) => None, + (Some(retry), _) => Some(retry), + } + } + + pub(super) fn register_confirmed_tx( + &mut self, + confirmed_tx: SentTx, + confirmation_type: ConfirmationType, + ) { + match confirmation_type { + ConfirmationType::Normal => self.confirmations.normal_confirmations.push(confirmed_tx), + ConfirmationType::Reclaim => self.confirmations.reclaims.push(confirmed_tx), + } + } + + pub(super) fn register_new_failure(&mut self, failed_tx: FailedTx) { + self.failures + .tx_failures + .push(PresortedTxFailure::NewEntry(failed_tx)); + } + + pub(super) fn register_finalization_of_unproven_failure(&mut self, tx_hash: TxHash) { + self.failures + .tx_failures + .push(PresortedTxFailure::RecheckCompleted(tx_hash)); + } + + pub(super) fn register_rpc_failure(&mut self, status_update: FailedValidationByTable) { + self.failures.tx_receipt_rpc_failures.push(status_update); + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedConfirmations { + pub normal_confirmations: Vec, + pub reclaims: Vec, +} + +impl DetectedConfirmations { + pub(super) fn is_empty(&self) -> bool { + self.normal_confirmations.is_empty() && self.reclaims.is_empty() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ConfirmationType { + Normal, + Reclaim, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedFailures { + pub tx_failures: Vec, + pub tx_receipt_rpc_failures: Vec, +} + +impl DetectedFailures { + fn requires_retry(&self) -> Option { + if self.tx_failures.is_empty() && self.tx_receipt_rpc_failures.is_empty() { + None + } else if !self.tx_failures.is_empty() { + Some(Retry::RetryPayments) + } else { + Some(Retry::RetryTxStatusCheckOnly) + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PresortedTxFailure { + NewEntry(FailedTx), + RecheckCompleted(TxHash), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum FailedValidationByTable { + SentPayable(FailedValidation), + FailedPayable(FailedValidation), +} + +impl FailedValidationByTable { + pub fn new( + tx_hash: TxHash, + error: AppRpcError, + status: Either, + ) -> Self { + match status { + Either::Left(tx_status) => Self::SentPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + tx_status, + )), + Either::Right(failure_reason) => Self::FailedPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + failure_reason, + )), + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct FailedValidation { + pub tx_hash: TxHash, + pub validation_failure: BlockchainErrorKind, + pub current_status: RecordStatus, +} + +impl FailedValidation +where + RecordStatus: UpdatableValidationStatus, +{ + pub fn new( + tx_hash: TxHash, + validation_failure: BlockchainErrorKind, + current_status: RecordStatus, + ) -> Self { + Self { + tx_hash, + validation_failure, + current_status, + } + } + + pub fn new_status(&self, clock: &dyn ValidationFailureClock) -> Option { + self.current_status + .update_after_failure(self.validation_failure, clock) + } +} + +pub trait UpdatableValidationStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option + where + Self: Sized; +} + +impl UpdatableValidationStatus for TxStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + TxStatus::Pending(ValidationStatus::Waiting) => Some(TxStatus::Pending( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )), + TxStatus::Pending(ValidationStatus::Reattempting(previous_attempts)) => { + Some(TxStatus::Pending(ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error, clock), + ))) + } + TxStatus::Confirmed { .. } => None, + } + } +} + +impl UpdatableValidationStatus for FailureStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + FailureStatus::RecheckRequired(ValidationStatus::Waiting) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )) + } + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(previous_attempts)) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error.into(), clock), + ), + )) + } + FailureStatus::RetryRequired | FailureStatus::Concluded => None, + } + } +} + +pub struct MismatchReport { + pub noticed_with: TxHashByTable, + pub remaining_hashes: Vec, +} + +pub trait PendingPayableCache { + fn load_cache(&mut self, records: Vec); + fn get_record_by_hash(&mut self, hash: TxHash) -> Option; + fn ensure_empty_cache(&mut self, logger: &Logger); + fn dump_cache(&mut self) -> HashMap; +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct CurrentPendingPayables { + pub(super) sent_payables: HashMap, +} + +impl PendingPayableCache for CurrentPendingPayables { + fn load_cache(&mut self, records: Vec) { + self.sent_payables + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.sent_payables.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.sent_payables.is_empty() { + debug!( + logger, + "Cache misuse - some pending payables left unprocessed: {:?}. Dumping.", + self.sent_payables + ); + } + self.sent_payables.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.sent_payables.drain().collect() + } +} + +impl CurrentPendingPayables { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct RecheckRequiringFailures { + pub(super) failures: HashMap, +} + +impl PendingPayableCache for RecheckRequiringFailures { + fn load_cache(&mut self, records: Vec) { + self.failures + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.failures.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.failures.is_empty() { + debug!( + logger, + "Cache misuse - some tx failures left unprocessed: {:?}. Dumping.", self.failures + ); + } + self.failures.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.failures.drain().collect() + } +} + +impl RecheckRequiringFailures { + pub fn new() -> Self { + Self::default() } } #[derive(Debug, PartialEq, Eq)] pub enum PendingPayableScanResult { NoPendingPayablesLeft(Option), - PaymentRetryRequired, -} - -pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() -} - -pub fn handle_none_status( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Pending transaction {:?} couldn't be confirmed at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let elapsed = elapsed.as_secs(); - if elapsed > max_pending_interval { - error!( - logger, - "Pending transaction {:?} has exceeded the maximum pending time \ - ({}sec) with the age {}sec and the confirmation process is going to be aborted now \ - at the final attempt {}; manual resolution is required from the \ - user to complete the transaction.", - fingerprint.hash, - max_pending_interval, - elapsed, - fingerprint.attempt - ); - scan_report.failures.push(fingerprint.into()) - } else { - scan_report.still_pending.push(fingerprint.into()) - } - scan_report -} - -pub fn handle_status_with_success( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Transaction {:?} has been added to the blockchain; detected locally at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.confirmed.push(fingerprint); - scan_report -} - -//TODO: failures handling is going to need enhancement suggested by GH-693 -pub fn handle_status_with_failure( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - error!( - logger, - "Pending transaction {:?} announced as a failure, interpreting attempt \ - {} after {}ms from the sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.failures.push(fingerprint.into()); - scan_report -} - -pub fn handle_none_receipt( - mut scan_report: PendingPayableScanReport, - payable: PendingPayableFingerprint, - error_msg: &str, - logger: &Logger, -) -> PendingPayableScanReport { - debug!( - logger, - "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", - payable.hash, - error_msg, - payable.attempt, - elapsed_in_ms(payable.timestamp) - ); - - scan_report - .still_pending - .push(PendingPayableId::new(payable.rowid, payable.hash)); - scan_report + PaymentRetryRequired(Either), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Retry { + RetryPayments, + RetryTxStatusCheckOnly, +} + +pub struct TxCaseToBeInterpreted { + pub tx_by_table: TxByTable, + pub tx_receipt_result: TxReceiptResult, +} + +impl TxCaseToBeInterpreted { + pub fn new(tx_by_table: TxByTable, tx_receipt_result: TxReceiptResult) -> Self { + Self { + tx_by_table, + tx_receipt_result, + } + } +} + +#[derive(Debug)] +pub enum TxByTable { + SentPayable(SentTx), + FailedPayable(FailedTx), +} + +impl TxByTable { + pub fn hash(&self) -> TxHash { + match self { + TxByTable::SentPayable(tx) => tx.hash, + TxByTable::FailedPayable(tx) => tx.hash, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum TxHashByTable { + SentPayable(TxHash), + FailedPayable(TxHash), +} + +impl TxHashByTable { + pub fn hash(&self) -> TxHash { + match self { + TxHashByTable::SentPayable(hash) => *hash, + TxHashByTable::FailedPayable(hash) => *hash, + } + } +} + +impl From<&TxByTable> for TxHashByTable { + fn from(tx: &TxByTable) -> Self { + match tx { + TxByTable::SentPayable(tx) => TxHashByTable::SentPayable(tx.hash), + TxByTable::FailedPayable(tx) => TxHashByTable::FailedPayable(tx.hash), + } + } } #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, TxStatus}; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PresortedTxFailure, ReceiptScanReport, + RecheckRequiringFailures, Retry, TxByTable, TxHashByTable, + }; + use crate::accountant::test_utils::{make_failed_tx, make_sent_tx}; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::make_tx_hash; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::ops::Sub; + use std::time::{Duration, SystemTime}; + use std::vec; + + #[test] + fn detected_confirmations_is_empty_works() { + let subject = DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }; + + assert_eq!(subject.is_empty(), true); + } + + #[test] + fn requires_payments_retry() { + // Maximalist approach: exhaustive set of tested variants: + let tx_failures_feedings = vec![ + vec![PresortedTxFailure::NewEntry(make_failed_tx(456))], + vec![PresortedTxFailure::RecheckCompleted(make_tx_hash(123))], + vec![ + PresortedTxFailure::NewEntry(make_failed_tx(123)), + PresortedTxFailure::NewEntry(make_failed_tx(456)), + ], + vec![ + PresortedTxFailure::RecheckCompleted(make_tx_hash(654)), + PresortedTxFailure::RecheckCompleted(make_tx_hash(321)), + ], + vec![ + PresortedTxFailure::NewEntry(make_failed_tx(456)), + PresortedTxFailure::RecheckCompleted(make_tx_hash(654)), + ], + ]; + let tx_receipt_rpc_failures_feeding = vec![ + vec![], + vec![FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ))], + vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + make_tx_hash(12121), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + )], + ]; + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(456)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for tx_failures in &tx_failures_feedings { + for rpc_failures in &tx_receipt_rpc_failures_feeding { + for detected_confirmations in &detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: tx_failures.clone(), + tx_receipt_rpc_failures: rpc_failures.clone(), + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, + Some(Retry::RetryPayments), + "Expected Some(Retry::RetryPayments) but got {:?} for case {:?}", + result, + case + ); + } + } + } + } #[test] - fn requires_payments_retry_says_yes() { - todo!("complete this test with GH-604") - // let cases = vec![ - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // ]; - // - // cases.into_iter().enumerate().for_each(|(idx, case)| { - // let result = case.requires_payments_retry(); - // assert_eq!( - // result, true, - // "We expected true, but got false for case of idx {}", - // idx - // ) - // }) + fn requires_only_receipt_retrieval_retry() { + let rpc_failure_feedings = vec![ + vec![FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ))], + vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + make_tx_hash(1234), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ), + )], + vec![ + FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ))), + )), + FailedValidationByTable::FailedPayable(FailedValidation::new( + make_tx_hash(1234), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + )), + ], + ]; + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for rpc_failures in &rpc_failure_feedings { + for detected_confirmations in &detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], // This is the determinant + tx_receipt_rpc_failures: rpc_failures.clone(), + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, + Some(Retry::RetryTxStatusCheckOnly), + "Expected Some(Retry::RetryTxStatusCheckOnly) but got {:?} for case {:?}", + result, + case + ); + } + } } #[test] fn requires_payments_retry_says_no() { - todo!("complete this test with GH-604") - // let report = PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }; - // - // let result = report.requires_payments_retry(); - // - // assert_eq!(result, false) + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for detected_confirmations in detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, None, + "We expected None but got {:?} for case {:?}", + result, case + ); + } + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: reading tx receipts gave no results, \ + but always should" + )] + fn requires_payments_retry_with_no_results_in_whole_summary() { + let report = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + }; + + let _ = report.requires_payments_retry(); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_single_record() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let state_before = subject.sent_payables.clone(); + subject.load_cache(records); + + let first_attempt = subject.get_record_by_hash(tx_hash); + let second_attempt = subject.get_record_by_hash(tx_hash); + + assert_eq!(state_before, hashmap!()); + assert_eq!(first_attempt, Some(sent_tx)); + assert_eq!(second_attempt, None); + assert!( + subject.sent_payables.is_empty(), + "Should be empty but was {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_multiple_records() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx_1 = make_sent_tx(123); + let tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(456); + let tx_hash_2 = sent_tx_2.hash; + let sent_tx_3 = make_sent_tx(789); + let tx_hash_3 = sent_tx_3.hash; + let sent_tx_4 = make_sent_tx(101); + let tx_hash_4 = sent_tx_4.hash; + let nonexistent_tx_hash = make_tx_hash(234); + let records = vec![ + sent_tx_1.clone(), + sent_tx_2.clone(), + sent_tx_3.clone(), + sent_tx_4.clone(), + ]; + + let first_query = subject.get_record_by_hash(tx_hash_1); + subject.load_cache(records); + let second_query = subject.get_record_by_hash(nonexistent_tx_hash); + let third_query = subject.get_record_by_hash(tx_hash_2); + let fourth_query = subject.get_record_by_hash(tx_hash_1); + let fifth_query = subject.get_record_by_hash(tx_hash_4); + let sixth_query = subject.get_record_by_hash(tx_hash_1); + let seventh_query = subject.get_record_by_hash(tx_hash_1); + let eighth_query = subject.get_record_by_hash(tx_hash_3); + + assert_eq!(first_query, None); + assert_eq!(second_query, None); + assert_eq!(third_query, Some(sent_tx_2)); + assert_eq!(fourth_query, Some(sent_tx_1)); + assert_eq!(fifth_query, Some(sent_tx_4)); + assert_eq!(sixth_query, None); + assert_eq!(seventh_query, None); + assert_eq!(eighth_query, Some(sent_tx_3)); + assert!( + subject.sent_payables.is_empty(), + "Expected empty cache, but got {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_ensure_empty_happy_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_happy_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(567); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + let _ = subject.get_record_by_hash(tx_hash); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.sent_payables.is_empty(), + "Should be empty by now but was {:?}", + subject.sent_payables + ); + TestLogHandler::default().exists_no_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some pending payables left unprocessed:" + )); + } + + #[test] + fn pending_payable_cache_ensure_empty_sad_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_sad_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(567); + let tx_timestamp = sent_tx.timestamp; + let records = vec![sent_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.sent_payables.is_empty(), + "Should be empty by now but was {:?}", + subject.sent_payables + ); + TestLogHandler::default().exists_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some pending payables left unprocessed: \ + {{0x0000000000000000000000000000000000000000000000000000000000000237: SentTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ + 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ + {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, status: Pending(Waiting) }}}}. \ + Dumping." + )); + } + + #[test] + fn pending_payable_cache_dump_works() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx_1 = make_sent_tx(567); + let tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(456); + let tx_hash_2 = sent_tx_2.hash; + let sent_tx_3 = make_sent_tx(789); + let tx_hash_3 = sent_tx_3.hash; + let records = vec![sent_tx_1.clone(), sent_tx_2.clone(), sent_tx_3.clone()]; + subject.load_cache(records); + + let result = subject.dump_cache(); + + assert_eq!( + result, + hashmap! ( + tx_hash_1 => sent_tx_1, + tx_hash_2 => sent_tx_2, + tx_hash_3 => sent_tx_3 + ) + ); + } + + #[test] + fn failure_cache_insert_and_get_methods_single_record() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_hash = failed_tx.hash; + let records = vec![failed_tx.clone()]; + let state_before = subject.failures.clone(); + subject.load_cache(records); + + let first_attempt = subject.get_record_by_hash(tx_hash); + let second_attempt = subject.get_record_by_hash(tx_hash); + + assert_eq!(state_before, hashmap!()); + assert_eq!(first_attempt, Some(failed_tx)); + assert_eq!(second_attempt, None); + assert!( + subject.failures.is_empty(), + "Should be empty but was {:?}", + subject.failures + ); + } + + #[test] + fn failure_cache_insert_and_get_methods_multiple_records() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx_1 = make_failed_tx(123); + let tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(456); + let tx_hash_2 = failed_tx_2.hash; + let failed_tx_3 = make_failed_tx(789); + let tx_hash_3 = failed_tx_3.hash; + let failed_tx_4 = make_failed_tx(101); + let tx_hash_4 = failed_tx_4.hash; + let nonexistent_tx_hash = make_tx_hash(234); + let records = vec![ + failed_tx_1.clone(), + failed_tx_2.clone(), + failed_tx_3.clone(), + failed_tx_4.clone(), + ]; + + let first_query = subject.get_record_by_hash(tx_hash_1); + subject.load_cache(records); + let second_query = subject.get_record_by_hash(nonexistent_tx_hash); + let third_query = subject.get_record_by_hash(tx_hash_2); + let fourth_query = subject.get_record_by_hash(tx_hash_1); + let fifth_query = subject.get_record_by_hash(tx_hash_4); + let sixth_query = subject.get_record_by_hash(tx_hash_1); + let seventh_query = subject.get_record_by_hash(tx_hash_1); + let eighth_query = subject.get_record_by_hash(tx_hash_3); + + assert_eq!(first_query, None); + assert_eq!(second_query, None); + assert_eq!(third_query, Some(failed_tx_2)); + assert_eq!(fourth_query, Some(failed_tx_1)); + assert_eq!(fifth_query, Some(failed_tx_4)); + assert_eq!(sixth_query, None); + assert_eq!(seventh_query, None); + assert_eq!(eighth_query, Some(failed_tx_3)); + assert!( + subject.failures.is_empty(), + "Expected empty cache, but got {:?}", + subject.failures + ); + } + + #[test] + fn failure_cache_ensure_empty_happy_path() { + init_test_logging(); + let test_name = "failure_cache_ensure_empty_happy_path"; + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_hash = failed_tx.hash; + let records = vec![failed_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + let _ = subject.get_record_by_hash(tx_hash); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.failures.is_empty(), + "Should be empty by now but was {:?}", + subject.failures + ); + TestLogHandler::default().exists_no_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some tx failures left unprocessed:" + )); + } + + #[test] + fn failure_cache_ensure_empty_sad_path() { + init_test_logging(); + let test_name = "failure_cache_ensure_empty_sad_path"; + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_timestamp = failed_tx.timestamp; + let records = vec![failed_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.failures.is_empty(), + "Should be empty by now but was {:?}", + subject.failures + ); + TestLogHandler::default().exists_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some tx failures left unprocessed: \ + {{0x0000000000000000000000000000000000000000000000000000000000000237: FailedTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ + 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ + {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: \ + RetryRequired }}}}. Dumping." + )); + } + + #[test] + fn failure_cache_dump_works() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx_1 = make_failed_tx(567); + let tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(456); + let tx_hash_2 = failed_tx_2.hash; + let failed_tx_3 = make_failed_tx(789); + let tx_hash_3 = failed_tx_3.hash; + let records = vec![ + failed_tx_1.clone(), + failed_tx_2.clone(), + failed_tx_3.clone(), + ]; + subject.load_cache(records); + + let result = subject.dump_cache(); + + assert_eq!( + result, + hashmap! ( + tx_hash_1 => failed_tx_1, + tx_hash_2 => failed_tx_2, + tx_hash_3 => failed_tx_3 + ) + ); + } + + #[test] + fn failed_validation_new_status_works_for_tx_statuses() { + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(11)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(22)); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_c); + let cases = vec![ + ( + FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ), + Some(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ), + ))), + ), + ( + FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + Some(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + ))), + ), + ]; + + cases.into_iter().for_each(|(input, expected)| { + assert_eq!(input.new_status(&clock), expected); + }); + } + + #[test] + fn failed_validation_new_status_works_for_failure_statuses() { + let timestamp_a = SystemTime::now().sub(Duration::from_secs(222)); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(3333)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(44444)); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b); + let cases = vec![ + ( + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ), + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_a), + )), + )), + ), + ( + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ), + )), + ), + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + ), + )), + ), + ]; + + cases.into_iter().for_each(|(input, expected)| { + assert_eq!(input.new_status(&clock), expected); + }) + } + + #[test] + fn failed_validation_new_status_has_no_effect_on_unexpected_tx_status() { + let validation_failure_clock = ValidationFailureClockMock::default(); + let mal_validated_tx_status = FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Confirmed { + block_hash: "".to_string(), + block_number: 0, + detection: Detection::Normal, + }, + ); + + assert_eq!( + mal_validated_tx_status.new_status(&validation_failure_clock), + None + ); + } + + #[test] + fn failed_validation_new_status_has_no_effect_on_unexpected_failure_status() { + let validation_failure_clock = ValidationFailureClockMock::default(); + let mal_validated_failure_statuses = vec![ + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RetryRequired, + ), + FailedValidation::new( + make_tx_hash(789), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + FailureStatus::Concluded, + ), + ]; + + mal_validated_failure_statuses + .into_iter() + .enumerate() + .for_each(|(idx, failed_validation)| { + let result = failed_validation.new_status(&validation_failure_clock); + assert_eq!( + result, None, + "Failed validation should evaluate to 'None' but was '{:?}' for idx: {}", + result, idx + ) + }); + } + + #[test] + fn tx_hash_by_table_provides_plain_hash() { + let expected_hash_a = make_tx_hash(123); + let a = TxHashByTable::SentPayable(expected_hash_a); + let expected_hash_b = make_tx_hash(654); + let b = TxHashByTable::FailedPayable(expected_hash_b); + + let result_a = a.hash(); + let result_b = b.hash(); + + assert_eq!(result_a, expected_hash_a); + assert_eq!(result_b, expected_hash_b); + } + + #[test] + fn tx_by_table_can_provide_hash() { + let sent_tx = make_sent_tx(123); + let expected_hash_a = sent_tx.hash; + let a = TxByTable::SentPayable(sent_tx); + let failed_tx = make_failed_tx(654); + let expected_hash_b = failed_tx.hash; + let b = TxByTable::FailedPayable(failed_tx); + + let result_a = a.hash(); + let result_b = b.hash(); + + assert_eq!(result_a, expected_hash_a); + assert_eq!(result_b, expected_hash_b); + } + + #[test] + fn tx_by_table_can_be_converted_into_tx_hash_by_table() { + let sent_tx = make_sent_tx(123); + let expected_hash_a = sent_tx.hash; + let a = TxByTable::SentPayable(sent_tx); + let failed_tx = make_failed_tx(654); + let expected_hash_b = failed_tx.hash; + let b = TxByTable::FailedPayable(failed_tx); + + let result_a = TxHashByTable::from(&a); + let result_b = TxHashByTable::from(&b); + + assert_eq!(result_a, TxHashByTable::SentPayable(expected_hash_a)); + assert_eq!(result_b, TxHashByTable::FailedPayable(expected_hash_b)); } } diff --git a/node/src/accountant/scanners/receivable_scanner/mod.rs b/node/src/accountant/scanners/receivable_scanner/mod.rs index b7222df0d..eff0df95e 100644 --- a/node/src/accountant/scanners/receivable_scanner/mod.rs +++ b/node/src/accountant/scanners/receivable_scanner/mod.rs @@ -111,7 +111,7 @@ impl ReceivableScanner { { Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", + "Attempt to advance the start block to {} failed due to: {:?}", start_block_number, e ), } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 7a99605b0..03dad9942 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -44,7 +44,7 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq, Eq)] -pub enum ScanRescheduleAfterEarlyStop { +pub enum ScanReschedulingAfterEarlyStop { Schedule(ScanType), DoNotSchedule, } @@ -245,7 +245,7 @@ pub trait RescheduleScanOnErrorResolver { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop; + ) -> ScanReschedulingAfterEarlyStop; } #[derive(Default)] @@ -258,7 +258,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let reschedule_hint = match scanner { PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) @@ -285,16 +285,16 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } } @@ -304,9 +304,9 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else { unreachable!( "{:?} should be impossible with RetryPayableScanner in automatic mode", @@ -319,12 +319,12 @@ impl RescheduleScanOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -340,7 +340,7 @@ impl RescheduleScanOnErrorResolverReal { // the user. // TODO Correctly, a check-point during the bootstrap that wouldn't allow to come // this far should be the solution. Part of the issue mentioned in GH-799 - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) } else { unreachable!( "PendingPayableScanner called later than the initial attempt, but \ @@ -359,7 +359,7 @@ impl RescheduleScanOnErrorResolverReal { scanner: PayableSequenceScanner, is_externally_triggered: bool, logger: &Logger, - reschedule_hint: &ScanRescheduleAfterEarlyStop, + reschedule_hint: &ScanReschedulingAfterEarlyStop, ) { let scan_mode = if is_externally_triggered { "Manual" @@ -381,7 +381,7 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, }; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; @@ -634,7 +634,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::DoNotSchedule, + ScanReschedulingAfterEarlyStop::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, @@ -669,7 +669,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); @@ -722,7 +722,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables), "We expected Schedule(PendingPayables) but got {:?} for {:?}", result, scanner @@ -904,7 +904,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, ); diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 3747728ab..c459f7226 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -1,30 +1,29 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod payable_scanner_utils { - use crate::accountant::db_access_objects::utils::ThresholdUtils; - use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; + use crate::accountant::db_access_objects::utils::{ThresholdUtils, TxHash}; + use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; - use crate::accountant::{comma_joined_stringifiable, SentPayables}; + use crate::accountant::{PendingPayable, SentPayables}; use crate::sub_lib::accountant::PaymentThresholds; - use crate::sub_lib::wallet::Wallet; use itertools::Itertools; use masq_lib::logger::Logger; use std::cmp::Ordering; + use std::collections::HashSet; use std::ops::Not; use std::time::SystemTime; use thousands::Separable; - use web3::types::H256; + use web3::types::{Address, H256}; use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; #[derive(Debug, PartialEq, Eq)] pub enum PayableTransactingErrorEnum { LocallyCausedError(PayableTransactionError), - RemotelyCausedErrors(Vec), + RemotelyCausedErrors(HashSet), } #[derive(Debug, PartialEq)] @@ -102,6 +101,7 @@ pub mod payable_scanner_utils { oldest.balance_wei, oldest.age) } + // TODO lifetimes simplification??? pub fn separate_errors<'a, 'b>( sent_payables: &'a SentPayables, logger: &'b Logger, @@ -111,15 +111,28 @@ pub mod payable_scanner_utils { if individual_batch_responses.is_empty() { panic!("Broken code: An empty vector of processed payments claiming to be an Ok value") } - let (oks, err_hashes_opt) = + + let separated_txs_by_result = separate_rpc_results(individual_batch_responses, logger); - let remote_errs_opt = err_hashes_opt.map(RemotelyCausedErrors); + + let remote_errs_opt = if separated_txs_by_result.err_results.is_empty() { + None + } else { + warning!( + logger, + "Please check your blockchain service URL configuration due \ + to detected remote failures" + ); + Some(RemotelyCausedErrors(separated_txs_by_result.err_results)) + }; + let oks = separated_txs_by_result.ok_results; + (oks, remote_errs_opt) } Err(e) => { warning!( logger, - "Any persisted data from failed process will be deleted. Caused by: {}", + "Any persisted data from the failed process will be deleted. Caused by: {}", e ); @@ -128,55 +141,49 @@ pub mod payable_scanner_utils { } } - fn separate_rpc_results<'a, 'b>( + fn separate_rpc_results<'a>( batch_request_responses: &'a [ProcessedPayableFallible], - logger: &'b Logger, - ) -> (Vec<&'a PendingPayable>, Option>) { + logger: &Logger, + ) -> SeparatedTxsByResult<'a> { //TODO maybe we can return not tuple but struct with remote_errors_opt member - let (oks, errs) = batch_request_responses + let init = SeparatedTxsByResult::default(); + batch_request_responses .iter() - .fold((vec![], vec![]), |acc, rpc_result| { - fold_guts(acc, rpc_result, logger) - }); - - let errs_opt = if !errs.is_empty() { Some(errs) } else { None }; - - (oks, errs_opt) - } - - fn add_pending_payable<'a>( - (mut oks, errs): (Vec<&'a PendingPayable>, Vec), - pending_payable: &'a PendingPayable, - ) -> SeparateTxsByResult<'a> { - oks.push(pending_payable); - (oks, errs) + .fold(init, |acc, rpc_result| { + separate_rpc_results_fold_guts(acc, rpc_result, logger) + }) } - fn add_rpc_failure((oks, mut errs): SeparateTxsByResult, hash: H256) -> SeparateTxsByResult { - errs.push(hash); - (oks, errs) + #[derive(Default)] + pub struct SeparatedTxsByResult<'a> { + pub ok_results: Vec<&'a PendingPayable>, + pub err_results: HashSet, } - type SeparateTxsByResult<'a> = (Vec<&'a PendingPayable>, Vec); - - fn fold_guts<'a, 'b>( - acc: SeparateTxsByResult<'a>, + fn separate_rpc_results_fold_guts<'a>( + mut acc: SeparatedTxsByResult<'a>, rpc_result: &'a ProcessedPayableFallible, - logger: &'b Logger, - ) -> SeparateTxsByResult<'a> { + logger: &Logger, + ) -> SeparatedTxsByResult<'a> { match rpc_result { ProcessedPayableFallible::Correct(pending_payable) => { - add_pending_payable(acc, pending_payable) + acc.ok_results.push(pending_payable); + acc } ProcessedPayableFallible::Failed(RpcPayableFailure { rpc_error, recipient_wallet, hash, }) => { - warning!(logger, "Remote transaction failure: '{}' for payment to {} and transaction hash {:?}. \ - Please check your blockchain service URL configuration.", rpc_error, recipient_wallet, hash + warning!( + logger, + "Remote sent payable failure '{}' for wallet {} and tx hash {:?}", + rpc_error, + recipient_wallet, + hash ); - add_rpc_failure(acc, *hash) + acc.err_results.insert(*hash); + acc } } } @@ -194,14 +201,14 @@ pub mod payable_scanner_utils { .duration_since(payable.last_paid_timestamp) .expect("Payable time is corrupt"); format!( - "{} wei owed for {} sec exceeds threshold: {} wei; creditor: {}", + "{} wei owed for {} sec exceeds the threshold {} wei for creditor {}", payable.balance_wei.separate_with_commas(), p_age.as_secs(), threshold_point.separate_with_commas(), payable.wallet ) }) - .join("\n") + .join(".\n") }) } @@ -234,51 +241,23 @@ pub mod payable_scanner_utils { } #[derive(Debug, PartialEq, Eq)] - pub struct PendingPayableMetadata<'a> { - pub recipient: &'a Wallet, + pub struct PendingPayableMissingInDb { + pub recipient: Address, pub hash: H256, - pub rowid_opt: Option, } - impl<'a> PendingPayableMetadata<'a> { - pub fn new( - recipient: &'a Wallet, - hash: H256, - rowid_opt: Option, - ) -> PendingPayableMetadata<'a> { - PendingPayableMetadata { - recipient, - hash, - rowid_opt, - } + impl PendingPayableMissingInDb { + pub fn new(recipient: Address, hash: H256) -> PendingPayableMissingInDb { + PendingPayableMissingInDb { recipient, hash } } } - pub fn mark_pending_payable_fatal_error( - sent_payments: &[&PendingPayable], - nonexistent: &[PendingPayableMetadata], - error: PayableDaoError, - missing_fingerprints_msg_maker: fn(&[PendingPayableMetadata]) -> String, - logger: &Logger, - ) { - if !nonexistent.is_empty() { - error!(logger, "{}", missing_fingerprints_msg_maker(nonexistent)) - }; - panic!( - "Unable to create a mark in the payable table for wallets {} due to {:?}", - comma_joined_stringifiable(sent_payments, |pending_p| pending_p - .recipient_wallet - .to_string()), - error - ) - } - - pub fn err_msg_for_failure_with_expected_but_missing_fingerprints( + pub fn err_msg_for_failure_with_expected_but_missing_sent_tx_record( nonexistent: Vec, serialize_hashes: fn(&[H256]) -> String, ) -> Option { nonexistent.is_empty().not().then_some(format!( - "Ran into failed transactions {} with missing fingerprints. System no longer reliable", + "Ran into failed payables {} with missing records. The system has become unreliable", serialize_hashes(&nonexistent), )) } @@ -333,15 +312,14 @@ mod tests { payables_debug_summary, separate_errors, PayableThresholdsGauge, PayableThresholdsGaugeReal, }; - use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; + use crate::accountant::{checked_conversion, gwei_to_wei, PendingPayable, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; use crate::test_utils::make_wallet; use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use std::time::SystemTime; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; + use std::time::{SystemTime}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -414,7 +392,7 @@ mod tests { init_test_logging(); let error = PayableTransactionError::Sending { msg: "Bad luck".to_string(), - hashes: vec![make_tx_hash(0x7b)], + hashes: hashset![make_tx_hash(0x7b)], }; let sent_payable = SentPayables { payment_procedure_result: Err(error.clone()), @@ -427,8 +405,8 @@ mod tests { assert_eq!(errs, Some(LocallyCausedError(error))); TestLogHandler::new().exists_log_containing( "WARN: test_logger: Any persisted data from \ - failed process will be deleted. Caused by: Sending phase: \"Bad luck\". Signed and hashed \ - transactions: 0x000000000000000000000000000000000000000000000000000000000000007b", + the failed process will be deleted. Caused by: Sending phase: \"Bad luck\". Signed and hashed txs: \ + 0x000000000000000000000000000000000000000000000000000000000000007b", ); } @@ -455,11 +433,14 @@ mod tests { let (oks, errs) = separate_errors(&sent_payable, &Logger::new("test_logger")); assert_eq!(oks, vec![&payable_ok]); - assert_eq!(errs, Some(RemotelyCausedErrors(vec![make_tx_hash(0x315)]))); - TestLogHandler::new().exists_log_containing("WARN: test_logger: Remote transaction failure: \ - 'Got invalid response: That jackass screwed it up' for payment to 0x000000000000000000000000\ - 00000077686f6f61 and transaction hash 0x0000000000000000000000000000000000000000000000000000\ - 000000000315. Please check your blockchain service URL configuration."); + assert_eq!( + errs, + Some(RemotelyCausedErrors(hashset![make_tx_hash(0x315)])) + ); + TestLogHandler::new().exists_log_containing("WARN: test_logger: Remote sent payable \ + failure 'Got invalid response: That jackass screwed it up' for wallet 0x00000000000000000000\ + 000000000077686f6f61 and tx hash 0x000000000000000000000000000000000000000000000000000000000\ + 0000315"); } #[test] @@ -522,10 +503,10 @@ mod tests { payables_debug_summary(&qualified_payables_and_threshold_points, &logger); TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ - 10,002,000,000,000,000 wei owed for 2678400 sec exceeds threshold: \ - 10,000,000,001,152,000 wei; creditor: 0x0000000000000000000000000077616c6c657430\n\ - 999,999,999,000,000,000 wei owed for 86455 sec exceeds threshold: \ - 999,978,993,055,555,580 wei; creditor: 0x0000000000000000000000000077616c6c657431"); + 10,002,000,000,000,000 wei owed for 2678400 sec exceeds the threshold \ + 10,000,000,001,152,000 wei for creditor 0x0000000000000000000000000077616c6c657430.\n\ + 999,999,999,000,000,000 wei owed for 86455 sec exceeds the threshold \ + 999,978,993,055,555,580 wei for creditor 0x0000000000000000000000000077616c6c657431"); } #[test] @@ -665,7 +646,7 @@ mod tests { fn count_total_errors_works_correctly_for_local_error_after_signing() { let error = PayableTransactionError::Sending { msg: "Ouuuups".to_string(), - hashes: vec![make_tx_hash(333), make_tx_hash(666)], + hashes: hashset![make_tx_hash(333), make_tx_hash(666)], }; let sent_payable = Some(LocallyCausedError(error)); @@ -676,7 +657,7 @@ mod tests { #[test] fn count_total_errors_works_correctly_for_remote_errors() { - let sent_payable = Some(RemotelyCausedErrors(vec![ + let sent_payable = Some(RemotelyCausedErrors(hashset![ make_tx_hash(123), make_tx_hash(456), ])); diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 637325091..ecd0781fe 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,16 +2,19 @@ #![cfg(test)] +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::scanners::payable_scanner_extension::{ MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, }; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableCache, PendingPayableScanResult, +}; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, - ScanRescheduleAfterEarlyStop, + ScanReschedulingAfterEarlyStop, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::{ @@ -19,8 +22,7 @@ use crate::accountant::scanners::{ Scanner, StartScanError, StartableScanner, }; use crate::accountant::{ - ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, - SentPayables, + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, SentPayables, TxReceiptsMessage, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, OutboundPaymentsInstructions}; @@ -32,6 +34,7 @@ use masq_lib::ui_gateway::NodeToUiMessage; use regex::Regex; use std::any::type_name; use std::cell::RefCell; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use time::{format_description, PrimitiveDateTime}; @@ -367,11 +370,7 @@ pub enum ScannerReplacement { PendingPayable( ReplacementType< PendingPayableScanner, - ScannerMock< - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - >, + ScannerMock, >, ), Receivable( @@ -387,8 +386,8 @@ pub enum MarkScanner<'a> { Started(SystemTime), } -// Cautious: Don't compare to another timestamp on a full match; this timestamp is trimmed in -// nanoseconds down to three digits +// Cautious: Don't compare to another timestamp on an exact match. This timestamp is trimmed in +// nanoseconds down to three digits. Works only for the format bound by TIME_FORMATTING_STRING pub fn parse_system_time_from_str(examined_str: &str) -> Vec { let regex = Regex::new(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})").unwrap(); let captures = regex.captures_iter(examined_str); @@ -444,7 +443,7 @@ pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec>>, - resolve_rescheduling_on_error_results: RefCell>, + resolve_rescheduling_on_error_results: RefCell>, } impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { @@ -454,7 +453,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { self.resolve_rescheduling_on_error_params .lock() .unwrap() @@ -480,7 +479,7 @@ impl RescheduleScanOnErrorResolverMock { } pub fn resolve_rescheduling_on_error_result( self, - result: ScanRescheduleAfterEarlyStop, + result: ScanReschedulingAfterEarlyStop, ) -> Self { self.resolve_rescheduling_on_error_results .borrow_mut() @@ -492,3 +491,70 @@ impl RescheduleScanOnErrorResolverMock { pub fn make_zeroed_consuming_wallet_balances() -> ConsumingWalletBalances { ConsumingWalletBalances::new(0.into(), 0.into()) } + +pub struct PendingPayableCacheMock { + load_cache_params: Arc>>>, + load_cache_results: RefCell>>, + get_record_by_hash_params: Arc>>, + get_record_by_hash_results: RefCell>>, + ensure_empty_cache_params: Arc>>, +} + +impl Default for PendingPayableCacheMock { + fn default() -> Self { + Self { + load_cache_params: Arc::new(Mutex::new(vec![])), + load_cache_results: RefCell::new(vec![]), + get_record_by_hash_params: Arc::new(Mutex::new(vec![])), + get_record_by_hash_results: RefCell::new(vec![]), + ensure_empty_cache_params: Arc::new(Mutex::new(vec![])), + } + } +} + +impl PendingPayableCache for PendingPayableCacheMock { + fn load_cache(&mut self, records: Vec) { + self.load_cache_params.lock().unwrap().push(records); + self.load_cache_results.borrow_mut().remove(0); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.get_record_by_hash_params.lock().unwrap().push(hash); + self.get_record_by_hash_results.borrow_mut().remove(0) + } + + fn ensure_empty_cache(&mut self, _logger: &Logger) { + self.ensure_empty_cache_params.lock().unwrap().push(()); + } + + fn dump_cache(&mut self) -> HashMap { + unimplemented!("not needed yet") + } +} + +impl PendingPayableCacheMock { + pub fn load_cache_params(mut self, params: &Arc>>>) -> Self { + self.load_cache_params = params.clone(); + self + } + + pub fn load_cache_result(self, result: HashMap) -> Self { + self.load_cache_results.borrow_mut().push(result); + self + } + + pub fn get_record_by_hash_params(mut self, params: &Arc>>) -> Self { + self.get_record_by_hash_params = params.clone(); + self + } + + pub fn get_record_by_hash_result(self, result: Option) -> Self { + self.get_record_by_hash_results.borrow_mut().push(result); + self + } + + pub fn ensure_empty_cache_params(mut self, params: &Arc>>) -> Self { + self.ensure_empty_cache_params = params.clone(); + self + } +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 44c888ff7..81b612e47 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -4,18 +4,21 @@ use crate::accountant::db_access_objects::banned_dao::{BannedDao, BannedDaoFactory}; use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureReason, FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::payable_dao::{ - PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, -}; -use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayableDao, PendingPayableDaoError, PendingPayableDaoFactory, TransactionHashes, + MarkPendingPayableID, PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, }; use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDaoError, SentTx, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + SentPayableDao, SentPayableDaoFactory, TxStatus, +}; use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, TxHash, TxIdentifiers, }; @@ -25,15 +28,17 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, }; use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; +use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableCache; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; +use crate::accountant::scanners::test_utils::PendingPayableCacheMock; use crate::accountant::scanners::PayableScanner; -use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; -use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; -use crate::blockchain::test_utils::make_tx_hash; +use crate::accountant::{gwei_to_wei, Accountant}; +use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, TxBlock}; +use crate::blockchain::errors::validation_status::{ValidationFailureClock, ValidationStatus}; +use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::bootstrapper::BootstrapperConfig; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; @@ -45,8 +50,7 @@ use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::make_bc_with_defaults; -use actix::System; -use ethereum_types::H256; +use ethereum_types::U64; use masq_lib::logger::Logger; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; @@ -57,6 +61,7 @@ use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::SystemTime; +use web3::types::Address; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_unix_timestamp(SystemTime::now()); @@ -94,13 +99,73 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( } } +pub fn make_sent_tx(num: u64) -> SentTx { + if num == 0 { + panic!("num for generating must be greater than 0"); + } + let params = TxRecordCommonParts::new(num); + SentTx { + hash: params.hash, + receiver_address: params.receiver_address, + amount_minor: params.amount_minor, + timestamp: params.timestamp, + gas_price_minor: params.gas_price_minor, + nonce: params.nonce, + status: TxStatus::Pending(ValidationStatus::Waiting), + } +} + +pub fn make_failed_tx(num: u64) -> FailedTx { + let params = TxRecordCommonParts::new(num); + FailedTx { + hash: params.hash, + receiver_address: params.receiver_address, + amount_minor: params.amount_minor, + timestamp: params.timestamp, + gas_price_minor: params.gas_price_minor, + nonce: params.nonce, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + } +} + +pub fn make_transaction_block(num: u64) -> TxBlock { + TxBlock { + block_hash: make_block_hash(num as u32), + block_number: U64::from(num * num * num), + } +} + +struct TxRecordCommonParts { + hash: TxHash, + receiver_address: Address, + amount_minor: u128, + timestamp: i64, + gas_price_minor: u128, + nonce: u64, +} + +impl TxRecordCommonParts { + fn new(num: u64) -> Self { + Self { + hash: make_tx_hash(num as u32), + receiver_address: make_wallet(&format!("wallet{}", num)).address(), + amount_minor: gwei_to_wei(num * num), + timestamp: to_unix_timestamp(SystemTime::now()) - (num as i64 * 60), + gas_price_minor: gwei_to_wei(num), + nonce: num, + } + } +} + pub struct AccountantBuilder { config_opt: Option, consuming_wallet_opt: Option, logger_opt: Option, payable_dao_factory_opt: Option, receivable_dao_factory_opt: Option, - pending_payable_dao_factory_opt: Option, + sent_payable_dao_factory_opt: Option, + failed_payable_dao_factory_opt: Option, banned_dao_factory_opt: Option, config_dao_factory_opt: Option, } @@ -113,7 +178,8 @@ impl Default for AccountantBuilder { logger_opt: None, payable_dao_factory_opt: None, receivable_dao_factory_opt: None, - pending_payable_dao_factory_opt: None, + sent_payable_dao_factory_opt: None, + failed_payable_dao_factory_opt: None, banned_dao_factory_opt: None, config_dao_factory_opt: None, } @@ -251,7 +317,11 @@ const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::PendingPayableScanner, ]; -const PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ +//TODO Utkarsh should also update this +const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 1] = + [DestinationMarker::PendingPayableScanner]; + +const SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, DestinationMarker::PayableScanner, DestinationMarker::PendingPayableScanner, @@ -278,16 +348,16 @@ impl AccountantBuilder { self } - pub fn pending_payable_daos( + pub fn sent_payable_daos( mut self, - specially_configured_daos: Vec>, + specially_configured_daos: Vec>, ) -> Self { create_or_update_factory!( specially_configured_daos, - PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - pending_payable_dao_factory_opt, - PendingPayableDaoFactoryMock, - PendingPayableDao, + SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, + sent_payable_dao_factory_opt, + SentPayableDaoFactoryMock, + SentPayableDao, self ) } @@ -306,6 +376,27 @@ impl AccountantBuilder { ) } + pub fn failed_payable_daos( + mut self, + mut specially_configured_daos: Vec>, + ) -> Self { + specially_configured_daos.iter_mut().for_each(|dao| { + if let DaoWithDestination::ForPendingPayableScanner(dao) = dao { + let mut extended_queue = vec![vec![]]; + extended_queue.append(&mut dao.retrieve_txs_results.borrow_mut()); + dao.retrieve_txs_results.replace(extended_queue); + } + }); + create_or_update_factory!( + specially_configured_daos, + FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, + failed_payable_dao_factory_opt, + FailedPayableDaoFactoryMock, + FailedPayableDao, + self + ) + } + pub fn receivable_daos( mut self, specially_configured_daos: Vec>, @@ -352,12 +443,15 @@ impl AccountantBuilder { .make_result(ReceivableDaoMock::new()) .make_result(ReceivableDaoMock::new()), ); - let pending_payable_dao_factory = self.pending_payable_dao_factory_opt.unwrap_or( - PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()), + let sent_payable_dao_factory = self.sent_payable_dao_factory_opt.unwrap_or( + SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()), ); + let failed_payable_dao_factory = self + .failed_payable_dao_factory_opt + .unwrap_or(FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new())); let banned_dao_factory = self .banned_dao_factory_opt .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); @@ -368,7 +462,8 @@ impl AccountantBuilder { config, DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), @@ -394,7 +489,8 @@ impl PayableDaoFactory for PayableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied \ + for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -430,7 +526,8 @@ impl ReceivableDaoFactory for ReceivableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only \ + supplied for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -530,7 +627,7 @@ pub struct PayableDaoMock { non_pending_payables_results: RefCell>>, mark_pending_payables_rowids_params: Arc>>>, mark_pending_payables_rowids_results: RefCell>>, - transactions_confirmed_params: Arc>>>, + transactions_confirmed_params: Arc>>>, transactions_confirmed_results: RefCell>>, custom_query_params: Arc>>>, custom_query_result: RefCell>>>, @@ -542,37 +639,36 @@ impl PayableDao for PayableDaoMock { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError> { - self.more_money_payable_parameters - .lock() - .unwrap() - .push((now, wallet.clone(), amount)); + self.more_money_payable_parameters.lock().unwrap().push(( + now, + wallet.clone(), + amount_minor, + )); self.more_money_payable_results.borrow_mut().remove(0) } fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> Result<(), PayableDaoError> { - self.mark_pending_payables_rowids_params - .lock() - .unwrap() - .push( - wallets_and_rowids - .iter() - .map(|(wallet, id)| ((*wallet).clone(), *id)) - .collect(), - ); - self.mark_pending_payables_rowids_results - .borrow_mut() - .remove(0) - } - - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], + _mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError> { + todo!("will be removed in the associated card - GH-662") + // self.mark_pending_payables_rowids_params + // .lock() + // .unwrap() + // .push( + // mark_instructions + // .iter() + // .map(|(wallet, id)| ((*wallet).clone(), *id)) + // .collect(), + // ); + // self.mark_pending_payables_rowids_results + // .borrow_mut() + // .remove(0) + } + + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError> { self.transactions_confirmed_params .lock() .unwrap() @@ -643,10 +739,7 @@ impl PayableDaoMock { self } - pub fn transactions_confirmed_params( - mut self, - params: &Arc>>>, - ) -> Self { + pub fn transactions_confirmed_params(mut self, params: &Arc>>>) -> Self { self.transactions_confirmed_params = params.clone(); self } @@ -694,12 +787,13 @@ impl ReceivableDao for ReceivableDaoMock { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError> { - self.more_money_receivable_parameters - .lock() - .unwrap() - .push((now, wallet.clone(), amount)); + self.more_money_receivable_parameters.lock().unwrap().push(( + now, + wallet.clone(), + amount_minor, + )); self.more_money_receivable_results.borrow_mut().remove(0) } @@ -886,178 +980,169 @@ pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> Boot } #[derive(Default)] -pub struct PendingPayableDaoMock { - fingerprints_rowids_params: Arc>>>, - fingerprints_rowids_results: RefCell>, - delete_fingerprints_params: Arc>>>, - delete_fingerprints_results: RefCell>>, - insert_new_fingerprints_params: Arc, SystemTime)>>>, - insert_new_fingerprints_results: RefCell>>, - increment_scan_attempts_params: Arc>>>, - increment_scan_attempts_result: RefCell>>, - mark_failures_params: Arc>>>, - mark_failures_results: RefCell>>, - return_all_errorless_fingerprints_params: Arc>>, - return_all_errorless_fingerprints_results: RefCell>>, - pub have_return_all_errorless_fingerprints_shut_down_the_system: bool, -} - -impl PendingPayableDao for PendingPayableDaoMock { - fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes { - self.fingerprints_rowids_params +pub struct SentPayableDaoMock { + get_tx_identifiers_params: Arc>>>, + get_tx_identifiers_results: RefCell>, + insert_new_records_params: Arc>>>, + insert_new_records_results: RefCell>>, + retrieve_txs_params: Arc>>>, + retrieve_txs_results: RefCell>>, + confirm_tx_params: Arc>>>, + confirm_tx_results: RefCell>>, + update_statuses_params: Arc>>>, + update_statuses_results: RefCell>>, + replace_records_params: Arc>>>, + replace_records_results: RefCell>>, + delete_records_params: Arc>>>, + delete_records_results: RefCell>>, +} + +impl SentPayableDao for SentPayableDaoMock { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + self.get_tx_identifiers_params .lock() .unwrap() - .push(hashes.to_vec()); - self.fingerprints_rowids_results.borrow_mut().remove(0) + .push(hashes.clone()); + self.get_tx_identifiers_results.borrow_mut().remove(0) } - - fn return_all_errorless_fingerprints(&self) -> Vec { - self.return_all_errorless_fingerprints_params + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + self.insert_new_records_params .lock() .unwrap() - .push(()); - if self.have_return_all_errorless_fingerprints_shut_down_the_system - && self - .return_all_errorless_fingerprints_results - .borrow() - .is_empty() - { - System::current().stop(); - return vec![]; - } - self.return_all_errorless_fingerprints_results - .borrow_mut() - .remove(0) + .push(txs.to_vec()); + self.insert_new_records_results.borrow_mut().remove(0) } - - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError> { - self.insert_new_fingerprints_params + fn retrieve_txs(&self, condition: Option) -> Vec { + self.retrieve_txs_params.lock().unwrap().push(condition); + self.retrieve_txs_results.borrow_mut().remove(0) + } + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { + self.confirm_tx_params .lock() .unwrap() - .push((hashes_and_amounts.to_vec(), batch_wide_timestamp)); - self.insert_new_fingerprints_results.borrow_mut().remove(0) + .push(hash_map.clone()); + self.confirm_tx_results.borrow_mut().remove(0) } - - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.delete_fingerprints_params + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + self.replace_records_params .lock() .unwrap() - .push(ids.to_vec()); - self.delete_fingerprints_results.borrow_mut().remove(0) + .push(new_txs.to_vec()); + self.replace_records_results.borrow_mut().remove(0) } - fn increment_scan_attempts(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.increment_scan_attempts_params + fn update_statuses( + &self, + hash_map: &HashMap, + ) -> Result<(), SentPayableDaoError> { + self.update_statuses_params .lock() .unwrap() - .push(ids.to_vec()); - self.increment_scan_attempts_result.borrow_mut().remove(0) + .push(hash_map.clone()); + self.update_statuses_results.borrow_mut().remove(0) } - fn mark_failures(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.mark_failures_params.lock().unwrap().push(ids.to_vec()); - self.mark_failures_results.borrow_mut().remove(0) + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + self.delete_records_params + .lock() + .unwrap() + .push(hashes.clone()); + self.delete_records_results.borrow_mut().remove(0) } } -impl PendingPayableDaoMock { +impl SentPayableDaoMock { pub fn new() -> Self { - PendingPayableDaoMock::default() + SentPayableDaoMock::default() } - pub fn fingerprints_rowids_params(mut self, params: &Arc>>>) -> Self { - self.fingerprints_rowids_params = params.clone(); + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + self.get_tx_identifiers_params = params.clone(); self } - pub fn fingerprints_rowids_result(self, result: TransactionHashes) -> Self { - self.fingerprints_rowids_results.borrow_mut().push(result); + pub fn get_tx_identifiers_result(self, result: TxIdentifiers) -> Self { + self.get_tx_identifiers_results.borrow_mut().push(result); self } - pub fn insert_fingerprints_params( + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + self.insert_new_records_params = params.clone(); + self + } + + pub fn insert_new_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.insert_new_records_results.borrow_mut().push(result); + self + } + + pub fn retrieve_txs_params( mut self, - params: &Arc, SystemTime)>>>, + params: &Arc>>>, ) -> Self { - self.insert_new_fingerprints_params = params.clone(); + self.retrieve_txs_params = params.clone(); self } - pub fn insert_fingerprints_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.insert_new_fingerprints_results - .borrow_mut() - .push(result); + pub fn retrieve_txs_result(self, result: Vec) -> Self { + self.retrieve_txs_results.borrow_mut().push(result); self } - pub fn delete_fingerprints_params(mut self, params: &Arc>>>) -> Self { - self.delete_fingerprints_params = params.clone(); + pub fn confirm_tx_params(mut self, params: &Arc>>>) -> Self { + self.confirm_tx_params = params.clone(); self } - pub fn delete_fingerprints_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.delete_fingerprints_results.borrow_mut().push(result); + pub fn confirm_tx_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.confirm_tx_results.borrow_mut().push(result); self } - pub fn return_all_errorless_fingerprints_params( - mut self, - params: &Arc>>, - ) -> Self { - self.return_all_errorless_fingerprints_params = params.clone(); + pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { + self.replace_records_params = params.clone(); self } - pub fn return_all_errorless_fingerprints_result( - self, - result: Vec, - ) -> Self { - self.return_all_errorless_fingerprints_results - .borrow_mut() - .push(result); + pub fn replace_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.replace_records_results.borrow_mut().push(result); self } - pub fn mark_failures_params(mut self, params: &Arc>>>) -> Self { - self.mark_failures_params = params.clone(); + pub fn update_statuses_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.update_statuses_params = params.clone(); self } - pub fn mark_failures_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.mark_failures_results.borrow_mut().push(result); + pub fn update_statuses_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.update_statuses_results.borrow_mut().push(result); self } - pub fn increment_scan_attempts_params(mut self, params: &Arc>>>) -> Self { - self.increment_scan_attempts_params = params.clone(); + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + self.delete_records_params = params.clone(); self } - pub fn increment_scan_attempts_result( - self, - result: Result<(), PendingPayableDaoError>, - ) -> Self { - self.increment_scan_attempts_result - .borrow_mut() - .push(result); + pub fn delete_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.delete_records_results.borrow_mut().push(result); self } } -pub struct PendingPayableDaoFactoryMock { +pub struct SentPayableDaoFactoryMock { make_params: Arc>>, - make_results: RefCell>>, + make_results: RefCell>>, } -impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { - fn make(&self) -> Box { +impl SentPayableDaoFactory for SentPayableDaoFactoryMock { + fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "PendingPayableDao Missing. This problem mostly occurs when PendingPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "SentPayableDao Missing. This problem mostly occurs when SentPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -1065,7 +1150,7 @@ impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { } } -impl PendingPayableDaoFactoryMock { +impl SentPayableDaoFactoryMock { pub fn new() -> Self { Self { make_params: Arc::new(Mutex::new(vec![])), @@ -1078,7 +1163,7 @@ impl PendingPayableDaoFactoryMock { self } - pub fn make_result(self, result: PendingPayableDaoMock) -> Self { + pub fn make_result(self, result: SentPayableDaoMock) -> Self { self.make_results.borrow_mut().push(Box::new(result)); self } @@ -1122,12 +1207,12 @@ impl FailedPayableDao for FailedPayableDaoMock { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { self.update_statuses_params .lock() .unwrap() - .push(status_updates); + .push(status_updates.clone()); self.update_statuses_results.borrow_mut().remove(0) } @@ -1209,9 +1294,6 @@ pub struct FailedPayableDaoFactoryMock { impl FailedPayableDaoFactory for FailedPayableDaoFactoryMock { fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!("FailedPayableDao Missing.") - }; self.make_params.lock().unwrap().push(()); self.make_results.borrow_mut().remove(0) } @@ -1238,7 +1320,7 @@ impl FailedPayableDaoFactoryMock { pub struct PayableScannerBuilder { payable_dao: PayableDaoMock, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, payment_thresholds: PaymentThresholds, payment_adjuster: PaymentAdjusterMock, } @@ -1247,7 +1329,7 @@ impl PayableScannerBuilder { pub fn new() -> Self { Self { payable_dao: PayableDaoMock::new(), - pending_payable_dao: PendingPayableDaoMock::new(), + sent_payable_dao: SentPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), payment_adjuster: PaymentAdjusterMock::default(), } @@ -1271,18 +1353,18 @@ impl PayableScannerBuilder { self } - pub fn pending_payable_dao( + pub fn sent_payable_dao( mut self, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, ) -> PayableScannerBuilder { - self.pending_payable_dao = pending_payable_dao; + self.sent_payable_dao = sent_payable_dao; self } pub fn build(self) -> PayableScanner { PayableScanner::new( Box::new(self.payable_dao), - Box::new(self.pending_payable_dao), + Box::new(self.sent_payable_dao), Rc::new(self.payment_thresholds), Box::new(self.payment_adjuster), ) @@ -1291,20 +1373,26 @@ impl PayableScannerBuilder { pub struct PendingPayableScannerBuilder { payable_dao: PayableDaoMock, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, + failed_payable_dao: FailedPayableDaoMock, payment_thresholds: PaymentThresholds, - when_pending_too_long_sec: u64, financial_statistics: FinancialStatistics, + current_sent_payables: Box>, + yet_unproven_failed_payables: Box>, + clock: Box, } impl PendingPayableScannerBuilder { pub fn new() -> Self { Self { payable_dao: PayableDaoMock::new(), - pending_payable_dao: PendingPayableDaoMock::new(), + sent_payable_dao: SentPayableDaoMock::new(), + failed_payable_dao: FailedPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, financial_statistics: FinancialStatistics::default(), + current_sent_payables: Box::new(PendingPayableCacheMock::default()), + yet_unproven_failed_payables: Box::new(PendingPayableCacheMock::default()), + clock: Box::new(ValidationFailureClockMock::default()), } } @@ -1313,24 +1401,46 @@ impl PendingPayableScannerBuilder { self } - pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { - self.pending_payable_dao = pending_payable_dao; + pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { + self.sent_payable_dao = sent_payable_dao; self } - pub fn when_pending_too_long_sec(mut self, interval: u64) -> Self { - self.when_pending_too_long_sec = interval; + pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { + self.failed_payable_dao = failed_payable_dao; + self + } + + pub fn sent_payable_cache(mut self, cache: Box>) -> Self { + self.current_sent_payables = cache; + self + } + + pub fn failed_payable_cache( + mut self, + failures: Box>, + ) -> Self { + self.yet_unproven_failed_payables = failures; + self + } + + pub fn validation_failure_clock(mut self, clock: Box) -> Self { + self.clock = clock; self } pub fn build(self) -> PendingPayableScanner { - PendingPayableScanner::new( + let mut scanner = PendingPayableScanner::new( Box::new(self.payable_dao), - Box::new(self.pending_payable_dao), + Box::new(self.sent_payable_dao), + Box::new(self.failed_payable_dao), Rc::new(self.payment_thresholds), - self.when_pending_too_long_sec, Rc::new(RefCell::new(self.financial_statistics)), - ) + ); + scanner.current_sent_payables = self.current_sent_payables; + scanner.yet_unproven_failed_payables = self.yet_unproven_failed_payables; + scanner.clock = self.clock; + scanner } } @@ -1398,17 +1508,6 @@ pub fn make_custom_payment_thresholds() -> PaymentThresholds { } } -pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { - PendingPayableFingerprint { - rowid: 33, - timestamp: from_unix_timestamp(222_222_222), - hash: make_tx_hash(456), - attempt: 1, - amount: 12345, - process_error: None, - } -} - pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, @@ -1490,10 +1589,10 @@ where { let conn = Connection::open_in_memory().unwrap(); let execute = |sql: &str| conn.execute(sql, []).unwrap(); - execute("create table whatever (exclamations text)"); - execute("insert into whatever (exclamations) values ('Gosh')"); + execute("create table whatever (exclamation text)"); + execute("insert into whatever (exclamation) values ('Gosh')"); - conn.query_row("select exclamations from whatever", [], tested_fn) + conn.query_row("select exclamation from whatever", [], tested_fn) .unwrap(); } diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 9e15f9c5d..8b24da722 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -473,7 +473,8 @@ impl ActorFactory for ActorFactoryReal { ) -> AccountantSubs { let data_directory = config.data_directory.as_path(); let payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); - let pending_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let sent_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let failed_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let receivable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let banned_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let config_dao_factory = Box::new(Accountant::dao_factory(data_directory)); @@ -484,7 +485,8 @@ impl ActorFactory for ActorFactoryReal { config, DaoFactories { payable_dao_factory, - pending_payable_dao_factory, + sent_payable_dao_factory, + failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, config_dao_factory, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 421fc6bd5..d1d41337b 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,17 +1,21 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, PricedQualifiedPayables}; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayablesMessage, +}; use crate::accountant::{ - ReceivedPayments, ResponseSkeleton, ScanError, - SentPayables, SkeletonOptHolder, + ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, TxReceiptResult, }; -use crate::accountant::{ReportTransactionReceipts, RequestTransactionReceipts}; +use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; use crate::actor_system_factory::SubsFactory; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainInterfaceError, PayableTransactionError, }; -use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible; +use crate::blockchain::blockchain_interface::data_structures::{ + ProcessedPayableFallible, StatusReadFromReceiptCheck, +}; use crate::blockchain::blockchain_interface::BlockchainInterface; use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal}; @@ -19,12 +23,10 @@ use crate::db_config::config_dao::ConfigDaoReal; use crate::db_config::persistent_configuration::{ PersistentConfiguration, PersistentConfigurationReal, }; -use crate::sub_lib::blockchain_bridge::{ - BlockchainBridgeSubs, OutboundPaymentsInstructions, -}; +use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, OutboundPaymentsInstructions}; use crate::sub_lib::peer_actors::BindMessage; use crate::sub_lib::utils::{db_connection_launch_panic, handle_ui_crash_request}; -use crate::sub_lib::wallet::{Wallet}; +use crate::sub_lib::wallet::Wallet; use actix::Actor; use actix::Context; use actix::Handler; @@ -33,19 +35,16 @@ use actix::{Addr, Recipient}; use futures::Future; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; +use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; +use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; use std::string::ToString; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -use ethabi::Hash; use web3::types::H256; -use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; -use masq_lib::messages::ScanType; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; pub const DEFAULT_BLOCKCHAIN_SERVICE_URL: &str = "https://0.0.0.0"; @@ -59,12 +58,12 @@ pub struct BlockchainBridge { received_payments_subs_opt: Option>, scan_error_subs_opt: Option>, crashable: bool, - pending_payable_confirmation: TransactionConfirmationTools, + pending_payable_confirmation: TxConfirmationTools, } -struct TransactionConfirmationTools { - new_pp_fingerprints_sub_opt: Option>, - report_transaction_receipts_sub_opt: Option>, +struct TxConfirmationTools { + register_new_pending_payables_sub_opt: Option>, + report_tx_receipts_sub_opt: Option>, } #[derive(PartialEq, Eq)] @@ -88,11 +87,10 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: BindMessage, _ctx: &mut Self::Context) -> Self::Result { self.pending_payable_confirmation - .new_pp_fingerprints_sub_opt = - Some(msg.peer_actors.accountant.init_pending_payable_fingerprints); - self.pending_payable_confirmation - .report_transaction_receipts_sub_opt = - Some(msg.peer_actors.accountant.report_transaction_receipts); + .register_new_pending_payables_sub_opt = + Some(msg.peer_actors.accountant.register_new_pending_payables); + self.pending_payable_confirmation.report_tx_receipts_sub_opt = + Some(msg.peer_actors.accountant.report_transaction_status); self.payable_payments_setup_subs_opt = Some(msg.peer_actors.accountant.report_payable_payments_setup); self.sent_payable_subs_opt = Some(msg.peer_actors.accountant.report_sent_payments); @@ -164,21 +162,14 @@ impl Handler for BlockchainBridge { } #[derive(Debug, Clone, PartialEq, Eq, Message)] -pub struct PendingPayableFingerprintSeeds { - pub batch_wide_timestamp: SystemTime, - pub hashes_and_balances: Vec, +pub struct RegisterNewPendingPayables { + pub new_sent_txs: Vec, } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct PendingPayableFingerprint { - // Sqlite begins counting from 1 - pub rowid: u64, - pub timestamp: SystemTime, - pub hash: H256, - // We have Sqlite begin counting from 1 - pub attempt: u16, - pub amount: u128, - pub process_error: Option, +impl RegisterNewPendingPayables { + pub fn new(new_sent_txs: Vec) -> Self { + Self { new_sent_txs } + } } impl Handler for BlockchainBridge { @@ -204,9 +195,9 @@ impl BlockchainBridge { scan_error_subs_opt: None, crashable, logger: Logger::new("BlockchainBridge"), - pending_payable_confirmation: TransactionConfirmationTools { - new_pp_fingerprints_sub_opt: None, - report_transaction_receipts_sub_opt: None, + pending_payable_confirmation: TxConfirmationTools { + register_new_pending_payables_sub_opt: None, + report_tx_receipts_sub_opt: None, }, } } @@ -394,21 +385,21 @@ impl BlockchainBridge { fn log_status_of_tx_receipts( logger: &Logger, - transaction_receipts_results: &[TransactionReceiptResult], + transaction_receipts_results: &[&TxReceiptResult], ) { logger.debug(|| { let (successful_count, failed_count, pending_count) = transaction_receipts_results.iter().fold( (0, 0, 0), |(success, fail, pending), transaction_receipt| match transaction_receipt { - TransactionReceiptResult::RpcResponse(tx_receipt) => { - match tx_receipt.status { - TxStatus::Failed => (success, fail + 1, pending), - TxStatus::Pending => (success, fail, pending + 1), - TxStatus::Succeeded(_) => (success + 1, fail, pending), + Ok(tx_status) => match tx_status { + StatusReadFromReceiptCheck::Reverted => (success, fail + 1, pending), + StatusReadFromReceiptCheck::Succeeded(_) => { + (success + 1, fail, pending) } - } - TransactionReceiptResult::LocalError(_) => (success, fail, pending + 1), + StatusReadFromReceiptCheck::Pending => (success, fail, pending + 1), + }, + Err(_) => (success, fail, pending + 1), }, ); format!( @@ -425,30 +416,21 @@ impl BlockchainBridge { let logger = self.logger.clone(); let accountant_recipient = self .pending_payable_confirmation - .report_transaction_receipts_sub_opt + .report_tx_receipts_sub_opt .clone() .expect("Accountant is unbound"); - - let transaction_hashes = msg - .pending_payable_fingerprints - .iter() - .map(|finger_print| finger_print.hash) - .collect::>(); Box::new( self.blockchain_interface - .process_transaction_receipts(transaction_hashes) + .process_transaction_receipts(msg.tx_hashes) .map_err(move |e| e.to_string()) - .and_then(move |transaction_receipts_results| { - Self::log_status_of_tx_receipts(&logger, &transaction_receipts_results); - - let pairs = transaction_receipts_results - .into_iter() - .zip(msg.pending_payable_fingerprints.into_iter()) - .collect_vec(); - + .and_then(move |tx_receipt_results| { + Self::log_status_of_tx_receipts( + &logger, + tx_receipt_results.values().collect_vec().as_slice(), + ); accountant_recipient - .try_send(ReportTransactionReceipts { - fingerprints_with_receipts: pairs, + .try_send(TxReceiptsMessage { + results: tx_receipt_results, response_skeleton_opt: msg.response_skeleton_opt, }) .expect("Accountant is dead"); @@ -488,19 +470,19 @@ impl BlockchainBridge { affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { - let new_fingerprints_recipient = self.new_fingerprints_recipient(); + let recipient = self.new_pending_payables_recipient(); let logger = self.logger.clone(); self.blockchain_interface.submit_payables_in_batch( logger, agent, - new_fingerprints_recipient, + recipient, affordable_accounts, ) } - fn new_fingerprints_recipient(&self) -> Recipient { + fn new_pending_payables_recipient(&self) -> Recipient { self.pending_payable_confirmation - .new_pp_fingerprints_sub_opt + .register_new_pending_payables_sub_opt .clone() .expect("Accountant unbound") } @@ -552,18 +534,26 @@ impl SubsFactory for BlockchainBridgeSub mod tests { use super::*; use crate::accountant::db_access_objects::payable_dao::PayableAccount; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; + use crate::accountant::db_access_objects::sent_payable_dao::TxStatus; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, + }; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint, make_priced_qualified_payables}; + use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::accountant::test_utils::make_priced_qualified_payables; + use crate::accountant::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainAgentBuildError, PayableTransactionError, }; use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible::Correct; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, RetrievedBlockchainTransactions, + BlockchainTransaction, RetrievedBlockchainTransactions, TxBlock, }; + use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; + use crate::blockchain::errors::validation_status::ValidationStatus; use crate::blockchain::test_utils::{ make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; @@ -584,6 +574,7 @@ mod tests { use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; + use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; @@ -597,9 +588,6 @@ mod tests { use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160}; - use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; - use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, QualifiedPayableWithGasPrice}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt}; impl Handler> for BlockchainBridge { type Result = (); @@ -897,18 +885,18 @@ mod tests { system.run(); let time_after = SystemTime::now(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let pending_payable_fingerprint_seeds_msg = - accountant_recording.get_record::(0); + let register_new_pending_payables_msg = + accountant_recording.get_record::(0); let sent_payables_msg = accountant_recording.get_record::(1); + let expected_hash = + H256::from_str("81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c") + .unwrap(); assert_eq!( sent_payables_msg, &SentPayables { payment_procedure_result: Ok(vec![Correct(PendingPayable { - recipient_wallet: account.wallet, - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap() + recipient_wallet: account.wallet.clone(), + hash: expected_hash })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -916,17 +904,26 @@ mod tests { }) } ); - assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp >= time_before); - assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp <= time_after); + let first_actual_sent_tx = ®ister_new_pending_payables_msg.new_sent_txs[0]; assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap(), - amount: account.balance_wei - }] + first_actual_sent_tx.receiver_address, + account.wallet.address() + ); + assert_eq!(first_actual_sent_tx.hash, expected_hash); + assert_eq!(first_actual_sent_tx.amount_minor, account.balance_wei); + assert_eq!(first_actual_sent_tx.gas_price_minor, 111_222_333); + assert_eq!(first_actual_sent_tx.nonce, 0x20); + assert_eq!( + first_actual_sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting) + ); + assert!( + to_unix_timestamp(time_before) <= first_actual_sent_tx.timestamp + && first_actual_sent_tx.timestamp <= to_unix_timestamp(time_after), + "We thought the timestamp was between {:?} and {:?}, but it was {:?}", + time_before, + time_after, + from_unix_timestamp(first_actual_sent_tx.timestamp) ); assert_eq!(accountant_recording.len(), 2); } @@ -945,7 +942,7 @@ mod tests { let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); - let wallet_account = make_wallet("blah"); + let account_wallet = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); let persistent_configuration_mock = PersistentConfigurationMock::default(); let subject = BlockchainBridge::new( @@ -958,7 +955,7 @@ mod tests { let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); let account = PayableAccount { - wallet: wallet_account, + wallet: account_wallet.clone(), balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, @@ -986,8 +983,8 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let pending_payable_fingerprint_seeds_msg = - accountant_recording.get_record::(0); + let actual_register_new_pending_payables_msg = + accountant_recording.get_record::(0); let sent_payables_msg = accountant_recording.get_record::(1); let scan_error_msg = accountant_recording.get_record::(2); assert_sending_error( @@ -998,14 +995,23 @@ mod tests { "Transport error: Error(IncompleteMessage)", ); assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap(), - amount: account.balance_wei - }] + actual_register_new_pending_payables_msg.new_sent_txs[0].receiver_address, + account_wallet.address() + ); + assert_eq!( + actual_register_new_pending_payables_msg.new_sent_txs[0].hash, + H256::from_str("81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c") + .unwrap() + ); + assert_eq!( + actual_register_new_pending_payables_msg.new_sent_txs[0].amount_minor, + account.balance_wei + ); + let number_of_requested_txs = actual_register_new_pending_payables_msg.new_sent_txs.len(); + assert_eq!( + number_of_requested_txs, 1, + "We expected only one sent tx, but got {}", + number_of_requested_txs ); assert_eq!( *scan_error_msg, @@ -1016,7 +1022,8 @@ mod tests { context_id: 4321 }), msg: format!( - "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" + "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". \ + Signed and hashed txs: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) } ); @@ -1058,7 +1065,7 @@ mod tests { let (accountant, _, accountant_recording) = make_recorder(); subject .pending_payable_confirmation - .new_pp_fingerprints_sub_opt = Some(accountant.start().recipient()); + .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject .process_payments(msg.agent, msg.affordable_accounts) @@ -1119,7 +1126,7 @@ mod tests { let (accountant, _, accountant_recording) = make_recorder(); subject .pending_payable_confirmation - .new_pp_fingerprints_sub_opt = Some(accountant.start().recipient()); + .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject .process_payments(msg.agent, msg.affordable_accounts) @@ -1154,21 +1161,13 @@ mod tests { #[test] fn blockchain_bridge_processes_requests_for_a_complete_and_null_transaction_receipt() { let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); - let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); - let hash_1 = pending_payable_fingerprint_1.hash; - let hash_2 = make_tx_hash(78989); - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 4565, - process_error: None, - }; + let accountant = + accountant.system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)); + let tx_hash_1 = make_tx_hash(123); + let tx_hash_2 = make_tx_hash(456); let first_response = ReceiptResponseBuilder::default() .status(U64::from(1)) - .transaction_hash(hash_1) + .transaction_hash(tx_hash_1) .build(); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) @@ -1189,9 +1188,9 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1205,26 +1204,20 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 1); - let report_transaction_receipt_message = - accountant_recording.get_record::(0); + let tx_receipts_message = accountant_recording.get_record::(0); let mut expected_receipt = TransactionReceipt::default(); - expected_receipt.transaction_hash = hash_1; + expected_receipt.transaction_hash = tx_hash_1; expected_receipt.status = Some(U64::from(1)); assert_eq!( - report_transaction_receipt_message, - &ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(expected_receipt.into()), - pending_payable_fingerprint_1 - ), - ( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash_2, - status: TxStatus::Pending - }), - pending_payable_fingerprint_2 + tx_receipts_message, + &TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(tx_hash_1) => Ok( + expected_receipt.into() ), + TxHashByTable::FailedPayable(tx_hash_2) => Ok( + StatusReadFromReceiptCheck::Pending + ) ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1286,8 +1279,7 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_on_failure_from_remote_process_sends_back_all_good_results_and_logs_abort( - ) { + fn handle_request_transaction_receipts_sends_back_results() { init_test_logging(); let port = find_free_port(); let block_number = U64::from(4545454); @@ -1302,62 +1294,26 @@ mod tests { .begin_batch() .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .raw_response(tx_receipt_response) - .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .err_response( 429, "The requests per second (RPS) of your requests are higher than your plan allows." .to_string(), 7, ) + .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .end_batch() .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!( - ReportTransactionReceipts, - ScanError - )) + .system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)) .start(); - let report_transaction_receipt_recipient: Recipient = + let report_transaction_receipt_recipient: Recipient = accountant_addr.clone().recipient(); let scan_error_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(111334); - let hash_2 = make_tx_hash(100000); - let hash_3 = make_tx_hash(0x1348d); - let hash_4 = make_tx_hash(11111); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.hash = hash_1; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_3 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_3, - attempt: 3, - amount: 4565, - process_error: None, - }; - let fingerprint_4 = PendingPayableFingerprint { - rowid: 450, - timestamp: from_unix_timestamp(230_000_000), - hash: hash_4, - attempt: 1, - amount: 7879, - process_error: None, - }; - let transaction_receipt = TxReceipt { - transaction_hash: Default::default(), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number, - }), - }; + let tx_hash_1 = make_tx_hash(1334); + let tx_hash_2 = make_tx_hash(1000); + let tx_hash_3 = make_tx_hash(1212); + let tx_hash_4 = make_tx_hash(1111); let blockchain_interface = make_blockchain_interface_web3(port); let system = System::new("test_transaction_receipts"); let mut subject = BlockchainBridge::new( @@ -1367,14 +1323,14 @@ mod tests { ); subject .pending_payable_confirmation - .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); + .report_tx_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - fingerprint_1.clone(), - fingerprint_2.clone(), - fingerprint_3.clone(), - fingerprint_4.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::SentPayable(tx_hash_2), + TxHashByTable::SentPayable(tx_hash_3), + TxHashByTable::SentPayable(tx_hash_4), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1388,15 +1344,18 @@ mod tests { assert_eq!(system.run(), 0); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 1); - let report_receipts_msg = accountant_recording.get_record::(0); + let report_receipts_msg = accountant_recording.get_record::(0); assert_eq!( *report_receipts_msg, - ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - (TransactionReceiptResult::RpcResponse(TxReceipt{ transaction_hash: hash_1, status: TxStatus::Pending }), fingerprint_1), - (TransactionReceiptResult::RpcResponse(transaction_receipt), fingerprint_2), - (TransactionReceiptResult::RpcResponse(TxReceipt{ transaction_hash: hash_3, status: TxStatus::Pending }), fingerprint_3), - (TransactionReceiptResult::LocalError("RPC error: Error { code: ServerError(429), message: \"The requests per second (RPS) of your requests are higher than your plan allows.\", data: None }".to_string()), fingerprint_4) + TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: Default::default(), + block_number, + })), + TxHashByTable::SentPayable(tx_hash_3) => Err( + AppRpcError:: Remote(RemoteError::Web3RpcError { code: 429, message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string()})), + TxHashByTable::SentPayable(tx_hash_4) => Ok(StatusReadFromReceiptCheck::Pending), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1410,32 +1369,17 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_if_submit_batch_fails() { + fn handle_request_transaction_receipts_failing_submit_the_batch() { init_test_logging(); let (accountant, _, accountant_recording) = make_recorder(); let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); - let report_transaction_recipient: Recipient = + let report_transaction_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(0x1b2e6); - let fingerprint_1 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_1, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: make_tx_hash(222444), - attempt: 3, - amount: 4565, - process_error: None, - }; + let tx_hash_1 = make_tx_hash(10101); + let tx_hash_2 = make_tx_hash(10102); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); let blockchain_interface = make_blockchain_interface_web3(port); @@ -1446,10 +1390,13 @@ mod tests { ); subject .pending_payable_confirmation - .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); + .report_tx_receipts_sub_opt = Some(report_transaction_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![fingerprint_1, fingerprint_2], + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + ], response_skeleton_opt: None, }; let system = System::new("test"); @@ -2033,10 +1980,11 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); + subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, ScanType::Receivables, @@ -2045,7 +1993,9 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let msg_opt = accountant_recording.get_record_opt::(0); + let received_msg = accountant_recording.get_record::(0); + assert_eq!(received_msg.new_start_block, BlockMarker::Value(0xc8 + 1)); + let msg_opt = accountant_recording.get_record_opt::(1); assert_eq!(msg_opt, None, "We didnt expect a scan error: {:?}", msg_opt); } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index c93c07b53..7a4d6ddfb 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -6,104 +6,12 @@ use crate::blockchain::blockchain_interface::data_structures::errors::Blockchain use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use ethereum_types::{H256, U256, U64}; use futures::Future; -use serde_derive::{Deserialize, Serialize}; use serde_json::Value; -use std::fmt::Display; -use std::str::FromStr; use web3::contract::{Contract, Options}; use web3::transports::{Batch, Http}; -use web3::types::{Address, BlockNumber, Filter, Log, TransactionReceipt}; +use web3::types::{Address, BlockNumber, Filter, Log}; use web3::{Error, Web3}; -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TransactionReceiptResult { - RpcResponse(TxReceipt), - LocalError(String), -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TxStatus { - Failed, - Pending, - Succeeded(TransactionBlock), -} - -impl FromStr for TxStatus { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "Pending" => Ok(TxStatus::Pending), - "Failed" => Ok(TxStatus::Failed), // TODO: GH-631: This should be removed - s if s.starts_with("Succeeded") => { - // The format is "Succeeded(block_number, block_hash)" - let parts: Vec<&str> = s[10..s.len() - 1].split(',').collect(); - if parts.len() != 2 { - return Err("Invalid Succeeded format".to_string()); - } - let block_number: u64 = parts[0] - .parse() - .map_err(|_| "Invalid block number".to_string())?; - let block_hash = - H256::from_str(&parts[1][2..]).map_err(|_| "Invalid block hash".to_string())?; - Ok(TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number: U64::from(block_number), - })) - } - _ => Err(format!("Unknown status: {}", s)), - } - } -} - -impl Display for TxStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TxStatus::Failed => write!(f, "Failed"), - TxStatus::Pending => write!(f, "Pending"), - TxStatus::Succeeded(block) => { - write!( - f, - "Succeeded({},{:?})", - block.block_number, block.block_hash - ) - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct TxReceipt { - pub transaction_hash: H256, - pub status: TxStatus, -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct TransactionBlock { - pub block_hash: H256, - pub block_number: U64, -} - -impl From for TxReceipt { - fn from(receipt: TransactionReceipt) -> Self { - let status = match (receipt.status, receipt.block_hash, receipt.block_number) { - (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { - TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }) - } - (Some(status), _, _) if status == U64::from(0) => TxStatus::Failed, - _ => TxStatus::Pending, - }; - - TxReceipt { - transaction_hash: receipt.transaction_hash, - status, - } - } -} - pub struct LowBlockchainIntWeb3 { web3: Web3, web3_batch: Web3>, @@ -222,7 +130,7 @@ mod tests { use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_LITERAL; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::{BlockchainInterfaceError, BlockchainInterface}; - use crate::blockchain::test_utils::make_blockchain_interface_web3; + use crate::blockchain::test_utils::{make_block_hash, make_blockchain_interface_web3, make_tx_hash, TransactionReceiptBuilder}; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use ethereum_types::{H256, U64}; @@ -230,8 +138,8 @@ mod tests { use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::utils::find_free_port; use std::str::FromStr; - use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, TransactionReceipt, U256}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, U256}; + use crate::blockchain::blockchain_interface::data_structures::StatusReadFromReceiptCheck; #[test] fn get_transaction_fee_balance_works() { @@ -601,17 +509,17 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_successful_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - Some(U64::from(10)), - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - match tx_receipt.status { - TxStatus::Succeeded(ref block) => { - assert_eq!(block.block_hash, H256::from_low_u64_be(0x1234)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .block_hash(make_block_hash(0x1234)) + .block_number(10.into()) + .build() + .into(); + + match tx_status { + StatusReadFromReceiptCheck::Succeeded(ref block) => { + assert_eq!(block.block_hash, make_block_hash(0x1234)); assert_eq!(block.block_number, U64::from(10)); } _ => panic!("Expected status to be Succeeded"), @@ -620,139 +528,43 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_failed_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(0)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(0)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Failed); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Reverted); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status() { - let tx_receipt: TxReceipt = - create_tx_receipt(None, None, None, H256::from_low_u64_be(0x5678)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status_and_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - None, - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); - } - - #[test] - fn tx_status_display_works() { - // Test Failed - assert_eq!(TxStatus::Failed.to_string(), "Failed"); - - // Test Pending - assert_eq!(TxStatus::Pending.to_string(), "Pending"); - - // Test Succeeded - let block_number = U64::from(12345); - let block_hash = H256::from_low_u64_be(0xabcdef); - let succeeded = TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }); - assert_eq!( - succeeded.to_string(), - format!("Succeeded({},0x{:x})", block_number, block_hash) - ); - } - - #[test] - fn tx_status_from_str_works() { - // Test Pending - assert_eq!(TxStatus::from_str("Pending"), Ok(TxStatus::Pending)); - - // Test Failed - assert_eq!(TxStatus::from_str("Failed"), Ok(TxStatus::Failed)); - - // Test Succeeded with valid input - let block_number = 123456789; - let block_hash = H256::from_low_u64_be(0xabcdef); - let input = format!("Succeeded({},0x{:x})", block_number, block_hash); - assert_eq!( - TxStatus::from_str(&input), - Ok(TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number: U64::from(block_number), - })) - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - // Test Succeeded with invalid format - assert_eq!( - TxStatus::from_str("Succeeded(123)"), - Err("Invalid Succeeded format".to_string()) - ); - - // Test Succeeded with invalid block number - assert_eq!( - TxStatus::from_str( - "Succeeded(abc,0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef)" - ), - Err("Invalid block number".to_string()) - ); - - // Test Succeeded with invalid block hash - assert_eq!( - TxStatus::from_str("Succeeded(123,0xinvalidhash)"), - Err("Invalid block hash".to_string()) - ); - - // Test unknown status - assert_eq!( - TxStatus::from_str("InProgress"), - Err("Unknown status: InProgress".to_string()) - ); - } - - fn create_tx_receipt( - status: Option, - block_hash: Option, - block_number: Option, - transaction_hash: H256, - ) -> TxReceipt { - let receipt = TransactionReceipt { - status, - root: None, - block_hash, - block_number, - cumulative_gas_used: Default::default(), - gas_used: None, - contract_address: None, - transaction_hash, - transaction_index: Default::default(), - logs: vec![], - logs_bloom: Default::default(), - }; - receipt.into() + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index bb9cde491..7178d9d90 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,8 +4,9 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; +use std::collections::{HashMap}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; +use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible, StatusReadFromReceiptCheck}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainInterface}; @@ -20,12 +21,15 @@ use actix::Recipient; use ethereum_types::U64; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, PricedQualifiedPayables}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{LowBlockchainIntWeb3, TransactionReceiptResult, TxReceipt, TxStatus}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, RegisterNewPendingPayables}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::LowBlockchainIntWeb3; use crate::blockchain::blockchain_interface::blockchain_interface_web3::utils::{create_blockchain_agent_web3, send_payables_within_batch, BlockchainAgentFutureResult}; - +use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; // TODO We should probably begin to attach these constants to the interfaces more tightly, so that // we aren't baffled by which interface they belong with. I suggest to declare them inside // their inherent impl blocks. They will then need to be preceded by the class name @@ -182,7 +186,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { Box::new( get_gas_price .map_err(BlockchainAgentBuildError::GasPrice) - .and_then(move |gas_price_wei| { + .and_then(move |gas_price_minor| { get_transaction_fee_balance .map_err(move |e| { BlockchainAgentBuildError::TransactionFeeBalance(wallet_address, e) @@ -195,7 +199,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { .and_then(move |masq_token_balance| { let blockchain_agent_future_result = BlockchainAgentFutureResult { - gas_price_wei, + gas_price_minor, transaction_fee_balance, masq_token_balance, }; @@ -213,38 +217,44 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>> - { + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = HashMap, + Error = BlockchainInterfaceError, + >, + > { Box::new( self.lower_interface() - .get_transaction_receipt_in_batch(transaction_hashes.clone()) + .get_transaction_receipt_in_batch(Self::collect_plain_hashes(&tx_hashes)) .map_err(move |e| e) .and_then(move |batch_response| { Ok(batch_response .into_iter() - .zip(transaction_hashes) - .map(|(response, hash)| match response { + .zip(tx_hashes.into_iter()) + .map(|(response, tx_hash)| match response { Ok(result) => { match serde_json::from_value::(result) { Ok(receipt) => { - TransactionReceiptResult::RpcResponse(receipt.into()) + (tx_hash, Ok(StatusReadFromReceiptCheck::from(receipt))) } Err(e) => { if e.to_string().contains("invalid type: null") { - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }) + (tx_hash, Ok(StatusReadFromReceiptCheck::Pending)) } else { - TransactionReceiptResult::LocalError(e.to_string()) + ( + tx_hash, + Err(AppRpcError::Remote( + RemoteError::InvalidResponse(e.to_string()), + )), + ) } } } } - Err(e) => TransactionReceiptResult::LocalError(e.to_string()), + Err(e) => (tx_hash, Err(AppRpcError::from(e))), }) - .collect::>()) + .collect::>()) }), ) } @@ -253,7 +263,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { @@ -274,7 +284,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &web3_batch, consuming_wallet, pending_nonce, - fingerprints_recipient, + new_pending_payables_recipient, affordable_accounts, ) }), @@ -285,7 +295,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub struct HashAndAmount { pub hash: H256, - pub amount: u128, + pub amount_minor: u128, } impl BlockchainInterfaceWeb3 { @@ -431,22 +441,43 @@ impl BlockchainInterfaceWeb3 { Ok(transactions) } } + + fn collect_plain_hashes(hashes_by_table: &[TxHashByTable]) -> Vec { + hashes_by_table + .iter() + .map(|hash_by_table| match hash_by_table { + TxHashByTable::SentPayable(hash) => *hash, + TxHashByTable::FailedPayable(hash) => *hash, + }) + .collect() + } } #[cfg(test)] mod tests { use super::*; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayableWithGasPrice, QualifiedPayablesBeforeGasPriceSelection, + UnpricedQualifiedPayables, + }; + use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; - use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; + use crate::blockchain::blockchain_interface::data_structures::{ + BlockchainTransaction, TxBlock, + }; use crate::blockchain::blockchain_interface::{ BlockchainAgentBuildError, BlockchainInterfaceError, BlockchainInterface, RetrievedBlockchainTransactions, }; - use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; + use crate::blockchain::test_utils::{ + all_chains, make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, + }; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; @@ -462,10 +493,7 @@ mod tests { use std::str::FromStr; use web3::transports::Http; use web3::types::{H256, U256}; - use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayableWithGasPrice}; - use crate::accountant::test_utils::make_payable_account; - use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; #[test] fn constants_are_correct() { @@ -1046,27 +1074,19 @@ mod tests { #[test] fn process_transaction_receipts_works() { let port = find_free_port(); - let tx_hash_1 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0e") - .unwrap(); - let tx_hash_2 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0f") - .unwrap(); - let tx_hash_3 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0a") - .unwrap(); - let tx_hash_4 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0b") - .unwrap(); - let tx_hash_5 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0c") - .unwrap(); - let tx_hash_6 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0d") - .unwrap(); - let tx_hash_vec = vec![ - tx_hash_1, tx_hash_2, tx_hash_3, tx_hash_4, tx_hash_5, tx_hash_6, - ]; + let tx_hash_1 = make_tx_hash(3300); + let tx_hash_2 = make_tx_hash(3401); + let tx_hash_3 = make_tx_hash(3502); + let tx_hash_4 = make_tx_hash(3603); + let tx_hash_5 = make_tx_hash(3704); + let tx_hash_6 = make_tx_hash(3805); + let tx_hbt_1 = TxHashByTable::FailedPayable(tx_hash_1); + let tx_hbt_2 = TxHashByTable::FailedPayable(tx_hash_2); + let tx_hbt_3 = TxHashByTable::SentPayable(tx_hash_3); + let tx_hbt_4 = TxHashByTable::SentPayable(tx_hash_4); + let tx_hbt_5 = TxHashByTable::SentPayable(tx_hash_5); + let tx_hbt_6 = TxHashByTable::SentPayable(tx_hash_6); + let sent_tx_vec = vec![tx_hbt_1, tx_hbt_2, tx_hbt_3, tx_hbt_4, tx_hbt_5, tx_hbt_6]; let block_hash = H256::from_str("6d0abccae617442c26104c2bc63d1bc05e1e002e555aec4ab62a46e826b18f18") .unwrap(); @@ -1108,48 +1128,45 @@ mod tests { let subject = make_blockchain_interface_web3(port); let result = subject - .process_transaction_receipts(tx_hash_vec) + .process_transaction_receipts(sent_tx_vec.clone()) .wait() .unwrap(); - assert_eq!(result[0], TransactionReceiptResult::LocalError("RPC error: Error { code: ServerError(429), message: \"The requests per second (RPS) of your requests are higher than your plan allows.\", data: None }".to_string())); + assert_eq!(result.get(&tx_hbt_1).unwrap(), &Err( + AppRpcError::Remote( + RemoteError::Web3RpcError { + code: 429, + message: + "The requests per second (RPS) of your requests are higher than your plan allows." + .to_string() + } + )) + ); assert_eq!( - result[1], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_2, - status: TxStatus::Pending - }) + result.get(&tx_hbt_2).unwrap(), + &Ok(StatusReadFromReceiptCheck::Pending) ); assert_eq!( - result[2], - TransactionReceiptResult::LocalError( + result.get(&tx_hbt_3).unwrap(), + &Err(AppRpcError::Remote(RemoteError::InvalidResponse( "invalid type: string \"trash\", expected struct Receipt".to_string() - ) + ))) ); assert_eq!( - result[3], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_4, - status: TxStatus::Pending - }) + result.get(&tx_hbt_4).unwrap(), + &Ok(StatusReadFromReceiptCheck::Pending) ); assert_eq!( - result[4], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_5, - status: TxStatus::Failed, - }) + result.get(&tx_hbt_5).unwrap(), + &Ok(StatusReadFromReceiptCheck::Reverted) ); assert_eq!( - result[5], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_6, - status: TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }), - }) - ); + result.get(&tx_hbt_6).unwrap(), + &Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }),) + ) } #[test] @@ -1157,13 +1174,12 @@ mod tests { let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); let subject = make_blockchain_interface_web3(port); - let tx_hash_1 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0e") - .unwrap(); - let tx_hash_2 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0f") - .unwrap(); - let tx_hash_vec = vec![tx_hash_1, tx_hash_2]; + let tx_hash_1 = make_tx_hash(789); + let tx_hash_2 = make_tx_hash(123); + let tx_hash_vec = vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::SentPayable(tx_hash_2), + ]; let error = subject .process_transaction_receipts(tx_hash_vec) @@ -1346,4 +1362,33 @@ mod tests { BlockMarker::Uninitialized ); } + + #[test] + fn collect_plain_hashes_works() { + let hash_sent_tx_1 = make_tx_hash(456); + let hash_sent_tx_2 = make_tx_hash(789); + let hash_sent_tx_3 = make_tx_hash(234); + let hash_failed_tx_1 = make_tx_hash(123); + let hash_failed_tx_2 = make_tx_hash(345); + let inputs = vec![ + TxHashByTable::SentPayable(hash_sent_tx_1), + TxHashByTable::FailedPayable(hash_failed_tx_1), + TxHashByTable::SentPayable(hash_sent_tx_2), + TxHashByTable::SentPayable(hash_sent_tx_3), + TxHashByTable::FailedPayable(hash_failed_tx_2), + ]; + + let result = BlockchainInterfaceWeb3::collect_plain_hashes(&inputs); + + assert_eq!( + result, + vec![ + hash_sent_tx_1, + hash_failed_tx_1, + hash_sent_tx_2, + hash_sent_tx_3, + hash_failed_tx_2 + ] + ); + } } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index 00489febc..d8e1729f9 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -1,18 +1,21 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; +use crate::accountant::db_access_objects::utils::{to_unix_timestamp, TxHash}; use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::accountant::PendingPayable; use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ - BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, + BlockchainInterfaceWeb3, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ ProcessedPayableFallible, RpcPayableFailure, }; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use actix::Recipient; @@ -22,66 +25,47 @@ use masq_lib::constants::WALLET_ADDRESS_LENGTH; use masq_lib::logger::Logger; use secp256k1secrets::SecretKey; use serde_json::Value; +use std::collections::HashSet; use std::iter::once; use std::time::SystemTime; use thousands::Separable; use web3::transports::{Batch, Http}; use web3::types::{Bytes, SignedTransaction, TransactionParameters, U256}; -use web3::Error as Web3Error; use web3::Web3; #[derive(Debug)] pub struct BlockchainAgentFutureResult { - pub gas_price_wei: U256, + pub gas_price_minor: U256, pub transaction_fee_balance: U256, pub masq_token_balance: U256, } -pub fn advance_used_nonce(current_nonce: U256) -> U256 { - current_nonce - .checked_add(U256::one()) - .expect("unexpected limits") -} - -fn error_with_hashes( - error: Web3Error, - hashes_and_paid_amounts: Vec, -) -> PayableTransactionError { - let hashes = hashes_and_paid_amounts - .into_iter() - .map(|hash_and_amount| hash_and_amount.hash) - .collect(); - PayableTransactionError::Sending { - msg: error.to_string(), - hashes, - } -} +// TODO using these three vectors like this is dangerous; who guarantees that all three have their +// items sorted in the right order? pub fn merged_output_data( responses: Vec>, - hashes_and_paid_amounts: Vec, + sent_tx_hashes: Vec, accounts: Vec, ) -> Vec { let iterator_with_all_data = responses .into_iter() - .zip(hashes_and_paid_amounts.into_iter()) + .zip(sent_tx_hashes.into_iter()) .zip(accounts.iter()); iterator_with_all_data - .map( - |((rpc_result, hash_and_amount), account)| match rpc_result { - Ok(_rpc_result) => { - // TODO: GH-547: This rpc_result should be validated - ProcessedPayableFallible::Correct(PendingPayable { - recipient_wallet: account.wallet.clone(), - hash: hash_and_amount.hash, - }) - } - Err(rpc_error) => ProcessedPayableFallible::Failed(RpcPayableFailure { - rpc_error, + .map(|((rpc_result, hash), account)| match rpc_result { + Ok(_rpc_result) => { + // TODO: GH-547: This rpc_result should be validated + ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: account.wallet.clone(), - hash: hash_and_amount.hash, - }), - }, - ) + hash, + }) + } + Err(rpc_error) => ProcessedPayableFallible::Failed(RpcPayableFailure { + rpc_error, + recipient_wallet: account.wallet.clone(), + hash, + }), + }) .collect() } @@ -143,11 +127,11 @@ pub fn transmission_log( introduction.chain(body).collect() } -pub fn sign_transaction_data(amount: u128, recipient_wallet: Wallet) -> [u8; 68] { +pub fn sign_transaction_data(amount_minor: u128, recipient_wallet: Wallet) -> [u8; 68] { let mut data = [0u8; 4 + 32 + 32]; data[0..4].copy_from_slice(&TRANSFER_METHOD_ID); data[16..36].copy_from_slice(&recipient_wallet.address().0[..]); - U256::from(amount).to_big_endian(&mut data[36..68]); + U256::from(amount_minor).to_big_endian(&mut data[36..68]); data } @@ -164,11 +148,11 @@ pub fn sign_transaction( web3_batch: &Web3>, recipient_wallet: Wallet, consuming_wallet: Wallet, - amount: u128, + amount_minor: u128, nonce: U256, gas_price_in_wei: u128, ) -> SignedTransaction { - let data = sign_transaction_data(amount, recipient_wallet); + let data = sign_transaction_data(amount_minor, recipient_wallet); let gas_limit = gas_limit(data, chain); // Warning: If you set gas_price or nonce to None in transaction_parameters, sign_transaction // will start making RPC calls which we don't want (Do it at your own risk). @@ -215,7 +199,7 @@ pub fn sign_and_append_payment( consuming_wallet: Wallet, nonce: U256, gas_price_in_wei: u128, -) -> HashAndAmount { +) -> TxHash { let signed_tx = sign_transaction( chain, web3_batch, @@ -227,10 +211,7 @@ pub fn sign_and_append_payment( ); append_signed_transaction_to_batch(web3_batch, signed_tx.raw_transaction); - HashAndAmount { - hash: signed_tx.transaction_hash, - amount: recipient.balance_wei, - } + signed_tx.transaction_hash } pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_transaction: Bytes) { @@ -239,37 +220,51 @@ pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_tr } pub fn sign_and_append_multiple_payments( + now: SystemTime, logger: &Logger, chain: Chain, web3_batch: &Web3>, consuming_wallet: Wallet, - mut pending_nonce: U256, + initial_pending_nonce: U256, accounts: &PricedQualifiedPayables, -) -> Vec { - let mut hash_and_amount_list = vec![]; - accounts.payables.iter().for_each(|payable_pack| { - let payable = &payable_pack.payable; - debug!( - logger, - "Preparing payable future of {} wei to {} with nonce {}", - payable.balance_wei.separate_with_commas(), - payable.wallet, - pending_nonce - ); - - let hash_and_amount = sign_and_append_payment( - chain, - web3_batch, - payable, - consuming_wallet.clone(), - pending_nonce, - payable_pack.gas_price_minor, - ); - - pending_nonce = advance_used_nonce(pending_nonce); - hash_and_amount_list.push(hash_and_amount); - }); - hash_and_amount_list +) -> Vec { + let unix_mow = to_unix_timestamp(now); + accounts + .payables + .iter() + .enumerate() + .map(|(idx, payable_pack)| { + let current_pending_nonce = initial_pending_nonce + U256::from(idx); + let payable = &payable_pack.payable; + + debug!( + logger, + "Preparing tx of {} wei to {} with nonce {}", + payable.balance_wei.separate_with_commas(), + payable.wallet, + current_pending_nonce + ); + + let hash = sign_and_append_payment( + chain, + web3_batch, + payable, + consuming_wallet.clone(), + current_pending_nonce, + payable_pack.gas_price_minor, + ); + + SentTx { + hash, + receiver_address: payable.wallet.address(), + amount_minor: payable.balance_wei, + timestamp: unix_mow, + gas_price_minor: payable_pack.gas_price_minor, + nonce: current_pending_nonce.as_u64(), + status: TxStatus::Pending(ValidationStatus::Waiting), + } + }) + .collect() } #[allow(clippy::too_many_arguments)] @@ -279,7 +274,7 @@ pub fn send_payables_within_batch( web3_batch: &Web3>, consuming_wallet: Wallet, pending_nonce: U256, - new_fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError> + 'static> { @@ -291,7 +286,10 @@ pub fn send_payables_within_batch( chain.rec().num_chain_id, ); - let hashes_and_paid_amounts = sign_and_append_multiple_payments( + let common_timestamp = SystemTime::now(); + + let prepared_sent_txs_records = sign_and_append_multiple_payments( + common_timestamp, logger, chain, web3_batch, @@ -300,15 +298,16 @@ pub fn send_payables_within_batch( &accounts, ); - let timestamp = SystemTime::now(); - let hashes_and_paid_amounts_error = hashes_and_paid_amounts.clone(); - let hashes_and_paid_amounts_ok = hashes_and_paid_amounts.clone(); + let sent_txs_hashes: Vec = prepared_sent_txs_records + .iter() + .map(|sent_tx| sent_tx.hash) + .collect(); + let planned_sent_txs_hashes = HashSet::from_iter(sent_txs_hashes.clone().into_iter()); - new_fingerprints_recipient - .try_send(PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: hashes_and_paid_amounts, - }) + let new_pending_payables_message = RegisterNewPendingPayables::new(prepared_sent_txs_records); + + new_pending_payables_recipient + .try_send(new_pending_payables_message) .expect("Accountant is dead"); info!( @@ -321,11 +320,14 @@ pub fn send_payables_within_batch( web3_batch .transport() .submit_batch() - .map_err(|e| error_with_hashes(e, hashes_and_paid_amounts_error)) + .map_err(move |e| PayableTransactionError::Sending { + msg: e.to_string(), + hashes: planned_sent_txs_hashes, + }) .and_then(move |batch_response| { Ok(merged_output_data( batch_response, - hashes_and_paid_amounts_ok, + sent_txs_hashes, accounts.into(), )) }), @@ -346,7 +348,7 @@ pub fn create_blockchain_agent_web3( masq_token_balance_in_minor_units, ); Box::new(BlockchainAgentWeb3::new( - blockchain_agent_future_result.gas_price_wei.as_u128(), + blockchain_agent_future_result.gas_price_minor.as_u128(), gas_limit_const_part, wallet, cons_wallet_balances, @@ -431,13 +433,8 @@ mod tests { let mut batch_result = web3_batch.eth().transport().submit_batch().wait().unwrap(); assert_eq!( result, - HashAndAmount { - hash: H256::from_str( - "94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2" - ) - .unwrap(), - amount: account.balance_wei - } + H256::from_str("94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2") + .unwrap() ); assert_eq!( batch_result.pop().unwrap().unwrap(), @@ -448,9 +445,10 @@ mod tests { } #[test] - fn send_and_append_multiple_payments_works() { + fn sign_and_append_multiple_payments_works() { + let now = SystemTime::now(); let port = find_free_port(); - let logger = Logger::new("send_and_append_multiple_payments_works"); + let logger = Logger::new("sign_and_append_multiple_payments_works"); let (_event_loop_handle, transport) = Http::with_max_parallel( &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), REQUESTS_IN_PARALLEL, @@ -463,11 +461,12 @@ mod tests { let account_1 = make_payable_account(1); let account_2 = make_payable_account(2); let accounts = make_priced_qualified_payables(vec![ - (account_1, 111_111_111), - (account_2, 222_222_222), + (account_1.clone(), 111_234_111), + (account_2.clone(), 222_432_222), ]); - let result = sign_and_append_multiple_payments( + let mut result = sign_and_append_multiple_payments( + now, &logger, chain, &web3_batch, @@ -476,25 +475,47 @@ mod tests { &accounts, ); + let first_actual_sent_tx = result.remove(0); + let second_actual_sent_tx = result.remove(0); + assert_prepared_sent_tx_record( + first_actual_sent_tx, + now, + account_1, + "0x6b85347ff8edf8b126dffb85e7517ac7af1b23eace4ed5ad099d783fd039b1ee", + 1, + 111_234_111, + ); + assert_prepared_sent_tx_record( + second_actual_sent_tx, + now, + account_2, + "0x3dac025697b994920c9cd72ab0d2df82a7caaa24d44e78b7c04e223299819d54", + 2, + 222_432_222, + ); + } + + fn assert_prepared_sent_tx_record( + actual_sent_tx: SentTx, + now: SystemTime, + account_1: PayableAccount, + expected_tx_hash_including_prefix: &str, + expected_nonce: u64, + expected_gas_price_minor: u128, + ) { + assert_eq!(actual_sent_tx.receiver_address, account_1.wallet.address()); assert_eq!( - result, - vec![ - HashAndAmount { - hash: H256::from_str( - "374b7d023f4ac7d99e612d82beda494b0747116e9b9dc975b33b865f331ee934" - ) - .unwrap(), - amount: 1000000000 - }, - HashAndAmount { - hash: H256::from_str( - "5708afd876bc2573f9db984ec6d0e7f8ef222dd9f115643c9b9056d8bef8bbd9" - ) - .unwrap(), - amount: 2000000000 - } - ] + actual_sent_tx.hash, + H256::from_str(&expected_tx_hash_including_prefix[2..]).unwrap() + ); + assert_eq!(actual_sent_tx.amount_minor, account_1.balance_wei); + assert_eq!(actual_sent_tx.gas_price_minor, expected_gas_price_minor); + assert_eq!(actual_sent_tx.nonce, expected_nonce); + assert_eq!( + actual_sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting) ); + assert_eq!(actual_sent_tx.timestamp, to_unix_timestamp(now)); } #[test] @@ -626,16 +647,7 @@ mod tests { pending_payable_opt: None, }, ]; - let fingerprint_inputs = vec![ - HashAndAmount { - hash: make_tx_hash(444), - amount: 2_345_678, - }, - HashAndAmount { - hash: make_tx_hash(333), - amount: 6_543_210, - }, - ]; + let tx_hashes = vec![make_tx_hash(444), make_tx_hash(333)]; let responses = vec![ Ok(Value::String(String::from("blah"))), Err(web3::Error::Rpc(Error { @@ -645,7 +657,7 @@ mod tests { })), ]; - let result = merged_output_data(responses, fingerprint_inputs, accounts.to_vec()); + let result = merged_output_data(responses, tx_hashes, accounts.to_vec()); assert_eq!( result, @@ -679,13 +691,13 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let pending_nonce: U256 = 1.into(); + let pending_nonce: U256 = 3.into(); let web3_batch = Web3::new(Batch::new(transport)); let (accountant, _, accountant_recording) = make_recorder(); let logger = Logger::new(test_name); let chain = DEFAULT_CHAIN; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let new_fingerprints_recipient = accountant.start().recipient(); + let new_pending_payables_recipient = accountant.start().recipient(); let system = System::new(test_name); let timestamp_before = SystemTime::now(); @@ -695,7 +707,7 @@ mod tests { &web3_batch, consuming_wallet.clone(), pending_nonce, - new_fingerprints_recipient, + new_pending_payables_recipient, accounts.clone(), ) .wait(); @@ -703,12 +715,30 @@ mod tests { System::current().stop(); system.run(); let timestamp_after = SystemTime::now(); + assert_eq!(result, expected_result); let accountant_recording_result = accountant_recording.lock().unwrap(); - let ppfs_message = - accountant_recording_result.get_record::(0); + let rnpp_message = accountant_recording_result.get_record::(0); assert_eq!(accountant_recording_result.len(), 1); - assert!(timestamp_before <= ppfs_message.batch_wide_timestamp); - assert!(timestamp_after >= ppfs_message.batch_wide_timestamp); + let nonces = 3_64..(accounts.payables.len() as u64 + 3); + rnpp_message + .new_sent_txs + .iter() + .zip(accounts.payables.iter()) + .zip(nonces) + .for_each(|((tx, payable_account), nonce)| { + assert_eq!( + tx.receiver_address, + payable_account.payable.wallet.address() + ); + assert_eq!(tx.amount_minor, payable_account.payable.balance_wei); + assert_eq!(tx.gas_price_minor, payable_account.gas_price_minor); + assert_eq!(tx.nonce, nonce); + assert_eq!(tx.status, TxStatus::Pending(ValidationStatus::Waiting)); + assert!( + timestamp_before <= from_unix_timestamp(tx.timestamp) + && from_unix_timestamp(tx.timestamp) <= timestamp_after + ); + }); let tlh = TestLogHandler::new(); tlh.exists_log_containing( &format!("DEBUG: {test_name}: Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", @@ -721,7 +751,6 @@ mod tests { "INFO: {test_name}: {}", transmission_log(chain, &accounts, pending_nonce) )); - assert_eq!(result, expected_result); } #[test] @@ -740,14 +769,14 @@ mod tests { Correct(PendingPayable { recipient_wallet: account_1.wallet.clone(), hash: H256::from_str( - "6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2", + "0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3", ) .unwrap(), }), Correct(PendingPayable { recipient_wallet: account_2.wallet.clone(), hash: H256::from_str( - "b67a61b29c0c48d8b63a64fda73b3247e8e2af68082c710325675d4911e113d4", + "6b485dbd4d769b5a19fa57058d612fad99cdd78769db6b3be129f981c42657ac", ) .unwrap(), }), @@ -775,9 +804,9 @@ mod tests { let port = find_free_port(); let expected_result = Err(Sending { msg: format!("Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", os_code, os_msg).to_string(), - hashes: vec![ - H256::from_str("ec7ac48060b75889f949f5e8d301b386198218e60e2635c95cb6b0934a0887ea").unwrap(), - H256::from_str("c2d5059db0ec2fbf15f83d9157eeb0d793d6242de5e73a607935fb5660e7e925").unwrap() + hashes: hashset![ + H256::from_str("5bbe90ad19d86b69ee49879cec4b3f8b769223e6a872aae0be88773de2fc3beb").unwrap(), + H256::from_str("a1b609dbe9cc77ad586dbe4e5c1079d6ad76020a353c960928d6daeafd43f366").unwrap() ], }); @@ -818,7 +847,7 @@ mod tests { data: None, }), recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), + hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -827,7 +856,7 @@ mod tests { data: None, }), recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), + hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), }), ]); @@ -861,7 +890,7 @@ mod tests { let expected_result = Ok(vec![ Correct(PendingPayable { recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), + hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -870,7 +899,7 @@ mod tests { data: None, }), recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), + hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), }), ]); @@ -885,15 +914,6 @@ mod tests { ); } - #[test] - fn advance_used_nonce_works() { - let initial_nonce = U256::from(55); - - let result = advance_used_nonce(initial_nonce); - - assert_eq!(result, U256::from(56)) - } - #[test] #[should_panic( expected = "Consuming wallet doesn't contain a secret key: Signature(\"Cannot sign with non-keypair wallet: Address(0x000000000000000000006261645f77616c6c6574).\")" diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index ffdfa4c20..1d01532ec 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -1,11 +1,13 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::comma_joined_stringifiable; -use itertools::Either; +use crate::accountant::db_access_objects::utils::TxHash; +use itertools::{Either, Itertools}; +use std::collections::HashSet; use std::fmt; use std::fmt::{Display, Formatter}; use variant_count::VariantCount; -use web3::types::{Address, H256}; +use web3::types::Address; const BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED: &str = "Uninitialized blockchain interface. To avoid \ being delinquency-banned, you should restart the Node with a value for blockchain-service-url"; @@ -39,7 +41,10 @@ pub enum PayableTransactionError { TransactionID(BlockchainInterfaceError), UnusableWallet(String), Signing(String), - Sending { msg: String, hashes: Vec }, + Sending { + msg: String, + hashes: HashSet, + }, UninitializedInterface, } @@ -61,12 +66,15 @@ impl Display for PayableTransactionError { msg ), Self::Signing(msg) => write!(f, "Signing phase: \"{}\"", msg), - Self::Sending { msg, hashes } => write!( - f, - "Sending phase: \"{}\". Signed and hashed transactions: {}", - msg, - comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) - ), + Self::Sending { msg, hashes } => { + let hashes = hashes.iter().map(|hash| *hash).sorted().collect_vec(); + write!( + f, + "Sending phase: \"{}\". Signed and hashed txs: {}", + msg, + comma_joined_stringifiable(&hashes, |hash| format!("{:?}", hash)) + ) + } Self::UninitializedInterface => { write!(f, "{}", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED) } @@ -180,7 +188,7 @@ mod tests { ), PayableTransactionError::Sending { msg: "Sending to cosmos belongs elsewhere".to_string(), - hashes: vec![make_tx_hash(0x6f), make_tx_hash(0xde)], + hashes: hashset![make_tx_hash(0x6f), make_tx_hash(0xde)], }, PayableTransactionError::UninitializedInterface, ]; @@ -202,7 +210,7 @@ mod tests { LEDGER wallet, stupid.\"", "Signing phase: \"You cannot sign with just three crosses here, clever boy\"", "Sending phase: \"Sending to cosmos belongs elsewhere\". Signed and hashed \ - transactions: 0x000000000000000000000000000000000000000000000000000000000000006f, \ + txs: 0x000000000000000000000000000000000000000000000000000000000000006f, \ 0x00000000000000000000000000000000000000000000000000000000000000de", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED ]) diff --git a/node/src/blockchain/blockchain_interface/data_structures/mod.rs b/node/src/blockchain/blockchain_interface/data_structures/mod.rs index a33a1f889..1e8c918de 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -2,12 +2,16 @@ pub mod errors; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::PendingPayable; use crate::blockchain::blockchain_bridge::BlockMarker; use crate::sub_lib::wallet::Wallet; +use ethereum_types::U64; +use serde_derive::{Deserialize, Serialize}; use std::fmt; -use std::fmt::Formatter; -use web3::types::H256; +use std::fmt::{Display, Formatter}; +use web3::types::{TransactionReceipt, H256}; use web3::Error; #[derive(Clone, Debug, Eq, PartialEq)] @@ -37,7 +41,7 @@ pub struct RetrievedBlockchainTransactions { pub struct RpcPayableFailure { pub rpc_error: Error, pub recipient_wallet: Wallet, - pub hash: H256, + pub hash: TxHash, } #[derive(Debug, PartialEq, Clone)] @@ -45,3 +49,90 @@ pub enum ProcessedPayableFallible { Correct(PendingPayable), Failed(RpcPayableFailure), } + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RetrievedTxStatus { + pub tx_hash: TxHashByTable, + pub status: StatusReadFromReceiptCheck, +} + +impl RetrievedTxStatus { + pub fn new(tx_hash: TxHashByTable, status: StatusReadFromReceiptCheck) -> Self { + Self { tx_hash, status } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum StatusReadFromReceiptCheck { + Reverted, + Succeeded(TxBlock), + Pending, +} + +impl Display for StatusReadFromReceiptCheck { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StatusReadFromReceiptCheck::Reverted => { + write!(f, "Reverted") + } + StatusReadFromReceiptCheck::Succeeded(block) => { + write!( + f, + "Succeeded({},{:?})", + block.block_number, block.block_hash + ) + } + StatusReadFromReceiptCheck::Pending => write!(f, "Pending"), + } + } +} + +impl From for StatusReadFromReceiptCheck { + fn from(receipt: TransactionReceipt) -> Self { + match (receipt.status, receipt.block_hash, receipt.block_number) { + (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { + StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }) + } + (Some(status), _, _) if status == U64::from(0) => StatusReadFromReceiptCheck::Reverted, + _ => StatusReadFromReceiptCheck::Pending, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Ord, PartialOrd, Serialize, Deserialize)] +pub struct TxBlock { + pub block_hash: H256, + pub block_number: U64, +} + +#[cfg(test)] +mod tests { + use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, + }; + use ethereum_types::{H256, U64}; + + #[test] + fn tx_status_display_works() { + // Test Failed + assert_eq!(StatusReadFromReceiptCheck::Reverted.to_string(), "Reverted"); + + // Test Pending + assert_eq!(StatusReadFromReceiptCheck::Pending.to_string(), "Pending"); + + // Test Succeeded + let block_number = U64::from(12345); + let block_hash = H256::from_low_u64_be(0xabcdef); + let succeeded = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }); + assert_eq!( + succeeded.to_string(), + format!("Succeeded({},0x{:x})", block_number, block_hash) + ); + } +} diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index eb736b2a3..09961776e 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -4,20 +4,27 @@ pub mod blockchain_interface_web3; pub mod data_structures; pub mod lower_level_interface; -use actix::Recipient; -use ethereum_types::H256; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; +use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_bridge::{ + BlockMarker, BlockScanRange, RegisterNewPendingPayables, +}; +use crate::blockchain::blockchain_interface::data_structures::errors::{ + BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError, +}; +use crate::blockchain::blockchain_interface::data_structures::{ + ProcessedPayableFallible, RetrievedBlockchainTransactions, +}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::sub_lib::wallet::Wallet; +use actix::Recipient; use futures::Future; use masq_lib::blockchains::chains::Chain; -use web3::types::Address; use masq_lib::logger::Logger; -use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; +use std::collections::HashMap; +use web3::types::Address; pub trait BlockchainInterface { fn contract_address(&self) -> Address; @@ -40,14 +47,19 @@ pub trait BlockchainInterface { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>>; + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = HashMap, + Error = BlockchainInterfaceError, + >, + >; fn submit_payables_in_batch( &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>>; diff --git a/node/src/blockchain/errors/internal_errors.rs b/node/src/blockchain/errors/internal_errors.rs index fb6a4bf63..9982d0667 100644 --- a/node/src/blockchain/errors/internal_errors.rs +++ b/node/src/blockchain/errors/internal_errors.rs @@ -7,7 +7,7 @@ pub enum InternalError { PendingTooLongNotReplaced, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum InternalErrorKind { PendingTooLongNotReplaced, } diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index 5cd1a6f3c..e406a96b1 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -14,7 +14,7 @@ pub enum BlockchainError { Internal(InternalError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum BlockchainErrorKind { AppRpc(AppRpcErrorKind), Internal(InternalErrorKind), diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index e717fbf25..bf78fa53b 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -53,13 +53,13 @@ impl From for AppRpcError { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AppRpcErrorKind { Local(LocalErrorKind), Remote(RemoteErrorKind), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum LocalErrorKind { Decoder, Internal, @@ -68,7 +68,7 @@ pub enum LocalErrorKind { Transport, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum RemoteErrorKind { InvalidResponse, Unreachable, diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index f3b354931..2ce57f261 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -229,6 +229,55 @@ pub fn transport_error_message() -> String { } } +pub struct TransactionReceiptBuilder { + status_opt: Option, + block_hash_opt: Option, + block_number_opt: Option, + transaction_hash: H256, +} + +impl TransactionReceiptBuilder { + pub fn new(transaction_hash: H256) -> Self { + Self { + status_opt: None, + block_hash_opt: None, + block_number_opt: None, + transaction_hash, + } + } + + pub fn status(mut self, status: U64) -> Self { + self.status_opt = Some(status); + self + } + + pub fn block_hash(mut self, block_hash: H256) -> Self { + self.block_hash_opt = Some(block_hash); + self + } + + pub fn block_number(mut self, block_number: U64) -> Self { + self.block_number_opt = Some(block_number); + self + } + + pub fn build(self) -> TransactionReceipt { + TransactionReceipt { + status: self.status_opt, + root: None, + block_hash: self.block_hash_opt, + block_number: self.block_number_opt, + cumulative_gas_used: Default::default(), + gas_used: None, + contract_address: None, + transaction_hash: self.transaction_hash, + transaction_index: Default::default(), + logs: vec![], + logs_bloom: Default::default(), + } + } +} + #[derive(Default)] pub struct ValidationFailureClockMock { now_results: RefCell>, diff --git a/node/src/database/db_migrations/migrations/migration_4_to_5.rs b/node/src/database/db_migrations/migrations/migration_4_to_5.rs index 4b5bbb50a..204ab49a5 100644 --- a/node/src/database/db_migrations/migrations/migration_4_to_5.rs +++ b/node/src/database/db_migrations/migrations/migration_4_to_5.rs @@ -150,7 +150,7 @@ mod tests { conn: &dyn ConnectionWrapper, transaction_hash_opt: Option, wallet: &Wallet, - amount: i64, + amount_minor: i64, timestamp: SystemTime, ) { let hash_str = transaction_hash_opt @@ -159,7 +159,7 @@ mod tests { let mut stm = conn.prepare("insert into payable (wallet_address, balance, last_paid_timestamp, pending_payment_transaction) values (?,?,?,?)").unwrap(); let params: &[&dyn ToSql] = &[ &wallet, - &amount, + &amount_minor, &to_unix_timestamp(timestamp), if !hash_str.is_empty() { &hash_str diff --git a/node/src/database/rusqlite_wrappers.rs b/node/src/database/rusqlite_wrappers.rs index ec867482f..2177a250b 100644 --- a/node/src/database/rusqlite_wrappers.rs +++ b/node/src/database/rusqlite_wrappers.rs @@ -5,15 +5,15 @@ use crate::masq_lib::utils::ExpectValue; use rusqlite::{Connection, Error, Statement, ToSql, Transaction}; use std::fmt::Debug; -// We were challenged multiple times to device mocks for testing stubborn, hard to tame, data +// We were challenged multiple times to devise mocks for testing stubborn, hard to tame, data // structures from the 'rusqlite' library. After all, we've adopted two of them, the Connection, // that came first, and the Transaction to come much later. Of these, only the former complies // with the standard policy we follow for mock designs. // // The delay until the second one became a thing, even though we would've been glad having it -// on hand much earlier, was caused by vacuum of ideas on how we could create a mock of these +// on hand much earlier, was caused by a vacuum of ideas on how we could create a mock of these // parameters and have it accepted by the compiler. Passing a lot of time, we came up with a hybrid, -// at least. That said, it has costed us a considerably high price of giving up on simplicity. +// at least. That said, it has cost us a considerably high price of giving up on simplicity. // // The firmest blocker of the design has always rooted in a relationship of serialized lifetimes, // affecting each other, that has been so hard to maintain right. Yet the choices made @@ -74,12 +74,12 @@ impl ConnectionWrapperReal { } } -// Whole point of this outer wrapper, that is common to both the real and mock transactions, is to +// The whole point of this outer wrapper that is common to both the real and mock transactions is to // make a chance to deconstruct all components of a transaction in place. It plays a crucial role -// during the final commit. Note that an usual mock based on the direct use of a trait object +// during the final commit. Note that a usual mock based on the direct use of a trait object // cannot be consumed by any of its methods because of the Rust rules for trait objects. They say // clearly that we can access it via '&self', '&mut self' but not 'self'. However, to have a thing -// consume itself we need to be provided with the full ownership. +// consume itself, we need to be provided with the full ownership. // // Leaving remains of an already committed transaction around would expose us to a risk. Let's // imagine somebody trying to make use of it the second time, while the inner element providing diff --git a/node/src/database/test_utils/transaction_wrapper_mock.rs b/node/src/database/test_utils/transaction_wrapper_mock.rs index d0577c72f..5b9a717e9 100644 --- a/node/src/database/test_utils/transaction_wrapper_mock.rs +++ b/node/src/database/test_utils/transaction_wrapper_mock.rs @@ -137,7 +137,7 @@ impl TransactionInnerWrapper for TransactionInnerWrapperMock { // is to be formed. // With that said, we're relieved to have at least one working solution now. Speaking of the 'prepare' -// method, an error would be hardly needed because the production code simply unwraps the results by +// method, an error would hardly be needed because the production code simply unwraps the results by // using 'expect'. That is a function excluded from the requirement of writing tests for. // The 'Statement' produced by this method must be better understood. The 'prepare' method has @@ -199,12 +199,12 @@ impl SetupForProdCodeAndAlteredStmts { // necessary base. If the continuity is broken the later statement might not work. If // we record some changes on the transaction, other changes tried to be done from // a different connection might meet a different state of the database and thwart the - // efforts. (This behaviour probably depends on the global setup of the db). + // efforts. (This behavior probably depends on the global setup of the db). // // // Also imagine a 'Statement' that wouldn't cause an error whereupon any potential // rollback of this txn should best drag off both the prod code and altered statements - // all together, disappearing. If we did not use this txn some of the changes would stay. + // all together, disappearing. If we did not use this txn some changes would stay. { self.txn_bearing_prod_code_stmts_opt .as_ref() diff --git a/node/src/stream_handler_pool.rs b/node/src/stream_handler_pool.rs index 470f0c44f..5772e9cf8 100644 --- a/node/src/stream_handler_pool.rs +++ b/node/src/stream_handler_pool.rs @@ -1760,7 +1760,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); @@ -1927,7 +1927,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index f1f174e6e..e8d3477e6 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -1,15 +1,15 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; +use crate::accountant::db_access_objects::failed_payable_dao::FailedPayableDaoFactory; use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoFactory; +use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoFactory; use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::{ - checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, - SentPayables, + checked_conversion, Accountant, ReceivedPayments, ScanError, SentPayables, TxReceiptsMessage, }; use crate::actor_system_factory::SubsFactory; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::db_config::config_dao::ConfigDaoFactory; use crate::sub_lib::neighborhood::ConfigChangeMsg; use crate::sub_lib::peer_actors::{BindMessage, StartMessage}; @@ -71,7 +71,8 @@ impl PaymentThresholds { pub struct DaoFactories { pub payable_dao_factory: Box, - pub pending_payable_dao_factory: Box, + pub sent_payable_dao_factory: Box, + pub failed_payable_dao_factory: Box, pub receivable_dao_factory: Box, pub banned_dao_factory: Box, pub config_dao_factory: Box, @@ -100,8 +101,8 @@ pub struct AccountantSubs { pub report_services_consumed: Recipient, pub report_payable_payments_setup: Recipient, pub report_inbound_payments: Recipient, - pub init_pending_payable_fingerprints: Recipient, - pub report_transaction_receipts: Recipient, + pub register_new_pending_payables: Recipient, + pub report_transaction_status: Recipient, pub report_sent_payments: Recipient, pub scan_errors: Recipient, pub ui_message_sub: Recipient, diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 588eb87e6..546149ae6 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -570,6 +570,18 @@ pub mod unshared_test_utils { pub assertions: Box, } + pub fn capture_digits_with_separators_from_str( + surveyed_str: &str, + length_between_separators: usize, + separator: char, + ) -> Vec { + let regex = + format!("(\\d{{1,{length_between_separators}}}(?:{separator}\\d{{{length_between_separators}}})+)"); + let re = regex::Regex::new(®ex).unwrap(); + let captures = re.captures_iter(surveyed_str); + captures.map(|capture| capture[1].to_string()).collect() + } + pub fn assert_on_initialization_with_panic_on_migration(data_dir: &Path, act: &A) where A: Fn(&Path) + ?Sized, @@ -934,8 +946,7 @@ pub mod unshared_test_utils { ) -> Box { if self.panic_on_schedule_attempt { panic!( - "Message scheduling request for {:?} and interval {}ms, thought not \ - expected", + "Message scheduling request for {:?} and interval {}ms, thought not expected", msg, interval.as_millis() ); diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 6633ee948..ed35378c2 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -7,8 +7,8 @@ use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::accountant::{ReportTransactionReceipts, ScanForPendingPayables, ScanForRetryPayables}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::accountant::{ScanForPendingPayables, ScanForRetryPayables, TxReceiptsMessage}; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::daemon::crash_notification::CrashNotification; use crate::daemon::DaemonBindMessage; @@ -153,7 +153,7 @@ recorder_message_handler_t_m_p!(NodeFromUiMessage); recorder_message_handler_t_m_p!(NodeToUiMessage); recorder_message_handler_t_m_p!(NoLookupIncipientCoresPackage); recorder_message_handler_t_p!(OutboundPaymentsInstructions); -recorder_message_handler_t_m_p!(PendingPayableFingerprintSeeds); +recorder_message_handler_t_m_p!(RegisterNewPendingPayables); recorder_message_handler_t_m_p!(PoolBindMessage); recorder_message_handler_t_m_p!(QualifiedPayablesMessage); recorder_message_handler_t_m_p!(ReceivedPayments); @@ -162,7 +162,7 @@ recorder_message_handler_t_m_p!(RemoveStreamMsg); recorder_message_handler_t_m_p!(ReportExitServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportRoutingServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportServicesConsumedMessage); -recorder_message_handler_t_m_p!(ReportTransactionReceipts); +recorder_message_handler_t_m_p!(TxReceiptsMessage); recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); @@ -529,8 +529,8 @@ pub fn make_accountant_subs_from_recorder(addr: &Addr) -> AccountantSu report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), - init_pending_payable_fingerprints: recipient!(addr, PendingPayableFingerprintSeeds), - report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), + register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), + report_transaction_status: recipient!(addr, TxReceiptsMessage), report_sent_payments: recipient!(addr, SentPayables), scan_errors: recipient!(addr, ScanError), ui_message_sub: recipient!(addr, NodeFromUiMessage), From 6e020c7235bb16599cb1aac3847dc39b2adb97b1 Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:10:03 +0200 Subject: [PATCH 13/37] GH-689: Amend scanner scheduling: Handling ScanError msg (#691) * GH-689: finished * GH-689: fixes after auto-review --------- Co-authored-by: Bert --- node/src/accountant/mod.rs | 357 +++++++++++++----- node/src/accountant/scanners/mod.rs | 37 +- .../scanners/pending_payable_scanner/mod.rs | 10 +- .../tx_receipt_interpreter.rs | 4 +- node/src/blockchain/blockchain_bridge.rs | 77 ++-- node/src/sub_lib/accountant.rs | 27 +- .../test_utils/recorder_stop_conditions.rs | 8 +- 7 files changed, 363 insertions(+), 157 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f54a7dbd6..b8651d990 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -43,12 +43,12 @@ use crate::blockchain::blockchain_interface::data_structures::{ use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::bootstrapper::BootstrapperConfig; use crate::database::db_initializer::DbInitializationConfig; -use crate::sub_lib::accountant::AccountantSubs; use crate::sub_lib::accountant::DaoFactories; use crate::sub_lib::accountant::FinancialStatistics; use crate::sub_lib::accountant::ReportExitServiceProvidedMessage; use crate::sub_lib::accountant::ReportRoutingServiceProvidedMessage; use crate::sub_lib::accountant::ReportServicesConsumedMessage; +use crate::sub_lib::accountant::{AccountantSubs, DetailedScanType}; use crate::sub_lib::accountant::{MessageIdGenerator, MessageIdGeneratorReal}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::neighborhood::{ConfigChange, ConfigChangeMsg}; @@ -177,7 +177,7 @@ pub struct ScanForReceivables { #[derive(Debug, Clone, Message, PartialEq, Eq)] pub struct ScanError { - pub scan_type: ScanType, + pub scan_type: DetailedScanType, pub response_skeleton_opt: Option, pub msg: String, } @@ -417,31 +417,51 @@ impl Handler for Accountant { impl Handler for Accountant { type Result = (); - fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, scan_error: ScanError, ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); self.scanners .acknowledge_scan_error(&scan_error, &self.logger); - if let Some(response_skeleton) = scan_error.response_skeleton_opt { - let error_msg = NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: MessageBody { - opcode: "scan".to_string(), - path: MessagePath::Conversation(response_skeleton.context_id), - payload: Err(( - SCAN_ERROR, - format!( - "{:?} scan failed: '{}'", - scan_error.scan_type, scan_error.msg - ), - )), - }, - }; - error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway not bound") - .try_send(error_msg) - .expect("UiGateway is dead"); + + match scan_error.response_skeleton_opt { + None => match scan_error.scan_type { + DetailedScanType::NewPayables => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + DetailedScanType::RetryPayables => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, None, &self.logger), + DetailedScanType::PendingPayables => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + DetailedScanType::Receivables => { + self.scan_schedulers.receivable.schedule(ctx, &self.logger) + } + }, + Some(response_skeleton) => { + let error_msg = NodeToUiMessage { + target: ClientId(response_skeleton.client_id), + body: MessageBody { + opcode: "scan".to_string(), + path: MessagePath::Conversation(response_skeleton.context_id), + payload: Err(( + SCAN_ERROR, + format!( + "{:?} scan failed: '{}'", + scan_error.scan_type, scan_error.msg + ), + )), + }, + }; + error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway not bound") + .try_send(error_msg) + .expect("UiGateway is dead"); + } } } } @@ -5563,29 +5583,205 @@ mod tests { const EXAMPLE_ERROR_MSG: &str = "My tummy hurts"; + fn do_setup_and_prepare_assertions_for_new_payables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = SystemTime::now(); + scan_schedulers.payable.dyn_interval_computer = Box::new( + NewPayableScanDynIntervalComputerMock::default() + .compute_interval_result(Some(Duration::from_secs(152))), + ); + scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_later_params = notify_later_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => assert_eq!( + *notify_later_params, + vec![(ScanForNewPayables::default(), Duration::from_secs(152))] + ), + Some(_) => { + assert!( + notify_later_params.is_empty(), + "Should be empty but contained {:?}", + notify_later_params + ) + } + } + }) + }, + ) + } + + fn do_setup_and_prepare_assertions_for_retry_payables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(¬ify_params_arc)); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_params = notify_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => { + // Response skeleton must be None + assert_eq!( + *notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt: None + }] + ) + } + Some(_) => { + assert!( + notify_params.is_empty(), + "Should be empty but contained {:?}", + notify_params + ) + } + } + }) + }, + ) + } + + fn do_setup_and_prepare_assertions_for_pending_payables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers.pending_payable.interval = Duration::from_secs(600); + scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + let sent_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_sent_tx_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_failed_tx_params_arc); + let scanner = PendingPayableScannerBuilder::new() + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + scanners.replace_scanner(ScannerReplacement::PendingPayable( + ReplacementType::Real(scanner), + )); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_later_params = notify_later_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => { + assert_eq!( + *notify_later_params, + vec![(ScanForPendingPayables::default(), Duration::from_secs(600))] + ) + } + Some(_) => { + assert!( + notify_later_params.is_empty(), + "Should be empty but contained {:?}", + notify_later_params + ) + } + } + let ensure_empty_cache_sent_tx_params = + ensure_empty_cache_sent_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_sent_tx_params, vec![()]); + let ensure_empty_cache_failed_tx_params = + ensure_empty_cache_failed_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_failed_tx_params, vec![()]); + }) + }, + ) + } + + fn do_setup_and_prepare_assertions_for_receivables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers.receivable.interval = Duration::from_secs(600); + scan_schedulers.receivable.handle = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_later_params = notify_later_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => { + assert_eq!( + *notify_later_params, + vec![(ScanForReceivables::default(), Duration::from_secs(600))] + ) + } + Some(_) => { + assert!( + notify_later_params.is_empty(), + "Should be empty but contained {:?}", + notify_later_params + ) + } + } + }) + }, + ) + } + #[test] - fn handling_scan_error_for_externally_triggered_payables() { + fn handling_scan_error_for_externally_triggered_new_payables() { test_scan_error_is_handled_properly( - "handling_scan_error_for_externally_triggered_payables", + "handling_scan_error_for_externally_triggered_new_payables", ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_new_payables(), ); } + #[test] + fn handling_scan_error_for_externally_triggered_retry_payables() { + test_scan_error_is_handled_properly( + "handling_scan_error_for_externally_triggered_retry_payables", + ScanError { + scan_type: DetailedScanType::RetryPayables, + response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), + msg: EXAMPLE_ERROR_MSG.to_string(), + }, + do_setup_and_prepare_assertions_for_retry_payables(), + ) + } + #[test] fn handling_scan_error_for_externally_triggered_pending_payables() { - let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); - test_scan_error_is_handled_properly_more_specifically( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_pending_payables", ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, - Some(additional_test_setup_and_assertions), + do_setup_and_prepare_assertions_for_pending_payables(), ); } @@ -5594,36 +5790,50 @@ mod tests { test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_receivables", ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_receivables(), ); } #[test] - fn handling_scan_error_for_internally_triggered_payables() { + fn handling_scan_error_for_internally_triggered_new_payables() { test_scan_error_is_handled_properly( - "handling_scan_error_for_internally_triggered_payables", + "handling_scan_error_for_internally_triggered_new_payables", ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_new_payables(), + ); + } + + #[test] + fn handling_scan_error_for_internally_triggered_retry_payables() { + test_scan_error_is_handled_properly( + "handling_scan_error_for_internally_triggered_retry_payables", + ScanError { + scan_type: DetailedScanType::RetryPayables, + response_skeleton_opt: None, + msg: EXAMPLE_ERROR_MSG.to_string(), + }, + do_setup_and_prepare_assertions_for_retry_payables(), ); } #[test] fn handling_scan_error_for_internally_triggered_pending_payables() { - let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); - test_scan_error_is_handled_properly_more_specifically( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_pending_payables", ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, - Some(additional_test_setup_and_assertions), + do_setup_and_prepare_assertions_for_pending_payables(), ); } @@ -5632,41 +5842,14 @@ mod tests { test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_receivables", ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_receivables(), ); } - fn prepare_setup_and_assertion_fns() -> (Box, Box) { - let ensure_empty_cache_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); - let ensure_empty_cache_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); - let sent_payable_cache = PendingPayableCacheMock::default() - .ensure_empty_cache_params(&ensure_empty_cache_sent_tx_params_arc); - let failed_payable_cache = PendingPayableCacheMock::default() - .ensure_empty_cache_params(&ensure_empty_cache_failed_tx_params_arc); - let scanner = PendingPayableScannerBuilder::new() - .sent_payable_cache(Box::new(sent_payable_cache)) - .failed_payable_cache(Box::new(failed_payable_cache)) - .build(); - ( - Box::new(|scanners: &mut Scanners| { - scanners.replace_scanner(ScannerReplacement::PendingPayable( - ReplacementType::Real(scanner), - )); - }) as Box, - Box::new(move || { - let ensure_empty_cache_sent_tx_params = - ensure_empty_cache_sent_tx_params_arc.lock().unwrap(); - assert_eq!(*ensure_empty_cache_sent_tx_params, vec![()]); - let ensure_empty_cache_failed_tx_params = - ensure_empty_cache_failed_tx_params_arc.lock().unwrap(); - assert_eq!(*ensure_empty_cache_failed_tx_params, vec![()]); - }) as Box, - ) - } - #[test] fn financials_request_with_nothing_to_respond_to_is_refused() { let system = System::new("test"); @@ -6412,32 +6595,29 @@ mod tests { let _: u64 = wei_to_gwei(u128::MAX); } - fn test_scan_error_is_handled_properly(test_name: &str, message: ScanError) { - test_scan_error_is_handled_properly_more_specifically(test_name, message, None) - } - fn test_scan_error_is_handled_properly_more_specifically( + type RunSchedulersAssertions = Box)>; + + fn test_scan_error_is_handled_properly( test_name: &str, message: ScanError, - additional_assertion_opt: Option<(Box, Box)>, + set_up_schedulers_and_prepare_assertions: Box< + dyn FnOnce(&mut Scanners, &mut ScanSchedulers) -> RunSchedulersAssertions, + >, ) { init_test_logging(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("blah")) .logger(Logger::new(test_name)) .build(); - let (adjust_scanner, run_additional_assertion) = match additional_assertion_opt { - Some(two_functions) => two_functions, - None => ( - Box::new(|scanners: &mut Scanners| { - scanners.reset_scan_started( - message.scan_type, - MarkScanner::Started(SystemTime::now()), - ) - }) as Box, - Box::new(|| ()) as Box, - ), - }; - adjust_scanner(&mut subject.scanners); + subject.scanners.reset_scan_started( + message.scan_type.into(), + MarkScanner::Started(SystemTime::now()), + ); + let run_schedulers_assertions = set_up_schedulers_and_prepare_assertions( + &mut subject.scanners, + &mut subject.scan_schedulers, + ); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -6448,13 +6628,15 @@ mod tests { subject_addr .try_send(AssertionsMessage { assertions: Box::new(move |actor: &mut Accountant| { - let scan_started_at_opt = actor.scanners.scan_started_at(message.scan_type); + let scan_started_at_opt = + actor.scanners.scan_started_at(message.scan_type.into()); assert_eq!(scan_started_at_opt, None); }), }) .unwrap(); System::current().stop(); assert_eq!(system.run(), 0); + run_schedulers_assertions(message.response_skeleton_opt); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); match message.response_skeleton_opt { Some(response_skeleton) => { @@ -6494,7 +6676,6 @@ mod tests { )); } } - run_additional_assertion(); } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 71ff0f62c..1d69ab3c9 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -12,7 +12,10 @@ use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_sent_tx_record, investigate_debt_extremes, payables_debug_summary, separate_errors, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMissingInDb}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_sent_tx_record, + investigate_debt_extremes, payables_debug_summary, separate_errors, OperationOutcome, PayableScanResult, + PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMissingInDb}; use crate::accountant::{PendingPayable, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, @@ -20,9 +23,7 @@ use crate::accountant::{ ScanForReceivables, SentPayables, }; use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; -use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, -}; +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}; @@ -271,14 +272,14 @@ impl Scanners { pub fn acknowledge_scan_error(&mut self, error: &ScanError, logger: &Logger) { match error.scan_type { - ScanType::Payables => { - self.payable.mark_as_ended(logger); + DetailedScanType::NewPayables | DetailedScanType::RetryPayables => { + self.payable.mark_as_ended(logger) } - ScanType::PendingPayables => { + DetailedScanType::PendingPayables => { self.empty_caches(logger); self.pending_payable.mark_as_ended(logger); } - ScanType::Receivables => { + DetailedScanType::Receivables => { self.receivable.mark_as_ended(logger); } }; @@ -1072,7 +1073,8 @@ mod tests { use crate::db_config::mocks::ConfigDaoMock; use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, + DaoFactories, 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; @@ -3199,7 +3201,7 @@ mod tests { #[test] fn acknowledge_scan_error_works() { - fn scan_error(scan_type: ScanType) -> ScanError { + fn scan_error(scan_type: DetailedScanType) -> ScanError { ScanError { scan_type, response_skeleton_opt: None, @@ -3210,22 +3212,27 @@ mod tests { init_test_logging(); let test_name = "acknowledge_scan_error_works"; let inputs: Vec<( - ScanType, + DetailedScanType, Box, Box Option>, )> = vec![ ( - ScanType::Payables, + DetailedScanType::NewPayables, + Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.payable.scan_started_at()), + ), + ( + DetailedScanType::RetryPayables, Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.payable.scan_started_at()), ), ( - ScanType::PendingPayables, + DetailedScanType::PendingPayables, Box::new(|subject| subject.pending_payable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.pending_payable.scan_started_at()), ), ( - ScanType::Receivables, + DetailedScanType::Receivables, Box::new(|subject| subject.receivable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.receivable.scan_started_at()), ), @@ -3258,7 +3265,7 @@ mod tests { ); test_log_handler.exists_log_containing(&format!( "INFO: {test_name}: The {:?} scan ended in", - scan_type + ScanType::from(scan_type) )); }) } diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index f501a7be2..70c043909 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -865,7 +865,7 @@ mod tests { let pending_payable_cache_before = subject.current_sent_payables.dump_cache(); let failed_payable_cache_before = subject.yet_unproven_failed_payables.dump_cache(); - let result = subject.start_scan(&make_wallet("bluh"), SystemTime::now(), None, &logger); + let result = subject.start_scan(&make_wallet("blah"), SystemTime::now(), None, &logger); assert_eq!( result, @@ -1344,7 +1344,7 @@ mod tests { #[should_panic( expected = "Unable to update pending-tx statuses for validation failures '[FailedValidation \ { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ - AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"bluh\")" + AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"blah\")" )] fn update_validation_status_for_sent_txs_panics_on_update_statuses() { let failed_validation = FailedValidation::new( @@ -1353,7 +1353,7 @@ mod tests { TxStatus::Pending(ValidationStatus::Waiting), ); let sent_payable_dao = SentPayableDaoMock::default() - .update_statuses_result(Err(SentPayableDaoError::InvalidInput("bluh".to_string()))); + .update_statuses_result(Err(SentPayableDaoError::InvalidInput("blah".to_string()))); let subject = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) @@ -1367,7 +1367,7 @@ mod tests { #[should_panic( expected = "Unable to update failed-tx statuses for validation failures '[FailedValidation \ { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ - AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"bluh\")" + AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"blah\")" )] fn update_validation_status_for_failed_txs_panics_on_update_statuses() { let failed_validation = FailedValidation::new( @@ -1376,7 +1376,7 @@ mod tests { FailureStatus::RecheckRequired(ValidationStatus::Waiting), ); let failed_payable_dao = FailedPayableDaoMock::default() - .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("bluh".to_string()))); + .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("blah".to_string()))); let subject = PendingPayableScannerBuilder::new() .failed_payable_dao(failed_payable_dao) .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) 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 4bf96bf1e..e01425d69 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 @@ -607,7 +607,7 @@ mod tests { let mut sent_tx = make_sent_tx(456); sent_tx.hash = tx_hash; sent_tx.status = current_tx_status.clone(); - let rpc_error = AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())); + let rpc_error = AppRpcError::Remote(RemoteError::InvalidResponse("blah".to_string())); let scan_report = ReceiptScanReport::default(); let result = TxReceiptInterpreter::handle_rpc_failure( @@ -635,7 +635,7 @@ mod tests { ); TestLogHandler::new().exists_log_containing( &format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000\ - 000000000000000000000000000000000000000000000000000391): Remote(InvalidResponse(\"bluh\")). \ + 000000000000000000000000000000000000000000000000000391): Remote(InvalidResponse(\"blah\")). \ Will retry receipt retrieval next cycle")); } diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index d1d41337b..1a5cad399 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -23,6 +23,7 @@ use crate::db_config::config_dao::ConfigDaoReal; use crate::db_config::persistent_configuration::{ PersistentConfiguration, PersistentConfigurationReal, }; +use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, OutboundPaymentsInstructions}; use crate::sub_lib::peer_actors::BindMessage; use crate::sub_lib::utils::{db_connection_launch_panic, handle_ui_crash_request}; @@ -123,7 +124,7 @@ impl Handler for BlockchainBridge { ) -> >::Result { self.handle_scan_future( Self::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg, ) } @@ -135,7 +136,7 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: RequestTransactionReceipts, _ctx: &mut Self::Context) { self.handle_scan_future( Self::handle_request_transaction_receipts, - ScanType::PendingPayables, + DetailedScanType::PendingPayables, msg, ) } @@ -145,7 +146,13 @@ impl Handler for BlockchainBridge { type Result = (); fn handle(&mut self, msg: QualifiedPayablesMessage, _ctx: &mut Self::Context) { - self.handle_scan_future(Self::handle_qualified_payable_msg, ScanType::Payables, msg); + 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" + ), + msg, + ); } } @@ -155,7 +162,9 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: OutboundPaymentsInstructions, _ctx: &mut Self::Context) { self.handle_scan_future( Self::handle_outbound_payments_instructions, - ScanType::Payables, + todo!( + "This needs to be decided on GH-605. Look what mode you run and set it accordingly" + ), msg, ) } @@ -440,7 +449,7 @@ impl BlockchainBridge { ) } - fn handle_scan_future(&mut self, handler: F, scan_type: ScanType, msg: M) + fn handle_scan_future(&mut self, handler: F, scan_type: DetailedScanType, msg: M) where F: FnOnce(&mut BlockchainBridge, M) -> Box>, M: SkeletonOptHolder, @@ -562,15 +571,10 @@ mod tests { use crate::node_test_utils::check_timestamp; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; - use crate::test_utils::recorder::{ - make_accountant_subs_from_recorder, make_recorder, peer_actors_builder, - }; + use crate::test_utils::recorder::{make_accountant_subs_from_recorder, make_blockchain_bridge_subs_from_recorder, make_recorder, peer_actors_builder}; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use crate::test_utils::unshared_test_utils::{ - assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, - prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, ZERO, - }; + use crate::test_utils::unshared_test_utils::{assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, SubsFactoryTestAddrLeaker, ZERO}; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; @@ -601,6 +605,17 @@ mod tests { } } + impl SubsFactory + for SubsFactoryTestAddrLeaker + { + fn make(&self, addr: &Addr) -> BlockchainBridgeSubs { + self.send_leaker_msg_and_return_meaningless_subs( + addr, + make_blockchain_bridge_subs_from_recorder, + ) + } + } + #[test] fn constants_have_correct_values() { assert_eq!(CRASH_KEY, "BLOCKCHAINBRIDGE"); @@ -1016,7 +1031,7 @@ mod tests { assert_eq!( *scan_error_msg, ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 @@ -1267,7 +1282,7 @@ mod tests { assert_eq!( scan_error, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: None, msg: "Error while retrieving transactions: QueryFailed(\"Transport error: Error(IncompleteMessage)\")".to_string() } @@ -1403,7 +1418,7 @@ mod tests { let _ = subject.handle_scan_future( BlockchainBridge::handle_request_transaction_receipts, - ScanType::PendingPayables, + DetailedScanType::PendingPayables, msg, ); @@ -1412,7 +1427,7 @@ mod tests { assert_eq!( recording.get_record::(0), &ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: None, msg: "Blockchain error: Query failed: Transport error: Error(IncompleteMessage)" .to_string() @@ -1781,7 +1796,7 @@ mod tests { assert_eq!( scan_error_msg, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 @@ -1841,7 +1856,7 @@ mod tests { assert_eq!( scan_error_msg, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 @@ -1987,7 +2002,7 @@ mod tests { subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, retrieve_transactions, ); @@ -2041,7 +2056,7 @@ mod tests { subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg.clone(), ); @@ -2051,7 +2066,7 @@ mod tests { assert_eq!( message, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: msg.response_skeleton_opt, msg: "Error while retrieving transactions: QueryFailed(\"RPC error: Error { code: ServerError(-32005), message: \\\"My tummy hurts\\\", data: None }\")" .to_string() @@ -2190,22 +2205,4 @@ mod tests { assert_eq!(increase_gas_price_by_margin(1_000_000_000), 1_300_000_000); assert_eq!(increase_gas_price_by_margin(9_000_000_000), 11_700_000_000); } -} - -#[cfg(test)] -pub mod exportable_test_parts { - use super::*; - use crate::test_utils::recorder::make_blockchain_bridge_subs_from_recorder; - use crate::test_utils::unshared_test_utils::SubsFactoryTestAddrLeaker; - - impl SubsFactory - for SubsFactoryTestAddrLeaker - { - fn make(&self, addr: &Addr) -> BlockchainBridgeSubs { - self.send_leaker_msg_and_return_meaningless_subs( - addr, - make_blockchain_bridge_subs_from_recorder, - ) - } - } -} +} \ No newline at end of file diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index e8d3477e6..78379b890 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -192,23 +192,44 @@ impl MessageIdGenerator for MessageIdGeneratorReal { as_any_ref_in_trait_impl!(); } +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum DetailedScanType { + NewPayables, + RetryPayables, + PendingPayables, + Receivables, +} + #[cfg(test)] mod tests { use crate::accountant::test_utils::AccountantBuilder; use crate::accountant::{checked_conversion, Accountant}; use crate::sub_lib::accountant::{ - AccountantSubsFactoryReal, MessageIdGenerator, MessageIdGeneratorReal, PaymentThresholds, - ScanIntervals, SubsFactory, DEFAULT_EARNING_WALLET, DEFAULT_PAYMENT_THRESHOLDS, - DEFAULT_SCAN_INTERVALS, MSG_ID_INCREMENTER, TEMPORARY_CONSUMING_WALLET, + AccountantSubsFactoryReal, DetailedScanType, MessageIdGenerator, MessageIdGeneratorReal, + PaymentThresholds, ScanIntervals, SubsFactory, DEFAULT_EARNING_WALLET, + DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS, MSG_ID_INCREMENTER, + TEMPORARY_CONSUMING_WALLET, }; use crate::sub_lib::wallet::Wallet; use crate::test_utils::recorder::{make_accountant_subs_from_recorder, Recorder}; use actix::Actor; + use masq_lib::messages::ScanType; use std::str::FromStr; use std::sync::atomic::Ordering; use std::sync::Mutex; use std::time::Duration; + impl From for ScanType { + fn from(scan_type: DetailedScanType) -> Self { + match scan_type { + DetailedScanType::NewPayables => ScanType::Payables, + DetailedScanType::RetryPayables => ScanType::Payables, + DetailedScanType::PendingPayables => ScanType::PendingPayables, + DetailedScanType::Receivables => ScanType::Receivables, + } + } + } + static MSG_ID_GENERATOR_TEST_GUARD: Mutex<()> = Mutex::new(()); impl PaymentThresholds { diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 9a3214eea..f10e0e4a6 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -184,9 +184,9 @@ macro_rules! match_lazily_every_type_id{ mod tests { use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; + use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; - use masq_lib::messages::ScanType; use std::any::TypeId; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::vec; @@ -249,16 +249,16 @@ mod tests { let mut cond_set = StopConditions::AllGreedily(vec![MsgIdentification::ByPredicate { predicate: Box::new(|msg| { let scan_err_msg: &ScanError = msg.downcast_ref().unwrap(); - scan_err_msg.scan_type == ScanType::PendingPayables + scan_err_msg.scan_type == DetailedScanType::PendingPayables }), }]); let wrong_msg = ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: None, msg: "booga".to_string(), }; let good_msg = ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: None, msg: "blah".to_string(), }; From 647d61ac0ff0dd91a00a1757d6767246b6396627 Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:11:29 +0200 Subject: [PATCH 14/37] GH-606: Sweeping for ending the first stage (#706) * GH-606-v2: db and help msgs + the interface document * GH-606-v2: constraint/raletion between scan intervals established * GH-606: final clean-up * GH-606-v2: after review * GH-606-v2: formatting --------- Co-authored-by: Bert --- USER-INTERFACE-INTERFACE.md | 19 +- .../src/blockchains/blockchain_records.rs | 29 +- masq_lib/src/blockchains/chains.rs | 1 + masq_lib/src/constants.rs | 42 +- masq_lib/src/shared_schema.rs | 57 +- .../db_access_objects/payable_dao.rs | 98 +- .../db_access_objects/pending_payable_dao.rs | 933 ------------------ node/src/accountant/mod.rs | 19 +- .../accountant/scanners/scan_schedulers.rs | 23 +- node/src/accountant/test_utils.rs | 9 +- node/src/actor_system_factory.rs | 2 +- node/src/blockchain/blockchain_bridge.rs | 16 +- node/src/bootstrapper.rs | 5 +- node/src/daemon/setup_reporter.rs | 45 +- node/src/database/config_dumper.rs | 37 +- node/src/database/db_initializer.rs | 73 +- .../migrations/migration_10_to_11.rs | 9 +- .../migrations/migration_5_to_6.rs | 38 +- node/src/db_config/config_dao_null.rs | 11 +- .../unprivileged_parse_args_configuration.rs | 74 +- node/src/sub_lib/accountant.rs | 58 +- node/src/sub_lib/combined_parameters.rs | 9 +- node/src/test_utils/database_utils.rs | 7 +- node/src/test_utils/mod.rs | 15 +- 24 files changed, 381 insertions(+), 1248 deletions(-) delete mode 100644 node/src/accountant/db_access_objects/pending_payable_dao.rs diff --git a/USER-INTERFACE-INTERFACE.md b/USER-INTERFACE-INTERFACE.md index 443abc65a..3a5cacffa 100644 --- a/USER-INTERFACE-INTERFACE.md +++ b/USER-INTERFACE-INTERFACE.md @@ -358,8 +358,8 @@ Another reason the secrets might be missing is that there are not yet any secret "exitServiceRate: " }, "scanIntervals": { - "pendingPayableSec": , "payableSec": , + "pendingPayableSec": , "receivableSec": }, } @@ -453,20 +453,21 @@ database password. If you want to know whether the password you have is the corr * `scanIntervals`: These three intervals describe the length of three different scan cycles running automatically in the background since the Node has connected to a qualified neighborhood that consists of neighbors enabling a complete - 3-hop route. Each parameter can be set independently, but by default are all the same which currently is most desirable - for the consistency of service payments to and from your Node. Technically, there doesn't have to be any lower limit - for the minimum of time you can set; two scans of the same sort would never run at the same time but the next one is + 3-hop route. Each parameter can be set independently. Technically, there doesn't have to be any lower limit for +* the minimum of time you can set; two scans of the same sort would never run at the same time but the next one is always scheduled not earlier than the end of the previous one. These are ever present values, no matter if the user's set any value, because defaults are prepared. -* `pendingPayableSec`: Amount of seconds between two sequential cycles of scanning for payments that are marked as currently - pending; the payments were sent to pay our debts, the payable. The purpose of this process is to confirm the status of - the pending payment; either the payment transaction was written on blockchain as successful or failed. - -* `payableSec`: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts of that meet +* `payableSec`: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question. +* `pendingPayableSec`: The time elapsed since the last payable transaction was processed. This scan operates + on an irregular schedule and is triggered after new transactions are sent or when failed transactions need + to be replaced. The scanner monitors pending transactions and verifies their blockchain status, determining whether + each payment was successfully recorded or failed. Any failed transaction is automatically resubmitted as soon + as the failure is detected. + * `receivableSec`: Amount of seconds between two sequential cycles of scanning for payments on the blockchain that have been sent by our creditors to us, which are credited against receivables recorded for services provided. diff --git a/masq_lib/src/blockchains/blockchain_records.rs b/masq_lib/src/blockchains/blockchain_records.rs index 00ac1bb66..821b0d7f1 100644 --- a/masq_lib/src/blockchains/blockchain_records.rs +++ b/masq_lib/src/blockchains/blockchain_records.rs @@ -4,7 +4,9 @@ use crate::blockchains::chains::Chain; use crate::constants::{ BASE_GAS_PRICE_CEILING_WEI, BASE_MAINNET_CHAIN_ID, BASE_MAINNET_CONTRACT_CREATION_BLOCK, BASE_MAINNET_FULL_IDENTIFIER, BASE_SEPOLIA_CHAIN_ID, BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, - BASE_SEPOLIA_FULL_IDENTIFIER, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, + BASE_SEPOLIA_FULL_IDENTIFIER, DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, DEV_GAS_PRICE_CEILING_WEI, ETH_GAS_PRICE_CEILING_WEI, ETH_MAINNET_CHAIN_ID, ETH_MAINNET_CONTRACT_CREATION_BLOCK, ETH_MAINNET_FULL_IDENTIFIER, ETH_ROPSTEN_CHAIN_ID, ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, ETH_ROPSTEN_FULL_IDENTIFIER, @@ -15,14 +17,13 @@ use crate::constants::{ }; use ethereum_types::{Address, H160}; -// TODO these should probably be a static (it's a shame that we construct the data every time anew -// when we ask for the chain specs), and dynamic initialization should be allowed as well -pub const CHAINS: [BlockchainRecord; 7] = [ +pub static CHAINS: [BlockchainRecord; 7] = [ BlockchainRecord { self_id: Chain::PolyMainnet, num_chain_id: POLYGON_MAINNET_CHAIN_ID, literal_identifier: POLYGON_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -31,6 +32,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: ETH_MAINNET_CHAIN_ID, literal_identifier: ETH_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -39,6 +41,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: BASE_MAINNET_CHAIN_ID, literal_identifier: BASE_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -47,6 +50,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: BASE_SEPOLIA_CHAIN_ID, literal_identifier: BASE_SEPOLIA_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, }, @@ -55,6 +59,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: POLYGON_AMOY_CHAIN_ID, literal_identifier: POLYGON_AMOY_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, }, @@ -63,6 +68,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: ETH_ROPSTEN_CHAIN_ID, literal_identifier: ETH_ROPSTEN_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, }, @@ -71,6 +77,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: DEV_CHAIN_ID, literal_identifier: DEV_CHAIN_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: DEV_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, }, @@ -82,6 +89,7 @@ pub struct BlockchainRecord { pub num_chain_id: u64, pub literal_identifier: &'static str, pub gas_price_safe_ceiling_minor: u128, + pub default_pending_payable_interval_sec: u64, pub contract: Address, pub contract_creation_block: u64, } @@ -128,7 +136,11 @@ const POLYGON_MAINNET_CONTRACT_ADDRESS: Address = H160([ mod tests { use super::*; use crate::blockchains::chains::chain_from_chain_identifier_opt; - use crate::constants::{BASE_MAINNET_CONTRACT_CREATION_BLOCK, WEIS_IN_GWEI}; + use crate::constants::{ + BASE_MAINNET_CONTRACT_CREATION_BLOCK, DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, WEIS_IN_GWEI, + }; use std::collections::HashSet; use std::iter::FromIterator; @@ -209,6 +221,7 @@ mod tests { self_id: examined_chain, literal_identifier: "eth-mainnet", gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -226,6 +239,7 @@ mod tests { self_id: examined_chain, literal_identifier: "eth-ropsten", gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, } @@ -243,6 +257,7 @@ mod tests { self_id: examined_chain, literal_identifier: "polygon-mainnet", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -260,6 +275,7 @@ mod tests { self_id: examined_chain, literal_identifier: "polygon-amoy", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, } @@ -277,6 +293,7 @@ mod tests { self_id: examined_chain, literal_identifier: "base-mainnet", gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -294,6 +311,7 @@ mod tests { self_id: examined_chain, literal_identifier: "base-sepolia", gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, } @@ -311,6 +329,7 @@ mod tests { self_id: examined_chain, literal_identifier: "dev", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, } diff --git a/masq_lib/src/blockchains/chains.rs b/masq_lib/src/blockchains/chains.rs index b7061d899..cedde32d1 100644 --- a/masq_lib/src/blockchains/chains.rs +++ b/masq_lib/src/blockchains/chains.rs @@ -142,6 +142,7 @@ mod tests { self_id: Chain::PolyMainnet, literal_identifier: "", gas_price_safe_ceiling_minor: 0, + default_pending_payable_interval_sec: 0, contract: Default::default(), contract_creation_block: 0, } diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index 67338f5a3..fcbec6826 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -18,24 +18,16 @@ pub const MASQ_URL_PREFIX: &str = "masq://"; pub const CURRENT_LOGFILE_NAME: &str = "MASQNode_rCURRENT.log"; pub const MASQ_PROMPT: &str = "masq> "; -pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really -pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; - pub const WALLET_ADDRESS_LENGTH: usize = 42; -pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; pub const WEIS_IN_GWEI: i128 = 1_000_000_000; -pub const DEFAULT_MAX_BLOCK_COUNT: u64 = 100_000; +pub const COMBINED_PARAMETERS_DELIMITER: char = '|'; pub const PAYLOAD_ZERO_SIZE: usize = 0usize; -pub const ETH_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 11_170_708; -pub const ETH_ROPSTEN_CONTRACT_CREATION_BLOCK: u64 = 8_688_171; -pub const POLYGON_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 14_863_650; -pub const POLYGON_AMOY_CONTRACT_CREATION_BLOCK: u64 = 5_323_366; -pub const BASE_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 19_711_235; -pub const BASE_SEPOLIA_CONTRACT_CREATION_BLOCK: u64 = 14_732_730; -pub const MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK: u64 = 0; +//descriptor +pub const CENTRAL_DELIMITER: char = '@'; +pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; //Migration versions //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -88,11 +80,11 @@ pub const VALUE_EXCEEDS_ALLOWED_LIMIT: u64 = ACCOUNTANT_PREFIX | 3; //////////////////////////////////////////////////////////////////////////////////////////////////// -pub const COMBINED_PARAMETERS_DELIMITER: char = '|'; +pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; -//descriptor -pub const CENTRAL_DELIMITER: char = '@'; -pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; +pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really +pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; +pub const DEFAULT_MAX_BLOCK_COUNT: u64 = 100_000; //chains pub const POLYGON_MAINNET_CHAIN_ID: u64 = 137; @@ -114,11 +106,25 @@ pub const ETH_ROPSTEN_FULL_IDENTIFIER: &str = concatcp!(ETH_FAMILY, LINK, "ropst pub const BASE_MAINNET_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, MAINNET); pub const BASE_SEPOLIA_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, "sepolia"); pub const DEV_CHAIN_FULL_IDENTIFIER: &str = "dev"; + +pub const ETH_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 11_170_708; +pub const ETH_ROPSTEN_CONTRACT_CREATION_BLOCK: u64 = 8_688_171; +pub const POLYGON_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 14_863_650; +pub const POLYGON_AMOY_CONTRACT_CREATION_BLOCK: u64 = 5_323_366; +pub const BASE_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 19_711_235; +pub const BASE_SEPOLIA_CONTRACT_CREATION_BLOCK: u64 = 14_732_730; +pub const MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK: u64 = 0; + pub const POLYGON_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; pub const ETH_GAS_PRICE_CEILING_WEI: u128 = 100_000_000_000; pub const BASE_GAS_PRICE_CEILING_WEI: u128 = 50_000_000_000; pub const DEV_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC: u64 = 600; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC: u64 = 120; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC: u64 = 180; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC: u64 = 120; + #[cfg(test)] mod tests { use super::*; @@ -204,6 +210,10 @@ mod tests { assert_eq!(ETH_GAS_PRICE_CEILING_WEI, 100_000_000_000); assert_eq!(BASE_GAS_PRICE_CEILING_WEI, 50_000_000_000); assert_eq!(DEV_GAS_PRICE_CEILING_WEI, 200_000_000_000); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, 600); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, 120); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, 180); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, 120); assert_eq!( CLIENT_REQUEST_PAYLOAD_CURRENT_VERSION, DataVersion { major: 0, minor: 1 } diff --git a/masq_lib/src/shared_schema.rs b/masq_lib/src/shared_schema.rs index 11dfb865f..3a8b194da 100644 --- a/masq_lib/src/shared_schema.rs +++ b/masq_lib/src/shared_schema.rs @@ -20,8 +20,8 @@ pub const CHAIN_HELP: &str = "The blockchain network MASQ Node will configure itself to use. You must ensure the \ Ethereum client specified by --blockchain-service-url communicates with the same blockchain network."; pub const CONFIG_FILE_HELP: &str = - "Optional TOML file containing configuration that doesn't often change. Should contain only \ - scalar items, string or numeric, whose names are exactly the same as the command-line parameters \ + "Optional TOML file containing configuration that seldom changes. Should contain only \ + scalar items, string, or numeric, whose names are exactly the same as the command-line parameters \ they replace (except no '--' prefix). If you specify a relative path, or no path, the Node will \ look for your config file starting in the --data-directory. If you specify an absolute path, \ --data-directory will be ignored when searching for the config file. A few parameters \ @@ -138,9 +138,9 @@ pub const REAL_USER_HELP: &str = like ::."; pub const SCANS_HELP: &str = "The Node, when running, performs various periodic scans, including scanning for payables that need to be paid, \ - for pending payables that have arrived (and are no longer pending), for incoming receivables that need to be \ - recorded, and for delinquent Nodes that need to be banned. If you don't specify this parameter, or if you give \ - it the value 'on', these scans will proceed normally. But if you give the value 'off', the scans won't be \ + for pending payables that have arrived or happened to fail (and are no longer pending), for incoming receivables \ + that need to be recorded, and for delinquent Nodes that need to be banned. If you don't specify this parameter, \ + or if you give it the value 'on', these scans will proceed normally. But if you give the value 'off', the scans won't be \ started when the Node starts, and will have to be triggered later manually and individually with the \ MASQNode-UIv2 'scan' command. (If you don't, you'll most likely be delinquency-banned by all your neighbors.) \ This parameter is most useful for testing."; @@ -183,19 +183,18 @@ pub const PAYMENT_THRESHOLDS_HELP: &str = "\ pub const SCAN_INTERVALS_HELP:&str = "\ These three intervals describe the length of three different scan cycles running automatically in the background \ since the Node has connected to a qualified neighborhood that consists of neighbors enabling a complete 3-hop \ - route. Each parameter can be set independently, but by default are all the same which currently is most desirable \ - for the consistency of service payments to and from your Node. Technically, there doesn't have to be any lower \ + route. Each parameter can be set independently. Technically, there doesn't have to be any lower \ limit for the minimum of time you can set; two scans of the same sort would never run at the same time but the \ next one is always scheduled not earlier than the end of the previous one. These are ever present values, no matter \ if the user's set any value, they have defaults. The parameters must be always supplied all together, delimited by vertical \ bars and in the right order.\n\n\ - 1. Pending Payable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments that are \ - marked as currently pending; the payments were sent to pay our debts, the payable. The purpose of this process is to \ - confirm the status of the pending payment; either the payment transaction was written on blockchain as successful or \ - failed.\n\n\ - 2. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ - of that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If \ - they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question.\n\n\ + 1. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ + that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. \ + If they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question.\n\n\ + 2. Pending Payable Scan Interval: The time elapsed since the last payable transaction was processed. This scan operates \ + on an irregular schedule and is triggered after new transactions are sent or when failed transactions need to be replaced. \ + The scanner monitors pending transactions and verifies their blockchain status, determining whether each payment was \ + successfully recorded or failed. Any failed transaction is automatically resubmitted as soon as the failure is detected.\n\n\ 3. Receivable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments on the \ blockchain that have been sent by our creditors to us, which are credited against receivables recorded for services \ provided."; @@ -744,8 +743,8 @@ mod tests { ); assert_eq!( CONFIG_FILE_HELP, - "Optional TOML file containing configuration that doesn't often change. Should contain only \ - scalar items, string or numeric, whose names are exactly the same as the command-line parameters \ + "Optional TOML file containing configuration that seldom changes. Should contain only \ + scalar items, string, or numeric, whose names are exactly the same as the command-line parameters \ they replace (except no '--' prefix). If you specify a relative path, or no path, the Node will \ look for your config file starting in the --data-directory. If you specify an absolute path, \ --data-directory will be ignored when searching for the config file. A few parameters \ @@ -883,6 +882,16 @@ mod tests { you start the Node using pkexec or some other method that doesn't populate the SUDO_xxx variables. Use a value \ like ::." ); + assert_eq!( + SCANS_HELP, + "The Node, when running, performs various periodic scans, including scanning for payables that need to be paid, \ + for pending payables that have arrived or happened to fail (and are no longer pending), for incoming receivables \ + that need to be recorded, and for delinquent Nodes that need to be banned. If you don't specify this parameter, \ + or if you give it the value 'on', these scans will proceed normally. But if you give the value 'off', the scans won't be \ + started when the Node starts, and will have to be triggered later manually and individually with the \ + MASQNode-UIv2 'scan' command. (If you don't, you'll most likely be delinquency-banned by all your neighbors.) \ + This parameter is most useful for testing." + ); assert_eq!( DEFAULT_UI_PORT_VALUE.to_string(), @@ -959,19 +968,19 @@ mod tests { SCAN_INTERVALS_HELP, "These three intervals describe the length of three different scan cycles running automatically in the background \ since the Node has connected to a qualified neighborhood that consists of neighbors enabling a complete 3-hop \ - route. Each parameter can be set independently, but by default are all the same which currently is most desirable \ - for the consistency of service payments to and from your Node. Technically, there doesn't have to be any lower \ + route. Each parameter can be set independently. Technically, there doesn't have to be any lower \ limit for the minimum of time you can set; two scans of the same sort would never run at the same time but the \ next one is always scheduled not earlier than the end of the previous one. These are ever present values, no matter \ if the user's set any value, they have defaults. The parameters must be always supplied all together, delimited by \ vertical bars and in the right order.\n\n\ - 1. Pending Payable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments that are \ - marked as currently pending; the payments were sent to pay our debts, the payable. The purpose of this process is to \ - confirm the status of the pending payment; either the payment transaction was written on blockchain as successful or \ - failed.\n\n\ - 2. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ - of that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If \ + 1. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ + that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If \ they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question.\n\n\ + 2. Pending Payable Scan Interval: The time elapsed since the last payable transaction was processed. This scan operates \ + on an irregular schedule and is triggered after new transactions are sent or when failed transactions need \ + to be replaced. The scanner monitors pending transactions and verifies their blockchain status, determining whether \ + each payment was successfully recorded or failed. Any failed transaction is automatically resubmitted as soon \ + as the failure is detected.\n\n\ 3. Receivable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments on the \ blockchain that have been sent by our creditors to us, which are credited against receivables recorded for services \ provided." diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 0226c0c68..cff264a58 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -24,7 +24,6 @@ use masq_lib::utils::ExpectValue; use rusqlite::OptionalExtension; use rusqlite::{Error, Row}; use std::fmt::Debug; -use std::str::FromStr; use std::time::SystemTime; use web3::types::H256; @@ -313,39 +312,22 @@ impl PayableDaoReal { let balance_high_bytes_result = row.get(1); let balance_low_bytes_result = row.get(2); let last_paid_timestamp_result = row.get(3); - let pending_payable_rowid_result: Result, Error> = row.get(4); - let pending_payable_hash_result: Result, Error> = row.get(5); match ( wallet_result, balance_high_bytes_result, balance_low_bytes_result, last_paid_timestamp_result, - pending_payable_rowid_result, - pending_payable_hash_result, ) { - ( - Ok(wallet), - Ok(high_bytes), - Ok(low_bytes), - Ok(last_paid_timestamp), - Ok(rowid_opt), - Ok(hash_opt), - ) => Ok(PayableAccount { - wallet, - balance_wei: checked_conversion::(BigIntDivider::reconstitute( - high_bytes, low_bytes, - )), - last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), - pending_payable_opt: rowid_opt.map(|rowid| { - let hash_str = - hash_opt.expect("database corrupt; missing hash but existing rowid"); - PendingPayableId::new( - u64::try_from(rowid).unwrap(), - H256::from_str(&hash_str[2..]) - .unwrap_or_else(|_| panic!("wrong form of tx hash {}", hash_str)), - ) - }), - }), + (Ok(wallet), Ok(high_bytes), Ok(low_bytes), Ok(last_paid_timestamp)) => { + Ok(PayableAccount { + wallet, + balance_wei: checked_conversion::(BigIntDivider::reconstitute( + high_bytes, low_bytes, + )), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), + pending_payable_opt: None, + }) + } e => panic!( "Database is corrupt: PAYABLE table columns and/or types: {:?}", e @@ -359,13 +341,9 @@ impl PayableDaoReal { wallet_address, balance_high_b, balance_low_b, - last_paid_timestamp, - pending_payable_rowid, - pending_payable.transaction_hash + last_paid_timestamp from payable - left join pending_payable on - pending_payable.rowid = payable.pending_payable_rowid {} {} order by {}, @@ -560,7 +538,6 @@ mod tests { use rusqlite::ToSql; use rusqlite::{Connection, OpenFlags}; use std::path::Path; - use std::str::FromStr; use std::time::Duration; #[test] @@ -1314,9 +1291,9 @@ mod tests { #[test] fn custom_query_in_top_records_mode_with_default_ordering() { - //Accounts of balances smaller than one gwei don't qualify. - //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, - //here by balance and then by age. + // Accounts of balances smaller than one gwei don't qualify. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( @@ -1344,13 +1321,7 @@ mod tests { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), @@ -1364,9 +1335,9 @@ mod tests { #[test] fn custom_query_in_top_records_mode_ordered_by_age() { - //Accounts of balances smaller than one gwei don't qualify. - //Two accounts differ only in balance but not in the debt's age which allows to check doubled ordering, - //here by age and then by balance. + // Accounts of balances smaller than one gwei don't qualify. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( @@ -1388,13 +1359,7 @@ mod tests { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), @@ -1433,8 +1398,8 @@ mod tests { #[test] fn custom_query_in_range_mode() { - //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, - //by balance and then by age. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { insert( @@ -1518,13 +1483,7 @@ mod tests { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: gwei_to_wei(1_800_456_000_u32), last_paid_timestamp: from_unix_timestamp(now - 55_120), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None } ] ); @@ -1690,19 +1649,6 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); main_setup_fn(conn.as_ref(), &insert_payable_record_fn); - - let pending_payable_account: &[&dyn ToSql] = &[ - &String::from("0xabc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223"), - &40, - &478945, - &177777777, - &1, - ]; - conn - .prepare("insert into pending_payable (transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt) values (?,?,?,?,?)") - .unwrap() - .execute(pending_payable_account) - .unwrap(); PayableDaoReal::new(conn) } } diff --git a/node/src/accountant/db_access_objects/pending_payable_dao.rs b/node/src/accountant/db_access_objects/pending_payable_dao.rs deleted file mode 100644 index 414c364d8..000000000 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ /dev/null @@ -1,933 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::db_access_objects::utils::{ - from_unix_timestamp, to_unix_timestamp, DaoFactoryReal, VigilantRusqliteFlatten, -}; -use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; -use crate::database::rusqlite_wrappers::ConnectionWrapper; -use crate::sub_lib::wallet::Wallet; -use masq_lib::utils::ExpectValue; -use rusqlite::Row; -use std::collections::HashSet; -use std::fmt::Debug; -use std::str::FromStr; -use std::time::SystemTime; -use web3::types::H256; - -#[derive(Debug, PartialEq, Eq)] -pub enum PendingPayableDaoError { - InsertionFailed(String), - UpdateFailed(String), - SignConversionError(u64), - RecordCannotBeRead, - RecordDeletion(String), - ErrorMarkFailed(String), -} - -#[derive(Debug)] -pub struct TransactionHashes { - pub rowid_results: Vec<(u64, H256)>, - pub no_rowid_results: Vec, -} - -pub trait PendingPayableDao { - // Note that the order of the returned results is not guaranteed - fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes; - // fn return_all_errorless_fingerprints(&self) -> Vec; - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError>; - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError>; - fn increment_scan_attempts(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError>; - fn mark_failures(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError>; -} - -impl PendingPayableDao for PendingPayableDaoReal<'_> { - fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes { - //Vec<(Option, H256)> { - fn hash_and_rowid_in_single_row(row: &Row) -> rusqlite::Result<(u64, H256)> { - let hash_str: String = row.get(0).expectv("hash"); - let hash = H256::from_str(&hash_str[2..]).expect("hash inserted right turned wrong"); - let sqlite_signed_rowid: i64 = row.get(1).expectv("rowid"); - let rowid = u64::try_from(sqlite_signed_rowid).expect("SQlite goes from 1 to i64:MAX"); - Ok((rowid, hash)) - } - - let sql = format!( - "select transaction_hash, rowid from pending_payable where transaction_hash in ({})", - comma_joined_stringifiable(hashes, |hash| format!("'{:?}'", hash)) - ); - - let all_found_records = self - .conn - .prepare(&sql) - .expect("Internal error") - .query_map([], hash_and_rowid_in_single_row) - .expect("map query failed") - .vigilant_flatten() - .collect::>(); - let hashes_of_found_records = all_found_records - .iter() - .map(|(_, hash)| *hash) - .collect::>(); - let hashes_of_missing_rowids = hashes - .iter() - .filter(|hash| !hashes_of_found_records.contains(hash)) - .cloned() - .collect(); - - TransactionHashes { - rowid_results: all_found_records, - no_rowid_results: hashes_of_missing_rowids, - } - } - - // fn return_all_errorless_fingerprints(&self) -> Vec { - // let mut stm = self - // .conn - // .prepare( - // "select rowid, transaction_hash, amount_high_b, amount_low_b, \ - // payable_timestamp, attempt from pending_payable where process_error is null", - // ) - // .expect("Internal error"); - // stm.query_map([], |row| { - // let rowid: u64 = Self::get_with_expect(row, 0); - // let transaction_hash: String = Self::get_with_expect(row, 1); - // let amount_high_bytes: i64 = Self::get_with_expect(row, 2); - // let amount_low_bytes: i64 = Self::get_with_expect(row, 3); - // let timestamp: i64 = Self::get_with_expect(row, 4); - // let attempt: u16 = Self::get_with_expect(row, 5); - // Ok(SentTx { - // rowid, - // timestamp: from_unix_timestamp(timestamp), - // hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { - // panic!( - // "Invalid hash format (\"{}\": {:?}) - database corrupt", - // transaction_hash, e - // ) - // }), - // attempt, - // amount_minor: checked_conversion::(BigIntDivider::reconstitute( - // amount_high_bytes, - // amount_low_bytes, - // )), - // process_error: None, - // }) - // }) - // .expect("rusqlite failure") - // .vigilant_flatten() - // .collect() - // } - - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError> { - fn values_clause_for_fingerprints_to_insert( - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> String { - let time_t = to_unix_timestamp(batch_wide_timestamp); - comma_joined_stringifiable(hashes_and_amounts, |hash_and_amount| { - let amount_checked = checked_conversion::(hash_and_amount.amount); - let (high_bytes, low_bytes) = BigIntDivider::deconstruct(amount_checked); - format!( - "('{:?}', {}, {}, {}, 1, null)", - hash_and_amount.hash, high_bytes, low_bytes, time_t - ) - }) - } - - let insert_sql = format!( - "insert into pending_payable (\ - transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error\ - ) values {}", - values_clause_for_fingerprints_to_insert(hashes_and_amounts, batch_wide_timestamp) - ); - match self - .conn - .prepare(&insert_sql) - .expect("Internal error") - .execute([]) - { - Ok(x) if x == hashes_and_amounts.len() => Ok(()), - Ok(x) => panic!( - "expected {} changed rows but got {}", - hashes_and_amounts.len(), - x - ), - Err(e) => Err(PendingPayableDaoError::InsertionFailed(e.to_string())), - } - } - - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - let sql = format!( - "delete from pending_payable where rowid in ({})", - Self::serialize_ids(ids) - ); - match self - .conn - .prepare(&sql) - .expect("delete command wrong") - .execute([]) - { - Ok(x) if x == ids.len() => Ok(()), - Ok(num) => panic!( - "deleting sent tx record, expected {} rows to be changed, but the actual number is {}", - ids.len(), - num - ), - Err(e) => Err(PendingPayableDaoError::RecordDeletion(e.to_string())), - } - } - - fn increment_scan_attempts(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - let sql = format!( - "update pending_payable set attempt = attempt + 1 where rowid in ({})", - Self::serialize_ids(ids) - ); - match self.conn.prepare(&sql).expect("Internal error").execute([]) { - Ok(num) if num == ids.len() => Ok(()), - Ok(num) => panic!( - "Database corrupt: updating fingerprints: expected to update {} rows but did {}", - ids.len(), - num - ), - Err(e) => Err(PendingPayableDaoError::UpdateFailed(e.to_string())), - } - } - - fn mark_failures(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - let sql = format!( - "update pending_payable set process_error = 'ERROR' where rowid in ({})", - Self::serialize_ids(ids) - ); - match self - .conn - .prepare(&sql) - .expect("Internal error") - .execute([]) { - Ok(num) if num == ids.len() => Ok(()), - Ok(num) => - panic!( - "Database corrupt: marking failure at fingerprints: expected to change {} rows but did {}", - ids.len(), num - ) - , - Err(e) => Err(PendingPayableDaoError::ErrorMarkFailed(e.to_string())), - } - } -} - -#[derive(Debug)] -pub struct PendingPayableDaoReal<'a> { - conn: Box, -} - -impl<'a> PendingPayableDaoReal<'a> { - pub fn new(conn: Box) -> Self { - Self { conn } - } - - fn get_with_expect(row: &Row, index: usize) -> T { - row.get(index).expect("database is corrupt") - } - - fn serialize_ids(ids: &[u64]) -> String { - comma_joined_stringifiable(ids, |id| id.to_string()) - } -} - -pub trait PendingPayableDaoFactory { - fn make(&self) -> Box; -} - -impl PendingPayableDaoFactory for DaoFactoryReal { - fn make(&self) -> Box { - Box::new(PendingPayableDaoReal::new(self.make_connection())) - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::checked_conversion; - use crate::accountant::db_access_objects::sent_payable_dao::{ - PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, - }; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; - use crate::blockchain::test_utils::make_tx_hash; - use crate::database::db_initializer::{ - DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, - }; - use crate::database::rusqlite_wrappers::ConnectionWrapperReal; - use crate::database::test_utils::ConnectionWrapperMock; - use masq_lib::test_utils::utils::ensure_node_home_directory_exists; - use rusqlite::{Connection, OpenFlags}; - use std::str::FromStr; - use std::time::SystemTime; - use web3::types::H256; - - // #[test] - // fn insert_new_fingerprints_happy_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "insert_new_fingerprints_happy_path", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let hash_1 = make_tx_hash(4546); - // let amount_1 = 55556; - // let hash_2 = make_tx_hash(6789); - // let amount_2 = 44445; - // let batch_wide_timestamp = from_unix_timestamp(200_000_000); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: amount_2, - // }; - // - // let _ = subject - // .insert_new_fingerprints( - // &[hash_and_amount_1, hash_and_amount_2], - // batch_wide_timestamp, - // ) - // .unwrap(); - // - // let records = subject.return_all_errorless_fingerprints(); - // assert_eq!( - // records, - // vec![ - // SentTx { - // rowid: 1, - // timestamp: batch_wide_timestamp, - // hash: hash_and_amount_1.hash, - // attempt: 1, - // amount_minor: hash_and_amount_1.amount, - // process_error: None - // }, - // SentTx { - // rowid: 2, - // timestamp: batch_wide_timestamp, - // hash: hash_and_amount_2.hash, - // attempt: 1, - // amount_minor: hash_and_amount_2.amount, - // process_error: None - // } - // ] - // ) - // } - // - // #[test] - // fn insert_new_fingerprints_sad_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "insert_new_fingerprints_sad_path", - // ); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let hash = make_tx_hash(45466); - // let amount = 55556; - // let timestamp = from_unix_timestamp(200_000_000); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // let hash_and_amount = HashAndAmount { hash, amount }; - // - // let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::InsertionFailed( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic(expected = "expected 1 changed rows but got 0")] - // fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { - // let setup_conn = Connection::open_in_memory().unwrap(); - // // injecting a by-plan failing statement into the mocked connection in order to provoke - // // a reaction that would've been untestable directly on the table the act is closely coupled with - // let statement = { - // setup_conn - // .execute("create table example (id integer)", []) - // .unwrap(); - // setup_conn.prepare("select id from example").unwrap() - // }; - // let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); - // let hash_1 = make_tx_hash(4546); - // let amount_1 = 55556; - // let batch_wide_timestamp = from_unix_timestamp(200_000_000); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // let hash_and_amount = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // - // let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); - // } - // - // #[test] - // fn fingerprints_rowids_when_records_reachable() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "fingerprints_rowids_when_records_reachable", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let timestamp = from_unix_timestamp(195_000_000); - // // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, - // // then complain about its excessive size if supplied in unquoted strings - // let hash_1 = - // H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") - // .unwrap(); - // let hash_2 = - // H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") - // .unwrap(); - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: 4567, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: 6789, - // }; - // let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; - // { - // subject - // .insert_new_fingerprints(&fingerprints_init_input, timestamp) - // .unwrap(); - // } - // - // let result = subject.fingerprints_rowids(&[hash_1, hash_2]); - // - // let first_expected_pair = &(1, hash_1); - // assert!( - // result.rowid_results.contains(first_expected_pair), - // "Returned rowid pairs should have contained {:?} but all it did is {:?}", - // first_expected_pair, - // result.rowid_results - // ); - // let second_expected_pair = &(2, hash_2); - // assert!( - // result.rowid_results.contains(second_expected_pair), - // "Returned rowid pairs should have contained {:?} but all it did is {:?}", - // second_expected_pair, - // result.rowid_results - // ); - // assert_eq!(result.rowid_results.len(), 2); - // } - // - // #[test] - // fn fingerprints_rowids_when_nonexistent_records() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "fingerprints_rowids_when_nonexistent_records", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let hash_1 = make_tx_hash(11119); - // let hash_2 = make_tx_hash(22229); - // let hash_3 = make_tx_hash(33339); - // let hash_4 = make_tx_hash(44449); - // // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for - // // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of - // // just an ambiguous 1 - // subject - // .insert_new_fingerprints( - // &[HashAndAmount { - // hash: hash_2, - // amount_minor: 8901234, - // }], - // SystemTime::now(), - // ) - // .unwrap(); - // subject - // .insert_new_fingerprints( - // &[HashAndAmount { - // hash: hash_3, - // amount_minor: 1234567, - // }], - // SystemTime::now(), - // ) - // .unwrap(); - // subject.delete_fingerprints(&[1]).unwrap(); - // - // let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); - // - // assert_eq!(result.rowid_results, vec![(2, hash_3),]); - // assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); - // } - // - // #[test] - // fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let batch_wide_timestamp = from_unix_timestamp(195_000_000); - // let hash_1 = make_tx_hash(11119); - // let amount_1 = 787; - // let hash_2 = make_tx_hash(10000); - // let amount_2 = 333; - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: amount_2, - // }; - // - // { - // subject - // .insert_new_fingerprints( - // &[hash_and_amount_1, hash_and_amount_2], - // batch_wide_timestamp, - // ) - // .unwrap(); - // } - // - // let result = subject.return_all_errorless_fingerprints(); - // - // assert_eq!( - // result, - // vec![ - // SentTx { - // rowid: 1, - // timestamp: batch_wide_timestamp, - // hash: hash_1, - // attempt: 1, - // amount_minor: amount_1, - // process_error: None - // }, - // SentTx { - // rowid: 2, - // timestamp: batch_wide_timestamp, - // hash: hash_2, - // attempt: 1, - // amount_minor: amount_2, - // process_error: None - // } - // ] - // ) - // } - // - // #[test] - // fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let timestamp = from_unix_timestamp(198_000_000); - // let hash = make_tx_hash(10000); - // let amount = 333; - // let hash_and_amount_1 = HashAndAmount { - // hash: make_tx_hash(11119), - // amount_minor: 2000, - // }; - // let hash_and_amount_2 = HashAndAmount { hash, amount }; - // { - // subject - // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - // .unwrap(); - // subject.mark_failures(&[1]).unwrap(); - // } - // - // let result = subject.return_all_errorless_fingerprints(); - // - // assert_eq!( - // result, - // vec![SentTx { - // rowid: 2, - // timestamp, - // hash, - // attempt: 1, - // amount, - // process_error: None - // }] - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" - // )] - // fn return_all_errorless_fingerprints_panics_on_malformed_hash() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "return_all_errorless_fingerprints_panics_on_malformed_hash", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // { - // wrapped_conn - // .prepare("insert into pending_payable \ - // (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ - // values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") - // .unwrap() - // .execute([]) - // .unwrap(); - // } - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // - // let _ = subject.return_all_errorless_fingerprints(); - // } - // - // #[test] - // fn delete_fingerprints_happy_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "delete_fingerprints_happy_path", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints( - // &[ - // HashAndAmount { - // hash: make_tx_hash(1234), - // amount_minor: 1111, - // }, - // HashAndAmount { - // hash: make_tx_hash(2345), - // amount_minor: 5555, - // }, - // HashAndAmount { - // hash: make_tx_hash(3456), - // amount_minor: 2222, - // }, - // ], - // SystemTime::now(), - // ) - // .unwrap(); - // } - // - // let result = subject.delete_fingerprints(&[2, 3]); - // - // assert_eq!(result, Ok(())); - // let records_in_the_db = subject.return_all_errorless_fingerprints(); - // let record_left_in = &records_in_the_db[0]; - // assert_eq!(record_left_in.hash, make_tx_hash(1234)); - // assert_eq!(record_left_in.rowid, 1); - // assert_eq!(records_in_the_db.len(), 1); - // } - // - // #[test] - // fn delete_fingerprints_sad_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "delete_fingerprints_sad_path", - // ); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let rowid = 45; - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // - // let result = subject.delete_fingerprints(&[rowid]); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::RecordDeletion( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "deleting sent tx record, expected 2 rows to be changed, but the actual number is 1" - // )] - // fn delete_fingerprints_changed_different_number_of_rows_than_expected() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "delete_fingerprints_changed_different_number_of_rows_than_expected", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let rowid_1 = 1; - // let rowid_2 = 2; - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints( - // &[HashAndAmount { - // hash: make_tx_hash(666666), - // amount_minor: 5555, - // }], - // SystemTime::now(), - // ) - // .unwrap(); - // } - // - // let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); - // } - // - // #[test] - // fn increment_scan_attempts_works() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "increment_scan_attempts_works", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let hash_1 = make_tx_hash(345); - // let hash_2 = make_tx_hash(456); - // let hash_3 = make_tx_hash(567); - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: 1122, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: 2233, - // }; - // let hash_and_amount_3 = HashAndAmount { - // hash: hash_3, - // amount_minor: 3344, - // }; - // let timestamp = from_unix_timestamp(190_000_000); - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints( - // &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], - // timestamp, - // ) - // .unwrap(); - // } - // - // let result = subject.increment_scan_attempts(&[2, 3]); - // - // assert_eq!(result, Ok(())); - // let mut all_records = subject.return_all_errorless_fingerprints(); - // assert_eq!(all_records.len(), 3); - // let record_1 = all_records.remove(0); - // assert_eq!(record_1.hash, hash_1); - // assert_eq!(record_1.attempt, 1); - // let record_2 = all_records.remove(0); - // assert_eq!(record_2.hash, hash_2); - // assert_eq!(record_2.attempt, 2); - // let record_3 = all_records.remove(0); - // assert_eq!(record_3.hash, hash_3); - // assert_eq!(record_3.attempt, 2); - // } - // - // #[test] - // fn increment_scan_attempts_works_sad_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "increment_scan_attempts_works_sad_path", - // ); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // - // let result = subject.increment_scan_attempts(&[1]); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::UpdateFailed( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" - // )] - // fn increment_scan_attempts_panics_on_unexpected_row_change_count() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "increment_scan_attempts_panics_on_unexpected_row_change_count", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(conn); - // - // let _ = subject.increment_scan_attempts(&[1, 2]); - // } - // - // #[test] - // fn mark_failures_works() { - // let home_dir = - // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_works"); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let hash_1 = make_tx_hash(555); - // let amount_1 = 1234; - // let hash_2 = make_tx_hash(666); - // let amount_2 = 2345; - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: amount_2, - // }; - // let timestamp = from_unix_timestamp(190_000_000); - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - // .unwrap(); - // } - // - // let result = subject.mark_failures(&[2]); - // - // assert_eq!(result, Ok(())); - // let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); - // let mut assert_stm = assert_conn - // .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") - // .unwrap(); - // let found_fingerprints = assert_stm - // .query_map([], |row| { - // let rowid: u64 = row.get(0).unwrap(); - // let transaction_hash: String = row.get(1).unwrap(); - // let amount_high_b: i64 = row.get(2).unwrap(); - // let amount_low_b: i64 = row.get(3).unwrap(); - // let timestamp: i64 = row.get(4).unwrap(); - // let attempt: u16 = row.get(5).unwrap(); - // let process_error: Option = row.get(6).unwrap(); - // Ok(SentTx { - // rowid, - // timestamp: from_unix_timestamp(timestamp), - // hash: H256::from_str(&transaction_hash[2..]).unwrap(), - // attempt, - // amount_minor: checked_conversion::(BigIntDivider::reconstitute( - // amount_high_b, - // amount_low_b, - // )), - // process_error, - // }) - // }) - // .unwrap() - // .flatten() - // .collect::>(); - // assert_eq!( - // *found_fingerprints, - // vec![ - // SentTx { - // rowid: 1, - // timestamp, - // hash: hash_1, - // attempt: 1, - // amount_minor: amount_1, - // process_error: None - // }, - // SentTx { - // rowid: 2, - // timestamp, - // hash: hash_2, - // attempt: 1, - // amount_minor: amount_2, - // process_error: Some("ERROR".to_string()) - // } - // ] - // ) - // } - // - // #[test] - // fn mark_failures_sad_path() { - // let home_dir = - // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_sad_path"); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // - // let result = subject.mark_failures(&[1]); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::ErrorMarkFailed( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" - // )] - // fn mark_failures_panics_on_wrong_row_change_count() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "mark_failures_panics_on_wrong_row_change_count", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(conn); - // - // let _ = subject.mark_failures(&[10, 20]); - // } -} diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index b8651d990..b9a3d093b 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1330,7 +1330,7 @@ mod tests { use ethsign_crypto::Keccak256; use log::Level; use masq_lib::constants::{ - REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, + DEFAULT_CHAIN, REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, VALUE_EXCEEDS_ALLOWED_LIMIT, }; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; @@ -1340,7 +1340,7 @@ mod tests { }; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; - use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use masq_lib::test_utils::utils::{ensure_node_home_directory_exists, TEST_DEFAULT_CHAIN}; use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; use std::any::TypeId; @@ -1370,7 +1370,7 @@ mod tests { #[test] fn new_calls_factories_properly() { - let config = make_bc_with_defaults(); + 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![])); @@ -1435,7 +1435,8 @@ mod tests { #[test] fn accountant_have_proper_defaulted_values() { - let bootstrapper_config = make_bc_with_defaults(); + let chain = TEST_DEFAULT_CHAIN; + let bootstrapper_config = make_bc_with_defaults(chain); let payable_dao_factory = Box::new( PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) // For Accountant @@ -1475,7 +1476,7 @@ mod tests { ); let financial_statistics = result.financial_statistics().clone(); - let default_scan_intervals = ScanIntervals::default(); + let default_scan_intervals = ScanIntervals::compute_default(chain); assert_eq!( result.scan_schedulers.payable.new_payable_interval, default_scan_intervals.payable_scan_interval @@ -1558,7 +1559,7 @@ mod tests { { init_test_logging(); let mut subject = AccountantBuilder::default() - .bootstrapper_config(make_bc_with_defaults()) + .bootstrapper_config(make_bc_with_defaults(TEST_DEFAULT_CHAIN)) .build(); subject.logger = Logger::new("ConfigChange"); @@ -4537,7 +4538,7 @@ mod tests { #[test] fn report_services_consumed_message_is_received() { init_test_logging(); - let config = make_bc_with_defaults(); + let config = make_bc_with_defaults(TEST_DEFAULT_CHAIN); let more_money_payable_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new() .more_money_payable_params(more_money_payable_params_arc.clone()) @@ -4879,7 +4880,7 @@ mod tests { expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" )] fn accountant_can_be_crashed_properly_but_not_improperly() { - let mut config = make_bc_with_defaults(); + let mut config = make_bc_with_defaults(TEST_DEFAULT_CHAIN); config.crash_point = CrashPoint::Message; let accountant = AccountantBuilder::default() .bootstrapper_config(config) @@ -5936,7 +5937,7 @@ mod tests { let receivable_dao = ReceivableDaoMock::new().total_result(987_654_328_996); let system = System::new("test"); let subject = AccountantBuilder::default() - .bootstrapper_config(make_bc_with_defaults()) + .bootstrapper_config(make_bc_with_defaults(TEST_DEFAULT_CHAIN)) .payable_daos(vec![ForAccountantBody(payable_dao)]) .receivable_daos(vec![ForAccountantBody(receivable_dao)]) .build(); diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 03dad9942..dd23e05bd 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -385,6 +385,7 @@ mod tests { }; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; + use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; use itertools::Itertools; use lazy_static::lazy_static; use masq_lib::logger::Logger; @@ -596,7 +597,7 @@ mod tests { #[test] fn resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let test_name = "resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered"; @@ -652,7 +653,7 @@ mod tests { fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( ) { init_test_logging(); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let test_name = "resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true"; let logger = Logger::new(test_name); @@ -687,7 +688,7 @@ mod tests { )] fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( ) { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let _ = subject .reschedule_on_error_resolver @@ -706,7 +707,7 @@ mod tests { init_test_logging(); let test_name = "resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan"; let logger = Logger::new(test_name); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let scanner = PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, }; @@ -740,7 +741,7 @@ mod tests { possible" )] fn pending_p_scan_attempt_if_no_consuming_wallet_found_mustnt_happen_if_not_initial_scan() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let scanner = PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }; @@ -795,7 +796,7 @@ mod tests { StartScanError::NothingToProcess, StartScanError::NoConsumingWalletFound, ]); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); test_forbidden_states(&subject, &inputs, false); test_forbidden_states(&subject, &inputs, true); @@ -805,7 +806,7 @@ mod tests { fn resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered() { let test_name = "resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered"; - let subject = ScanSchedulers::new(ScanIntervals::default(), false); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, false); test_what_if_externally_triggered( test_name, @@ -816,7 +817,7 @@ mod tests { #[test] fn any_automatic_scan_with_start_scan_error_is_fatal_for_retry_payables() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); ALL_START_SCAN_ERRORS.iter().for_each(|error| { let panic = catch_unwind(AssertUnwindSafe(|| { @@ -849,7 +850,7 @@ mod tests { fn resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered() { let test_name = "resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered"; - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); test_what_if_externally_triggered( test_name, @@ -864,7 +865,7 @@ mod tests { should never interfere with itself ScanAlreadyRunning { cross_scan_cause_opt: None, started_at:" )] fn resolve_hint_for_new_payables_if_scan_is_already_running_error_and_is_automatic_scan() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let _ = subject .reschedule_on_error_resolver @@ -890,7 +891,7 @@ mod tests { ]); let logger = Logger::new(test_name); let test_log_handler = TestLogHandler::new(); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); inputs.errors.iter().for_each(|error| { let result = subject diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 81b612e47..2f777e57b 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -52,6 +52,7 @@ use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMoc use crate::test_utils::unshared_test_utils::make_bc_with_defaults; use ethereum_types::U64; use masq_lib::logger::Logger; +use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; @@ -431,7 +432,9 @@ impl AccountantBuilder { } pub fn build(self) -> Accountant { - let config = self.config_opt.unwrap_or(make_bc_with_defaults()); + let config = self + .config_opt + .unwrap_or(make_bc_with_defaults(TEST_DEFAULT_CHAIN)); let payable_dao_factory = self.payable_dao_factory_opt.unwrap_or( PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) @@ -967,13 +970,13 @@ impl BannedDaoMock { } pub fn bc_from_earning_wallet(earning_wallet: Wallet) -> BootstrapperConfig { - let mut bc = make_bc_with_defaults(); + let mut bc = make_bc_with_defaults(TEST_DEFAULT_CHAIN); bc.earning_wallet = earning_wallet; bc } pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> BootstrapperConfig { - let mut bc = make_bc_with_defaults(); + let mut bc = make_bc_with_defaults(TEST_DEFAULT_CHAIN); bc.consuming_wallet_opt = Some(consuming_wallet); bc.earning_wallet = earning_wallet; bc diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 8b24da722..61c5ff9c0 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -1167,7 +1167,7 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - scan_intervals_opt: Some(ScanIntervals::default()), + scan_intervals_opt: Some(ScanIntervals::compute_default(TEST_DEFAULT_CHAIN)), automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 1a5cad399..3458a4140 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -38,7 +38,6 @@ use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; @@ -571,10 +570,17 @@ mod tests { use crate::node_test_utils::check_timestamp; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; - use crate::test_utils::recorder::{make_accountant_subs_from_recorder, make_blockchain_bridge_subs_from_recorder, make_recorder, peer_actors_builder}; + use crate::test_utils::recorder::{ + make_accountant_subs_from_recorder, make_blockchain_bridge_subs_from_recorder, + make_recorder, peer_actors_builder, + }; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use crate::test_utils::unshared_test_utils::{assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, SubsFactoryTestAddrLeaker, ZERO}; + use crate::test_utils::unshared_test_utils::{ + assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, + prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, + SubsFactoryTestAddrLeaker, ZERO, + }; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; @@ -606,7 +612,7 @@ mod tests { } impl SubsFactory - for SubsFactoryTestAddrLeaker + for SubsFactoryTestAddrLeaker { fn make(&self, addr: &Addr) -> BlockchainBridgeSubs { self.send_leaker_msg_and_return_meaningless_subs( @@ -2205,4 +2211,4 @@ mod tests { assert_eq!(increase_gas_price_by_margin(1_000_000_000), 1_300_000_000); assert_eq!(increase_gas_price_by_margin(9_000_000_000), 11_700_000_000); } -} \ No newline at end of file +} diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index 71a0751b0..aa3943183 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -1233,6 +1233,7 @@ mod tests { vec![SocketAddr::new(IpAddr::from_str("1.2.3.4").unwrap(), 1111)]; let mut unprivileged_config = BootstrapperConfig::new(); //values from unprivileged config + let chain = unprivileged_config.blockchain_bridge_config.chain; let gas_price = 123; let blockchain_url_opt = Some("some.service@earth.abc".to_string()); let clandestine_port_opt = Some(44444); @@ -1252,7 +1253,7 @@ mod tests { unprivileged_config.earning_wallet = earning_wallet.clone(); unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); - unprivileged_config.scan_intervals_opt = Some(ScanIntervals::default()); + unprivileged_config.scan_intervals_opt = Some(ScanIntervals::compute_default(chain)); unprivileged_config.automatic_scans_enabled = true; unprivileged_config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; @@ -1276,7 +1277,7 @@ mod tests { assert_eq!(privileged_config.db_password_opt, db_password_opt); assert_eq!( privileged_config.scan_intervals_opt, - Some(ScanIntervals::default()) + Some(ScanIntervals::compute_default(chain)) ); assert_eq!(privileged_config.automatic_scans_enabled, true); assert_eq!( diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 3d0a79b6b..b03251842 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -21,7 +21,6 @@ use crate::node_configurator::{ data_directory_from_context, determine_user_specific_data, DirsWrapper, DirsWrapperReal, }; use crate::sub_lib::accountant::PaymentThresholds as PaymentThresholdsFromAccountant; -use crate::sub_lib::accountant::DEFAULT_SCAN_INTERVALS; use crate::sub_lib::neighborhood::NodeDescriptor; use crate::sub_lib::neighborhood::{NeighborhoodMode as NeighborhoodModeEnum, DEFAULT_RATE_PACK}; use crate::sub_lib::utils::make_new_multi_config; @@ -1083,12 +1082,16 @@ impl ValueRetriever for ScanIntervals { fn computed_default( &self, - _bootstrapper_config: &BootstrapperConfig, + bootstrapper_config: &BootstrapperConfig, pc: &dyn PersistentConfiguration, _db_password_opt: &Option, ) -> Option<(String, UiSetupResponseValueStatus)> { let pc_value = pc.scan_intervals().expectv("scan-intervals"); - payment_thresholds_rate_pack_and_scan_intervals(pc_value, *DEFAULT_SCAN_INTERVALS) + let chain = bootstrapper_config.blockchain_bridge_config.chain; + payment_thresholds_rate_pack_and_scan_intervals( + pc_value, + crate::sub_lib::accountant::ScanIntervals::compute_default(chain), + ) } fn is_required(&self, _params: &SetupCluster) -> bool { @@ -1208,7 +1211,9 @@ mod tests { use crate::daemon::dns_inspector::dns_inspector::DnsInspector; use crate::daemon::dns_inspector::DnsInspectionError; use crate::daemon::setup_reporter; - use crate::database::db_initializer::{DbInitializer, DbInitializerReal, DATABASE_FILE}; + use crate::database::db_initializer::{ + DbInitializer, DbInitializerReal, InitializationMode, DATABASE_FILE, + }; use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::db_config::config_dao::{ConfigDao, ConfigDaoReal}; use crate::db_config::persistent_configuration::{ @@ -1229,6 +1234,7 @@ mod tests { use crate::test_utils::unshared_test_utils::{ make_persistent_config_real_with_config_dao_null, make_pre_populated_mocked_directory_wrapper, make_simplified_multi_config, + TEST_SCAN_INTERVALS, }; use crate::test_utils::{assert_string_contains, rate_pack}; use core::option::Option; @@ -1335,15 +1341,19 @@ mod tests { "setup_reporter", "get_modified_setup_database_populated_only_requireds_set", ); + let chain = DEFAULT_CHAIN; + let mut init_config = DbInitializationConfig::test_default(); + if let InitializationMode::CreationAndMigration { external_data } = &mut init_config.mode { + external_data.chain = chain + } else { + panic!("unexpected initialization mode"); + } let data_dir = home_dir.join("data_dir"); - let chain_specific_data_dir = data_dir.join(DEFAULT_CHAIN.rec().literal_identifier); + let chain_specific_data_dir = data_dir.join(chain.rec().literal_identifier); std::fs::create_dir_all(&chain_specific_data_dir).unwrap(); let db_initializer = DbInitializerReal::default(); let conn = db_initializer - .initialize( - &chain_specific_data_dir, - DbInitializationConfig::test_default(), - ) + .initialize(&chain_specific_data_dir, init_config) .unwrap(); let mut config = PersistentConfigurationReal::from(conn); config.change_password(None, "password").unwrap(); @@ -1448,7 +1458,7 @@ mod tests { ), ( "scan-intervals", - &DEFAULT_SCAN_INTERVALS.to_string(), + &accountant::ScanIntervals::compute_default(chain).to_string(), Default, ), ("scans", "on", Default), @@ -3358,6 +3368,7 @@ mod tests { fn rate_pack_computed_default_when_persistent_config_like_default() { assert_computed_default_when_persistent_config_like_default( &RatePack {}, + None, DEFAULT_RATE_PACK.to_string(), ) } @@ -3437,15 +3448,19 @@ mod tests { #[test] fn scan_intervals_computed_default_when_persistent_config_like_default() { + let chain = DEFAULT_CHAIN; + let mut bootstrapper_config = BootstrapperConfig::new(); + bootstrapper_config.blockchain_bridge_config.chain = chain; assert_computed_default_when_persistent_config_like_default( &ScanIntervals {}, - *DEFAULT_SCAN_INTERVALS, + Some(bootstrapper_config), + accountant::ScanIntervals::compute_default(chain), ) } #[test] fn scan_intervals_computed_default_persistent_config_unequal_to_default() { - let mut scan_intervals = *DEFAULT_SCAN_INTERVALS; + let mut scan_intervals = *TEST_SCAN_INTERVALS; scan_intervals.payable_scan_interval = scan_intervals .payable_scan_interval .add(Duration::from_secs(15)); @@ -3469,6 +3484,7 @@ mod tests { fn payment_thresholds_computed_default_when_persistent_config_like_default() { assert_computed_default_when_persistent_config_like_default( &PaymentThresholds {}, + None, DEFAULT_PAYMENT_THRESHOLDS.to_string(), ) } @@ -3491,12 +3507,13 @@ mod tests { fn assert_computed_default_when_persistent_config_like_default( subject: &dyn ValueRetriever, + bootstrapper_config_opt: Option, default: T, ) where T: Display + PartialEq, { - let mut bootstrapper_config = BootstrapperConfig::new(); - //the rate_pack within the mode setting does not determine the result, so I just set a nonsense + let mut bootstrapper_config = bootstrapper_config_opt.unwrap_or(BootstrapperConfig::new()); + //the rate_pack within the mode setting does not affect the result, so I set nonsense bootstrapper_config.neighborhood_config.mode = NeighborhoodModeEnum::OriginateOnly(vec![], rate_pack(0)); let persistent_config = diff --git a/node/src/database/config_dumper.rs b/node/src/database/config_dumper.rs index 17e24899e..a1a435818 100644 --- a/node/src/database/config_dumper.rs +++ b/node/src/database/config_dumper.rs @@ -168,7 +168,8 @@ mod tests { use crate::db_config::typed_config_layer::encode_bytes; use crate::node_configurator::DirsWrapperReal; use crate::node_test_utils::DirsWrapperMock; - use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; + use crate::sub_lib::accountant; + use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::cryptde::PlainData; use crate::sub_lib::neighborhood::{NodeDescriptor, DEFAULT_RATE_PACK}; use crate::test_utils::database_utils::bring_db_0_back_to_life_and_return_connection; @@ -327,6 +328,7 @@ mod tests { .initialize(&database_path, DbInitializationConfig::panic_on_migration()) .unwrap(); let dao = ConfigDaoReal::new(conn); + let chain = Chain::PolyMainnet; assert_value("blockchainServiceUrl", "https://infura.io/ID", &map); assert_value("clandestinePort", "3456", &map); assert_encrypted_value( @@ -340,11 +342,7 @@ mod tests { "0x0123456789012345678901234567890123456789", &map, ); - assert_value( - "chainName", - Chain::PolyMainnet.rec().literal_identifier, - &map, - ); + assert_value("chainName", chain.rec().literal_identifier, &map); assert_value("gasPrice", "1", &map); assert_value( "pastNeighbors", @@ -365,8 +363,12 @@ mod tests { &map, ); assert_value("ratePack", &DEFAULT_RATE_PACK.to_string(), &map); - assert_value("scanIntervals", &DEFAULT_SCAN_INTERVALS.to_string(), &map); - assert!(output.ends_with("\n}\n")) //asserting that there is a blank line at the end + assert_value( + "scanIntervals", + &accountant::ScanIntervals::compute_default(chain).to_string(), + &map, + ); + assert!(output.ends_with("\n}\n")) // To assert a blank line at the end } #[test] @@ -510,7 +512,11 @@ mod tests { &map, ); assert_value("ratePack", &DEFAULT_RATE_PACK.to_string(), &map); - assert_value("scanIntervals", &DEFAULT_SCAN_INTERVALS.to_string(), &map); + assert_value( + "scanIntervals", + &accountant::ScanIntervals::compute_default(Chain::PolyMainnet).to_string(), + &map, + ); } #[test] @@ -586,6 +592,7 @@ mod tests { .initialize(&data_dir, DbInitializationConfig::panic_on_migration()) .unwrap(); let dao = Box::new(ConfigDaoReal::new(conn)); + let chain = Chain::PolyMainnet; assert_value("blockchainServiceUrl", "https://infura.io/ID", &map); assert_value("clandestinePort", "3456", &map); assert_encrypted_value( @@ -599,11 +606,7 @@ mod tests { "0x0123456789012345678901234567890123456789", &map, ); - assert_value( - "chainName", - Chain::PolyMainnet.rec().literal_identifier, - &map, - ); + assert_value("chainName", chain.rec().literal_identifier, &map); assert_value("gasPrice", "1", &map); assert_value( "pastNeighbors", @@ -624,7 +627,11 @@ mod tests { &map, ); assert_value("ratePack", &DEFAULT_RATE_PACK.to_string(), &map); - assert_value("scanIntervals", &DEFAULT_SCAN_INTERVALS.to_string(), &map); + assert_value( + "scanIntervals", + &accountant::ScanIntervals::compute_default(chain).to_string(), + &map, + ); } #[test] diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index 674786766..6eb69b4a6 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -4,7 +4,8 @@ use crate::database::rusqlite_wrappers::{ConnectionWrapper, ConnectionWrapperRea use crate::database::db_migrations::db_migrator::{DbMigrator, DbMigratorReal}; use crate::db_config::secure_config_layer::EXAMPLE_ENCRYPTED; use crate::neighborhood::DEFAULT_MIN_HOPS; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use crate::sub_lib::utils::db_connection_launch_panic; use masq_lib::blockchains::chains::Chain; @@ -137,7 +138,6 @@ impl DbInitializerReal { Self::create_payable_table(conn); Self::create_sent_payable_table(conn); Self::create_failed_payable_table(conn); - Self::create_pending_payable_table(conn); Self::create_receivable_table(conn); Self::create_banned_table(conn); } @@ -253,7 +253,7 @@ impl DbInitializerReal { Self::set_config_value( conn, "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(external_params.chain).to_string()), false, "scan intervals", ); @@ -311,27 +311,6 @@ impl DbInitializerReal { .expect("Can't create transaction hash index in failed payments"); } - pub fn create_pending_payable_table(conn: &Connection) { - conn.execute( - "create table if not exists pending_payable ( - rowid integer primary key, - transaction_hash text not null, - amount_high_b integer not null, - amount_low_b integer not null, - payable_timestamp integer not null, - attempt integer not null, - process_error text null - )", - [], - ) - .expect("Can't create pending_payable table"); - conn.execute( - "CREATE UNIQUE INDEX pending_payable_hash_idx ON pending_payable (transaction_hash)", - [], - ) - .expect("Can't create transaction hash index in pending payments"); - } - pub fn create_payable_table(conn: &Connection) { conn.execute( "create table if not exists payable ( @@ -736,50 +715,6 @@ mod tests { assert_no_index_exists_for_table(conn.as_ref(), "config") } - #[test] - fn db_initialize_creates_pending_payable_table() { - let home_dir = ensure_node_home_directory_does_not_exist( - "db_initializer", - "db_initialize_creates_pending_payable_table", - ); - let subject = DbInitializerReal::default(); - - let conn = subject - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - - let mut stmt = conn - .prepare( - "SELECT rowid, - transaction_hash, - amount_high_b, - amount_low_b, - payable_timestamp, - attempt, - process_error - FROM pending_payable", - ) - .unwrap(); - let result = stmt.execute([]).unwrap(); - assert_eq!(result, 1); - let expected_key_words: &[&[&str]] = &[ - &["rowid", "integer", "primary", "key"], - &["transaction_hash", "text", "not", "null"], - &["amount_high_b", "integer", "not", "null"], - &["amount_low_b", "integer", "not", "null"], - &["payable_timestamp", "integer", "not", "null"], - &["attempt", "integer", "not", "null"], - &["process_error", "text", "null"], - ]; - assert_create_table_stm_contains_all_parts(&*conn, "pending_payable", expected_key_words); - let expected_key_words: &[&[&str]] = &[&["transaction_hash"]]; - assert_index_stm_is_coupled_with_right_parameter( - conn.as_ref(), - "pending_payable_hash_idx", - expected_key_words, - ) - } - #[test] fn db_initialize_creates_sent_payable_table() { let home_dir = ensure_node_home_directory_does_not_exist( @@ -1117,7 +1052,7 @@ mod tests { verify( &mut config_vec, "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(TEST_DEFAULT_CHAIN).to_string()), false, ); verify( diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs index 5e4e18368..b3f2a157a 100644 --- a/node/src/database/db_migrations/migrations/migration_10_to_11.rs +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -36,9 +36,12 @@ impl DatabaseMigration for Migrate_10_to_11 { status text not null )"; + let sql_statement_for_pending_payable = "drop table pending_payable"; + declaration_utils.execute_upon_transaction(&[ &sql_statement_for_sent_payable, &sql_statement_for_failed_payable, + &sql_statement_for_pending_payable, ]) } @@ -55,10 +58,7 @@ mod tests { use crate::database::test_utils::{ SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, }; - use crate::test_utils::database_utils::{ - assert_create_table_stm_contains_all_parts, assert_table_exists, - bring_db_0_back_to_life_and_return_connection, make_external_data, - }; + use crate::test_utils::database_utils::{assert_create_table_stm_contains_all_parts, assert_table_does_not_exist, assert_table_exists, bring_db_0_back_to_life_and_return_connection, make_external_data}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use std::fs::create_dir_all; @@ -103,6 +103,7 @@ mod tests { "failed_payable", SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, ); + assert_table_does_not_exist(connection.as_ref(), "pending_payable"); TestLogHandler::new().assert_logs_contain_in_order(vec![ "DbMigrator: Database successfully migrated from version 10 to 11", ]); diff --git a/node/src/database/db_migrations/migrations/migration_5_to_6.rs b/node/src/database/db_migrations/migrations/migration_5_to_6.rs index a5f902cb9..b32e3b2d0 100644 --- a/node/src/database/db_migrations/migrations/migration_5_to_6.rs +++ b/node/src/database/db_migrations/migrations/migration_5_to_6.rs @@ -2,8 +2,10 @@ use crate::database::db_migrations::db_migrator::DatabaseMigration; use crate::database::db_migrations::migrator_utils::DBMigDeclarator; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; +use masq_lib::blockchains::chains::Chain; #[allow(non_camel_case_types)] pub struct Migrate_5_to_6; @@ -19,9 +21,18 @@ impl DatabaseMigration for Migrate_5_to_6 { ); let statement_2 = Self::make_initialization_statement("rate_pack", &DEFAULT_RATE_PACK.to_string()); + let tx = declaration_utils.transaction(); + let chain = tx + .prepare("SELECT value FROM config WHERE name = 'chain_name'") + .expect("internal error") + .query_row([], |row| { + let res_str = row.get::<_, String>(0); + res_str.map(|str| Chain::from(str.as_str())) + }) + .expect("failed to read the chain from db"); let statement_3 = Self::make_initialization_statement( "scan_intervals", - &DEFAULT_SCAN_INTERVALS.to_string(), + &accountant::ScanIntervals::compute_default(chain).to_string(), ); declaration_utils.execute_upon_transaction(&[&statement_1, &statement_2, &statement_3]) } @@ -45,11 +56,13 @@ mod tests { use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; - use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; + use crate::sub_lib::accountant; + use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use crate::test_utils::database_utils::{ bring_db_0_back_to_life_and_return_connection, make_external_data, retrieve_config_row, }; + use masq_lib::blockchains::chains::Chain; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; #[test] @@ -59,15 +72,21 @@ mod tests { let db_path = dir_path.join(DATABASE_FILE); let _ = bring_db_0_back_to_life_and_return_connection(&db_path); let subject = DbInitializerReal::default(); - { - subject + let chain = { + let conn = subject .initialize_to_version( &dir_path, 5, DbInitializationConfig::create_or_migrate(make_external_data()), ) .unwrap(); - } + let chain = conn + .prepare("SELECT value FROM config WHERE name = 'chain_name'") + .unwrap() + .query_row([], |row| row.get::<_, String>(0)) + .unwrap(); + chain + }; let result = subject.initialize_to_version( &dir_path, @@ -88,7 +107,12 @@ mod tests { assert_eq!(encrypted, false); let (scan_intervals, encrypted) = retrieve_config_row(connection.as_ref(), "scan_intervals"); - assert_eq!(scan_intervals, Some(DEFAULT_SCAN_INTERVALS.to_string())); + assert_eq!( + scan_intervals, + Some( + accountant::ScanIntervals::compute_default(Chain::from(chain.as_str())).to_string() + ) + ); assert_eq!(encrypted, false); } } diff --git a/node/src/db_config/config_dao_null.rs b/node/src/db_config/config_dao_null.rs index f1fc58cd4..8cd87c075 100644 --- a/node/src/db_config/config_dao_null.rs +++ b/node/src/db_config/config_dao_null.rs @@ -4,13 +4,13 @@ use crate::database::db_initializer::DbInitializerReal; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoError, ConfigDaoRecord}; use crate::neighborhood::DEFAULT_MIN_HOPS; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::{CURRENT_SCHEMA_VERSION, DEFAULT_GAS_PRICE}; use std::collections::HashMap; - /* This class exists because the Daemon uses the same configuration code that the Node uses, and @@ -139,7 +139,10 @@ impl Default for ConfigDaoNull { ); data.insert( "scan_intervals".to_string(), - (Some(DEFAULT_SCAN_INTERVALS.to_string()), false), + ( + Some(accountant::ScanIntervals::compute_default(Chain::default()).to_string()), + false, + ), ); data.insert("max_block_count".to_string(), (None, false)); Self { data } @@ -208,7 +211,7 @@ mod tests { subject.get("scan_intervals").unwrap(), ConfigDaoRecord::new( "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(Chain::default()).to_string()), false ) ); diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 801aa4456..a66e74c5f 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -330,7 +330,7 @@ fn get_public_ip(multi_config: &MultiConfig) -> Result match IpAddr::from_str(&ip_str) { Ok(ip_addr) => Ok(ip_addr), - Err(_) => todo!("Drive in a better error message"), //Err(ConfiguratorError::required("ip", &format! ("blockety blip: '{}'", ip_str), + Err(_) => todo!("Drive in a better error message. The multiconfig wouldn't allow a bad format, though."), //Err(ConfiguratorError::required("ip", &format! ("blockety blip: '{}'", ip_str), }, None => Ok(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), // sentinel: means "Try Automap" } @@ -494,7 +494,7 @@ fn configure_accountant_config( |pc: &mut dyn PersistentConfiguration, curves| pc.set_payment_thresholds(curves), )?; - check_payment_thresholds(&payment_thresholds)?; + validate_payment_thresholds(&payment_thresholds)?; let scan_intervals = process_combined_params( "scan-intervals", @@ -505,6 +505,8 @@ fn configure_accountant_config( |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), )?; + validate_scan_intervals(&scan_intervals)?; + let automatic_scans_enabled = value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == "on"; @@ -515,12 +517,13 @@ fn configure_accountant_config( Ok(()) } -fn check_payment_thresholds( +fn validate_payment_thresholds( payment_thresholds: &PaymentThresholds, ) -> Result<(), ConfiguratorError> { if payment_thresholds.debt_threshold_gwei <= payment_thresholds.permanent_debt_allowed_gwei { let msg = format!( - "Value of DebtThresholdGwei ({}) must be bigger than PermanentDebtAllowedGwei ({})", + "Value of DebtThresholdGwei ({}) must be bigger than PermanentDebtAllowedGwei ({}) \ + as the smallest value", payment_thresholds.debt_threshold_gwei, payment_thresholds.permanent_debt_allowed_gwei ); return Err(ConfiguratorError::required("payment-thresholds", &msg)); @@ -534,6 +537,21 @@ fn check_payment_thresholds( Ok(()) } +fn validate_scan_intervals(scan_intervals: &ScanIntervals) -> Result<(), ConfiguratorError> { + if scan_intervals.payable_scan_interval < scan_intervals.pending_payable_scan_interval { + Err(ConfiguratorError::required( + "scan-intervals", + &format!( + "The PendingPayableScanInterval value ({} s) must not exceed the PayableScanInterval \ + value ({} s) and should ideally be approximately half of it", + scan_intervals.pending_payable_scan_interval.as_secs(), + scan_intervals.payable_scan_interval.as_secs()), + )) + } else { + Ok(()) + } +} + fn configure_rate_pack( multi_config: &MultiConfig, persist_config: &mut dyn PersistentConfiguration, @@ -2099,8 +2117,8 @@ mod tests { } #[test] - fn configure_accountant_config_discovers_invalid_payment_thresholds_params_combination_given_from_users_input( - ) { + fn configure_accountant_config_discovers_invalid_payment_thresholds_combination_in_users_input() + { let multi_config = make_simplified_multi_config([ "--payment-thresholds", "19999|10000|1000|20000|1000|20000", @@ -2116,7 +2134,8 @@ mod tests { &mut persistent_config, ); - let expected_msg = "Value of DebtThresholdGwei (19999) must be bigger than PermanentDebtAllowedGwei (20000)"; + let expected_msg = "Value of DebtThresholdGwei (19999) must be bigger than \ + PermanentDebtAllowedGwei (20000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2127,14 +2146,15 @@ mod tests { } #[test] - fn check_payment_thresholds_works_for_equal_debt_parameters() { + fn validate_payment_thresholds_works_for_equal_debt_parameters() { let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; payment_thresholds.permanent_debt_allowed_gwei = 10000; payment_thresholds.debt_threshold_gwei = 10000; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); - let expected_msg = "Value of DebtThresholdGwei (10000) must be bigger than PermanentDebtAllowedGwei (10000)"; + let expected_msg = "Value of DebtThresholdGwei (10000) must be bigger than \ + PermanentDebtAllowedGwei (10000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2145,14 +2165,15 @@ mod tests { } #[test] - fn check_payment_thresholds_works_for_too_small_debt_threshold() { + fn validate_payment_thresholds_works_for_too_small_debt_threshold() { let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; payment_thresholds.permanent_debt_allowed_gwei = 10000; payment_thresholds.debt_threshold_gwei = 9999; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); - let expected_msg = "Value of DebtThresholdGwei (9999) must be bigger than PermanentDebtAllowedGwei (10000)"; + let expected_msg = "Value of DebtThresholdGwei (9999) must be bigger than \ + PermanentDebtAllowedGwei (10000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2163,7 +2184,8 @@ mod tests { } #[test] - fn check_payment_thresholds_does_not_permit_threshold_interval_longer_than_1_000_000_000_s() { + fn validate_payment_thresholds_does_not_permit_threshold_interval_longer_than_1_000_000_000_s() + { //this goes to the furthest extreme where the delta of debt limits is just 1 gwei, which, //if divided by the slope interval equal or longer 10^9 and rounded, gives 0 let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; @@ -2171,7 +2193,7 @@ mod tests { payment_thresholds.debt_threshold_gwei = 101; payment_thresholds.threshold_interval_sec = 1_000_000_001; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); let expected_msg = "Value of ThresholdIntervalSec must not exceed 1,000,000,000 s"; assert_eq!( @@ -2186,6 +2208,28 @@ mod tests { assert_eq!(last_value_possible, -1) } + #[test] + fn configure_accountant_config_discovers_invalid_scan_intervals_combination_in_users_input() { + let multi_config = make_simplified_multi_config(["--scan-intervals", "600|601|600"]); + let mut bootstrapper_config = BootstrapperConfig::new(); + let mut persistent_config = + configure_default_persistent_config(ACCOUNTANT_CONFIG_PARAMS | MAPPING_PROTOCOL) + .set_scan_intervals_result(Ok(())); + + let result = configure_accountant_config( + &multi_config, + &mut bootstrapper_config, + &mut persistent_config, + ); + + let expected_msg = "The PendingPayableScanInterval value (601 s) must not exceed \ + the PayableScanInterval value (600 s) and should ideally be approximately half of it"; + assert_eq!( + result, + Err(ConfiguratorError::required("scan-intervals", expected_msg)) + ) + } + #[test] fn unprivileged_parse_args_with_invalid_consuming_wallet_private_key_reacts_correctly() { running_test(); diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 78379b890..317070c09 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -17,6 +17,7 @@ use crate::sub_lib::wallet::Wallet; use actix::Recipient; use actix::{Addr, Message}; use lazy_static::lazy_static; +use masq_lib::blockchains::chains::Chain; use masq_lib::ui_gateway::NodeFromUiMessage; use std::fmt::{Debug, Formatter}; use std::str::FromStr; @@ -37,11 +38,6 @@ lazy_static! { threshold_interval_sec: 21600, unban_below_gwei: 500_000_000, }; - pub static ref DEFAULT_SCAN_INTERVALS: ScanIntervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(600), - pending_payable_scan_interval: Duration::from_secs(60), - receivable_scan_interval: Duration::from_secs(600) - }; } //please, alphabetical order @@ -85,9 +81,15 @@ pub struct ScanIntervals { pub receivable_scan_interval: Duration, } -impl Default for ScanIntervals { - fn default() -> Self { - *DEFAULT_SCAN_INTERVALS +impl ScanIntervals { + pub fn compute_default(chain: Chain) -> Self { + Self { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs( + chain.rec().default_pending_payable_interval_sec, + ), + receivable_scan_interval: Duration::from_secs(600), + } } } @@ -207,12 +209,12 @@ mod tests { use crate::sub_lib::accountant::{ AccountantSubsFactoryReal, DetailedScanType, MessageIdGenerator, MessageIdGeneratorReal, PaymentThresholds, ScanIntervals, SubsFactory, DEFAULT_EARNING_WALLET, - DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS, MSG_ID_INCREMENTER, - TEMPORARY_CONSUMING_WALLET, + DEFAULT_PAYMENT_THRESHOLDS, MSG_ID_INCREMENTER, TEMPORARY_CONSUMING_WALLET, }; use crate::sub_lib::wallet::Wallet; use crate::test_utils::recorder::{make_accountant_subs_from_recorder, Recorder}; use actix::Actor; + use masq_lib::blockchains::chains::Chain; use masq_lib::messages::ScanType; use std::str::FromStr; use std::sync::atomic::Ordering; @@ -252,12 +254,6 @@ mod tests { threshold_interval_sec: 21600, unban_below_gwei: 500_000_000, }; - let scan_intervals_expected = ScanIntervals { - payable_scan_interval: Duration::from_secs(600), - pending_payable_scan_interval: Duration::from_secs(60), - receivable_scan_interval: Duration::from_secs(600), - }; - assert_eq!(*DEFAULT_SCAN_INTERVALS, scan_intervals_expected); assert_eq!(*DEFAULT_PAYMENT_THRESHOLDS, payment_thresholds_expected); assert_eq!(*DEFAULT_EARNING_WALLET, default_earning_wallet_expected); assert_eq!( @@ -310,4 +306,34 @@ mod tests { assert_eq!(id, 0) } + + #[test] + fn default_for_scan_intervals_can_be_computed() { + let chain_a = Chain::BaseMainnet; + let chain_b = Chain::PolyMainnet; + + let result_a = ScanIntervals::compute_default(chain_a); + let result_b = ScanIntervals::compute_default(chain_b); + + assert_eq!( + result_a, + ScanIntervals { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs( + chain_a.rec().default_pending_payable_interval_sec + ), + receivable_scan_interval: Duration::from_secs(600), + } + ); + assert_eq!( + result_b, + ScanIntervals { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs( + chain_b.rec().default_pending_payable_interval_sec + ), + receivable_scan_interval: Duration::from_secs(600), + } + ); + } } diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index 53a3e8488..bd26eb627 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -307,6 +307,7 @@ mod tests { use super::*; use crate::sub_lib::combined_parameters::CombinedParamsDataTypes::U128; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; + use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; use std::panic::catch_unwind; #[test] @@ -455,7 +456,7 @@ mod tests { let panic_3 = catch_unwind(|| { let _: &[(&str, CombinedParamsDataTypes)] = - (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))).into(); + (&CombinedParams::ScanIntervals(Initialized(*TEST_SCAN_INTERVALS))).into(); }) .unwrap_err(); let panic_3_msg = panic_3.downcast_ref::().unwrap(); @@ -464,7 +465,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - ScanIntervals::default() + *TEST_SCAN_INTERVALS ) ); } @@ -502,7 +503,7 @@ mod tests { ); let panic_3 = catch_unwind(|| { - (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))) + (&CombinedParams::ScanIntervals(Initialized(*TEST_SCAN_INTERVALS))) .initialize_objects(HashMap::new()); }) .unwrap_err(); @@ -512,7 +513,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - ScanIntervals::default() + *TEST_SCAN_INTERVALS ) ); } diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index fb8ba3a83..a2b6d9ee1 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -109,9 +109,10 @@ pub fn assert_table_exists(conn: &dyn ConnectionWrapper, table_name: &str) { } pub fn assert_table_does_not_exist(conn: &dyn ConnectionWrapper, table_name: &str) { - let error_stm = conn - .prepare(&format!("select * from {}", table_name)) - .unwrap_err(); + let error_stm = match conn.prepare(&format!("select * from {}", table_name)) { + Ok(_) => panic!("Table {} should not exist, but it does", table_name), + Err(e) => e, + }; let error_msg = match error_stm { Error::SqliteFailure(_, Some(msg)) => msg, x => panic!("we expected SqliteFailure but we got: {:?}", x), diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 546149ae6..601ee7bd1 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -548,6 +548,7 @@ pub mod unshared_test_utils { use crossbeam_channel::{unbounded, Receiver, Sender}; use itertools::Either; use lazy_static::lazy_static; + use masq_lib::blockchains::chains::Chain; use masq_lib::constants::HTTP_PORT; use masq_lib::messages::{ToMessageBody, UiCrashRequest}; use masq_lib::multi_config::MultiConfig; @@ -642,6 +643,14 @@ pub mod unshared_test_utils { MultiConfig::new_test_only(arg_matches) } + lazy_static! { + pub static ref TEST_SCAN_INTERVALS: ScanIntervals = ScanIntervals { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(360), + receivable_scan_interval: Duration::from_secs(600), + }; + } + pub const ZERO: u32 = 0b0; pub const MAPPING_PROTOCOL: u32 = 0b000010; pub const ACCOUNTANT_CONFIG_PARAMS: u32 = 0b000100; @@ -686,16 +695,16 @@ pub mod unshared_test_utils { ) -> PersistentConfigurationMock { persistent_config_mock .payment_thresholds_result(Ok(PaymentThresholds::default())) - .scan_intervals_result(Ok(ScanIntervals::default())) + .scan_intervals_result(Ok(*TEST_SCAN_INTERVALS)) } pub fn make_persistent_config_real_with_config_dao_null() -> PersistentConfigurationReal { PersistentConfigurationReal::new(Box::new(ConfigDaoNull::default())) } - pub fn make_bc_with_defaults() -> BootstrapperConfig { + pub fn make_bc_with_defaults(chain: Chain) -> BootstrapperConfig { let mut config = BootstrapperConfig::new(); - config.scan_intervals_opt = Some(ScanIntervals::default()); + config.scan_intervals_opt = Some(ScanIntervals::compute_default(chain)); config.automatic_scans_enabled = true; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; config.payment_thresholds_opt = Some(PaymentThresholds::default()); From 2d7c20ec45de6919efaad3e6ffb993608d9e44bf Mon Sep 17 00:00:00 2001 From: Utkarsh Gupta <32920299+utkarshg6@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:45:22 +0530 Subject: [PATCH 15/37] GH-605: Teach the PayableScanner on tx retries (#663) --- masq_lib/src/utils.rs | 61 + .../tests/bookkeeping_test.rs | 6 +- .../tests/verify_bill_payment.rs | 6 +- .../db_access_objects/failed_payable_dao.rs | 406 +++-- node/src/accountant/db_access_objects/mod.rs | 15 +- .../db_access_objects/payable_dao.rs | 150 +- .../db_access_objects/sent_payable_dao.rs | 435 +++-- .../db_access_objects/test_utils.rs | 95 +- .../src/accountant/db_access_objects/utils.rs | 43 +- node/src/accountant/mod.rs | 813 +++++---- node/src/accountant/payment_adjuster.rs | 21 +- node/src/accountant/scanners/mod.rs | 1456 ++--------------- .../scanners/payable_scanner/finish_scan.rs | 258 +++ .../scanners/payable_scanner/mod.rs | 1065 ++++++++++++ .../scanners/payable_scanner/msgs.rs | 69 + .../payment_adjuster_integration.rs | 60 + .../scanners/payable_scanner/start_scan.rs | 189 +++ .../scanners/payable_scanner/test_utils.rs | 97 ++ .../tx_templates/initial/mod.rs | 3 + .../tx_templates/initial/new.rs | 217 +++ .../tx_templates/initial/retry.rs | 216 +++ .../payable_scanner/tx_templates/mod.rs | 48 + .../tx_templates/priced/mod.rs | 3 + .../tx_templates/priced/new.rs | 107 ++ .../tx_templates/priced/retry.rs | 169 ++ .../tx_templates/signable/mod.rs | 253 +++ .../tx_templates/test_utils.rs | 108 ++ .../scanners/payable_scanner/utils.rs | 512 ++++++ .../scanners/payable_scanner_extension/mod.rs | 68 - .../payable_scanner_extension/msgs.rs | 139 -- .../scanners/pending_payable_scanner/mod.rs | 464 ++++-- .../tx_receipt_interpreter.rs | 15 +- .../scanners/pending_payable_scanner/utils.rs | 88 +- .../src/accountant/scanners/scanners_utils.rs | 692 -------- node/src/accountant/scanners/test_utils.rs | 53 +- node/src/accountant/test_utils.rs | 356 ++-- node/src/actor_system_factory.rs | 2 +- .../blockchain/blockchain_agent/agent_web3.rs | 577 +++---- node/src/blockchain/blockchain_agent/mod.rs | 18 +- .../blockchain_agent}/test_utils.rs | 16 +- node/src/blockchain/blockchain_bridge.rs | 493 +++--- .../blockchain_interface_web3/mod.rs | 158 +- .../blockchain_interface_web3/utils.rs | 937 +++++------ .../data_structures/errors.rs | 70 +- .../data_structures/mod.rs | 20 +- .../blockchain/blockchain_interface/mod.rs | 22 +- .../blockchain_interface_initializer.rs | 29 +- node/src/blockchain/errors/internal_errors.rs | 2 +- node/src/blockchain/errors/mod.rs | 2 +- node/src/blockchain/errors/rpc_errors.rs | 20 +- .../blockchain/errors/validation_status.rs | 163 +- node/src/blockchain/test_utils.rs | 38 +- node/src/sub_lib/accountant.rs | 4 +- node/src/sub_lib/blockchain_bridge.rs | 69 +- node/src/test_utils/recorder.rs | 18 +- 55 files changed, 6905 insertions(+), 4509 deletions(-) create mode 100644 node/src/accountant/scanners/payable_scanner/finish_scan.rs create mode 100644 node/src/accountant/scanners/payable_scanner/mod.rs create mode 100644 node/src/accountant/scanners/payable_scanner/msgs.rs create mode 100644 node/src/accountant/scanners/payable_scanner/payment_adjuster_integration.rs create mode 100644 node/src/accountant/scanners/payable_scanner/start_scan.rs create mode 100644 node/src/accountant/scanners/payable_scanner/test_utils.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/initial/mod.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/priced/mod.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs create mode 100644 node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs create mode 100644 node/src/accountant/scanners/payable_scanner/utils.rs delete mode 100644 node/src/accountant/scanners/payable_scanner_extension/mod.rs delete mode 100644 node/src/accountant/scanners/payable_scanner_extension/msgs.rs rename node/src/{accountant/scanners/payable_scanner_extension => blockchain/blockchain_agent}/test_utils.rs (80%) 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), From 37f73431709ec18919973313605c9f3449c3969b Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 29 Sep 2025 16:49:19 +0200 Subject: [PATCH 16/37] GH-598: added special lines for Windows --- node/src/accountant/mod.rs | 17 ++++++++++++++- .../accountant/scanners/scan_schedulers.rs | 21 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 20512a135..5d612ad68 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -3478,13 +3478,28 @@ mod tests { } #[test] + #[cfg(windows)] + #[should_panic( + expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ + cross_scan_cause_opt: None, started_at: SystemTime { intervals: 116444736000000000 } } \ + should be impossible with PendingPayableScanner in automatic mode" + )] + fn initial_pending_payable_scan_hits_unexpected_error() { + test_initial_pending_payable_scan_hits_unexpected_error() + } + + #[test] + #[cfg(not(windows))] #[should_panic( expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ cross_scan_cause_opt: None, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ should be impossible with PendingPayableScanner in automatic mode" )] fn initial_pending_payable_scan_hits_unexpected_error() { - init_test_logging(); + test_initial_pending_payable_scan_hits_unexpected_error() + } + + fn test_initial_pending_payable_scan_hits_unexpected_error() { let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("abc")) .build(); diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index dd23e05bd..2f9cc1d07 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -187,7 +187,7 @@ impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal .duration_since(last_new_payable_scan_timestamp) .unwrap_or_else(|_| { panic!( - "Unexpected now ({:?}) earlier than past timestamp ({:?})", + "Now ({:?}) earlier than past timestamp ({:?})", now, last_new_payable_scan_timestamp ) }); @@ -515,11 +515,26 @@ mod tests { } #[test] + #[cfg(windows)] #[should_panic( - expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier than past \ - timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + expected = "Now (SystemTime { intervals: 116454735990000000 }) earlier than past timestamp \ + (SystemTime { intervals: 116454736000000000 })" )] fn scan_dyn_interval_computer_panics() { + test_scan_dyn_interval_computer_panics() + } + + #[test] + #[cfg(not(windows))] + #[should_panic( + expected = "Now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier than past timestamp \ + (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + )] + fn scan_dyn_interval_computer_panics() { + test_scan_dyn_interval_computer_panics() + } + + fn test_scan_dyn_interval_computer_panics() { let now = UNIX_EPOCH .checked_add(Duration::from_secs(1_000_000)) .unwrap(); From f55708c0c2f56e27e6b90eb645b094631203bc5a Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 29 Sep 2025 20:35:27 +0200 Subject: [PATCH 17/37] GH-598: fixed tests in verify bill payments --- .../tests/verify_bill_payment.rs | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index 9b369e192..59a988507 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -17,6 +17,9 @@ use multinode_integration_tests_lib::utils::{ }; use node_lib::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoReal}; use node_lib::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoReal}; +use node_lib::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDao, SentPayableDaoReal, +}; use node_lib::blockchain::bip32::Bip32EncryptionKeyProvider; use node_lib::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, @@ -234,7 +237,7 @@ fn verify_bill_payment() { assert_balances( &contract_owner_wallet, &blockchain_interface, - "99995231980000000000", + "99994287232000000000", "471999999700000000000000000", ); @@ -355,17 +358,23 @@ fn verify_pending_payables() { "{}", node_wallet.clone() ))) + // Important + .scans(false) .ui_port(ui_port) .build(); let (consuming_node_name, consuming_node_index) = cluster.prepare_real_node(&consuming_config); let consuming_node_path = node_chain_specific_data_directory(&consuming_node_name); - let consuming_node_connection = DbInitializerReal::default() - .initialize( - Path::new(&consuming_node_path), - make_init_config(cluster.chain), - ) - .unwrap(); - let consuming_payable_dao = PayableDaoReal::new(consuming_node_connection); + let connect_to_consuming_node_db = || { + DbInitializerReal::default() + .initialize( + Path::new(&consuming_node_path), + make_init_config(cluster.chain), + ) + .unwrap() + }; + let consuming_payable_dao = PayableDaoReal::new(connect_to_consuming_node_db()); + let consuming_sent_payable_dao = SentPayableDaoReal::new(connect_to_consuming_node_db()); + open_all_file_permissions(consuming_node_path.clone().into()); assert_eq!( format!("{}", &contract_owner_wallet), @@ -400,7 +409,9 @@ fn verify_pending_payables() { ); let now = Instant::now(); - while !consuming_payable_dao.retrieve_payables(None).is_empty() + while consuming_sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::IsPending)) + .is_empty() && now.elapsed() < Duration::from_secs(10) { thread::sleep(Duration::from_millis(400)); @@ -409,7 +420,7 @@ fn verify_pending_payables() { assert_balances( &contract_owner_wallet, &blockchain_interface, - "99995231980000000000", + "99994287232000000000", "471999999700000000000000000", ); assert_balances( @@ -437,10 +448,24 @@ fn verify_pending_payables() { .tmb(0), ); - assert!(consuming_payable_dao.retrieve_payables(None).is_empty()); + let now = Instant::now(); + loop { + if !consuming_sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::IsPending)) + .is_empty() + { + if now.elapsed() < Duration::from_secs(5) { + thread::sleep(Duration::from_millis(400)) + } else { + panic!("Pending payables still aren't resolved even after 5 seconds") + } + } else { + break; + } + } MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Found 3 pending payables to process", + "Found 3 pending payables and 0 unfinalized failures to process", Duration::from_secs(5), ); MASQNodeUtils::assert_node_wrote_log_containing( @@ -450,17 +475,17 @@ fn verify_pending_payables() { ); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Transaction 0x75a8f185b7fb3ac0c4d1ee6b402a46940c9ae0477c0c7378a1308fb4bf539c5c has been added to the blockchain;", + "Pending tx 0x89acc46da0df6ef8c6f5574307ae237a812bd28af524a62131013b5e19ca3e26 was confirmed on-chain", Duration::from_secs(5), ); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Transaction 0x384a3bb5bbd9718a97322be2878fa88c7cacacb2ac3416f521a621ca1946ddfc has been added to the blockchain;", + "Pending tx 0xae0bf6400f0b9950a1d456e488549414d118714b81a39233b811b629cf41399b was confirmed on-chain", Duration::from_secs(5), ); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Transaction 0x6bc98d5db61ddd7676de1f25cb537156b3d9e066cec414fef8dbe9c695908215 has been added to the blockchain;", + "Pending tx 0xecab1c73ca90ebcb073526e28f1f8d4678704b74d1e0209779ddeefc8fb861f5 was confirmed on-chain", Duration::from_secs(5), ); } From 55c3339d71b5688d94fcb0132af4ca6196b3dc63 Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 29 Sep 2025 23:11:36 +0200 Subject: [PATCH 18/37] GH-598: rewording in messages --- node/src/accountant/scanners/mod.rs | 2 +- .../scanners/pending_payable_scanner/mod.rs | 18 +++++------ .../tx_receipt_interpreter.rs | 32 ++++++++----------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index a11813615..1dc1c3a9f 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1532,7 +1532,7 @@ mod tests { test_log_handler.exists_log_containing(&format!("WARN: {test_name}: Failed to retrieve tx receipt for FailedPayable(0x0000000000000000000000000000000000000000000000000000000000000555): Remote(InvalidResponse(\"game over\")). Will retry receipt retrieval next cycle")); test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000222 (block 2345) as confirmed on-chain")); test_log_handler.exists_log_containing(&format!( - "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000111 (block 1234) was confirmed", + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000111 (block 1234) recorded in local ledger", )); test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000333, 0x0000000000000000000000000000000000000000000000000000000000000666 were processed in the db")); } diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 7e179ac9d..cc1052d14 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -536,8 +536,8 @@ impl PendingPayableScanner { }) .join(", "); match tx_hashes_and_tx_blocks.len() { - 1 => format!("Tx {} was confirmed", pretty_pairs), - _ => format!("Txs {} were confirmed", pretty_pairs), + 1 => format!("Tx {} recorded in local ledger", pretty_pairs), + _ => format!("Txs {} recorded in local ledger", pretty_pairs), } }); } @@ -1931,7 +1931,7 @@ mod tests { log_handler.exists_log_containing(&format!( "INFO: {test_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ - (block 6789898789) were confirmed", + (block 6789898789) recorded in local ledger", )); } @@ -2010,7 +2010,7 @@ mod tests { )); log_handler.exists_log_containing(&format!( "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ - (block 4578989878) was confirmed", + (block 4578989878) recorded in local ledger", )); } @@ -2108,13 +2108,13 @@ mod tests { let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( - "INFO: {plural_case_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ - (block 1234501), 0x0000000000000000000000000000000000000000000000000000000000000567 \ - (block 1234502) were confirmed", + "INFO: {plural_case_name}: Txs 0x0000000000000000000000000000000000000000000000000000000\ + 000000123 (block 1234501), 0x00000000000000000000000000000000000000000000000000000000000\ + 00567 (block 1234502) recorded in local ledger", )); log_handler.exists_log_containing(&format!( - "INFO: {singular_case_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ - (block 1234501) was confirmed", + "INFO: {singular_case_name}: Tx 0x000000000000000000000000000000000000000000000000000000\ + 0000000123 (block 1234501) recorded in local ledger", )); } 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 6039cd711..833f5ef21 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 @@ -105,8 +105,8 @@ impl TxReceiptInterpreter { .hash; warning!( logger, - "Failed tx {:?} on a recheck was found pending on its receipt unexpectedly. \ - It was supposed to be replaced by {:?}", + "Previously failed tx {:?} found still pending unexpectedly; should have been \ + replaced by {:?}", failed_tx.hash, replacement_tx_hash ); @@ -137,10 +137,7 @@ impl TxReceiptInterpreter { ) -> ReceiptScanReport { match tx { TxByTable::SentPayable(sent_tx) => { - info!( - logger, - "Pending tx {:?} was confirmed on-chain", sent_tx.hash, - ); + info!(logger, "Tx {:?} confirmed", sent_tx.hash,); let completed_sent_tx = SentTx { status: TxStatus::Confirmed { @@ -155,8 +152,7 @@ impl TxReceiptInterpreter { TxByTable::FailedPayable(failed_tx) => { info!( logger, - "Failed tx {:?} was later confirmed on-chain and will be reclaimed", - failed_tx.hash + "Previously failed tx {:?} confirmed; will be reclaimed", failed_tx.hash ); let sent_tx = SentTx::from((failed_tx, tx_block)); @@ -177,7 +173,7 @@ impl TxReceiptInterpreter { let failure_reason = FailureReason::Reverted; let failed_tx = FailedTx::from((sent_tx, failure_reason)); - warning!(logger, "Pending tx {:?} was reverted", failed_tx.hash,); + warning!(logger, "Tx {:?} reverted", failed_tx.hash,); scan_report.register_new_failure(failed_tx); } @@ -290,8 +286,8 @@ mod tests { } ); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ - 00000cdef was confirmed on-chain", + "INFO: {test_name}: Tx 0x000000000000000000000000000000000000000000000000000000000000\ + cdef confirmed", )); } @@ -338,8 +334,8 @@ mod tests { } ); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ - 00000cdef was later confirmed on-chain and will be reclaimed", + "INFO: {test_name}: Previously failed tx 0x00000000000000000000000000000000000000000000\ + 0000000000000000cdef confirmed; will be reclaimed", )); } @@ -371,8 +367,8 @@ mod tests { } ); TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ - 000000abc was reverted", + "WARN: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000\ + 000000abc reverted", )); } @@ -525,9 +521,9 @@ mod tests { vec![Some(RetrieveCondition::ByNonce(vec![failed_tx_nonce]))] ); TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ - 000000913 on a recheck was found pending on its receipt unexpectedly. It was supposed \ - to be replaced by 0x00000000000000000000000000000000000000000000000000000000000007c6" + "WARN: {test_name}: Previously failed tx 0x00000000000000000000000000000000000000000000\ + 00000000000000000913 found still pending unexpectedly; should have been replaced \ + by 0x00000000000000000000000000000000000000000000000000000000000007c6" )); } From 8fbe7bf6767a03bf28e116fc0651b194a0a10940 Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 30 Sep 2025 10:47:25 +0200 Subject: [PATCH 19/37] GH-598: Adjusted assertions in MNT --- multinode_integration_tests/tests/verify_bill_payment.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index 59a988507..d421f82b2 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -475,17 +475,17 @@ fn verify_pending_payables() { ); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Pending tx 0x89acc46da0df6ef8c6f5574307ae237a812bd28af524a62131013b5e19ca3e26 was confirmed on-chain", + "Tx 0x89acc46da0df6ef8c6f5574307ae237a812bd28af524a62131013b5e19ca3e26 confirmed", Duration::from_secs(5), ); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Pending tx 0xae0bf6400f0b9950a1d456e488549414d118714b81a39233b811b629cf41399b was confirmed on-chain", + "Tx 0xae0bf6400f0b9950a1d456e488549414d118714b81a39233b811b629cf41399b confirmed", Duration::from_secs(5), ); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Pending tx 0xecab1c73ca90ebcb073526e28f1f8d4678704b74d1e0209779ddeefc8fb861f5 was confirmed on-chain", + "Tx 0xecab1c73ca90ebcb073526e28f1f8d4678704b74d1e0209779ddeefc8fb861f5 confirmed", Duration::from_secs(5), ); } From d564fc0e56998242a8ce5b8f0c0a20aea4cea489 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 2 Oct 2025 22:49:26 +0200 Subject: [PATCH 20/37] GH-598: main thing fixed --- masq_lib/src/lib.rs | 1 + masq_lib/src/simple_clock.rs | 16 + masq_lib/src/test_utils/mod.rs | 1 + .../src/test_utils/simple_clock.rs | 8 +- .../db_access_objects/failed_payable_dao.rs | 13 +- .../db_access_objects/sent_payable_dao.rs | 39 +- node/src/accountant/mod.rs | 337 ++++++++++-------- node/src/accountant/scanners/mod.rs | 10 +- .../scanners/pending_payable_scanner/mod.rs | 40 +-- .../tx_receipt_interpreter.rs | 9 +- .../scanners/pending_payable_scanner/utils.rs | 58 ++- .../accountant/scanners/scan_schedulers.rs | 226 ++++++++---- node/src/accountant/scanners/test_utils.rs | 32 +- node/src/accountant/test_utils.rs | 99 ++--- .../blockchain/errors/validation_status.rs | 45 +-- 15 files changed, 508 insertions(+), 426 deletions(-) create mode 100644 masq_lib/src/simple_clock.rs rename node/src/accountant/scanners/pending_payable_scanner/test_utils.rs => masq_lib/src/test_utils/simple_clock.rs (67%) diff --git a/masq_lib/src/lib.rs b/masq_lib/src/lib.rs index ae638163e..c7e2b107a 100644 --- a/masq_lib/src/lib.rs +++ b/masq_lib/src/lib.rs @@ -24,6 +24,7 @@ pub mod crash_point; pub mod data_version; pub mod exit_locations; pub mod shared_schema; +pub mod simple_clock; pub mod test_utils; pub mod ui_gateway; pub mod ui_traffic_converter; diff --git a/masq_lib/src/simple_clock.rs b/masq_lib/src/simple_clock.rs new file mode 100644 index 000000000..35bc34e97 --- /dev/null +++ b/masq_lib/src/simple_clock.rs @@ -0,0 +1,16 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use std::time::SystemTime; + +pub trait SimpleClock { + fn now(&self) -> SystemTime; +} + +#[derive(Default)] +pub struct SimpleClockReal {} + +impl SimpleClock for SimpleClockReal { + fn now(&self) -> SystemTime { + SystemTime::now() + } +} diff --git a/masq_lib/src/test_utils/mod.rs b/masq_lib/src/test_utils/mod.rs index 2dd76c962..293b48db0 100644 --- a/masq_lib/src/test_utils/mod.rs +++ b/masq_lib/src/test_utils/mod.rs @@ -5,5 +5,6 @@ pub mod fake_stream_holder; pub mod logging; pub mod mock_blockchain_client_server; pub mod mock_websockets_server; +pub mod simple_clock; pub mod ui_connection; pub mod utils; diff --git a/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs b/masq_lib/src/test_utils/simple_clock.rs similarity index 67% rename from node/src/accountant/scanners/pending_payable_scanner/test_utils.rs rename to masq_lib/src/test_utils/simple_clock.rs index 473fd28cb..d4fa5f29e 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs +++ b/masq_lib/src/test_utils/simple_clock.rs @@ -1,21 +1,21 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::blockchain::errors::validation_status::ValidationFailureClock; +use crate::simple_clock::SimpleClock; use std::cell::RefCell; use std::time::SystemTime; #[derive(Default)] -pub struct ValidationFailureClockMock { +pub struct SimpleClockMock { now_results: RefCell>, } -impl ValidationFailureClock for ValidationFailureClockMock { +impl SimpleClock for SimpleClockMock { fn now(&self) -> SystemTime { self.now_results.borrow_mut().remove(0) } } -impl ValidationFailureClockMock { +impl SimpleClockMock { pub fn now_result(self, result: SystemTime) -> Self { self.now_results.borrow_mut().push(result); self 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 7d4644ffa..2b83fdfab 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -427,17 +427,16 @@ mod tests { }; 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::validation_status::{PreviousAttempts, ValidationStatus}; use crate::blockchain::errors::BlockchainErrorKind; 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::simple_clock::SimpleClockReal; + use masq_lib::test_utils::simple_clock::SimpleClockMock; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; use std::collections::{BTreeSet, HashMap}; @@ -701,7 +700,7 @@ mod tests { #[test] fn failure_status_from_str_works() { - let validation_failure_clock = ValidationFailureClockMock::default().now_result( + let validation_failure_clock = SimpleClockMock::default().now_result( SystemTime::UNIX_EPOCH .add(Duration::from_secs(1755080031)) .add(Duration::from_nanos(612180914)), @@ -831,7 +830,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), ))) .build(); @@ -950,7 +949,7 @@ mod tests { ])) .unwrap(); let timestamp = SystemTime::now(); - let clock = ValidationFailureClockMock::default() + let clock = SimpleClockMock::default() .now_result(timestamp) .now_result(timestamp); let hashmap = HashMap::from([ 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 d0edbfa34..471e352aa 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -540,13 +540,10 @@ mod tests { make_read_only_db_connection, make_sent_tx, TxBuilder, }; 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::validation_status::{PreviousAttempts, ValidationStatus}; use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::{make_address, make_block_hash, make_tx_hash}; use crate::database::db_initializer::{ @@ -554,6 +551,8 @@ mod tests { }; use crate::database::test_utils::ConnectionWrapperMock; use ethereum_types::{H256, U64}; + use masq_lib::simple_clock::SimpleClockReal; + use masq_lib::test_utils::simple_clock::SimpleClockMock; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; use std::cmp::Ordering; @@ -578,13 +577,13 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), ))) .build(); @@ -822,7 +821,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), ))) .build(); @@ -1205,7 +1204,7 @@ mod tests { let mut tx2 = make_sent_tx(789); tx2.status = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), - &ValidationFailureClockMock::default().now_result(timestamp_b), + &SimpleClockMock::default().now_result(timestamp_b), ))); let mut tx3 = make_sent_tx(123); tx3.status = TxStatus::Pending(ValidationStatus::Waiting); @@ -1217,7 +1216,7 @@ mod tests { tx1.hash, TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), - &ValidationFailureClockMock::default().now_result(timestamp_a), + &SimpleClockMock::default().now_result(timestamp_a), ))), ), ( @@ -1227,13 +1226,13 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockMock::default().now_result(timestamp_b), + &SimpleClockMock::default().now_result(timestamp_b), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), )), ), @@ -1266,7 +1265,7 @@ mod tests { updated_txs[1].status, TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), - &ValidationFailureClockMock::default().now_result(timestamp_a) + &SimpleClockMock::default().now_result(timestamp_a) ))) ); assert_eq!( @@ -1276,13 +1275,13 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable )), - &ValidationFailureClockMock::default().now_result(timestamp_b) + &SimpleClockMock::default().now_result(timestamp_b) ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable )), - &ValidationFailureClockReal::default() + &SimpleClockReal::default() ) )) ); @@ -1318,7 +1317,7 @@ mod tests { make_tx_hash(1), TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ))), )])); @@ -1512,8 +1511,8 @@ mod tests { #[test] fn tx_status_from_str_works() { - let validation_failure_clock = ValidationFailureClockMock::default() - .now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); + let validation_failure_clock = + SimpleClockMock::default().now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); assert_eq!( TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), @@ -1579,15 +1578,15 @@ mod tests { 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(), + &SimpleClockReal::default(), ))); let tx_status_3 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ))); let tx_status_4 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ))); let tx_status_5 = TxStatus::Confirmed { block_hash: format!("{:?}", make_tx_hash(1)), diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 5d612ad68..0a58004cc 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -973,6 +973,10 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; + self.scan_schedulers + .payable + .update_last_new_payable_scan_timestamp(); + match result { Ok(scan_message) => { self.qualified_payables_sub_opt @@ -1517,10 +1521,13 @@ mod tests { let financial_statistics = result.financial_statistics().clone(); let default_scan_intervals = ScanIntervals::compute_default(chain); - assert_eq!( - result.scan_schedulers.payable.new_payable_interval, - default_scan_intervals.payable_scan_interval - ); + result + .scan_schedulers + .payable + .dyn_interval_computer + .as_any() + .downcast_ref::() + .unwrap(); assert_eq!( result.scan_schedulers.pending_payable.interval, default_scan_intervals.pending_payable_scan_interval, @@ -1637,7 +1644,8 @@ mod tests { // Making sure we would get a panic if another scan was scheduled subject.scan_schedulers.payable.new_payable_notify_later = Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); - subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); let subject_addr = subject.start(); let system = System::new("test"); let ui_message = NodeFromUiMessage { @@ -1969,7 +1977,8 @@ mod tests { // Making sure we would get a panic if another scan was scheduled subject.scan_schedulers.payable.new_payable_notify_later = Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); - subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); let subject_addr = subject.start(); let ui_message = NodeFromUiMessage { client_id: 1234, @@ -2338,27 +2347,70 @@ mod tests { } #[test] - fn accountant_sends_qualified_payable_msg_when_qualified_payable_found() { + fn accountant_sends_qualified_payable_msg_for_new_payable_scan_when_qualified_payable_found() { + let new_payable_templates = NewTxTemplates::from(&vec![make_payable_account(123)]); + accountant_sends_qualified_payable_msg_when_qualified_payable_found( + ScanForNewPayables { + response_skeleton_opt: None, + }, + Either::Left(new_payable_templates), + vec![()], + ) + } + + #[test] + fn accountant_sends_qualified_payable_msg_for_retry_payable_scan_when_qualified_payable_found() + { + let retry_payable_templates = RetryTxTemplates(vec![make_retry_tx_template(123)]); + accountant_sends_qualified_payable_msg_when_qualified_payable_found( + ScanForRetryPayables { + response_skeleton_opt: None, + }, + Either::Right(retry_payable_templates), + vec![], + ) + } + + fn accountant_sends_qualified_payable_msg_when_qualified_payable_found( + act_msg: ActorMessage, + initial_templates: Either, + zero_out_params_expected: Vec<()>, + ) where + ActorMessage: Message + Send + 'static, + ActorMessage::Result: Send, + Accountant: Handler, + { + let zero_out_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let now = SystemTime::now(); - let payment_thresholds = PaymentThresholds::default(); - let (qualified_payables, _, retrieved_payables) = - make_qualified_and_unqualified_payables(now, &payment_thresholds); - 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"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .consuming_wallet(consuming_wallet.clone()) - .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); + let initial_template_msg = InitialTemplatesMessage { + initial_templates, + consuming_wallet, + response_skeleton_opt: None, + }; + let payable_scanner = ScannerMock::default() + .scan_started_at_result(None) + .start_scan_result(Ok(initial_template_msg.clone())); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); subject .scanners .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.scan_schedulers.payable.dyn_interval_computer = Box::new( + NewPayableScanDynIntervalComputerMock::default().zero_out_params(&zero_out_params_arc), + ); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2366,26 +2418,16 @@ mod tests { .build(); send_bind_message!(accountant_subs, peer_actors); - accountant_addr - .try_send(ScanForNewPayables { - response_skeleton_opt: None, - }) - .unwrap(); + accountant_addr.try_send(act_msg).unwrap(); System::current().stop(); 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 expected_new_tx_templates = NewTxTemplates::from(&qualified_payables); - assert_eq!( - message, - &InitialTemplatesMessage { - initial_templates: Either::Left(expected_new_tx_templates), - consuming_wallet, - response_skeleton_opt: None, - } - ); + assert_eq!(message, &initial_template_msg); + let zero_out_params = zero_out_params_arc.lock().unwrap(); + assert_eq!(*zero_out_params, zero_out_params_expected) } #[test] @@ -2396,15 +2438,14 @@ mod tests { System::new("automatic_scan_for_new_payables_schedules_another_one_immediately_if_no_qualified_payables_found"); let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = AccountantBuilder::default() + .bootstrapper_config(make_bc_with_defaults(TEST_DEFAULT_CHAIN)) .consuming_wallet(consuming_wallet) .build(); subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), ); - subject.scan_schedulers.payable.dyn_interval_computer = Box::new( - NewPayableScanDynIntervalComputerMock::default() - .compute_interval_result(Some(Duration::from_secs(500))), - ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); let payable_scanner = ScannerMock::default() .scan_started_at_result(None) .scan_started_at_result(None) @@ -2431,14 +2472,29 @@ mod tests { System::current().stop(); assert_eq!(system.run(), 0); let mut notify_later_params = notify_later_params_arc.lock().unwrap(); - let (msg, interval) = notify_later_params.remove(0); + // As obvious, the next scan is scheduled for the future and should not run immediately. + let (msg, actual_interval) = notify_later_params.remove(0); assert_eq!( msg, ScanForNewPayables { response_skeleton_opt: None } ); - assert_eq!(interval, Duration::from_secs(500)); + let default_scan_intervals = ScanIntervals::compute_default(TEST_DEFAULT_CHAIN); + // The previous last_new_payable_scan_timestamp is UNIX_EPOCH, if the interval was derived + // from that timestamp, it would result in an immediate-scan command. This implies that + // the last_new_payable_scan_timestamp was reset to zero, which is how it is meant to be. + let left_bound = default_scan_intervals + .payable_scan_interval + .checked_sub(Duration::from_secs(5)) + .unwrap(); + let right_bound = default_scan_intervals + .payable_scan_interval + .checked_add(Duration::from_secs(5)) + .unwrap(); + // The divergence should be only a few milliseconds, definitely not seconds; the tested + // interval should be safe for slower machines too. + assert!(left_bound <= actual_interval && actual_interval <= right_bound); assert_eq!(notify_later_params.len(), 0); // Accountant is unbound; therefore, it is guaranteed that sending a message to // the BlockchainBridge wasn't attempted. It would've panicked otherwise. @@ -2794,7 +2850,7 @@ mod tests { .start_scan_params(&scan_params.receivable_start_scan) .start_scan_result(Err(StartScanError::NothingToProcess)); let (subject, new_payable_expected_computed_interval, receivable_scan_interval) = - set_up_subject_for_no_pending_payables_found_startup_test( + set_up_subject_for_no_p_p_found_startup_test( test_name, ¬ify_and_notify_later_params, &compute_interval_params_arc, @@ -2814,7 +2870,7 @@ mod tests { let before = SystemTime::now(); system.run(); let after = SystemTime::now(); - assert_pending_payable_scanner_for_no_pending_payable_found( + assert_pending_payable_scanner_for_no_p_p_found( test_name, consuming_wallet, &scan_params.pending_payable_start_scan, @@ -2822,12 +2878,11 @@ mod tests { before, after, ); - assert_payable_scanner_for_no_pending_payable_found( + assert_payable_scanner_for_no_p_p_found( + &scan_params.payable_start_scan, ¬ify_and_notify_later_params, compute_interval_params_arc, new_payable_expected_computed_interval, - before, - after, ); assert_receivable_scanner( test_name, @@ -2893,8 +2948,8 @@ mod tests { .scan_started_at_result(None) .start_scan_params(&scan_params.receivable_start_scan) .start_scan_result(Err(StartScanError::NothingToProcess)); - let (subject, pending_payable_expected_notify_later_interval, receivable_scan_interval) = - set_up_subject_for_some_pending_payable_found_startup_test( + let (subject, expected_pending_payable_notify_later_interval, receivable_scan_interval) = + set_up_subject_for_some_p_p_found_startup_test( test_name, ¬ify_and_notify_later_params, config, @@ -2948,17 +3003,17 @@ mod tests { let before = SystemTime::now(); system.run(); let after = SystemTime::now(); - assert_pending_payable_scanner_for_some_pending_payable_found( + assert_pending_payable_scanner_for_some_p_p_found( test_name, consuming_wallet.clone(), &scan_params, ¬ify_and_notify_later_params.pending_payables_notify_later, - pending_payable_expected_notify_later_interval, + expected_pending_payable_notify_later_interval, expected_tx_receipts_msg, before, after, ); - assert_payable_scanner_for_some_pending_payable_found( + assert_payable_scanner_for_some_p_p_found( test_name, consuming_wallet, &scan_params, @@ -2998,10 +3053,10 @@ mod tests { receivables_notify_later: Arc>>, } - fn set_up_subject_for_no_pending_payables_found_startup_test( + fn set_up_subject_for_no_p_p_found_startup_test( test_name: &str, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, - compute_interval_params_arc: &Arc>>, + compute_interval_params_arc: &Arc>>, config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, @@ -3057,7 +3112,7 @@ mod tests { ) } - fn set_up_subject_for_some_pending_payable_found_startup_test( + fn set_up_subject_for_some_p_p_found_startup_test( test_name: &str, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, config: BootstrapperConfig, @@ -3150,7 +3205,7 @@ mod tests { subject } - fn assert_pending_payable_scanner_for_no_pending_payable_found( + fn assert_pending_payable_scanner_for_no_p_p_found( test_name: &str, consuming_wallet: Wallet, pending_payable_start_scan_params_arc: &Arc< @@ -3162,7 +3217,7 @@ mod tests { act_started_at: SystemTime, act_finished_at: SystemTime, ) { - let pp_logger = pending_payable_common( + let pp_logger = assert_pending_payable_scanner_ran( consuming_wallet, pending_payable_start_scan_params_arc, act_started_at, @@ -3183,7 +3238,7 @@ mod tests { assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); } - fn assert_pending_payable_scanner_for_some_pending_payable_found( + fn assert_pending_payable_scanner_for_some_p_p_found( test_name: &str, consuming_wallet: Wallet, scan_params: &ScanParams, @@ -3195,7 +3250,7 @@ mod tests { act_started_at: SystemTime, act_finished_at: SystemTime, ) { - let pp_start_scan_logger = pending_payable_common( + let pp_start_scan_logger = assert_pending_payable_scanner_ran( consuming_wallet, &scan_params.pending_payable_start_scan, act_started_at, @@ -3226,7 +3281,7 @@ mod tests { ); } - fn pending_payable_common( + fn assert_pending_payable_scanner_ran( consuming_wallet: Wallet, pending_payable_start_scan_params_arc: &Arc< Mutex, Logger, String)>>, @@ -3264,12 +3319,13 @@ mod tests { pp_logger } - fn assert_payable_scanner_for_no_pending_payable_found( + fn assert_payable_scanner_for_no_p_p_found( + payable_scanner_start_scan_arc: &Arc< + Mutex, Logger, String)>>, + >, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, - compute_interval_params_arc: Arc>>, + compute_interval_until_next_new_payable_scan_params_arc: Arc>>, new_payable_expected_computed_interval: Duration, - act_started_at: SystemTime, - act_finished_at: SystemTime, ) { // Note that there is no functionality from the payable scanner actually running. // We only witness it to be scheduled. @@ -3286,10 +3342,19 @@ mod tests { new_payable_expected_computed_interval )] ); - let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); - let (p_scheduling_now, last_new_payable_scan_timestamp, _) = - compute_interval_params.remove(0); - assert_eq!(last_new_payable_scan_timestamp, UNIX_EPOCH); + let compute_interval_until_next_new_payable_scan_params = + compute_interval_until_next_new_payable_scan_params_arc + .lock() + .unwrap(); + assert_eq!( + *compute_interval_until_next_new_payable_scan_params, + vec![()] + ); + let payable_scanner_start_scan = payable_scanner_start_scan_arc.lock().unwrap(); + assert!( + payable_scanner_start_scan.is_empty(), + "We expected the payable scanner not to run in this test, but it did" + ); let scan_for_new_payables_notify_params = notify_and_notify_later_params .new_payables_notify .lock() @@ -3308,22 +3373,29 @@ mod tests { "We did not expect any scheduling of retry payables, but it happened {:?}", scan_for_retry_payables_notify_params ); - assert!( - act_started_at <= p_scheduling_now && p_scheduling_now <= act_finished_at, - "The payable scan scheduling was supposed to take place between {:?} and {:?} \ - but it was {:?}", - act_started_at, - act_finished_at, - p_scheduling_now - ); } - fn assert_payable_scanner_for_some_pending_payable_found( + fn assert_payable_scanner_for_some_p_p_found( test_name: &str, consuming_wallet: Wallet, scan_params: &ScanParams, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, expected_sent_payables: SentPayables, + ) { + assert_payable_scanner_ran_for_some_p_p_found( + test_name, + consuming_wallet, + scan_params, + expected_sent_payables, + ); + assert_scan_scheduling_for_some_p_p_found(notify_and_notify_later_params); + } + + fn assert_payable_scanner_ran_for_some_p_p_found( + test_name: &str, + consuming_wallet: Wallet, + scan_params: &ScanParams, + expected_sent_payables: SentPayables, ) { let mut payable_start_scan_params = scan_params.payable_start_scan.lock().unwrap(); let (p_wallet, _, p_response_skeleton_opt, p_start_scan_logger, p_trigger_msg_type_str) = @@ -3355,6 +3427,11 @@ mod tests { test_name, Some("retry payable finish"), ); + } + + fn assert_scan_scheduling_for_some_p_p_found( + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, + ) { let scan_for_new_payables_notify_later_params = notify_and_notify_later_params .new_payables_notify_later .lock() @@ -3395,6 +3472,20 @@ mod tests { Mutex>, >, receivable_scan_interval: Duration, + ) { + assert_receivable_scan_ran(test_name, receivable_start_scan_params_arc, earning_wallet); + assert_another_receivable_scan_scheduled( + scan_for_receivables_notify_later_params_arc, + receivable_scan_interval, + ) + } + + fn assert_receivable_scan_ran( + test_name: &str, + receivable_start_scan_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + earning_wallet: Wallet, ) { let mut receivable_start_scan_params = receivable_start_scan_params_arc.lock().unwrap(); let (r_wallet, _r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = @@ -3403,15 +3494,23 @@ mod tests { assert_eq!(r_response_skeleton_opt, None); assert!( r_trigger_msg_name_str.contains("Receivable"), - "Should contain Receivable but {}", + "Should contain 'Receivable' but {}", r_trigger_msg_name_str ); assert!( receivable_start_scan_params.is_empty(), - "Should be already empty but was {:?}", + "Should be empty by now but was {:?}", receivable_start_scan_params ); assert_using_the_same_logger(&r_logger, test_name, Some("r")); + } + + fn assert_another_receivable_scan_scheduled( + scan_for_receivables_notify_later_params_arc: &Arc< + Mutex>, + >, + receivable_scan_interval: Duration, + ) { let scan_for_receivables_notify_later_params = scan_for_receivables_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -5390,10 +5489,10 @@ mod tests { } #[test] - fn accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely() { + fn accountant_confirms_all_pending_txs_and_schedules_new_payable_scanner_timely() { init_test_logging(); let test_name = - "accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely"; + "accountant_confirms_all_pending_txs_and_schedules_new_payable_scanner_timely"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); @@ -5410,23 +5509,12 @@ mod tests { .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); - let last_new_payable_scan_timestamp = SystemTime::now() - .checked_sub(Duration::from_secs(3)) - .unwrap(); - let nominal_interval = Duration::from_secs(6); let expected_computed_interval = Duration::from_secs(3); let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) + // This determines the test .compute_interval_result(Some(expected_computed_interval)); - subject.scan_schedulers.payable.new_payable_interval = nominal_interval; subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); - subject - .scan_schedulers - .payable - .inner - .lock() - .unwrap() - .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); @@ -5463,15 +5551,7 @@ mod tests { "Should be empty but {:?}", finish_scan_params ); - let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); - let (_, last_new_payable_timestamp_actual, scan_interval_actual) = - compute_interval_params.remove(0); - assert_eq!( - last_new_payable_timestamp_actual, - last_new_payable_scan_timestamp - ); - assert_eq!(scan_interval_actual, nominal_interval); - assert!(compute_interval_params.is_empty()); + // Here, we see that the next payable scan is scheduled for the future, in the expected interval. let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); assert_eq!( *new_payable_notify_later, @@ -5505,22 +5585,11 @@ mod tests { .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); - let last_new_payable_scan_timestamp = SystemTime::now() - .checked_sub(Duration::from_secs(8)) - .unwrap(); - let nominal_interval = Duration::from_secs(6); let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) + // This determines the test .compute_interval_result(None); - subject.scan_schedulers.payable.new_payable_interval = nominal_interval; subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); - subject - .scan_schedulers - .payable - .inner - .lock() - .unwrap() - .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); @@ -5554,21 +5623,15 @@ mod tests { "Should be empty but {:?}", finish_scan_params ); - let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); - let (_, last_new_payable_timestamp_actual, scan_interval_actual) = - compute_interval_params.remove(0); - assert_eq!( - last_new_payable_timestamp_actual, - last_new_payable_scan_timestamp - ); - assert_eq!(scan_interval_actual, nominal_interval); - assert!(compute_interval_params.is_empty()); + let compute_interval_params = compute_interval_params_arc.lock().unwrap(); + assert_eq!(*compute_interval_params, vec![()]); let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); assert!( new_payable_notify_later.is_empty(), "should be empty but was: {:?}", new_payable_notify_later ); + // As a proof, the handle for an immediate launch of the new payable scanner was used let new_payable_notify = new_payable_notify_arc.lock().unwrap(); assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]); } @@ -5578,6 +5641,7 @@ mod tests { let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let test_name = "scheduler_for_new_payables_operates_with_proper_now_timestamp"; let mut subject = AccountantBuilder::default() + .bootstrapper_config(make_bc_with_defaults(TEST_DEFAULT_CHAIN)) .logger(Logger::new(test_name)) .build(); let pending_payable_scanner = ScannerMock::new() @@ -5587,21 +5651,21 @@ mod tests { .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); - let last_new_payable_scan_timestamp = SystemTime::now() - .checked_sub(Duration::from_millis(3500)) - .unwrap(); - let new_payable_interval = Duration::from_secs(6); - subject.scan_schedulers.payable.new_payable_interval = new_payable_interval; - subject - .scan_schedulers - .payable - .inner - .lock() - .unwrap() - .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); + let default_scan_intervals = ScanIntervals::compute_default(TEST_DEFAULT_CHAIN); + let mut assertion_interval_computer = NewPayableScanDynIntervalComputerReal::new( + default_scan_intervals.payable_scan_interval, + ); + { + subject + .scan_schedulers + .payable + .dyn_interval_computer + .zero_out(); + assertion_interval_computer.zero_out(); + } let system = System::new(test_name); let subject_addr = subject.start(); let (msg, _) = make_tx_receipts_msg(vec![SeedsToMakeUpPayableWithStatus { @@ -5611,26 +5675,15 @@ mod tests { block_number: U64::from(100), }), }]); + let left_side_bound = assertion_interval_computer.compute_interval().unwrap(); subject_addr.try_send(msg).unwrap(); - let before = SystemTime::now(); System::current().stop(); system.run(); - let after = SystemTime::now(); let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); let (_, actual_interval) = new_payable_notify_later[0]; - let interval_computer = NewPayableScanDynIntervalComputerReal::default(); - let left_side_bound = interval_computer - .compute_interval( - before, - last_new_payable_scan_timestamp, - new_payable_interval, - ) - .unwrap(); - let right_side_bound = interval_computer - .compute_interval(after, last_new_payable_scan_timestamp, new_payable_interval) - .unwrap(); + let right_side_bound = assertion_interval_computer.compute_interval().unwrap(); assert!( left_side_bound >= actual_interval && actual_interval >= right_side_bound, "expected actual {:?} to be between {:?} and {:?}", @@ -5802,12 +5855,6 @@ mod tests { |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { // Setup let notify_later_params_arc = Arc::new(Mutex::new(vec![])); - scan_schedulers - .payable - .inner - .lock() - .unwrap() - .last_new_payable_scan_timestamp = SystemTime::now(); scan_schedulers.payable.dyn_interval_computer = Box::new( NewPayableScanDynIntervalComputerMock::default() .compute_interval_result(Some(Duration::from_secs(152))), diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 1dc1c3a9f..787b9a8b2 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -580,7 +580,6 @@ mod tests { }; 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, TxHashByTable, }; @@ -631,6 +630,7 @@ mod tests { use masq_lib::logger::Logger; use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::simple_clock::SimpleClockMock; use masq_lib::ui_gateway::NodeToUiMessage; use regex::Regex; use rusqlite::{ffi, ErrorCode}; @@ -1440,7 +1440,7 @@ mod tests { failed_tx_5.status = FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), - &ValidationFailureClockMock::default().now_result(timestamp_c), + &SimpleClockMock::default().now_result(timestamp_c), ))); let tx_receipt_rpc_error_5 = AppRpcError::Remote(RemoteError::InvalidResponse("game over".to_string())); @@ -1456,7 +1456,7 @@ mod tests { let failed_payable_cache = PendingPayableCacheMock::default() .get_record_by_hash_result(Some(failed_tx_2.clone())) .get_record_by_hash_result(Some(failed_tx_5)); - let validation_failure_clock = ValidationFailureClockMock::default() + let validation_failure_clock = SimpleClockMock::default() .now_result(timestamp_a) .now_result(timestamp_b); let mut pending_payable_scanner = PendingPayableScannerBuilder::new() @@ -1512,7 +1512,7 @@ mod tests { assert_eq!( *update_statuses_pending_payable_params, vec![ - hashmap!(tx_hash_4 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_a))))) + hashmap!(tx_hash_4 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &SimpleClockMock::default().now_result(timestamp_a))))) ] ); let update_statuses_failed_payable_params = @@ -1520,7 +1520,7 @@ mod tests { assert_eq!( *update_statuses_failed_payable_params, vec![ - hashmap!(tx_hash_5 => FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_c)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_b))))) + hashmap!(tx_hash_5 => FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &SimpleClockMock::default().now_result(timestamp_c)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &SimpleClockMock::default().now_result(timestamp_b))))) ] ); assert_eq!(subject.scan_started_at(ScanType::PendingPayables), None); diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index cc1052d14..499ee0179 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -1,6 +1,5 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod test_utils; mod tx_receipt_interpreter; pub mod utils; @@ -28,15 +27,13 @@ use crate::accountant::{ TxReceiptResult, TxReceiptsMessage, }; use crate::blockchain::blockchain_interface::data_structures::TxBlock; -use crate::blockchain::errors::validation_status::{ - ValidationFailureClock, ValidationFailureClockReal, -}; use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; use crate::sub_lib::wallet::Wallet; use crate::time_marking_methods; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::simple_clock::{SimpleClock, SimpleClockReal}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use std::cell::RefCell; use std::collections::{BTreeSet, HashMap}; @@ -69,7 +66,7 @@ pub struct PendingPayableScanner { pub financial_statistics: Rc>, pub current_sent_payables: Box>, pub yet_unproven_failed_payables: Box>, - pub clock: Box, + pub clock: Box, } impl ExtendedPendingPayablePrivateScanner for PendingPayableScanner {} @@ -159,7 +156,7 @@ impl PendingPayableScanner { financial_statistics, current_sent_payables: Box::new(CurrentPendingPayables::default()), yet_unproven_failed_payables: Box::new(RecheckRequiringFailures::default()), - clock: Box::new(ValidationFailureClockReal::default()), + clock: Box::new(SimpleClockReal::default()), } } @@ -792,7 +789,7 @@ impl PendingPayableScanner { fn prepare_statuses_for_update( failures: &[FailedValidation], - clock: &dyn ValidationFailureClock, + clock: &dyn SimpleClock, logger: &Logger, ) -> HashMap { failures @@ -850,7 +847,6 @@ mod tests { 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, FailedValidationByTable, PendingPayableCache, PendingPayableScanResult, PresortedTxFailure, @@ -870,16 +866,16 @@ mod tests { use crate::blockchain::errors::rpc_errors::{ AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteErrorKind, }; - use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClockReal, ValidationStatus, - }; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::test_utils::{make_paying_wallet, make_wallet}; use itertools::Itertools; use masq_lib::logger::Logger; use masq_lib::messages::{ToMessageBody, UiScanResponse}; + use masq_lib::simple_clock::SimpleClockReal; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::simple_clock::SimpleClockMock; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use std::collections::{BTreeSet, HashMap}; use std::ops::Sub; @@ -1296,7 +1292,7 @@ mod tests { failed_tx_2.status = FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), - &ValidationFailureClockMock::default().now_result(timestamp_a), + &SimpleClockMock::default().now_result(timestamp_a), ))); let failed_payable_dao = FailedPayableDaoMock::default() .retrieve_txs_params(&retrieve_failed_txs_params_arc) @@ -1311,7 +1307,7 @@ mod tests { .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() + let validation_failure_clock = SimpleClockMock::default() .now_result(timestamp_a) .now_result(timestamp_b) .now_result(timestamp_c); @@ -1338,7 +1334,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockMock::default().now_result(timestamp_d), + &SimpleClockMock::default().now_result(timestamp_d), ), )), )), @@ -1358,7 +1354,7 @@ mod tests { assert_eq!( *update_statuses_sent_tx_params, vec![ - hashmap![hash_3 => TxStatus::Pending(ValidationStatus::Reattempting (PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_a))))] + hashmap![hash_3 => TxStatus::Pending(ValidationStatus::Reattempting (PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &SimpleClockMock::default().now_result(timestamp_a))))] ] ); let mut update_statuses_failed_tx_params = @@ -1370,10 +1366,10 @@ mod tests { .collect::>(); let expected_params = hashmap!( hash_1 => FailureStatus::RecheckRequired( - ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_b))) + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &SimpleClockMock::default().now_result(timestamp_b))) ), hash_2 => FailureStatus::RecheckRequired( - ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp_d)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockReal::default()))) + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &SimpleClockMock::default().now_result(timestamp_d)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &SimpleClockReal::default()))) ).into_iter().sorted_by_key(|(key,_)|*key).collect::>(); assert_eq!(actual_params, expected_params); assert!( @@ -1469,7 +1465,7 @@ mod tests { .update_statuses_result(Err(SentPayableDaoError::InvalidInput("blah".to_string()))); let subject = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) - .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) + .validation_failure_clock(Box::new(SimpleClockReal::default())) .build(); let _ = subject @@ -1492,7 +1488,7 @@ mod tests { .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("blah".to_string()))); let subject = PendingPayableScannerBuilder::new() .failed_payable_dao(failed_payable_dao) - .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) + .validation_failure_clock(Box::new(SimpleClockReal::default())) .build(); let _ = subject @@ -1522,9 +1518,7 @@ mod tests { let subject = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) .failed_payable_dao(failed_payable_dao) - .validation_failure_clock(Box::new( - ValidationFailureClockMock::default().now_result(timestamp), - )) + .validation_failure_clock(Box::new(SimpleClockMock::default().now_result(timestamp))) .build(); let detected_failures = DetectedFailures { tx_failures: vec![PresortedTxFailure::NewEntry(failed_tx_1.clone())], @@ -1550,7 +1544,7 @@ mod tests { assert_eq!( *update_statuses_params, vec![ - hashmap!(tx_hash_2 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp))))) + hashmap!(tx_hash_2 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &SimpleClockMock::default().now_result(timestamp))))) ] ); } 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 833f5ef21..fc16c5713 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 @@ -237,13 +237,12 @@ mod tests { use crate::blockchain::errors::rpc_errors::{ AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, }; - use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClockReal, ValidationStatus, - }; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::make_tx_hash; use crate::test_utils::unshared_test_utils::capture_digits_with_separators_from_str; use masq_lib::logger::Logger; + use masq_lib::simple_clock::SimpleClockReal; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::collections::BTreeSet; use std::sync::{Arc, Mutex}; @@ -590,7 +589,7 @@ mod tests { test_name, TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ))), ); } @@ -654,7 +653,7 @@ mod tests { test_name, FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ))), ); } diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index f86984df0..7a1d18eaa 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -5,12 +5,11 @@ use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::{ResponseSkeleton, TxReceiptResult}; use crate::blockchain::errors::rpc_errors::AppRpcError; -use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClock, ValidationStatus, -}; +use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; use crate::blockchain::errors::BlockchainErrorKind; use itertools::Either; use masq_lib::logger::Logger; +use masq_lib::simple_clock::SimpleClock; use masq_lib::ui_gateway::NodeToUiMessage; use std::cmp::Ordering; use std::collections::HashMap; @@ -153,7 +152,7 @@ where } } - pub fn new_status(&self, clock: &dyn ValidationFailureClock) -> Option { + pub fn new_status(&self, clock: &dyn SimpleClock) -> Option { self.current_status .update_after_failure(self.validation_failure, clock) } @@ -163,7 +162,7 @@ pub trait UpdatableValidationStatus { fn update_after_failure( &self, error: BlockchainErrorKind, - clock: &dyn ValidationFailureClock, + clock: &dyn SimpleClock, ) -> Option where Self: Sized; @@ -173,7 +172,7 @@ impl UpdatableValidationStatus for TxStatus { fn update_after_failure( &self, error: BlockchainErrorKind, - clock: &dyn ValidationFailureClock, + clock: &dyn SimpleClock, ) -> Option { match self { TxStatus::Pending(ValidationStatus::Waiting) => Some(TxStatus::Pending( @@ -193,7 +192,7 @@ impl UpdatableValidationStatus for FailureStatus { fn update_after_failure( &self, error: BlockchainErrorKind, - clock: &dyn ValidationFailureClock, + clock: &dyn SimpleClock, ) -> Option { match self { FailureStatus::RecheckRequired(ValidationStatus::Waiting) => { @@ -389,20 +388,19 @@ 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::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; - use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClockReal, ValidationStatus, - }; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::make_tx_hash; use masq_lib::logger::Logger; + use masq_lib::simple_clock::SimpleClockReal; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::simple_clock::SimpleClockMock; use std::cmp::Ordering; use std::collections::BTreeSet; use std::ops::Sub; @@ -456,7 +454,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), )), ), @@ -531,7 +529,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ))), )), FailedValidationByTable::FailedPayable(FailedValidation::new( @@ -544,7 +542,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), )), )), @@ -943,7 +941,7 @@ mod tests { let timestamp_a = SystemTime::now(); let timestamp_b = SystemTime::now().sub(Duration::from_secs(11)); let timestamp_c = SystemTime::now().sub(Duration::from_secs(22)); - let clock = ValidationFailureClockMock::default() + let clock = SimpleClockMock::default() .now_result(timestamp_a) .now_result(timestamp_c); let cases = vec![ @@ -958,7 +956,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockMock::default().now_result(timestamp_a), + &SimpleClockMock::default().now_result(timestamp_a), ), ))), ), @@ -973,13 +971,13 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockMock::default().now_result(timestamp_b), + &SimpleClockMock::default().now_result(timestamp_b), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), )), ), @@ -988,19 +986,19 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockMock::default().now_result(timestamp_c), + &SimpleClockMock::default().now_result(timestamp_c), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockMock::default().now_result(timestamp_b), + &SimpleClockMock::default().now_result(timestamp_b), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), ))), ), @@ -1016,7 +1014,7 @@ mod tests { let timestamp_a = SystemTime::now().sub(Duration::from_secs(222)); let timestamp_b = SystemTime::now().sub(Duration::from_secs(3333)); let timestamp_c = SystemTime::now().sub(Duration::from_secs(44444)); - let clock = ValidationFailureClockMock::default() + let clock = SimpleClockMock::default() .now_result(timestamp_a) .now_result(timestamp_b); let cases = vec![ @@ -1031,7 +1029,7 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( LocalErrorKind::Internal, )), - &ValidationFailureClockMock::default().now_result(timestamp_a), + &SimpleClockMock::default().now_result(timestamp_a), )), )), ), @@ -1046,13 +1044,13 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockMock::default().now_result(timestamp_b), + &SimpleClockMock::default().now_result(timestamp_b), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::InvalidResponse, )), - &ValidationFailureClockMock::default().now_result(timestamp_c), + &SimpleClockMock::default().now_result(timestamp_c), ), )), ), @@ -1062,19 +1060,19 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockMock::default().now_result(timestamp_b), + &SimpleClockMock::default().now_result(timestamp_b), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::InvalidResponse, )), - &ValidationFailureClockMock::default().now_result(timestamp_c), + &SimpleClockMock::default().now_result(timestamp_c), ) .add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( RemoteErrorKind::Unreachable, )), - &ValidationFailureClockReal::default(), + &SimpleClockReal::default(), ), ), )), @@ -1088,7 +1086,7 @@ mod tests { #[test] fn failed_validation_new_status_has_no_effect_on_unexpected_tx_status() { - let validation_failure_clock = ValidationFailureClockMock::default(); + let validation_failure_clock = SimpleClockMock::default(); let mal_validated_tx_status = FailedValidation::new( make_tx_hash(123), BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), @@ -1107,7 +1105,7 @@ mod tests { #[test] fn failed_validation_new_status_has_no_effect_on_unexpected_failure_status() { - let validation_failure_clock = ValidationFailureClockMock::default(); + let validation_failure_clock = SimpleClockMock::default(); let mal_validated_failure_statuses = vec![ FailedValidation::new( make_tx_hash(456), diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 2f9cc1d07..dbaf83681 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -12,8 +12,8 @@ use crate::sub_lib::utils::{ use actix::{Actor, Context, Handler}; use masq_lib::logger::Logger; use masq_lib::messages::ScanType; +use masq_lib::simple_clock::{SimpleClock, SimpleClockReal}; use std::fmt::{Debug, Display, Formatter}; -use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub struct ScanSchedulers { @@ -79,8 +79,8 @@ impl From for ScanType { pub struct PayableScanScheduler { pub new_payable_notify_later: Box>, pub dyn_interval_computer: Box, - pub inner: Arc>, - pub new_payable_interval: Duration, + // pub inner: Arc>, + // pub new_payable_interval: Duration, pub new_payable_notify: Box>, pub retry_payable_notify: Box>, } @@ -89,24 +89,18 @@ impl PayableScanScheduler { fn new(new_payable_interval: Duration) -> Self { Self { new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), - dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::default()), - inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), - new_payable_interval, + dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::new( + new_payable_interval, + )), + // inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), + // new_payable_interval, new_payable_notify: Box::new(NotifyHandleReal::default()), retry_payable_notify: Box::new(NotifyHandleReal::default()), } } pub fn schedule_new_payable_scan(&self, ctx: &mut Context, logger: &Logger) { - let inner = self.inner.lock().expect("couldn't acquire inner"); - let last_new_payable_scan_timestamp = inner.last_new_payable_scan_timestamp; - let new_payable_interval = self.new_payable_interval; - let now = SystemTime::now(); - if let Some(interval) = self.dyn_interval_computer.compute_interval( - now, - last_new_payable_scan_timestamp, - new_payable_interval, - ) { + if let Some(interval) = self.dyn_interval_computer.compute_interval() { debug!( logger, "Scheduling a new-payable scan in {}ms", @@ -132,6 +126,10 @@ impl PayableScanScheduler { } } + pub fn update_last_new_payable_scan_timestamp(&mut self) { + self.dyn_interval_computer.zero_out(); + } + // This message ships into the Accountant's mailbox with no delay. // Can also be triggered by command, following up after the PendingPayableScanner // that requests it. That's why the response skeleton is possible to be used. @@ -152,49 +150,51 @@ impl PayableScanScheduler { } } -pub struct PayableScanSchedulerInner { - pub last_new_payable_scan_timestamp: SystemTime, -} +pub trait NewPayableScanDynIntervalComputer { + fn compute_interval(&self) -> Option; -impl Default for PayableScanSchedulerInner { - fn default() -> Self { - Self { - last_new_payable_scan_timestamp: UNIX_EPOCH, - } - } -} + fn zero_out(&mut self); -pub trait NewPayableScanDynIntervalComputer { - fn compute_interval( - &self, - now: SystemTime, - last_new_payable_scan_timestamp: SystemTime, - interval: Duration, - ) -> Option; + as_any_ref_in_trait!(); } -#[derive(Default)] -pub struct NewPayableScanDynIntervalComputerReal {} +pub struct NewPayableScanDynIntervalComputerReal { + scan_interval: Duration, + last_scan_timestamp: SystemTime, + clock: Box, +} impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal { - fn compute_interval( - &self, - now: SystemTime, - last_new_payable_scan_timestamp: SystemTime, - interval: Duration, - ) -> Option { + fn compute_interval(&self) -> Option { + let now = self.clock.now(); let elapsed = now - .duration_since(last_new_payable_scan_timestamp) + .duration_since(self.last_scan_timestamp) .unwrap_or_else(|_| { panic!( "Now ({:?}) earlier than past timestamp ({:?})", - now, last_new_payable_scan_timestamp + now, self.last_scan_timestamp ) }); - if elapsed >= interval { + if elapsed >= self.scan_interval { None } else { - Some(interval - elapsed) + Some(self.scan_interval - elapsed) + } + } + + fn zero_out(&mut self) { + self.last_scan_timestamp = SystemTime::now(); + } + + as_any_ref_in_trait_impl!(); +} + +impl NewPayableScanDynIntervalComputerReal { + pub fn new(scan_interval: Duration) -> Self { + Self { + scan_interval, + last_scan_timestamp: UNIX_EPOCH, + clock: Box::new(SimpleClockReal::default()), } } } @@ -338,8 +338,8 @@ impl RescheduleScanOnErrorResolverReal { // StartScanError::NothingToProcess can be evaluated); but may be cautious and // prevent starting the NewPayableScanner. Repeating this scan endlessly may alarm // the user. - // TODO Correctly, a check-point during the bootstrap that wouldn't allow to come - // this far should be the solution. Part of the issue mentioned in GH-799 + // TODO Correctly, a check-point during the bootstrap, not allowing to come + // this far, should be the solution. Part of the issue mentioned in GH-799 ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) } else { unreachable!( @@ -383,6 +383,7 @@ mod tests { NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, }; + use crate::accountant::scanners::test_utils::NewPayableScanDynIntervalComputerMock; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; @@ -391,7 +392,10 @@ mod tests { use masq_lib::logger::Logger; use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::simple_clock::SimpleClockMock; + use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] @@ -405,15 +409,17 @@ mod tests { let schedulers = ScanSchedulers::new(scan_intervals, automatic_scans_enabled); + let payable_interval_computer = schedulers + .payable + .dyn_interval_computer + .as_any() + .downcast_ref::() + .unwrap(); assert_eq!( - schedulers.payable.new_payable_interval, + payable_interval_computer.scan_interval, scan_intervals.payable_scan_interval ); - let payable_scheduler_inner = schedulers.payable.inner.lock().unwrap(); - assert_eq!( - payable_scheduler_inner.last_new_payable_scan_timestamp, - UNIX_EPOCH - ); + assert_eq!(payable_interval_computer.last_scan_timestamp, UNIX_EPOCH); assert_eq!( schedulers.pending_payable.interval, scan_intervals.pending_payable_scan_interval @@ -427,7 +433,7 @@ mod tests { #[test] fn scan_dyn_interval_computer_computes_remaining_time_to_standard_interval_correctly() { - let now = SystemTime::now(); + let (clock, now) = fill_simple_clock_mock_and_return_now(); let inputs = vec![ ( now.checked_sub(Duration::from_secs(32)).unwrap(), @@ -445,12 +451,17 @@ mod tests { Duration::from_secs(4), ), ]; - let subject = NewPayableScanDynIntervalComputerReal::default(); + let mut subject = make_subject(); + subject.clock = Box::new(clock); inputs .into_iter() .for_each(|(past_instant, standard_interval, expected_result)| { - let result = subject.compute_interval(now, past_instant, standard_interval); + subject.scan_interval = standard_interval; + subject.last_scan_timestamp = past_instant; + + let result = subject.compute_interval(); + assert_eq!( result, Some(expected_result), @@ -463,28 +474,33 @@ mod tests { #[test] fn scan_dyn_interval_computer_realizes_the_standard_interval_has_been_exceeded() { - let now = SystemTime::now(); + let (clock, now) = fill_simple_clock_mock_and_return_now(); let inputs = vec![ ( now.checked_sub(Duration::from_millis(32001)).unwrap(), Duration::from_secs(32), ), ( - now.checked_sub(Duration::from_millis(1112)).unwrap(), - Duration::from_millis(1111), + now.checked_sub(Duration::from_nanos(1111112)).unwrap(), + Duration::from_nanos(1111111), ), ( now.checked_sub(Duration::from_secs(200)).unwrap(), Duration::from_secs(123), ), ]; - let subject = NewPayableScanDynIntervalComputerReal::default(); + let mut subject = make_subject(); + subject.clock = Box::new(clock); inputs .into_iter() .enumerate() .for_each(|(idx, (past_instant, standard_interval))| { - let result = subject.compute_interval(now, past_instant, standard_interval); + subject.scan_interval = standard_interval; + subject.last_scan_timestamp = past_instant; + + let result = subject.compute_interval(); + assert_eq!( result, None, @@ -498,13 +514,12 @@ mod tests { #[test] fn scan_dyn_interval_computer_realizes_standard_interval_just_met() { let now = SystemTime::now(); - let subject = NewPayableScanDynIntervalComputerReal::default(); + let mut subject = make_subject(); + subject.last_scan_timestamp = now.checked_sub(Duration::from_secs(180)).unwrap(); + subject.scan_interval = Duration::from_secs(180); + subject.clock = Box::new(SimpleClockMock::default().now_result(now)); - let result = subject.compute_interval( - now, - now.checked_sub(Duration::from_secs(32)).unwrap(), - Duration::from_secs(32), - ); + let result = subject.compute_interval(); assert_eq!( result, @@ -517,8 +532,8 @@ mod tests { #[test] #[cfg(windows)] #[should_panic( - expected = "Now (SystemTime { intervals: 116454735990000000 }) earlier than past timestamp \ - (SystemTime { intervals: 116454736000000000 })" + expected = "Now (SystemTime { intervals: 116454736000000000 }) earlier than past timestamp \ + (SystemTime { intervals: 116454737000000000 })" )] fn scan_dyn_interval_computer_panics() { test_scan_dyn_interval_computer_panics() @@ -527,8 +542,8 @@ mod tests { #[test] #[cfg(not(windows))] #[should_panic( - expected = "Now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier than past timestamp \ - (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + expected = "Now (SystemTime { tv_sec: 1000000, tv_nsec: 0 }) earlier than past timestamp \ + (SystemTime { tv_sec: 1000001, tv_nsec: 0 })" )] fn scan_dyn_interval_computer_panics() { test_scan_dyn_interval_computer_panics() @@ -538,15 +553,76 @@ mod tests { let now = UNIX_EPOCH .checked_add(Duration::from_secs(1_000_000)) .unwrap(); - let subject = NewPayableScanDynIntervalComputerReal::default(); + let mut subject = make_subject(); + subject.clock = Box::new(SimpleClockMock::default().now_result(now)); + subject.last_scan_timestamp = now.checked_add(Duration::from_secs(1)).unwrap(); - let _ = subject.compute_interval( - now.checked_sub(Duration::from_secs(1)).unwrap(), - now, - Duration::from_secs(32), + let _ = subject.compute_interval(); + } + + #[test] + fn zero_out_works_for_default_subject() { + let mut subject = make_subject(); + let last_scan_timestamp_before = subject.last_scan_timestamp; + let before_act = SystemTime::now(); + + subject.zero_out(); + + let after_act = SystemTime::now(); + let last_scan_timestamp_after = subject.last_scan_timestamp; + assert_eq!(last_scan_timestamp_before, UNIX_EPOCH); + assert!( + before_act <= last_scan_timestamp_after && last_scan_timestamp_after <= after_act, + "we expected the last_scan_timestamp to be reset to now, but it was not" ); } + #[test] + fn zero_out_works_for_general_subject() { + let mut subject = make_subject(); + subject.last_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(100)) + .unwrap(); + let before_act = SystemTime::now(); + + subject.zero_out(); + + let after_act = SystemTime::now(); + let last_scan_timestamp_after = subject.last_scan_timestamp; + assert!( + before_act <= last_scan_timestamp_after && last_scan_timestamp_after <= after_act, + "we expected the last_scan_timestamp to be reset to now, but it was not" + ); + } + + #[test] + fn update_last_new_payable_scan_timestamp_works() { + let zero_out_params_arc = Arc::new(Mutex::new(vec![])); + let scan_intervals = ScanIntervals::compute_default(TEST_DEFAULT_CHAIN); + let mut subject = ScanSchedulers::new(scan_intervals, true); + subject.payable.dyn_interval_computer = Box::new( + NewPayableScanDynIntervalComputerMock::default().zero_out_params(&zero_out_params_arc), + ); + + subject.payable.update_last_new_payable_scan_timestamp(); + + let zero_out_params = zero_out_params_arc.lock().unwrap(); + assert_eq!(*zero_out_params, vec![()]) + } + + fn make_subject() -> NewPayableScanDynIntervalComputerReal { + // The interval is just a garbage value, we reset it in the tests by injection if needed + NewPayableScanDynIntervalComputerReal::new(Duration::from_secs(100)) + } + + fn fill_simple_clock_mock_and_return_now() -> (SimpleClockMock, SystemTime) { + let now = SystemTime::now(); + ( + (0..3).fold(SimpleClockMock::default(), |clock, _| clock.now_result(now)), + now, + ) + } + lazy_static! { static ref ALL_START_SCAN_ERRORS: Vec = { diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 731fb508d..4b11abee2 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -336,31 +336,26 @@ impl ScannerMockMarker for ScannerMock>>, + compute_interval_params: Arc>>, compute_interval_results: RefCell>>, + zero_out_params: Arc>>, } impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerMock { - fn compute_interval( - &self, - now: SystemTime, - last_new_payable_scan_timestamp: SystemTime, - interval: Duration, - ) -> Option { - self.compute_interval_params.lock().unwrap().push(( - now, - last_new_payable_scan_timestamp, - interval, - )); + fn compute_interval(&self) -> Option { + self.compute_interval_params.lock().unwrap().push(()); self.compute_interval_results.borrow_mut().remove(0) } + + fn zero_out(&mut self) { + self.zero_out_params.lock().unwrap().push(()); + } + + as_any_ref_in_trait_impl!(); } impl NewPayableScanDynIntervalComputerMock { - pub fn compute_interval_params( - mut self, - params: &Arc>>, - ) -> Self { + pub fn compute_interval_params(mut self, params: &Arc>>) -> Self { self.compute_interval_params = params.clone(); self } @@ -369,6 +364,11 @@ impl NewPayableScanDynIntervalComputerMock { self.compute_interval_results.borrow_mut().push(result); self } + + pub fn zero_out_params(mut self, params: &Arc>>) -> Self { + self.zero_out_params = params.clone(); + self + } } pub enum ReplacementType diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 8d6fb49ed..b460363ee 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -25,14 +25,12 @@ use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdju 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::test_utils::PendingPayableCacheMock; use crate::accountant::{gwei_to_wei, Accountant}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, TxBlock}; -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; @@ -47,6 +45,8 @@ use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMoc use crate::test_utils::unshared_test_utils::make_bc_with_defaults; use ethereum_types::U64; use masq_lib::logger::Logger; +use masq_lib::simple_clock::SimpleClock; +use masq_lib::test_utils::simple_clock::SimpleClockMock; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; @@ -101,34 +101,6 @@ pub fn make_transaction_block(num: u64) -> TxBlock { } } -pub struct AccountantBuilder { - config_opt: Option, - consuming_wallet_opt: Option, - logger_opt: Option, - payable_dao_factory_opt: Option, - receivable_dao_factory_opt: Option, - sent_payable_dao_factory_opt: Option, - failed_payable_dao_factory_opt: Option, - banned_dao_factory_opt: Option, - config_dao_factory_opt: Option, -} - -impl Default for AccountantBuilder { - fn default() -> Self { - Self { - config_opt: None, - consuming_wallet_opt: None, - logger_opt: None, - payable_dao_factory_opt: None, - receivable_dao_factory_opt: None, - sent_payable_dao_factory_opt: None, - failed_payable_dao_factory_opt: None, - banned_dao_factory_opt: None, - config_dao_factory_opt: None, - } - } -} - pub enum DaoWithDestination { ForAccountantBody(T), ForPendingPayableScanner(T), @@ -276,6 +248,34 @@ const RECEIVABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 2] = DestinationMarker::ReceivableScanner, ]; +pub struct AccountantBuilder { + config_opt: Option, + consuming_wallet_opt: Option, + logger_opt: Option, + payable_dao_factory_opt: Option, + receivable_dao_factory_opt: Option, + sent_payable_dao_factory_opt: Option, + failed_payable_dao_factory_opt: Option, + banned_dao_factory_opt: Option, + config_dao_factory_opt: Option, +} + +impl Default for AccountantBuilder { + fn default() -> Self { + Self { + config_opt: None, + consuming_wallet_opt: None, + logger_opt: None, + payable_dao_factory_opt: None, + receivable_dao_factory_opt: None, + sent_payable_dao_factory_opt: None, + failed_payable_dao_factory_opt: None, + banned_dao_factory_opt: None, + config_dao_factory_opt: None, + } + } +} + impl AccountantBuilder { pub fn bootstrapper_config(mut self, config: BootstrapperConfig) -> Self { self.config_opt = Some(config); @@ -355,39 +355,6 @@ 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 { @@ -1315,7 +1282,7 @@ pub struct PendingPayableScannerBuilder { financial_statistics: FinancialStatistics, current_sent_payables: Box>, yet_unproven_failed_payables: Box>, - clock: Box, + clock: Box, } impl PendingPayableScannerBuilder { @@ -1328,7 +1295,7 @@ impl PendingPayableScannerBuilder { financial_statistics: FinancialStatistics::default(), current_sent_payables: Box::new(PendingPayableCacheMock::default()), yet_unproven_failed_payables: Box::new(PendingPayableCacheMock::default()), - clock: Box::new(ValidationFailureClockMock::default()), + clock: Box::new(SimpleClockMock::default()), } } @@ -1360,7 +1327,7 @@ impl PendingPayableScannerBuilder { self } - pub fn validation_failure_clock(mut self, clock: Box) -> Self { + pub fn validation_failure_clock(mut self, clock: Box) -> Self { self.clock = clock; self } diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index a3e8ada27..6346548a6 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -1,6 +1,7 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::blockchain::errors::BlockchainErrorKind; +use masq_lib::simple_clock::SimpleClock; use serde::de::{SeqAccess, Visitor}; use serde::ser::SerializeSeq; use serde::{ @@ -111,17 +112,13 @@ impl<'de> Visitor<'de> for PreviousAttemptsVisitor { } impl PreviousAttempts { - pub fn new(error: BlockchainErrorKind, clock: &dyn ValidationFailureClock) -> Self { + pub fn new(error: BlockchainErrorKind, clock: &dyn SimpleClock) -> Self { Self { inner: btreemap!(error => ErrorStats::now(clock)), } } - pub fn add_attempt( - mut self, - error: BlockchainErrorKind, - clock: &dyn ValidationFailureClock, - ) -> Self { + pub fn add_attempt(mut self, error: BlockchainErrorKind, clock: &dyn SimpleClock) -> Self { self.inner .entry(error) .and_modify(|stats| stats.increment()) @@ -138,7 +135,7 @@ pub struct ErrorStats { } impl ErrorStats { - pub fn now(clock: &dyn ValidationFailureClock) -> Self { + pub fn now(clock: &dyn SimpleClock) -> Self { Self { first_seen: clock.now(), attempts: 1, @@ -150,26 +147,14 @@ impl ErrorStats { } } -pub trait ValidationFailureClock { - fn now(&self) -> SystemTime; -} - -#[derive(Default)] -pub struct ValidationFailureClockReal {} - -impl ValidationFailureClock for ValidationFailureClockReal { - fn now(&self) -> SystemTime { - SystemTime::now() - } -} - #[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::test_utils::serde_serializer_mock::{SerdeSerializerMock, SerializeSeqMock}; + use masq_lib::simple_clock::SimpleClockReal; + use masq_lib::test_utils::simple_clock::SimpleClockMock; use serde::ser::Error as SerdeError; use std::collections::BTreeSet; use std::time::Duration; @@ -177,7 +162,7 @@ mod tests { #[test] fn previous_attempts_and_validation_failure_clock_work_together_fine() { - let validation_failure_clock = ValidationFailureClockReal::default(); + let validation_failure_clock = SimpleClockReal::default(); // new() let timestamp_a = SystemTime::now(); let subject = PreviousAttempts::new( @@ -261,7 +246,7 @@ mod tests { // #[test] // fn previous_attempts_hash_works_correctly() { // let now = SystemTime::now(); - // let clock = ValidationFailureClockMock::default() + // let clock = SimpleClockMock::default() // .now_result(now) // .now_result(now) // .now_result(now + Duration::from_secs(2)); @@ -300,7 +285,7 @@ mod tests { #[test] fn previous_attempts_ordering_works_correctly_with_mock() { let now = SystemTime::now(); - let clock = ValidationFailureClockMock::default() + let clock = SimpleClockMock::default() .now_result(now) .now_result(now + Duration::from_secs(1)) .now_result(now + Duration::from_secs(2)) @@ -331,7 +316,7 @@ mod tests { let timestamp = UNIX_EPOCH .checked_add(Duration::from_secs(1234567890)) .unwrap(); - let clock = ValidationFailureClockMock::default().now_result(timestamp); + let clock = SimpleClockMock::default().now_result(timestamp); let result = serde_json::to_string(&PreviousAttempts::new(err, &clock)).unwrap(); @@ -349,7 +334,7 @@ mod tests { let timestamp = UNIX_EPOCH .checked_add(Duration::from_secs(1234567890)) .unwrap(); - let clock = ValidationFailureClockMock::default().now_result(timestamp); + let clock = SimpleClockMock::default().now_result(timestamp); let result = PreviousAttempts::new(err, &clock).serialize(mock); @@ -366,7 +351,7 @@ mod tests { let timestamp = UNIX_EPOCH .checked_add(Duration::from_secs(1234567890)) .unwrap(); - let clock = ValidationFailureClockMock::default().now_result(timestamp); + let clock = SimpleClockMock::default().now_result(timestamp); let result = PreviousAttempts::new(err, &clock).serialize(mock); @@ -383,7 +368,7 @@ mod tests { let timestamp = UNIX_EPOCH .checked_add(Duration::from_secs(1234567890)) .unwrap(); - let clock = ValidationFailureClockMock::default().now_result(timestamp); + let clock = SimpleClockMock::default().now_result(timestamp); let result = PreviousAttempts::new(err, &clock).serialize(mock); @@ -399,7 +384,7 @@ mod tests { let timestamp = UNIX_EPOCH .checked_add(Duration::from_secs(1234567890)) .unwrap(); - let clock = ValidationFailureClockMock::default().now_result(timestamp); + let clock = SimpleClockMock::default().now_result(timestamp); assert_eq!( result.unwrap().inner, btreemap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) @@ -422,7 +407,7 @@ mod tests { #[test] fn validation_status_ordering_works_correctly() { let now = SystemTime::now(); - let clock = ValidationFailureClockMock::default() + let clock = SimpleClockMock::default() .now_result(now) .now_result(now + Duration::from_secs(1)); From 13c3910347fe4feeae7180edd77496548efc30f6 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 2 Oct 2025 23:41:21 +0200 Subject: [PATCH 21/37] GH-598: debug to trace --- node/src/accountant/mod.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 0a58004cc..4cb636385 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -776,7 +776,7 @@ impl Accountant { fn handle_report_services_consumed_message(&mut self, msg: ReportServicesConsumedMessage) { let msg_id = self.msg_id(); - debug!( + trace!( self.logger, "MsgId {}: Accruing debt to {} for consuming {} exited bytes", msg_id, @@ -791,7 +791,7 @@ impl Accountant { &msg.exit.earning_wallet, ); msg.routing.iter().for_each(|routing_service| { - debug!( + trace!( self.logger, "MsgId {}: Accruing debt to {} for consuming {} routed bytes", msg_id, @@ -4792,20 +4792,6 @@ mod tests { ) ] ); - let test_log_handler = TestLogHandler::new(); - - test_log_handler.exists_log_containing(&format!( - "DEBUG: Accountant: MsgId 123: Accruing debt to {} for consuming 1200 exited bytes", - earning_wallet_exit - )); - test_log_handler.exists_log_containing(&format!( - "DEBUG: Accountant: MsgId 123: Accruing debt to {} for consuming 3456 routed bytes", - earning_wallet_routing_1 - )); - test_log_handler.exists_log_containing(&format!( - "DEBUG: Accountant: MsgId 123: Accruing debt to {} for consuming 3456 routed bytes", - earning_wallet_routing_2 - )); } fn assert_that_we_do_not_charge_our_own_wallet_for_consumed_services( From 35dd4c4c94bb4057fc2f2d04b1177c30de4e32f6 Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 3 Oct 2025 00:40:50 +0200 Subject: [PATCH 22/37] GH-598: fixing due to windows issues --- node/src/accountant/scanners/scan_schedulers.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index dbaf83681..6c9b27a2c 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -480,10 +480,6 @@ mod tests { now.checked_sub(Duration::from_millis(32001)).unwrap(), Duration::from_secs(32), ), - ( - now.checked_sub(Duration::from_nanos(1111112)).unwrap(), - Duration::from_nanos(1111111), - ), ( now.checked_sub(Duration::from_secs(200)).unwrap(), Duration::from_secs(123), @@ -533,7 +529,7 @@ mod tests { #[cfg(windows)] #[should_panic( expected = "Now (SystemTime { intervals: 116454736000000000 }) earlier than past timestamp \ - (SystemTime { intervals: 116454737000000000 })" + (SystemTime { intervals: 116454736010000000 })" )] fn scan_dyn_interval_computer_panics() { test_scan_dyn_interval_computer_panics() From bedae7ba11132d4a8c71594250cc4b430b4169e3 Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 6 Oct 2025 16:07:59 +0200 Subject: [PATCH 23/37] GH-598: first QA fix - big half of the review done --- node/src/accountant/mod.rs | 113 +++++++----- .../accountant/scanners/scan_schedulers.rs | 172 ++++++++++-------- node/src/accountant/scanners/test_utils.rs | 41 +++-- .../blockchain/errors/validation_status.rs | 39 ---- 4 files changed, 178 insertions(+), 187 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 4cb636385..ce7006785 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -973,9 +973,7 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - self.scan_schedulers - .payable - .update_last_new_payable_scan_timestamp(); + self.scan_schedulers.payable.reset_scan_timer(); match result { Ok(scan_message) => { @@ -1311,11 +1309,11 @@ mod tests { 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, + NewPayableScanIntervalComputer, NewPayableScanIntervalComputerReal, ScanTiming, }; use crate::accountant::scanners::test_utils::{ - MarkScanner, NewPayableScanDynIntervalComputerMock, PendingPayableCacheMock, - ReplacementType, RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement, + MarkScanner, NewPayableScanIntervalComputerMock, PendingPayableCacheMock, ReplacementType, + RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement, }; use crate::accountant::scanners::StartScanError; use crate::accountant::test_utils::DaoWithDestination::{ @@ -1524,9 +1522,9 @@ mod tests { result .scan_schedulers .payable - .dyn_interval_computer + .interval_computer .as_any() - .downcast_ref::() + .downcast_ref::() .unwrap(); assert_eq!( result.scan_schedulers.pending_payable.interval, @@ -2374,13 +2372,13 @@ mod tests { fn accountant_sends_qualified_payable_msg_when_qualified_payable_found( act_msg: ActorMessage, initial_templates: Either, - zero_out_params_expected: Vec<()>, + reset_last_scan_timestamp_params_expected: Vec<()>, ) where ActorMessage: Message + Send + 'static, ActorMessage::Result: Send, Accountant: Handler, { - let zero_out_params_arc = Arc::new(Mutex::new(vec![])); + let reset_last_scan_timestamp_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let system = System::new("accountant_sends_qualified_payable_msg_when_qualified_payable_found"); @@ -2408,8 +2406,9 @@ mod tests { subject .scanners .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); - subject.scan_schedulers.payable.dyn_interval_computer = Box::new( - NewPayableScanDynIntervalComputerMock::default().zero_out_params(&zero_out_params_arc), + subject.scan_schedulers.payable.interval_computer = Box::new( + NewPayableScanIntervalComputerMock::default() + .reset_last_scan_timestamp_params(&reset_last_scan_timestamp_params_arc), ); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); @@ -2426,8 +2425,11 @@ mod tests { assert_eq!(blockchain_bridge_recorder.len(), 1); let message = blockchain_bridge_recorder.get_record::(0); assert_eq!(message, &initial_template_msg); - let zero_out_params = zero_out_params_arc.lock().unwrap(); - assert_eq!(*zero_out_params, zero_out_params_expected) + let reset_last_scan_timestamp_params = reset_last_scan_timestamp_params_arc.lock().unwrap(); + assert_eq!( + *reset_last_scan_timestamp_params, + reset_last_scan_timestamp_params_expected + ) } #[test] @@ -2831,7 +2833,7 @@ mod tests { let test_name = "accountant_scans_after_startup_and_does_not_detect_any_pending_payables"; let scan_params = ScanParams::default(); let notify_and_notify_later_params = NotifyAndNotifyLaterParams::default(); - let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let time_until_next_scan_params_arc = Arc::new(Mutex::new(vec![])); let earning_wallet = make_wallet("earning"); let consuming_wallet = make_wallet("consuming"); let system = System::new(test_name); @@ -2853,7 +2855,7 @@ mod tests { set_up_subject_for_no_p_p_found_startup_test( test_name, ¬ify_and_notify_later_params, - &compute_interval_params_arc, + &time_until_next_scan_params_arc, config, pending_payable_scanner, receivable_scanner, @@ -2881,7 +2883,7 @@ mod tests { assert_payable_scanner_for_no_p_p_found( &scan_params.payable_start_scan, ¬ify_and_notify_later_params, - compute_interval_params_arc, + time_until_next_scan_params_arc, new_payable_expected_computed_interval, ); assert_receivable_scanner( @@ -3056,7 +3058,7 @@ mod tests { fn set_up_subject_for_no_p_p_found_startup_test( test_name: &str, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, - compute_interval_params_arc: &Arc>>, + time_until_next_scan_params_arc: &Arc>>, config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, @@ -3101,10 +3103,12 @@ mod tests { .stop_system_on_count_received(1); subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); subject.scan_schedulers.receivable.interval = receivable_scan_interval; - let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() - .compute_interval_params(&compute_interval_params_arc) - .compute_interval_result(Some(new_payable_expected_computed_interval)); - subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + let interval_computer = NewPayableScanIntervalComputerMock::default() + .time_until_next_scan_params(&time_until_next_scan_params_arc) + .time_until_next_scan_result(ScanTiming::WaitFor( + new_payable_expected_computed_interval, + )); + subject.scan_schedulers.payable.interval_computer = Box::new(interval_computer); ( subject, new_payable_expected_computed_interval, @@ -3324,7 +3328,7 @@ mod tests { Mutex, Logger, String)>>, >, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, - compute_interval_until_next_new_payable_scan_params_arc: Arc>>, + time_until_next_scan_until_next_new_payable_scan_params_arc: Arc>>, new_payable_expected_computed_interval: Duration, ) { // Note that there is no functionality from the payable scanner actually running. @@ -3342,12 +3346,12 @@ mod tests { new_payable_expected_computed_interval )] ); - let compute_interval_until_next_new_payable_scan_params = - compute_interval_until_next_new_payable_scan_params_arc + let time_until_next_scan_until_next_new_payable_scan_params = + time_until_next_scan_until_next_new_payable_scan_params_arc .lock() .unwrap(); assert_eq!( - *compute_interval_until_next_new_payable_scan_params, + *time_until_next_scan_until_next_new_payable_scan_params, vec![()] ); let payable_scanner_start_scan = payable_scanner_start_scan_arc.lock().unwrap(); @@ -5480,7 +5484,7 @@ mod tests { let test_name = "accountant_confirms_all_pending_txs_and_schedules_new_payable_scanner_timely"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); - let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let time_until_next_scan_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); let system = System::new("new_payable_scanner_timely"); @@ -5496,11 +5500,11 @@ mod tests { pending_payable_scanner, ))); let expected_computed_interval = Duration::from_secs(3); - let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() - .compute_interval_params(&compute_interval_params_arc) + let interval_computer = NewPayableScanIntervalComputerMock::default() + .time_until_next_scan_params(&time_until_next_scan_params_arc) // This determines the test - .compute_interval_result(Some(expected_computed_interval)); - subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + .time_until_next_scan_result(ScanTiming::WaitFor(expected_computed_interval)); + subject.scan_schedulers.payable.interval_computer = Box::new(interval_computer); subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); @@ -5557,7 +5561,7 @@ mod tests { let test_name = "accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); - let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let time_until_next_scan_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default() @@ -5571,11 +5575,11 @@ mod tests { .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); - let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() - .compute_interval_params(&compute_interval_params_arc) + let interval_computer = NewPayableScanIntervalComputerMock::default() + .time_until_next_scan_params(&time_until_next_scan_params_arc) // This determines the test - .compute_interval_result(None); - subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + .time_until_next_scan_result(ScanTiming::ReadyNow); + subject.scan_schedulers.payable.interval_computer = Box::new(interval_computer); subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); @@ -5609,8 +5613,8 @@ mod tests { "Should be empty but {:?}", finish_scan_params ); - let compute_interval_params = compute_interval_params_arc.lock().unwrap(); - assert_eq!(*compute_interval_params, vec![()]); + let time_until_next_scan_params = time_until_next_scan_params_arc.lock().unwrap(); + assert_eq!(*time_until_next_scan_params, vec![()]); let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); assert!( new_payable_notify_later.is_empty(), @@ -5641,16 +5645,15 @@ mod tests { NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); let default_scan_intervals = ScanIntervals::compute_default(TEST_DEFAULT_CHAIN); - let mut assertion_interval_computer = NewPayableScanDynIntervalComputerReal::new( - default_scan_intervals.payable_scan_interval, - ); + let mut assertion_interval_computer = + NewPayableScanIntervalComputerReal::new(default_scan_intervals.payable_scan_interval); { subject .scan_schedulers .payable - .dyn_interval_computer - .zero_out(); - assertion_interval_computer.zero_out(); + .interval_computer + .reset_last_scan_timestamp(); + assertion_interval_computer.reset_last_scan_timestamp(); } let system = System::new(test_name); let subject_addr = subject.start(); @@ -5661,7 +5664,13 @@ mod tests { block_number: U64::from(100), }), }]); - let left_side_bound = assertion_interval_computer.compute_interval().unwrap(); + let left_side_bound = if let ScanTiming::WaitFor(interval) = + assertion_interval_computer.time_until_next_scan() + { + interval + } else { + panic!("expected an interval") + }; subject_addr.try_send(msg).unwrap(); @@ -5669,7 +5678,13 @@ mod tests { system.run(); let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); let (_, actual_interval) = new_payable_notify_later[0]; - let right_side_bound = assertion_interval_computer.compute_interval().unwrap(); + let right_side_bound = if let ScanTiming::WaitFor(interval) = + assertion_interval_computer.time_until_next_scan() + { + interval + } else { + panic!("expected an interval") + }; assert!( left_side_bound >= actual_interval && actual_interval >= right_side_bound, "expected actual {:?} to be between {:?} and {:?}", @@ -5841,9 +5856,9 @@ mod tests { |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { // Setup let notify_later_params_arc = Arc::new(Mutex::new(vec![])); - scan_schedulers.payable.dyn_interval_computer = Box::new( - NewPayableScanDynIntervalComputerMock::default() - .compute_interval_result(Some(Duration::from_secs(152))), + scan_schedulers.payable.interval_computer = Box::new( + NewPayableScanIntervalComputerMock::default() + .time_until_next_scan_result(ScanTiming::WaitFor(Duration::from_secs(152))), ); scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 6c9b27a2c..dedfee1e5 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -78,9 +78,7 @@ impl From for ScanType { pub struct PayableScanScheduler { pub new_payable_notify_later: Box>, - pub dyn_interval_computer: Box, - // pub inner: Arc>, - // pub new_payable_interval: Duration, + pub interval_computer: Box, pub new_payable_notify: Box>, pub retry_payable_notify: Box>, } @@ -89,18 +87,16 @@ impl PayableScanScheduler { fn new(new_payable_interval: Duration) -> Self { Self { new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), - dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::new( + interval_computer: Box::new(NewPayableScanIntervalComputerReal::new( new_payable_interval, )), - // inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), - // new_payable_interval, new_payable_notify: Box::new(NotifyHandleReal::default()), retry_payable_notify: Box::new(NotifyHandleReal::default()), } } pub fn schedule_new_payable_scan(&self, ctx: &mut Context, logger: &Logger) { - if let Some(interval) = self.dyn_interval_computer.compute_interval() { + if let ScanTiming::WaitFor(interval) = self.interval_computer.time_until_next_scan() { debug!( logger, "Scheduling a new-payable scan in {}ms", @@ -126,8 +122,8 @@ impl PayableScanScheduler { } } - pub fn update_last_new_payable_scan_timestamp(&mut self) { - self.dyn_interval_computer.zero_out(); + pub fn reset_scan_timer(&mut self) { + self.interval_computer.reset_last_scan_timestamp(); } // This message ships into the Accountant's mailbox with no delay. @@ -150,46 +146,47 @@ impl PayableScanScheduler { } } -pub trait NewPayableScanDynIntervalComputer { - fn compute_interval(&self) -> Option; +pub trait NewPayableScanIntervalComputer { + fn time_until_next_scan(&self) -> ScanTiming; - fn zero_out(&mut self); + fn reset_last_scan_timestamp(&mut self); as_any_ref_in_trait!(); } -pub struct NewPayableScanDynIntervalComputerReal { +pub struct NewPayableScanIntervalComputerReal { scan_interval: Duration, last_scan_timestamp: SystemTime, clock: Box, } -impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal { - fn compute_interval(&self) -> Option { - let now = self.clock.now(); - let elapsed = now +impl NewPayableScanIntervalComputer for NewPayableScanIntervalComputerReal { + fn time_until_next_scan(&self) -> ScanTiming { + let current_time = self.clock.now(); + let time_since_last_scan = current_time .duration_since(self.last_scan_timestamp) .unwrap_or_else(|_| { panic!( - "Now ({:?}) earlier than past timestamp ({:?})", - now, self.last_scan_timestamp + "Current time ({:?}) is earlier than last scan timestamp ({:?})", + current_time, self.last_scan_timestamp ) }); - if elapsed >= self.scan_interval { - None + + if time_since_last_scan >= self.scan_interval { + ScanTiming::ReadyNow } else { - Some(self.scan_interval - elapsed) + ScanTiming::WaitFor(self.scan_interval - time_since_last_scan) } } - fn zero_out(&mut self) { + fn reset_last_scan_timestamp(&mut self) { self.last_scan_timestamp = SystemTime::now(); } as_any_ref_in_trait_impl!(); } -impl NewPayableScanDynIntervalComputerReal { +impl NewPayableScanIntervalComputerReal { pub fn new(scan_interval: Duration) -> Self { Self { scan_interval, @@ -199,6 +196,12 @@ impl NewPayableScanDynIntervalComputerReal { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ScanTiming { + ReadyNow, + WaitFor(Duration), +} + pub struct SimplePeriodicalScanScheduler { pub handle: Box>, pub interval: Duration, @@ -231,13 +234,13 @@ where } } -// Scanners that take part in a scan sequence composed of different scanners must handle -// StartScanErrors delicately to maintain the continuity and periodicity of this process. Where -// possible, either the same, some other, but traditional, or even a totally unrelated scan chosen -// just in the event of emergency, may be scheduled. The intention is to prevent a full panic while -// ensuring no harmful, toxic issues are left behind for the future scans. Following that philosophy, -// panic is justified only if the error was thought to be impossible by design and contextual -// things but still happened. +// In a scan sequence incorporating different scanners, one makes another dependent on the previous +// one. Such scanners must be handling StartScanErrors delicately with the regard to ensuring +// further continuity and periodicity of this process. Where possible, either the same one, some +// tightly related, or even a totally unrelated scan, just for the event of emergency, should be +// scheduled. The intention is to prevent panics while not creating any harmful conditions for +// the scans running in the future. Following this philosophy, panics should be restricted just +// to so-believed unreachable conditions (by the intended design). pub trait RescheduleScanOnErrorResolver { fn resolve_rescheduling_on_error( &self, @@ -380,10 +383,10 @@ impl RescheduleScanOnErrorResolverReal { #[cfg(test)] mod tests { use crate::accountant::scanners::scan_schedulers::{ - NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, + NewPayableScanIntervalComputer, NewPayableScanIntervalComputerReal, PayableSequenceScanner, + ScanReschedulingAfterEarlyStop, ScanSchedulers, ScanTiming, }; - use crate::accountant::scanners::test_utils::NewPayableScanDynIntervalComputerMock; + use crate::accountant::scanners::test_utils::NewPayableScanIntervalComputerMock; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; @@ -411,9 +414,9 @@ mod tests { let payable_interval_computer = schedulers .payable - .dyn_interval_computer + .interval_computer .as_any() - .downcast_ref::() + .downcast_ref::() .unwrap(); assert_eq!( payable_interval_computer.scan_interval, @@ -432,8 +435,8 @@ mod tests { } #[test] - fn scan_dyn_interval_computer_computes_remaining_time_to_standard_interval_correctly() { - let (clock, now) = fill_simple_clock_mock_and_return_now(); + fn scan_interval_computer_computes_remaining_time_to_standard_interval_correctly() { + let (clock, now) = set_up_mocked_clock(); let inputs = vec![ ( now.checked_sub(Duration::from_secs(32)).unwrap(), @@ -451,7 +454,7 @@ mod tests { Duration::from_secs(4), ), ]; - let mut subject = make_subject(); + let mut subject = initialize_scan_interval_computer(); subject.clock = Box::new(clock); inputs @@ -460,21 +463,21 @@ mod tests { subject.scan_interval = standard_interval; subject.last_scan_timestamp = past_instant; - let result = subject.compute_interval(); + let result = subject.time_until_next_scan(); assert_eq!( result, - Some(expected_result), + ScanTiming::WaitFor(expected_result), "We expected Some({}) ms, but got {:?} ms", expected_result.as_millis(), - result.map(|duration| duration.as_millis()) + from_scan_timing_to_millis(result) ) }) } #[test] - fn scan_dyn_interval_computer_realizes_the_standard_interval_has_been_exceeded() { - let (clock, now) = fill_simple_clock_mock_and_return_now(); + fn scan_interval_computer_realizes_the_standard_interval_has_been_exceeded() { + let (clock, now) = set_up_mocked_clock(); let inputs = vec![ ( now.checked_sub(Duration::from_millis(32001)).unwrap(), @@ -485,7 +488,7 @@ mod tests { Duration::from_secs(123), ), ]; - let mut subject = make_subject(); + let mut subject = initialize_scan_interval_computer(); subject.clock = Box::new(clock); inputs @@ -495,74 +498,82 @@ mod tests { subject.scan_interval = standard_interval; subject.last_scan_timestamp = past_instant; - let result = subject.compute_interval(); + let result = subject.time_until_next_scan(); assert_eq!( result, - None, + ScanTiming::ReadyNow, "We expected None ms, but got {:?} ms at idx {}", - result.map(|duration| duration.as_millis()), + from_scan_timing_to_millis(result), idx ) }) } #[test] - fn scan_dyn_interval_computer_realizes_standard_interval_just_met() { + fn scan_interval_computer_realizes_standard_interval_just_met() { let now = SystemTime::now(); - let mut subject = make_subject(); + let mut subject = initialize_scan_interval_computer(); subject.last_scan_timestamp = now.checked_sub(Duration::from_secs(180)).unwrap(); subject.scan_interval = Duration::from_secs(180); subject.clock = Box::new(SimpleClockMock::default().now_result(now)); - let result = subject.compute_interval(); + let result = subject.time_until_next_scan(); assert_eq!( result, - None, + ScanTiming::ReadyNow, "We expected None ms, but got {:?} ms", - result.map(|duration| duration.as_millis()) + from_scan_timing_to_millis(result) ) } + fn from_scan_timing_to_millis(scan_timing: ScanTiming) -> u128 { + if let ScanTiming::WaitFor(interval) = scan_timing { + interval.as_millis() + } else { + panic!("expected an interval") + } + } + #[test] #[cfg(windows)] #[should_panic( - expected = "Now (SystemTime { intervals: 116454736000000000 }) earlier than past timestamp \ - (SystemTime { intervals: 116454736010000000 })" + expected = "Current time (SystemTime { intervals: 116454736000000000 }) is earlier than last \ + scan timestamp (SystemTime { intervals: 116454736010000000 })" )] - fn scan_dyn_interval_computer_panics() { - test_scan_dyn_interval_computer_panics() + fn scan_interval_computer_panics() { + test_scan_interval_computer_panics() } #[test] #[cfg(not(windows))] #[should_panic( - expected = "Now (SystemTime { tv_sec: 1000000, tv_nsec: 0 }) earlier than past timestamp \ - (SystemTime { tv_sec: 1000001, tv_nsec: 0 })" + expected = "Current time (SystemTime { tv_sec: 1000000, tv_nsec: 0 }) is earlier than last \ + scan timestamp (SystemTime { tv_sec: 1000001, tv_nsec: 0 })" )] - fn scan_dyn_interval_computer_panics() { - test_scan_dyn_interval_computer_panics() + fn scan_interval_computer_panics() { + test_scan_interval_computer_panics() } - fn test_scan_dyn_interval_computer_panics() { + fn test_scan_interval_computer_panics() { let now = UNIX_EPOCH .checked_add(Duration::from_secs(1_000_000)) .unwrap(); - let mut subject = make_subject(); + let mut subject = initialize_scan_interval_computer(); subject.clock = Box::new(SimpleClockMock::default().now_result(now)); subject.last_scan_timestamp = now.checked_add(Duration::from_secs(1)).unwrap(); - let _ = subject.compute_interval(); + let _ = subject.time_until_next_scan(); } #[test] - fn zero_out_works_for_default_subject() { - let mut subject = make_subject(); + fn reset_last_scan_timestamp_works_for_default_subject() { + let mut subject = initialize_scan_interval_computer(); let last_scan_timestamp_before = subject.last_scan_timestamp; let before_act = SystemTime::now(); - subject.zero_out(); + subject.reset_last_scan_timestamp(); let after_act = SystemTime::now(); let last_scan_timestamp_after = subject.last_scan_timestamp; @@ -574,14 +585,14 @@ mod tests { } #[test] - fn zero_out_works_for_general_subject() { - let mut subject = make_subject(); + fn reset_last_scan_timestamp_works_for_general_subject() { + let mut subject = initialize_scan_interval_computer(); subject.last_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(100)) .unwrap(); let before_act = SystemTime::now(); - subject.zero_out(); + subject.reset_last_scan_timestamp(); let after_act = SystemTime::now(); let last_scan_timestamp_after = subject.last_scan_timestamp; @@ -592,26 +603,27 @@ mod tests { } #[test] - fn update_last_new_payable_scan_timestamp_works() { - let zero_out_params_arc = Arc::new(Mutex::new(vec![])); + fn reset_scan_timer_works() { + let reset_last_scan_timestamp_params_arc = Arc::new(Mutex::new(vec![])); let scan_intervals = ScanIntervals::compute_default(TEST_DEFAULT_CHAIN); let mut subject = ScanSchedulers::new(scan_intervals, true); - subject.payable.dyn_interval_computer = Box::new( - NewPayableScanDynIntervalComputerMock::default().zero_out_params(&zero_out_params_arc), + subject.payable.interval_computer = Box::new( + NewPayableScanIntervalComputerMock::default() + .reset_last_scan_timestamp_params(&reset_last_scan_timestamp_params_arc), ); - subject.payable.update_last_new_payable_scan_timestamp(); + subject.payable.reset_scan_timer(); - let zero_out_params = zero_out_params_arc.lock().unwrap(); - assert_eq!(*zero_out_params, vec![()]) + let reset_last_scan_timestamp_params = reset_last_scan_timestamp_params_arc.lock().unwrap(); + assert_eq!(*reset_last_scan_timestamp_params, vec![()]) } - fn make_subject() -> NewPayableScanDynIntervalComputerReal { + fn initialize_scan_interval_computer() -> NewPayableScanIntervalComputerReal { // The interval is just a garbage value, we reset it in the tests by injection if needed - NewPayableScanDynIntervalComputerReal::new(Duration::from_secs(100)) + NewPayableScanIntervalComputerReal::new(Duration::from_secs(100)) } - fn fill_simple_clock_mock_and_return_now() -> (SimpleClockMock, SystemTime) { + fn set_up_mocked_clock() -> (SimpleClockMock, SystemTime) { let now = SystemTime::now(); ( (0..3).fold(SimpleClockMock::default(), |clock, _| clock.now_result(now)), diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 4b11abee2..08325dedc 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -18,8 +18,8 @@ use crate::accountant::scanners::pending_payable_scanner::{ CachesEmptiableScanner, ExtendedPendingPayablePrivateScanner, }; use crate::accountant::scanners::scan_schedulers::{ - NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, - ScanReschedulingAfterEarlyStop, + NewPayableScanIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, + ScanReschedulingAfterEarlyStop, ScanTiming, }; use crate::accountant::scanners::{ PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, Scanner, @@ -335,38 +335,41 @@ pub trait ScannerMockMarker {} impl ScannerMockMarker for ScannerMock {} #[derive(Default)] -pub struct NewPayableScanDynIntervalComputerMock { - compute_interval_params: Arc>>, - compute_interval_results: RefCell>>, - zero_out_params: Arc>>, +pub struct NewPayableScanIntervalComputerMock { + time_until_next_scan_params: Arc>>, + time_until_next_scan_results: RefCell>, + reset_last_scan_timestamp_params: Arc>>, } -impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerMock { - fn compute_interval(&self) -> Option { - self.compute_interval_params.lock().unwrap().push(()); - self.compute_interval_results.borrow_mut().remove(0) +impl NewPayableScanIntervalComputer for NewPayableScanIntervalComputerMock { + fn time_until_next_scan(&self) -> ScanTiming { + self.time_until_next_scan_params.lock().unwrap().push(()); + self.time_until_next_scan_results.borrow_mut().remove(0) } - fn zero_out(&mut self) { - self.zero_out_params.lock().unwrap().push(()); + fn reset_last_scan_timestamp(&mut self) { + self.reset_last_scan_timestamp_params + .lock() + .unwrap() + .push(()); } as_any_ref_in_trait_impl!(); } -impl NewPayableScanDynIntervalComputerMock { - pub fn compute_interval_params(mut self, params: &Arc>>) -> Self { - self.compute_interval_params = params.clone(); +impl NewPayableScanIntervalComputerMock { + pub fn time_until_next_scan_params(mut self, params: &Arc>>) -> Self { + self.time_until_next_scan_params = params.clone(); self } - pub fn compute_interval_result(self, result: Option) -> Self { - self.compute_interval_results.borrow_mut().push(result); + pub fn time_until_next_scan_result(self, result: ScanTiming) -> Self { + self.time_until_next_scan_results.borrow_mut().push(result); self } - pub fn zero_out_params(mut self, params: &Arc>>) -> Self { - self.zero_out_params = params.clone(); + pub fn reset_last_scan_timestamp_params(mut self, params: &Arc>>) -> Self { + self.reset_last_scan_timestamp_params = params.clone(); self } } diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 6346548a6..5fe46dc9a 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -243,45 +243,6 @@ mod tests { assert_eq!(other_error_stats, None); } - // #[test] - // fn previous_attempts_hash_works_correctly() { - // let now = SystemTime::now(); - // let clock = SimpleClockMock::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(); From 25f4be9ce1cc6bfc985820d33c4dfda5e8030c69 Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 7 Oct 2025 14:06:30 +0200 Subject: [PATCH 24/37] GH-598: first QA fix - review answered --- node/src/accountant/mod.rs | 77 ++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index ce7006785..153158dd4 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -732,9 +732,11 @@ impl Accountant { &mut self, msg: ReportRoutingServiceProvidedMessage, ) { - debug!( + trace!( self.logger, - "Charging routing of {} bytes to wallet {}", msg.payload_size, msg.paying_wallet + "Charging routing of {} bytes to wallet {}", + msg.payload_size, + msg.paying_wallet ); self.record_service_provided( msg.service_rate, @@ -749,7 +751,7 @@ impl Accountant { &mut self, msg: ReportExitServiceProvidedMessage, ) { - debug!( + trace!( self.logger, "Charging exit service for {} bytes to wallet {} at {} per service and {} per byte", msg.payload_size, @@ -2482,21 +2484,24 @@ mod tests { response_skeleton_opt: None } ); - let default_scan_intervals = ScanIntervals::compute_default(TEST_DEFAULT_CHAIN); - // The previous last_new_payable_scan_timestamp is UNIX_EPOCH, if the interval was derived - // from that timestamp, it would result in an immediate-scan command. This implies that - // the last_new_payable_scan_timestamp was reset to zero, which is how it is meant to be. - let left_bound = default_scan_intervals - .payable_scan_interval - .checked_sub(Duration::from_secs(5)) - .unwrap(); - let right_bound = default_scan_intervals - .payable_scan_interval - .checked_add(Duration::from_secs(5)) - .unwrap(); + // The initial last_new_payable_scan_timestamp is UNIX_EPOCH by this design. Such a value + // would've driven an immediate scan without an interval. Therefore, the performed interval + // implies that the last_new_payable_scan_timestamp must have been updated to the current + // time. (As the result of running into StartScanError::NothingToProcess) + let default_interval = + ScanIntervals::compute_default(TEST_DEFAULT_CHAIN).payable_scan_interval; + let tolerance = Duration::from_secs(5); + let min_interval = default_interval.checked_sub(tolerance).unwrap(); + let max_interval = default_interval.checked_add(tolerance).unwrap(); // The divergence should be only a few milliseconds, definitely not seconds; the tested // interval should be safe for slower machines too. - assert!(left_bound <= actual_interval && actual_interval <= right_bound); + assert!( + min_interval <= actual_interval && actual_interval <= max_interval, + "Expected interval between {:?} and {:?}, got {:?}", + min_interval, + max_interval, + actual_interval + ); assert_eq!(notify_later_params.len(), 0); // Accountant is unbound; therefore, it is guaranteed that sending a message to // the BlockchainBridge wasn't attempted. It would've panicked otherwise. @@ -2852,7 +2857,7 @@ mod tests { .start_scan_params(&scan_params.receivable_start_scan) .start_scan_result(Err(StartScanError::NothingToProcess)); let (subject, new_payable_expected_computed_interval, receivable_scan_interval) = - set_up_subject_for_no_p_p_found_startup_test( + configure_accountant_for_startup_with_preexisting_pending_payables( test_name, ¬ify_and_notify_later_params, &time_until_next_scan_params_arc, @@ -2872,7 +2877,7 @@ mod tests { let before = SystemTime::now(); system.run(); let after = SystemTime::now(); - assert_pending_payable_scanner_for_no_p_p_found( + assert_pending_payable_scanner_for_no_pending_payable_found( test_name, consuming_wallet, &scan_params.pending_payable_start_scan, @@ -2880,7 +2885,7 @@ mod tests { before, after, ); - assert_payable_scanner_for_no_p_p_found( + assert_payable_scanner_for_no_pending_payable_found( &scan_params.payable_start_scan, ¬ify_and_notify_later_params, time_until_next_scan_params_arc, @@ -2951,7 +2956,7 @@ mod tests { .start_scan_params(&scan_params.receivable_start_scan) .start_scan_result(Err(StartScanError::NothingToProcess)); let (subject, expected_pending_payable_notify_later_interval, receivable_scan_interval) = - set_up_subject_for_some_p_p_found_startup_test( + configure_accountant_for_startup_with_no_preexisting_pending_payables( test_name, ¬ify_and_notify_later_params, config, @@ -3005,7 +3010,7 @@ mod tests { let before = SystemTime::now(); system.run(); let after = SystemTime::now(); - assert_pending_payable_scanner_for_some_p_p_found( + assert_pending_payable_scanner_for_some_pending_payable_found( test_name, consuming_wallet.clone(), &scan_params, @@ -3015,7 +3020,7 @@ mod tests { before, after, ); - assert_payable_scanner_for_some_p_p_found( + assert_payable_scanner_for_some_pending_payable_found( test_name, consuming_wallet, &scan_params, @@ -3055,7 +3060,7 @@ mod tests { receivables_notify_later: Arc>>, } - fn set_up_subject_for_no_p_p_found_startup_test( + fn configure_accountant_for_startup_with_preexisting_pending_payables( test_name: &str, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, time_until_next_scan_params_arc: &Arc>>, @@ -3116,7 +3121,7 @@ mod tests { ) } - fn set_up_subject_for_some_p_p_found_startup_test( + fn configure_accountant_for_startup_with_no_preexisting_pending_payables( test_name: &str, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, config: BootstrapperConfig, @@ -3209,7 +3214,7 @@ mod tests { subject } - fn assert_pending_payable_scanner_for_no_p_p_found( + fn assert_pending_payable_scanner_for_no_pending_payable_found( test_name: &str, consuming_wallet: Wallet, pending_payable_start_scan_params_arc: &Arc< @@ -3242,7 +3247,7 @@ mod tests { assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); } - fn assert_pending_payable_scanner_for_some_p_p_found( + fn assert_pending_payable_scanner_for_some_pending_payable_found( test_name: &str, consuming_wallet: Wallet, scan_params: &ScanParams, @@ -3323,7 +3328,7 @@ mod tests { pp_logger } - fn assert_payable_scanner_for_no_p_p_found( + fn assert_payable_scanner_for_no_pending_payable_found( payable_scanner_start_scan_arc: &Arc< Mutex, Logger, String)>>, >, @@ -3379,23 +3384,23 @@ mod tests { ); } - fn assert_payable_scanner_for_some_p_p_found( + fn assert_payable_scanner_for_some_pending_payable_found( test_name: &str, consuming_wallet: Wallet, scan_params: &ScanParams, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, expected_sent_payables: SentPayables, ) { - assert_payable_scanner_ran_for_some_p_p_found( + assert_payable_scanner_ran_for_some_pending_payable_found( test_name, consuming_wallet, scan_params, expected_sent_payables, ); - assert_scan_scheduling_for_some_p_p_found(notify_and_notify_later_params); + assert_scan_scheduling_for_some_pending_payable_found(notify_and_notify_later_params); } - fn assert_payable_scanner_ran_for_some_p_p_found( + fn assert_payable_scanner_ran_for_some_pending_payable_found( test_name: &str, consuming_wallet: Wallet, scan_params: &ScanParams, @@ -3433,7 +3438,7 @@ mod tests { ); } - fn assert_scan_scheduling_for_some_p_p_found( + fn assert_scan_scheduling_for_some_pending_payable_found( notify_and_notify_later_params: &NotifyAndNotifyLaterParams, ) { let scan_for_new_payables_notify_later_params = notify_and_notify_later_params @@ -4489,10 +4494,6 @@ mod tests { more_money_receivable_parameters[0], (now, make_wallet("booga"), (1 * 42) + (1234 * 24)) ); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: Accountant: Charging routing of 1234 bytes to wallet {}", - paying_wallet - )); } #[test] @@ -4626,10 +4627,6 @@ mod tests { more_money_receivable_parameters[0], (now, make_wallet("booga"), (1 * 42) + (1234 * 24)) ); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: Accountant: Charging exit service for 1234 bytes to wallet {}", - paying_wallet - )); } #[test] From 06377691d163036d190d9fc6f0722a7bd47d2962 Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 8 Oct 2025 00:03:45 +0200 Subject: [PATCH 25/37] GH-598: second QA fix: the main thing done --- .../scanners/payable_scanner/start_scan.rs | 5 +-- .../tx_templates/initial/retry.rs | 39 +++++++++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/node/src/accountant/scanners/payable_scanner/start_scan.rs b/node/src/accountant/scanners/payable_scanner/start_scan.rs index 35cbd3ab2..9eda6b359 100644 --- a/node/src/accountant/scanners/payable_scanner/start_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/start_scan.rs @@ -157,11 +157,8 @@ mod tests { 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; - + 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!( 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 index 9990635cd..2e209c8d0 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs @@ -13,11 +13,11 @@ pub struct RetryTxTemplate { } impl RetryTxTemplate { - pub fn new(failed_tx: &FailedTx, payable_scan_amount_opt: Option) -> Self { + pub fn new(failed_tx: &FailedTx, updated_payable_balance_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; + if let Some(updated_payable_balance) = updated_payable_balance_opt { + retry_template.base.amount_in_wei = updated_payable_balance; } retry_template @@ -99,6 +99,39 @@ mod tests { use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; use crate::blockchain::test_utils::{make_address, make_tx_hash}; + #[test] + fn retry_tx_template_constructor_works() { + 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 fetched_balance_from_payable_table_opt_1 = None; + let fetched_balance_from_payable_table_opt_2 = Some(1_234_567); + + let result_1 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_1); + let result_2 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_2); + + let assert = |result: RetryTxTemplate, expected_amount_in_wei: u128| { + assert_eq!(result.base.receiver_address, receiver_address); + assert_eq!(result.base.amount_in_wei, expected_amount_in_wei); + assert_eq!(result.prev_gas_price_wei, gas_price); + assert_eq!(result.prev_nonce, nonce); + }; + assert(result_1, amount_in_wei); + assert(result_2, fetched_balance_from_payable_table_opt_2.unwrap()); + } + #[test] fn retry_tx_template_can_be_created_from_failed_tx() { let receiver_address = make_address(42); From 0e53e0439df5d913d313d2221b69167ac846b074 Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 8 Oct 2025 16:28:47 +0200 Subject: [PATCH 26/37] GH-598: second QA fix: added logging --- node/src/accountant/mod.rs | 13 +++-- node/src/accountant/scanners/mod.rs | 1 + .../scanners/payable_scanner/mod.rs | 1 + .../scanners/payable_scanner/start_scan.rs | 2 +- .../tx_templates/initial/retry.rs | 16 ++++-- .../scanners/pending_payable_scanner/mod.rs | 51 +++++++++++++------ .../tx_receipt_interpreter.rs | 7 +-- .../scanners/pending_payable_scanner/utils.rs | 32 ++++++------ .../accountant/scanners/scan_schedulers.rs | 5 +- .../blockchain_interface_web3/utils.rs | 14 ++++- 10 files changed, 93 insertions(+), 49 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 153158dd4..17eee2223 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -348,8 +348,8 @@ impl Handler for Accountant { 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." + "Re-running the pending payable scan is recommended, as some parts \ + did not finish last time." ); self.ui_message_sub_opt .as_ref() @@ -417,11 +417,14 @@ impl Handler for Accountant { fn handle(&mut self, scan_error: ScanError, ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); + self.scanners .acknowledge_scan_error(&scan_error, &self.logger); match scan_error.response_skeleton_opt { - None => match scan_error.scan_type { + None => { + debug!(self.logger, "Trying to restore continuity after a scan crash"); + match scan_error.scan_type { DetailedScanType::NewPayables => self .scan_schedulers .payable @@ -437,7 +440,7 @@ impl Handler for Accountant { DetailedScanType::Receivables => { self.scan_schedulers.receivable.schedule(ctx, &self.logger) } - }, + }}, Some(response_skeleton) => { let error_msg = NodeToUiMessage { target: ClientId(response_skeleton.client_id), @@ -975,7 +978,7 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - self.scan_schedulers.payable.reset_scan_timer(); + self.scan_schedulers.payable.reset_scan_timer(&self.logger); match result { Ok(scan_message) => { diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 787b9a8b2..a2da664ce 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -256,6 +256,7 @@ impl Scanners { } pub fn acknowledge_scan_error(&mut self, error: &ScanError, logger: &Logger) { + debug!(logger, "Acknowledging a scan that couldn't finish"); match error.scan_type { DetailedScanType::NewPayables | DetailedScanType::RetryPayables => { self.payable.mark_as_ended(logger) diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index b82d2c46e..2d8b7cb8b 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -251,6 +251,7 @@ impl PayableScanner { self.insert_records_in_sent_payables(&batch_results.sent_txs); } if failed > 0 { + debug!(logger, "Recording failed txs: {:?}", batch_results.failed_txs); self.insert_records_in_failed_payables(&batch_results.failed_txs); } } diff --git a/node/src/accountant/scanners/payable_scanner/start_scan.rs b/node/src/accountant/scanners/payable_scanner/start_scan.rs index 9eda6b359..457ab73ee 100644 --- a/node/src/accountant/scanners/payable_scanner/start_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/start_scan.rs @@ -66,7 +66,7 @@ impl StartableScanner for Payable 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); + let retry_tx_templates = RetryTxTemplates::new(&failed_txs, &amount_from_payables, logger); Ok(InitialTemplatesMessage { initial_templates: Either::Right(retry_tx_templates), 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 index 2e209c8d0..6b4cc9707 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs @@ -4,6 +4,7 @@ use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; use std::collections::{BTreeSet, HashMap}; use std::ops::{Deref, DerefMut}; use web3::types::Address; +use masq_lib::logger::Logger; #[derive(Debug, Clone, PartialEq, Eq)] pub struct RetryTxTemplate { @@ -13,10 +14,14 @@ pub struct RetryTxTemplate { } impl RetryTxTemplate { - pub fn new(failed_tx: &FailedTx, updated_payable_balance_opt: Option) -> Self { + pub fn new(failed_tx: &FailedTx, updated_payable_balance_opt: Option, logger: &Logger) -> Self { let mut retry_template = RetryTxTemplate::from(failed_tx); + debug!(logger, "Tx to retry {:?}", failed_tx); + if let Some(updated_payable_balance) = updated_payable_balance_opt { + debug!(logger, "Updating the pay for {:?} from former {} to latest accounted balance {} of minor", failed_tx.receiver_address, failed_tx.amount_minor, updated_payable_balance); + retry_template.base.amount_in_wei = updated_payable_balance; } @@ -44,6 +49,7 @@ impl RetryTxTemplates { pub fn new( txs_to_retry: &BTreeSet, amounts_from_payables: &HashMap, + logger: &Logger, ) -> Self { Self( txs_to_retry @@ -52,7 +58,7 @@ impl RetryTxTemplates { let payable_scan_amount_opt = amounts_from_payables .get(&tx_to_retry.receiver_address) .copied(); - RetryTxTemplate::new(tx_to_retry, payable_scan_amount_opt) + RetryTxTemplate::new(tx_to_retry, payable_scan_amount_opt, logger) }) .collect(), ) @@ -90,6 +96,7 @@ impl IntoIterator for RetryTxTemplates { #[cfg(test)] mod tests { + use masq_lib::logger::Logger; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedTx, FailureReason, FailureStatus, }; @@ -116,11 +123,12 @@ mod tests { reason: FailureReason::PendingTooLong, status: FailureStatus::RetryRequired, }; + let logger = Logger::new("test"); let fetched_balance_from_payable_table_opt_1 = None; let fetched_balance_from_payable_table_opt_2 = Some(1_234_567); - let result_1 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_1); - let result_2 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_2); + let result_1 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_1, &logger); + let result_2 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_2, &logger); let assert = |result: RetryTxTemplate, expected_amount_in_wei: u128| { assert_eq!(result.base.receiver_address, receiver_address); diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 499ee0179..d5bb5cde7 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -119,6 +119,8 @@ impl Scanner for PendingPayableScan let retry_opt = scan_report.requires_payments_retry(); + debug!(logger, "Payment retry requirement: {:?}", retry_opt); + self.process_txs_by_state(scan_report, logger); self.mark_as_ended(logger); @@ -161,6 +163,8 @@ impl PendingPayableScanner { } fn harvest_tables(&mut self, logger: &Logger) -> Result, StartScanError> { + debug!(logger,"Harvesting sent_payable and failed_payable tables"); + let pending_tx_hashes_opt = self.harvest_pending_payables(); let failure_hashes_opt = self.harvest_unproven_failures(); @@ -250,8 +254,8 @@ impl PendingPayableScanner { fn emptiness_check(&self, msg: &TxReceiptsMessage) { if msg.results.is_empty() { panic!( - "We should never receive an empty list of results. \ - Even receipts that could not be retrieved can be interpreted" + "We should never receive an empty list of results. Even receipts that could \ + not be retrieved can be interpreted" ) } } @@ -396,14 +400,18 @@ impl PendingPayableScanner { logger: &Logger, ) { self.handle_tx_failure_reclaims(confirmed_txs.reclaims, logger); - self.handle_normal_confirmations(confirmed_txs.normal_confirmations, logger); + self.handle_standard_confirmations(confirmed_txs.standard_confirmations, logger); } fn handle_tx_failure_reclaims(&mut self, reclaimed: Vec, logger: &Logger) { if reclaimed.is_empty() { + debug!(logger, "No failure reclaim to process"); + return; } + debug!(logger, "Processing failure reclaims: {:?}", reclaimed); + let hashes_and_blocks = Self::collect_and_sort_hashes_and_blocks(&reclaimed); self.replace_sent_tx_records(&reclaimed, &hashes_and_blocks, logger); @@ -495,11 +503,15 @@ impl PendingPayableScanner { } } - fn handle_normal_confirmations(&mut self, confirmed_txs: Vec, logger: &Logger) { + fn handle_standard_confirmations(&mut self, confirmed_txs: Vec, logger: &Logger) { if confirmed_txs.is_empty() { + debug!(logger, "No standard tx confirmations to process"); return; } + debug!(logger, "Processing {} standard tx confirmations", confirmed_txs.len()); + trace!(logger, "{:?}", confirmed_txs); + self.confirm_transactions(&confirmed_txs); self.update_tx_blocks(&confirmed_txs, logger); @@ -632,9 +644,12 @@ impl PendingPayableScanner { } if new_failures.is_empty() { + debug!(logger, "No reverted txs to process"); return; } + debug!(logger, "Processing reverted txs {:?}", new_failures); + let new_failures_btree_set: BTreeSet = new_failures.iter().cloned().collect(); if let Err(e) = self @@ -674,9 +689,13 @@ impl PendingPayableScanner { } if rechecks_completed.is_empty() { + debug!(logger, "No recheck-requiring failures to finalize"); return; } + debug!(logger, "Finalizing {} double-checked failures", rechecks_completed.len()); + trace!(logger, "{:?}", rechecks_completed); + match self .failed_payable_dao .update_statuses(&prepare_hashmap(&rechecks_completed)) @@ -830,7 +849,7 @@ impl PendingPayableScanner { debug!( logger, - "Found {} pending payables and {} unfinalized failures to process", + "Found {} pending payables and {} unproven failures to process", resolve_optional_vec(pending_tx_hashes_opt), resolve_optional_vec(failure_hashes_opt) ); @@ -1720,7 +1739,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], }, &logger, @@ -1785,7 +1804,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], }, &Logger::new("test"), @@ -1832,7 +1851,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], }, &Logger::new("test"), @@ -1854,7 +1873,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![sent_tx.clone()], }, &Logger::new("test"), @@ -1862,9 +1881,9 @@ mod tests { } #[test] - fn handles_normal_confirmations_alone() { + fn handles_standard_confirmations_alone() { init_test_logging(); - let test_name = "handles_normal_confirmations_alone"; + let test_name = "handles_standard_confirmations_alone"; let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() @@ -1905,7 +1924,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![sent_tx_1.clone(), sent_tx_2.clone()], + standard_confirmations: vec![sent_tx_1.clone(), sent_tx_2.clone()], reclaims: vec![], }, &logger, @@ -1981,7 +2000,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![sent_tx_1.clone()], + standard_confirmations: vec![sent_tx_1.clone()], reclaims: vec![sent_tx_2.clone()], }, &logger, @@ -2045,7 +2064,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![sent_tx_1, sent_tx_2], + standard_confirmations: vec![sent_tx_1, sent_tx_2], reclaims: vec![], }, &Logger::new("test"), @@ -2071,7 +2090,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![sent_tx], + standard_confirmations: vec![sent_tx], reclaims: vec![], }, &Logger::new("test"), @@ -2153,7 +2172,7 @@ mod tests { subject.handle_confirmed_transactions( DetectedConfirmations { - normal_confirmations: vec![sent_tx_1, sent_tx_2], + standard_confirmations: vec![sent_tx_1, sent_tx_2], reclaims: vec![sent_tx_3], }, &Logger::new(test_name), 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 fc16c5713..5f2703cb2 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 @@ -31,6 +31,7 @@ impl TxReceiptInterpreter { pending_payable_scanner: &PendingPayableScanner, logger: &Logger, ) -> ReceiptScanReport { + debug!(logger, "Composing receipt scan report"); let scan_report = ReceiptScanReport::default(); tx_cases .into_iter() @@ -162,7 +163,7 @@ impl TxReceiptInterpreter { scan_report } - //TODO: failures handling might need enhancement suggested by GH-693 + //TODO: if wanted, address GH-693 for more detailed failures fn handle_reverted_tx( mut scan_report: ReceiptScanReport, tx: TxByTable, @@ -279,7 +280,7 @@ mod tests { ReceiptScanReport { failures: DetectedFailures::default(), confirmations: DetectedConfirmations { - normal_confirmations: vec![updated_tx], + standard_confirmations: vec![updated_tx], reclaims: vec![] } } @@ -327,7 +328,7 @@ mod tests { ReceiptScanReport { failures: DetectedFailures::default(), confirmations: DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![sent_tx] } } diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index 7a1d18eaa..2ea92e289 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -38,7 +38,7 @@ impl ReceiptScanReport { confirmation_type: ConfirmationType, ) { match confirmation_type { - ConfirmationType::Normal => self.confirmations.normal_confirmations.push(confirmed_tx), + ConfirmationType::Normal => self.confirmations.standard_confirmations.push(confirmed_tx), ConfirmationType::Reclaim => self.confirmations.reclaims.push(confirmed_tx), } } @@ -62,13 +62,13 @@ impl ReceiptScanReport { #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct DetectedConfirmations { - pub normal_confirmations: Vec, + pub standard_confirmations: Vec, pub reclaims: Vec, } impl DetectedConfirmations { pub(super) fn is_empty(&self) -> bool { - self.normal_confirmations.is_empty() && self.reclaims.is_empty() + self.standard_confirmations.is_empty() && self.reclaims.is_empty() } } @@ -410,7 +410,7 @@ mod tests { #[test] fn detected_confirmations_is_empty_works() { let subject = DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![], }; @@ -462,19 +462,19 @@ mod tests { ]; let detected_confirmations_feeding = vec![ DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![], }, DetectedConfirmations { - normal_confirmations: vec![make_sent_tx(456)], + standard_confirmations: vec![make_sent_tx(456)], reclaims: vec![make_sent_tx(999)], }, DetectedConfirmations { - normal_confirmations: vec![make_sent_tx(777)], + standard_confirmations: vec![make_sent_tx(777)], reclaims: vec![], }, DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![make_sent_tx(999)], }, ]; @@ -550,19 +550,19 @@ mod tests { ]; let detected_confirmations_feeding = vec![ DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![], }, DetectedConfirmations { - normal_confirmations: vec![make_sent_tx(777)], + standard_confirmations: vec![make_sent_tx(777)], reclaims: vec![make_sent_tx(999)], }, DetectedConfirmations { - normal_confirmations: vec![make_sent_tx(777)], + standard_confirmations: vec![make_sent_tx(777)], reclaims: vec![], }, DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![make_sent_tx(999)], }, ]; @@ -594,15 +594,15 @@ mod tests { fn requires_payments_retry_says_no() { let detected_confirmations_feeding = vec![ DetectedConfirmations { - normal_confirmations: vec![make_sent_tx(777)], + standard_confirmations: vec![make_sent_tx(777)], reclaims: vec![make_sent_tx(999)], }, DetectedConfirmations { - normal_confirmations: vec![make_sent_tx(777)], + standard_confirmations: vec![make_sent_tx(777)], reclaims: vec![], }, DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![make_sent_tx(999)], }, ]; @@ -638,7 +638,7 @@ mod tests { tx_receipt_rpc_failures: vec![], }, confirmations: DetectedConfirmations { - normal_confirmations: vec![], + standard_confirmations: vec![], reclaims: vec![], }, }; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index dedfee1e5..54a33dd2b 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -122,7 +122,8 @@ impl PayableScanScheduler { } } - pub fn reset_scan_timer(&mut self) { + pub fn reset_scan_timer(&mut self, logger: &Logger) { + debug!(logger, "NewPayableScanIntervalComputer timer reset"); self.interval_computer.reset_last_scan_timestamp(); } @@ -612,7 +613,7 @@ mod tests { .reset_last_scan_timestamp_params(&reset_last_scan_timestamp_params_arc), ); - subject.payable.reset_scan_timer(); + subject.payable.reset_scan_timer(&Logger::new("test")); let reset_last_scan_timestamp_params = reset_last_scan_timestamp_params_arc.lock().unwrap(); assert_eq!(*reset_last_scan_timestamp_params, vec![()]) 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 16cab8c2d..4f2d91fe8 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -282,11 +282,16 @@ pub fn send_payables_within_batch( transmission_log(chain, &signable_tx_templates) ); + let logger_clone = logger.clone(); + Box::new( web3_batch .transport() .submit_batch() - .map_err(move |e| return_sending_error(&sent_txs_for_err, &e)) + .map_err(move |e| { + warning!(logger_clone, "Failed to submit batch to Web3 client: {}", e); + return_sending_error(&sent_txs_for_err, &e) + }) .and_then(move |batch_responses| Ok(return_batch_results(sent_txs, batch_responses))), ) } @@ -727,6 +732,7 @@ mod tests { #[test] fn send_payables_within_batch_fails_on_submit_batch_call() { + let test_name = "send_payables_within_batch_fails_on_submit_batch_call"; let port = find_free_port(); let (_event_loop_handle, transport) = Http::with_max_parallel( &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), @@ -780,11 +786,15 @@ mod tests { }); test_send_payables_within_batch( - "send_payables_within_batch_fails_on_submit_batch_call", + test_name, signable_tx_templates, expected_result, port, ); + + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: \ + Failed to submit batch to Web3 client: Transport error: Error(Connect, Os {{ code: 111, \ + kind: ConnectionRefused, message: \"Connection refused\" }}")); } #[test] From bc95399f27b1cd811b1ea3a5d711314d458d441b Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 8 Oct 2025 16:33:12 +0200 Subject: [PATCH 27/37] GH-598: second QA fix: formatting --- node/src/accountant/mod.rs | 40 ++++++++++--------- .../scanners/payable_scanner/mod.rs | 5 ++- .../tx_templates/initial/retry.rs | 30 +++++++++++--- .../scanners/pending_payable_scanner/mod.rs | 18 ++++++--- .../scanners/pending_payable_scanner/utils.rs | 4 +- .../blockchain_interface_web3/utils.rs | 13 +++--- 6 files changed, 71 insertions(+), 39 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 17eee2223..04d485663 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -417,30 +417,34 @@ impl Handler for Accountant { fn handle(&mut self, scan_error: ScanError, ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); - + self.scanners .acknowledge_scan_error(&scan_error, &self.logger); match scan_error.response_skeleton_opt { - None => { - debug!(self.logger, "Trying to restore continuity after a scan crash"); + None => { + debug!( + self.logger, + "Trying to restore the scan train after a crash" + ); match scan_error.scan_type { - DetailedScanType::NewPayables => self - .scan_schedulers - .payable - .schedule_new_payable_scan(ctx, &self.logger), - DetailedScanType::RetryPayables => self - .scan_schedulers - .payable - .schedule_retry_payable_scan(ctx, None, &self.logger), - DetailedScanType::PendingPayables => self - .scan_schedulers - .pending_payable - .schedule(ctx, &self.logger), - DetailedScanType::Receivables => { - self.scan_schedulers.receivable.schedule(ctx, &self.logger) + DetailedScanType::NewPayables => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + DetailedScanType::RetryPayables => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, None, &self.logger), + DetailedScanType::PendingPayables => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + DetailedScanType::Receivables => { + self.scan_schedulers.receivable.schedule(ctx, &self.logger) + } } - }}, + } Some(response_skeleton) => { let error_msg = NodeToUiMessage { target: ClientId(response_skeleton.client_id), diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index 2d8b7cb8b..4c9ac9804 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -251,7 +251,10 @@ impl PayableScanner { self.insert_records_in_sent_payables(&batch_results.sent_txs); } if failed > 0 { - debug!(logger, "Recording failed txs: {:?}", batch_results.failed_txs); + debug!( + logger, + "Recording failed txs: {:?}", batch_results.failed_txs + ); self.insert_records_in_failed_payables(&batch_results.failed_txs); } } 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 index 6b4cc9707..8157a373b 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs @@ -1,10 +1,10 @@ // 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 masq_lib::logger::Logger; use std::collections::{BTreeSet, HashMap}; use std::ops::{Deref, DerefMut}; use web3::types::Address; -use masq_lib::logger::Logger; #[derive(Debug, Clone, PartialEq, Eq)] pub struct RetryTxTemplate { @@ -14,13 +14,23 @@ pub struct RetryTxTemplate { } impl RetryTxTemplate { - pub fn new(failed_tx: &FailedTx, updated_payable_balance_opt: Option, logger: &Logger) -> Self { + pub fn new( + failed_tx: &FailedTx, + updated_payable_balance_opt: Option, + logger: &Logger, + ) -> Self { let mut retry_template = RetryTxTemplate::from(failed_tx); debug!(logger, "Tx to retry {:?}", failed_tx); if let Some(updated_payable_balance) = updated_payable_balance_opt { - debug!(logger, "Updating the pay for {:?} from former {} to latest accounted balance {} of minor", failed_tx.receiver_address, failed_tx.amount_minor, updated_payable_balance); + debug!( + logger, + "Updating the pay for {:?} from former {} to latest accounted balance {} of minor", + failed_tx.receiver_address, + failed_tx.amount_minor, + updated_payable_balance + ); retry_template.base.amount_in_wei = updated_payable_balance; } @@ -96,7 +106,6 @@ impl IntoIterator for RetryTxTemplates { #[cfg(test)] mod tests { - use masq_lib::logger::Logger; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedTx, FailureReason, FailureStatus, }; @@ -105,6 +114,7 @@ mod tests { }; use crate::accountant::scanners::payable_scanner::tx_templates::BaseTxTemplate; use crate::blockchain::test_utils::{make_address, make_tx_hash}; + use masq_lib::logger::Logger; #[test] fn retry_tx_template_constructor_works() { @@ -127,8 +137,16 @@ mod tests { let fetched_balance_from_payable_table_opt_1 = None; let fetched_balance_from_payable_table_opt_2 = Some(1_234_567); - let result_1 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_1, &logger); - let result_2 = RetryTxTemplate::new(&failed_tx, fetched_balance_from_payable_table_opt_2, &logger); + let result_1 = RetryTxTemplate::new( + &failed_tx, + fetched_balance_from_payable_table_opt_1, + &logger, + ); + let result_2 = RetryTxTemplate::new( + &failed_tx, + fetched_balance_from_payable_table_opt_2, + &logger, + ); let assert = |result: RetryTxTemplate, expected_amount_in_wei: u128| { assert_eq!(result.base.receiver_address, receiver_address); diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index d5bb5cde7..d8faa57fa 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -163,7 +163,7 @@ impl PendingPayableScanner { } fn harvest_tables(&mut self, logger: &Logger) -> Result, StartScanError> { - debug!(logger,"Harvesting sent_payable and failed_payable tables"); + debug!(logger, "Harvesting sent_payable and failed_payable tables"); let pending_tx_hashes_opt = self.harvest_pending_payables(); let failure_hashes_opt = self.harvest_unproven_failures(); @@ -509,7 +509,11 @@ impl PendingPayableScanner { return; } - debug!(logger, "Processing {} standard tx confirmations", confirmed_txs.len()); + debug!( + logger, + "Processing {} standard tx confirmations", + confirmed_txs.len() + ); trace!(logger, "{:?}", confirmed_txs); self.confirm_transactions(&confirmed_txs); @@ -649,7 +653,7 @@ impl PendingPayableScanner { } debug!(logger, "Processing reverted txs {:?}", new_failures); - + let new_failures_btree_set: BTreeSet = new_failures.iter().cloned().collect(); if let Err(e) = self @@ -693,9 +697,13 @@ impl PendingPayableScanner { return; } - debug!(logger, "Finalizing {} double-checked failures", rechecks_completed.len()); + debug!( + logger, + "Finalizing {} double-checked failures", + rechecks_completed.len() + ); trace!(logger, "{:?}", rechecks_completed); - + match self .failed_payable_dao .update_statuses(&prepare_hashmap(&rechecks_completed)) diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index 2ea92e289..2a1c5c7cb 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -38,7 +38,9 @@ impl ReceiptScanReport { confirmation_type: ConfirmationType, ) { match confirmation_type { - ConfirmationType::Normal => self.confirmations.standard_confirmations.push(confirmed_tx), + ConfirmationType::Normal => { + self.confirmations.standard_confirmations.push(confirmed_tx) + } ConfirmationType::Reclaim => self.confirmations.reclaims.push(confirmed_tx), } } 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 4f2d91fe8..00aeeee03 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -785,16 +785,13 @@ mod tests { failed_txs, }); - test_send_payables_within_batch( - test_name, - signable_tx_templates, - expected_result, - port, - ); + test_send_payables_within_batch(test_name, signable_tx_templates, expected_result, port); - TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: \ + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: \ Failed to submit batch to Web3 client: Transport error: Error(Connect, Os {{ code: 111, \ - kind: ConnectionRefused, message: \"Connection refused\" }}")); + kind: ConnectionRefused, message: \"Connection refused\" }}" + )); } #[test] From b9f6c648e767022a2b1fb6d09e3b132abe302f9c Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 8 Oct 2025 17:06:33 +0200 Subject: [PATCH 28/37] GH-598: second QA fix: finished all.sh --- node/src/accountant/scanners/mod.rs | 8 ++-- .../scanners/pending_payable_scanner/mod.rs | 41 +++++++++---------- .../tx_receipt_interpreter.rs | 2 +- .../scanners/pending_payable_scanner/utils.rs | 2 +- node/src/accountant/test_utils.rs | 8 ++-- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index a2da664ce..cbc337a8d 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -795,11 +795,11 @@ mod tests { false ); let dumped_records = pending_payable_scanner - .yet_unproven_failed_payables + .suspected_failed_payables .dump_cache(); assert!( dumped_records.is_empty(), - "There should be no yet unproven failures but found {:?}.", + "There should be no suspected failures but found {:?}.", dumped_records ); assert_eq!( @@ -1200,7 +1200,9 @@ mod tests { ); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!("INFO: {test_name}: Scanning for pending payable"), - &format!("DEBUG: {test_name}: Found 1 pending payables and 1 unfinalized failures to process"), + &format!( + "DEBUG: {test_name}: Found 1 pending payables and 1 suspected failures to process" + ), ]) } diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index d8faa57fa..4fe12add1 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -65,7 +65,7 @@ pub struct PendingPayableScanner { pub failed_payable_dao: Box, pub financial_statistics: Rc>, pub current_sent_payables: Box>, - pub yet_unproven_failed_payables: Box>, + pub suspected_failed_payables: Box>, pub clock: Box, } @@ -138,7 +138,7 @@ impl Scanner for PendingPayableScan 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); + self.suspected_failed_payables.ensure_empty_cache(logger); } } @@ -157,7 +157,7 @@ impl PendingPayableScanner { failed_payable_dao, financial_statistics, current_sent_payables: Box::new(CurrentPendingPayables::default()), - yet_unproven_failed_payables: Box::new(RecheckRequiringFailures::default()), + suspected_failed_payables: Box::new(RecheckRequiringFailures::default()), clock: Box::new(SimpleClockReal::default()), } } @@ -166,7 +166,7 @@ impl PendingPayableScanner { debug!(logger, "Harvesting sent_payable and failed_payable tables"); let pending_tx_hashes_opt = self.harvest_pending_payables(); - let failure_hashes_opt = self.harvest_unproven_failures(); + let failure_hashes_opt = self.harvest_suspected_failures(); if Self::is_there_nothing_to_process( pending_tx_hashes_opt.as_ref(), @@ -203,7 +203,7 @@ impl PendingPayableScanner { Some(pending_tx_hashes) } - fn harvest_unproven_failures(&mut self) -> Option> { + fn harvest_suspected_failures(&mut self) -> Option> { let failures = self .failed_payable_dao .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)) @@ -215,7 +215,7 @@ impl PendingPayableScanner { } let failure_hashes = Self::wrap_hashes(&failures, TxHashByTable::FailedPayable); - self.yet_unproven_failed_payables.load_cache(failures); + self.suspected_failed_payables.load_cache(failures); Some(failure_hashes) } @@ -329,7 +329,7 @@ impl PendingPayableScanner { }; self.current_sent_payables.ensure_empty_cache(logger); - self.yet_unproven_failed_payables.ensure_empty_cache(logger); + self.suspected_failed_payables.ensure_empty_cache(logger); cases } @@ -354,10 +354,7 @@ impl PendingPayableScanner { } } TxHashByTable::FailedPayable(tx_hash) => { - match self - .yet_unproven_failed_payables - .get_record_by_hash(tx_hash) - { + match self.suspected_failed_payables.get_record_by_hash(tx_hash) { Some(failed_tx) => { cases.push(TxCaseToBeInterpreted::new( TxByTable::FailedPayable(failed_tx), @@ -382,10 +379,10 @@ impl PendingPayableScanner { panic!( "Looking up '{:?}' in the cache, the record could not be found. Dumping \ - the remaining values. Pending payables: {:?}. Unproven failures: {:?}.", + the remaining values. Pending payables: {:?}. Suspected failures: {:?}.", missing_entry, rearrange(self.current_sent_payables.dump_cache()), - rearrange(self.yet_unproven_failed_payables.dump_cache()), + rearrange(self.suspected_failed_payables.dump_cache()), ) } @@ -632,7 +629,7 @@ impl PendingPayableScanner { }); self.add_new_failures(grouped_failures.new_failures, logger); - self.finalize_unproven_failures(grouped_failures.rechecks_completed, logger); + self.finalize_suspected_failures(grouped_failures.rechecks_completed, logger); } fn add_new_failures(&self, new_failures: Vec, logger: &Logger) { @@ -684,7 +681,7 @@ impl PendingPayableScanner { } } - fn finalize_unproven_failures(&self, rechecks_completed: Vec, logger: &Logger) { + fn finalize_suspected_failures(&self, rechecks_completed: Vec, logger: &Logger) { fn prepare_hashmap(rechecks_completed: &[TxHash]) -> HashMap { rechecks_completed .iter() @@ -857,7 +854,7 @@ impl PendingPayableScanner { debug!( logger, - "Found {} pending payables and {} unproven failures to process", + "Found {} pending payables and {} suspected failures to process", resolve_optional_vec(pending_tx_hashes_opt), resolve_optional_vec(failure_hashes_opt) ); @@ -932,7 +929,7 @@ mod tests { .build(); let logger = Logger::new("start_scan_fills_in_caches_and_returns_msg"); let pending_payable_cache_before = subject.current_sent_payables.dump_cache(); - let failed_payable_cache_before = subject.yet_unproven_failed_payables.dump_cache(); + let failed_payable_cache_before = subject.suspected_failed_payables.dump_cache(); let result = subject.start_scan(&make_wallet("blah"), SystemTime::now(), None, &logger); @@ -959,7 +956,7 @@ mod tests { failed_payable_cache_before ); let pending_payable_cache_after = subject.current_sent_payables.dump_cache(); - let failed_payable_cache_after = subject.yet_unproven_failed_payables.dump_cache(); + let failed_payable_cache_after = subject.suspected_failed_payables.dump_cache(); assert_eq!( pending_payable_cache_after, hashmap!(sent_tx_hash_1 => sent_tx_1, sent_tx_hash_2 => sent_tx_2) @@ -1067,7 +1064,7 @@ mod tests { failed_payable_cache.load_cache(vec![failed_tx_1, failed_tx_2]); let mut subject = PendingPayableScannerBuilder::new().build(); subject.current_sent_payables = Box::new(pending_payable_cache); - subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + subject.suspected_failed_payables = Box::new(failed_payable_cache); let logger = Logger::new("test"); let msg = TxReceiptsMessage { results: btreemap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok( @@ -1088,7 +1085,7 @@ mod tests { 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: []."; + status: Pending(Waiting) }]. Suspected failures: []."; assert_eq!(panic_msg, expected); } @@ -1107,7 +1104,7 @@ mod tests { failed_payable_cache.load_cache(vec![failed_tx_1]); let mut subject = PendingPayableScannerBuilder::new().build(); subject.current_sent_payables = Box::new(pending_payable_cache); - subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + subject.suspected_failed_payables = Box::new(failed_payable_cache); let logger = Logger::new("test"); let msg = TxReceiptsMessage { results: btreemap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), @@ -1130,7 +1127,7 @@ mod tests { 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: []."; + Pending(Waiting) }]. Suspected failures: []."; assert_eq!(panic_msg, expected); } 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 5f2703cb2..d04d458b4 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 @@ -186,7 +186,7 @@ impl TxReceiptInterpreter { failed_tx.reason, ); - scan_report.register_finalization_of_unproven_failure(failed_tx.hash); + scan_report.register_finalization_of_suspected_failure(failed_tx.hash); } } 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 2a1c5c7cb..2b8052496 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -51,7 +51,7 @@ impl ReceiptScanReport { .push(PresortedTxFailure::NewEntry(failed_tx)); } - pub(super) fn register_finalization_of_unproven_failure(&mut self, tx_hash: TxHash) { + pub(super) fn register_finalization_of_suspected_failure(&mut self, tx_hash: TxHash) { self.failures .tx_failures .push(PresortedTxFailure::RecheckCompleted(tx_hash)); diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index b460363ee..9e3e06575 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -1281,7 +1281,7 @@ pub struct PendingPayableScannerBuilder { payment_thresholds: PaymentThresholds, financial_statistics: FinancialStatistics, current_sent_payables: Box>, - yet_unproven_failed_payables: Box>, + suspected_failed_payables: Box>, clock: Box, } @@ -1294,7 +1294,7 @@ impl PendingPayableScannerBuilder { payment_thresholds: PaymentThresholds::default(), financial_statistics: FinancialStatistics::default(), current_sent_payables: Box::new(PendingPayableCacheMock::default()), - yet_unproven_failed_payables: Box::new(PendingPayableCacheMock::default()), + suspected_failed_payables: Box::new(PendingPayableCacheMock::default()), clock: Box::new(SimpleClockMock::default()), } } @@ -1323,7 +1323,7 @@ impl PendingPayableScannerBuilder { mut self, failures: Box>, ) -> Self { - self.yet_unproven_failed_payables = failures; + self.suspected_failed_payables = failures; self } @@ -1341,7 +1341,7 @@ impl PendingPayableScannerBuilder { Rc::new(RefCell::new(self.financial_statistics)), ); scanner.current_sent_payables = self.current_sent_payables; - scanner.yet_unproven_failed_payables = self.yet_unproven_failed_payables; + scanner.suspected_failed_payables = self.suspected_failed_payables; scanner.clock = self.clock; scanner } From 1acd12d0c428c68d9b32dc3dd85b1c3d79145bef Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 8 Oct 2025 17:17:44 +0200 Subject: [PATCH 29/37] GH-598: second QA fix: fix according to rev advice --- node/src/accountant/mod.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 04d485663..798bd5405 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -3340,7 +3340,7 @@ mod tests { Mutex, Logger, String)>>, >, notify_and_notify_later_params: &NotifyAndNotifyLaterParams, - time_until_next_scan_until_next_new_payable_scan_params_arc: Arc>>, + time_until_next_new_payable_scan_params_arc: Arc>>, new_payable_expected_computed_interval: Duration, ) { // Note that there is no functionality from the payable scanner actually running. @@ -3358,14 +3358,9 @@ mod tests { new_payable_expected_computed_interval )] ); - let time_until_next_scan_until_next_new_payable_scan_params = - time_until_next_scan_until_next_new_payable_scan_params_arc - .lock() - .unwrap(); - assert_eq!( - *time_until_next_scan_until_next_new_payable_scan_params, - vec![()] - ); + let time_until_next_new_payable_scan_params = + time_until_next_new_payable_scan_params_arc.lock().unwrap(); + assert_eq!(*time_until_next_new_payable_scan_params, vec![()]); let payable_scanner_start_scan = payable_scanner_start_scan_arc.lock().unwrap(); assert!( payable_scanner_start_scan.is_empty(), From f77eaa97b9bb789176a6e12dc4502610577d0300 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 9 Oct 2025 00:04:08 +0200 Subject: [PATCH 30/37] GH-598: fixed test to be OS agnostic --- multinode_integration_tests/tests/verify_bill_payment.rs | 2 +- .../blockchain_interface_web3/utils.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index d421f82b2..e5fddc67f 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -465,7 +465,7 @@ fn verify_pending_payables() { } MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), - "Found 3 pending payables and 0 unfinalized failures to process", + "Found 3 pending payables and 0 suspected failures to process", Duration::from_secs(5), ); MASQNodeUtils::assert_node_wrote_log_containing( 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 00aeeee03..fa893b19f 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -787,10 +787,12 @@ mod tests { test_send_payables_within_batch(test_name, signable_tx_templates, expected_result, port); + let os_specific_code = transport_error_code(); + let os_specific_msg = transport_error_message(); TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: \ - Failed to submit batch to Web3 client: Transport error: Error(Connect, Os {{ code: 111, \ - kind: ConnectionRefused, message: \"Connection refused\" }}" + "WARN: {test_name}: Failed to submit batch to Web3 client: Transport error: \ + Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: \"{}\" }}", + os_specific_code, os_specific_msg )); } From 738ead8ccf74d29856c6666790db957d4352a5b9 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 16 Oct 2025 12:27:50 +0530 Subject: [PATCH 31/37] GH-598: add 5 mins interval for retry scan --- node/src/accountant/mod.rs | 118 ++++++++++-------- .../accountant/scanners/scan_schedulers.rs | 18 ++- 2 files changed, 82 insertions(+), 54 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 798bd5405..f37a6ea1b 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1319,6 +1319,7 @@ mod tests { use crate::accountant::scanners::pending_payable_scanner::utils::TxByTable; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanIntervalComputer, NewPayableScanIntervalComputerReal, ScanTiming, + DEFAULT_RETRY_INTERVAL, }; use crate::accountant::scanners::test_utils::{ MarkScanner, NewPayableScanIntervalComputerMock, PendingPayableCacheMock, ReplacementType, @@ -2044,8 +2045,8 @@ mod tests { let system = System::new("test"); subject.scan_schedulers.automatic_scans_enabled = false; // Making sure we would kill the test if any sort of scan was scheduled - subject.scan_schedulers.payable.retry_payable_notify = - Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.retry_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify_later = Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify = @@ -3062,7 +3063,7 @@ mod tests { struct NotifyAndNotifyLaterParams { new_payables_notify_later: Arc>>, new_payables_notify: Arc>>, - retry_payables_notify: Arc>>, + retry_payables_notify_later: Arc>>, pending_payables_notify_later: Arc>>, receivables_notify_later: Arc>>, } @@ -3102,9 +3103,9 @@ mod tests { NotifyLaterHandleMock::default() .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), ); - subject.scan_schedulers.payable.retry_payable_notify = Box::new( - NotifyHandleMock::default() - .notify_params(¬ify_and_notify_later_params.retry_payables_notify), + subject.scan_schedulers.payable.retry_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.retry_payables_notify_later), ); subject.scan_schedulers.payable.new_payable_notify = Box::new( NotifyHandleMock::default() @@ -3164,9 +3165,9 @@ mod tests { NotifyLaterHandleMock::default() .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), ); - subject.scan_schedulers.payable.retry_payable_notify = Box::new( - NotifyHandleMock::default() - .notify_params(¬ify_and_notify_later_params.retry_payables_notify) + subject.scan_schedulers.payable.retry_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.retry_payables_notify_later) .capture_msg_and_let_it_fly_on(), ); subject.scan_schedulers.payable.new_payable_notify = Box::new( @@ -3376,7 +3377,7 @@ mod tests { scan_for_new_payables_notify_params ); let scan_for_retry_payables_notify_params = notify_and_notify_later_params - .retry_payables_notify + .retry_payables_notify_later .lock() .unwrap(); assert!( @@ -3462,14 +3463,17 @@ mod tests { scan_for_new_payables_notify_params ); let scan_for_retry_payables_notify_params = notify_and_notify_later_params - .retry_payables_notify + .retry_payables_notify_later .lock() .unwrap(); assert_eq!( *scan_for_retry_payables_notify_params, - vec![ScanForRetryPayables { - response_skeleton_opt: None - }], + vec![( + ScanForRetryPayables { + response_skeleton_opt: None + }, + DEFAULT_RETRY_INTERVAL + )], ); } @@ -5092,8 +5096,8 @@ mod tests { 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()); + subject.scan_schedulers.payable.retry_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let expected_tx = TxBuilder::default().hash(expected_hash.clone()).build(); let sent_payable = SentPayables { payment_procedure_result: Ok(BatchResults { @@ -5151,8 +5155,8 @@ mod tests { 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()); + subject.scan_schedulers.payable.retry_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let expected_tx = TxBuilder::default().hash(expected_hash.clone()).build(); let sent_payable = SentPayables { payment_procedure_result: Ok(BatchResults { @@ -5186,7 +5190,7 @@ mod tests { init_test_logging(); let test_name = "retry_payable_scan_is_requested_to_be_repeated"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); - let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); + let retry_payable_notify_later_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() @@ -5203,8 +5207,10 @@ mod tests { result: NextScanToRun::RetryPayableScan, }), ))); - subject.scan_schedulers.payable.retry_payable_notify = - Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + subject.scan_schedulers.payable.retry_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&retry_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 = @@ -5230,9 +5236,10 @@ 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_params = retry_payable_notify_params_arc.lock().unwrap(); - let scheduled_msg = payable_notify_params.remove(0); + let mut payable_notify_params = retry_payable_notify_later_params_arc.lock().unwrap(); + let (scheduled_msg, duration) = payable_notify_params.remove(0); assert_eq!(scheduled_msg, ScanForRetryPayables::default()); + assert_eq!(duration, DEFAULT_RETRY_INTERVAL); assert!( payable_notify_params.is_empty(), "Should be empty but {:?}", @@ -5247,7 +5254,7 @@ mod tests { let test_name = "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 retry_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); @@ -5265,8 +5272,10 @@ mod tests { Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.pending_payable.handle = Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); - subject.scan_schedulers.payable.retry_payable_notify = - Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + subject.scan_schedulers.payable.retry_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&retry_payable_notify_later_params_arc), + ); let system = System::new(test_name); let (mut msg, _) = make_tx_receipts_msg(vec![ SeedsToMakeUpPayableWithStatus { @@ -5288,12 +5297,15 @@ mod tests { let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); let (msg_actual, logger) = finish_scan_params.remove(0); assert_eq!(msg_actual, msg); - let retry_payable_notify_params = retry_payable_notify_params_arc.lock().unwrap(); + let retry_payable_notify_params = retry_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( *retry_payable_notify_params, - vec![ScanForRetryPayables { - response_skeleton_opt: None - }] + vec![( + ScanForRetryPayables { + response_skeleton_opt: None + }, + DEFAULT_RETRY_INTERVAL + )] ); assert_using_the_same_logger(&logger, test_name, None) } @@ -5316,8 +5328,8 @@ mod tests { .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.retry_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify_later = @@ -5382,8 +5394,8 @@ mod tests { .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.retry_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify_later = @@ -5445,8 +5457,9 @@ mod tests { .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); - subject.scan_schedulers.payable.retry_payable_notify = - Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + subject.scan_schedulers.payable.retry_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_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 = @@ -5470,9 +5483,12 @@ mod tests { 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) - }] + vec![( + ScanForRetryPayables { + response_skeleton_opt: Some(response_skeleton) + }, + DEFAULT_RETRY_INTERVAL + )] ); assert_using_the_same_logger(&logger, test_name, None) } @@ -5889,28 +5905,32 @@ mod tests { Box::new( |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { // Setup - let notify_params_arc = Arc::new(Mutex::new(vec![])); - scan_schedulers.payable.retry_payable_notify = - Box::new(NotifyHandleMock::default().notify_params(¬ify_params_arc)); + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers.payable.retry_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); // Assertions Box::new(move |response_skeleton_opt| { - let notify_params = notify_params_arc.lock().unwrap(); + let notify_later_params = notify_later_params_arc.lock().unwrap(); match response_skeleton_opt { None => { // Response skeleton must be None assert_eq!( - *notify_params, - vec![ScanForRetryPayables { - response_skeleton_opt: None - }] + *notify_later_params, + vec![( + ScanForRetryPayables { + response_skeleton_opt: None + }, + DEFAULT_RETRY_INTERVAL + )] ) } Some(_) => { assert!( - notify_params.is_empty(), + notify_later_params.is_empty(), "Should be empty but contained {:?}", - notify_params + notify_later_params ) } } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 54a33dd2b..fd6a79901 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -16,6 +16,8 @@ use masq_lib::simple_clock::{SimpleClock, SimpleClockReal}; use std::fmt::{Debug, Display, Formatter}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +pub const DEFAULT_RETRY_INTERVAL: Duration = Duration::from_secs(5 * 60); + pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, @@ -80,7 +82,7 @@ pub struct PayableScanScheduler { pub new_payable_notify_later: Box>, pub interval_computer: Box, pub new_payable_notify: Box>, - pub retry_payable_notify: Box>, + pub retry_payable_notify_later: Box>, } impl PayableScanScheduler { @@ -91,7 +93,7 @@ impl PayableScanScheduler { new_payable_interval, )), new_payable_notify: Box::new(NotifyHandleReal::default()), - retry_payable_notify: Box::new(NotifyHandleReal::default()), + retry_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), } } @@ -138,12 +140,13 @@ impl PayableScanScheduler { ) { debug!(logger, "Scheduling a retry-payable scan asap"); - self.retry_payable_notify.notify( + let _ = self.retry_payable_notify_later.notify_later( ScanForRetryPayables { response_skeleton_opt, }, + DEFAULT_RETRY_INTERVAL, ctx, - ) + ); } } @@ -385,7 +388,7 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanIntervalComputer, NewPayableScanIntervalComputerReal, PayableSequenceScanner, - ScanReschedulingAfterEarlyStop, ScanSchedulers, ScanTiming, + ScanReschedulingAfterEarlyStop, ScanSchedulers, ScanTiming, DEFAULT_RETRY_INTERVAL, }; use crate::accountant::scanners::test_utils::NewPayableScanIntervalComputerMock; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; @@ -402,6 +405,11 @@ mod tests { use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; + #[test] + fn constants_have_correct_values() { + assert_eq!(DEFAULT_RETRY_INTERVAL, Duration::from_secs(5 * 60)); + } + #[test] fn scan_schedulers_are_initialized_correctly() { let scan_intervals = ScanIntervals { From af1a0759f35b06991c9ba6bccf469826a84e1bd1 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Sun, 19 Oct 2025 21:55:09 +0530 Subject: [PATCH 32/37] GH-598: tests in accountant are passing --- node/src/accountant/mod.rs | 12 +++++++++++- node/src/accountant/scanners/scan_schedulers.rs | 13 ++++++++++--- node/src/db_config/persistent_configuration.rs | 1 + node/src/node_configurator/configurator.rs | 2 ++ .../unprivileged_parse_args_configuration.rs | 4 ++++ node/src/sub_lib/accountant.rs | 6 ++++++ node/src/sub_lib/combined_parameters.rs | 4 ++++ node/src/test_utils/mod.rs | 1 + 8 files changed, 39 insertions(+), 4 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f37a6ea1b..cc7933099 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1959,6 +1959,7 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), + retry_payable_scan_interval: Duration::from_millis(1), receivable_scan_interval: Duration::from_millis(10_000), pending_payable_scan_interval: Duration::from_secs(100), }); @@ -2282,6 +2283,7 @@ mod tests { ]) .build(); subject.scan_schedulers.automatic_scans_enabled = false; + subject.scan_schedulers.payable.retry_payable_scan_interval = Duration::from_millis(1); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let ui_gateway = @@ -2680,6 +2682,7 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(2_000), receivable_scan_interval: Duration::from_millis(10_000), }); @@ -2728,6 +2731,7 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(2_000), receivable_scan_interval: Duration::from_millis(10_000), }); @@ -3152,6 +3156,7 @@ mod tests { receivable_scanner, payable_scanner, ); + let retry_payble_scan_interval = Duration::from_millis(1); let pending_payable_scan_interval = Duration::from_secs(3600); let receivable_scan_interval = Duration::from_secs(3600); let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() @@ -3160,6 +3165,7 @@ mod tests { .stop_system_on_count_received(1); subject.scan_schedulers.pending_payable.handle = Box::new(pending_payable_notify_later_handle_mock); + subject.scan_schedulers.payable.retry_payable_scan_interval = retry_payble_scan_interval; subject.scan_schedulers.pending_payable.interval = pending_payable_scan_interval; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default() @@ -3472,7 +3478,7 @@ mod tests { ScanForRetryPayables { response_skeleton_opt: None }, - DEFAULT_RETRY_INTERVAL + Duration::from_millis(1) )], ); } @@ -3650,6 +3656,7 @@ mod tests { let mut config = bc_from_earning_wallet(earning_wallet.clone()); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(100), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(10), receivable_scan_interval: Duration::from_millis(99), }); @@ -3929,6 +3936,7 @@ mod tests { // This simply means that we're gonna surplus this value (it abides by how many pending // payable cycles have to go in between before the lastly submitted txs are confirmed), payable_scan_interval: Duration::from_millis(10), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner }); @@ -4017,6 +4025,7 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(100), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_millis(100), }); @@ -4139,6 +4148,7 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming"); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(50_000), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(10_000), receivable_scan_interval: Duration::from_secs(50_000), }); diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index fd6a79901..1b140a799 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -29,7 +29,10 @@ pub struct ScanSchedulers { impl ScanSchedulers { pub fn new(scan_intervals: ScanIntervals, automatic_scans_enabled: bool) -> Self { Self { - payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), + payable: PayableScanScheduler::new( + scan_intervals.payable_scan_interval, + scan_intervals.retry_payable_scan_interval, + ), pending_payable: SimplePeriodicalScanScheduler::new( scan_intervals.pending_payable_scan_interval, ), @@ -83,10 +86,11 @@ pub struct PayableScanScheduler { pub interval_computer: Box, pub new_payable_notify: Box>, pub retry_payable_notify_later: Box>, + pub retry_payable_scan_interval: Duration, } impl PayableScanScheduler { - fn new(new_payable_interval: Duration) -> Self { + fn new(new_payable_interval: Duration, retry_payable_scan_interval: Duration) -> Self { Self { new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), interval_computer: Box::new(NewPayableScanIntervalComputerReal::new( @@ -94,6 +98,7 @@ impl PayableScanScheduler { )), new_payable_notify: Box::new(NotifyHandleReal::default()), retry_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), + retry_payable_scan_interval, } } @@ -139,12 +144,13 @@ impl PayableScanScheduler { logger: &Logger, ) { debug!(logger, "Scheduling a retry-payable scan asap"); + let delay = self.retry_payable_scan_interval; let _ = self.retry_payable_notify_later.notify_later( ScanForRetryPayables { response_skeleton_opt, }, - DEFAULT_RETRY_INTERVAL, + delay, ctx, ); } @@ -414,6 +420,7 @@ mod tests { fn scan_schedulers_are_initialized_correctly() { let scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(14), + retry_payable_scan_interval: Default::default(), pending_payable_scan_interval: Duration::from_secs(2), receivable_scan_interval: Duration::from_secs(7), }; diff --git a/node/src/db_config/persistent_configuration.rs b/node/src/db_config/persistent_configuration.rs index 8567d7807..2b87147d1 100644 --- a/node/src/db_config/persistent_configuration.rs +++ b/node/src/db_config/persistent_configuration.rs @@ -2291,6 +2291,7 @@ mod tests { "60|5|50", ScanIntervals { payable_scan_interval: Duration::from_secs(60), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(5), receivable_scan_interval: Duration::from_secs(50), } diff --git a/node/src/node_configurator/configurator.rs b/node/src/node_configurator/configurator.rs index 95ad7115e..852729b20 100644 --- a/node/src/node_configurator/configurator.rs +++ b/node/src/node_configurator/configurator.rs @@ -2626,6 +2626,7 @@ mod tests { })) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(125), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(122), receivable_scan_interval: Duration::from_secs(128), })) @@ -2777,6 +2778,7 @@ mod tests { })) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Default::default(), + retry_payable_scan_interval: Default::default(), pending_payable_scan_interval: Default::default(), receivable_scan_interval: Default::default(), })) diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 313bf3048..35d682050 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -1829,6 +1829,7 @@ mod tests { configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(101), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(33), receivable_scan_interval: Duration::from_secs(102), })) @@ -1857,6 +1858,7 @@ mod tests { let expected_scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(180), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(50), receivable_scan_interval: Duration::from_secs(130), }; @@ -1907,6 +1909,7 @@ mod tests { configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(180), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), })) @@ -1939,6 +1942,7 @@ mod tests { }; let expected_scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(180), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), }; diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 039b1fe4f..b8eb5625f 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -5,6 +5,7 @@ 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::msgs::PricedTemplatesMessage; +use crate::accountant::scanners::scan_schedulers::DEFAULT_RETRY_INTERVAL; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ScanError, SentPayables, TxReceiptsMessage, }; @@ -77,6 +78,7 @@ pub struct DaoFactories { #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct ScanIntervals { pub payable_scan_interval: Duration, + pub retry_payable_scan_interval: Duration, pub pending_payable_scan_interval: Duration, pub receivable_scan_interval: Duration, } @@ -85,6 +87,7 @@ impl ScanIntervals { pub fn compute_default(chain: Chain) -> Self { Self { payable_scan_interval: Duration::from_secs(600), + retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs( chain.rec().default_pending_payable_interval_sec, ), @@ -204,6 +207,7 @@ pub enum DetailedScanType { #[cfg(test)] mod tests { + use crate::accountant::scanners::scan_schedulers::DEFAULT_RETRY_INTERVAL; use crate::accountant::test_utils::AccountantBuilder; use crate::accountant::{checked_conversion, Accountant}; use crate::sub_lib::accountant::{ @@ -319,6 +323,7 @@ mod tests { result_a, ScanIntervals { payable_scan_interval: Duration::from_secs(600), + retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs( chain_a.rec().default_pending_payable_interval_sec ), @@ -329,6 +334,7 @@ mod tests { result_b, ScanIntervals { payable_scan_interval: Duration::from_secs(600), + retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs( chain_b.rec().default_pending_payable_interval_sec ), diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index bd26eb627..2d1faebe7 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -178,6 +178,7 @@ impl CombinedParams { &parsed_values, Duration::from_secs, "payable_scan_interval", + "retry_payable_scan_interval", "pending_payable_scan_interval", "receivable_scan_interval" ))) @@ -305,6 +306,7 @@ fn unreachable() -> ! { #[cfg(test)] mod tests { use super::*; + use crate::accountant::scanners::scan_schedulers::DEFAULT_RETRY_INTERVAL; use crate::sub_lib::combined_parameters::CombinedParamsDataTypes::U128; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; @@ -559,6 +561,7 @@ mod tests { result, ScanIntervals { payable_scan_interval: Duration::from_secs(115), + retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs(55), receivable_scan_interval: Duration::from_secs(113) } @@ -569,6 +572,7 @@ mod tests { fn scan_intervals_to_combined_params() { let scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(90), + retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs(40), receivable_scan_interval: Duration::from_secs(100), }; diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index eeb2c9560..7605d2238 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -627,6 +627,7 @@ pub mod unshared_test_utils { lazy_static! { pub static ref TEST_SCAN_INTERVALS: ScanIntervals = ScanIntervals { payable_scan_interval: Duration::from_secs(600), + retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(360), receivable_scan_interval: Duration::from_secs(600), }; From 268eebaaf6b4d1f12331730a083e97c0dfa05bdb Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Mon, 20 Oct 2025 12:03:09 +0530 Subject: [PATCH 33/37] GH-598: retry payable scan interval is half the duration of payable scan interval --- node/src/accountant/mod.rs | 16 ++++---------- .../accountant/scanners/scan_schedulers.rs | 22 +++++-------------- .../src/db_config/persistent_configuration.rs | 1 - node/src/node_configurator/configurator.rs | 2 -- .../unprivileged_parse_args_configuration.rs | 4 ---- node/src/sub_lib/accountant.rs | 6 ----- node/src/sub_lib/combined_parameters.rs | 4 ---- node/src/test_utils/mod.rs | 1 - 8 files changed, 10 insertions(+), 46 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index cc7933099..31f3033b5 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1319,7 +1319,6 @@ mod tests { use crate::accountant::scanners::pending_payable_scanner::utils::TxByTable; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanIntervalComputer, NewPayableScanIntervalComputerReal, ScanTiming, - DEFAULT_RETRY_INTERVAL, }; use crate::accountant::scanners::test_utils::{ MarkScanner, NewPayableScanIntervalComputerMock, PendingPayableCacheMock, ReplacementType, @@ -1959,7 +1958,6 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), - retry_payable_scan_interval: Duration::from_millis(1), receivable_scan_interval: Duration::from_millis(10_000), pending_payable_scan_interval: Duration::from_secs(100), }); @@ -2682,7 +2680,6 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(2_000), receivable_scan_interval: Duration::from_millis(10_000), }); @@ -2731,7 +2728,6 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(2_000), receivable_scan_interval: Duration::from_millis(10_000), }); @@ -3656,7 +3652,6 @@ mod tests { let mut config = bc_from_earning_wallet(earning_wallet.clone()); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(100), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(10), receivable_scan_interval: Duration::from_millis(99), }); @@ -3936,7 +3931,6 @@ mod tests { // This simply means that we're gonna surplus this value (it abides by how many pending // payable cycles have to go in between before the lastly submitted txs are confirmed), payable_scan_interval: Duration::from_millis(10), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner }); @@ -4025,7 +4019,6 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(100), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_millis(100), }); @@ -4148,7 +4141,6 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming"); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(50_000), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(10_000), receivable_scan_interval: Duration::from_secs(50_000), }); @@ -5249,7 +5241,7 @@ mod tests { let mut payable_notify_params = retry_payable_notify_later_params_arc.lock().unwrap(); let (scheduled_msg, duration) = payable_notify_params.remove(0); assert_eq!(scheduled_msg, ScanForRetryPayables::default()); - assert_eq!(duration, DEFAULT_RETRY_INTERVAL); + assert_eq!(duration, Duration::from_secs(5 * 60)); assert!( payable_notify_params.is_empty(), "Should be empty but {:?}", @@ -5314,7 +5306,7 @@ mod tests { ScanForRetryPayables { response_skeleton_opt: None }, - DEFAULT_RETRY_INTERVAL + Duration::from_secs(5 * 60) )] ); assert_using_the_same_logger(&logger, test_name, None) @@ -5497,7 +5489,7 @@ mod tests { ScanForRetryPayables { response_skeleton_opt: Some(response_skeleton) }, - DEFAULT_RETRY_INTERVAL + Duration::from_secs(5 * 60) )] ); assert_using_the_same_logger(&logger, test_name, None) @@ -5932,7 +5924,7 @@ mod tests { ScanForRetryPayables { response_skeleton_opt: None }, - DEFAULT_RETRY_INTERVAL + Duration::from_secs(5 * 60) )] ) } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 1b140a799..97a3edd62 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -14,10 +14,9 @@ use masq_lib::logger::Logger; use masq_lib::messages::ScanType; use masq_lib::simple_clock::{SimpleClock, SimpleClockReal}; use std::fmt::{Debug, Display, Formatter}; +use std::ops::Div; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -pub const DEFAULT_RETRY_INTERVAL: Duration = Duration::from_secs(5 * 60); - pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, @@ -29,10 +28,7 @@ pub struct ScanSchedulers { impl ScanSchedulers { pub fn new(scan_intervals: ScanIntervals, automatic_scans_enabled: bool) -> Self { Self { - payable: PayableScanScheduler::new( - scan_intervals.payable_scan_interval, - scan_intervals.retry_payable_scan_interval, - ), + payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), pending_payable: SimplePeriodicalScanScheduler::new( scan_intervals.pending_payable_scan_interval, ), @@ -90,15 +86,15 @@ pub struct PayableScanScheduler { } impl PayableScanScheduler { - fn new(new_payable_interval: Duration, retry_payable_scan_interval: Duration) -> Self { + fn new(payable_scan_interval: Duration) -> Self { Self { new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), interval_computer: Box::new(NewPayableScanIntervalComputerReal::new( - new_payable_interval, + payable_scan_interval, )), new_payable_notify: Box::new(NotifyHandleReal::default()), retry_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), - retry_payable_scan_interval, + retry_payable_scan_interval: payable_scan_interval.div(2), } } @@ -394,7 +390,7 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanIntervalComputer, NewPayableScanIntervalComputerReal, PayableSequenceScanner, - ScanReschedulingAfterEarlyStop, ScanSchedulers, ScanTiming, DEFAULT_RETRY_INTERVAL, + ScanReschedulingAfterEarlyStop, ScanSchedulers, ScanTiming, }; use crate::accountant::scanners::test_utils::NewPayableScanIntervalComputerMock; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; @@ -411,16 +407,10 @@ mod tests { use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; - #[test] - fn constants_have_correct_values() { - assert_eq!(DEFAULT_RETRY_INTERVAL, Duration::from_secs(5 * 60)); - } - #[test] fn scan_schedulers_are_initialized_correctly() { let scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(14), - retry_payable_scan_interval: Default::default(), pending_payable_scan_interval: Duration::from_secs(2), receivable_scan_interval: Duration::from_secs(7), }; diff --git a/node/src/db_config/persistent_configuration.rs b/node/src/db_config/persistent_configuration.rs index 2b87147d1..8567d7807 100644 --- a/node/src/db_config/persistent_configuration.rs +++ b/node/src/db_config/persistent_configuration.rs @@ -2291,7 +2291,6 @@ mod tests { "60|5|50", ScanIntervals { payable_scan_interval: Duration::from_secs(60), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(5), receivable_scan_interval: Duration::from_secs(50), } diff --git a/node/src/node_configurator/configurator.rs b/node/src/node_configurator/configurator.rs index 852729b20..95ad7115e 100644 --- a/node/src/node_configurator/configurator.rs +++ b/node/src/node_configurator/configurator.rs @@ -2626,7 +2626,6 @@ mod tests { })) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(125), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(122), receivable_scan_interval: Duration::from_secs(128), })) @@ -2778,7 +2777,6 @@ mod tests { })) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Default::default(), - retry_payable_scan_interval: Default::default(), pending_payable_scan_interval: Default::default(), receivable_scan_interval: Default::default(), })) diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 35d682050..313bf3048 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -1829,7 +1829,6 @@ mod tests { configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(101), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(33), receivable_scan_interval: Duration::from_secs(102), })) @@ -1858,7 +1857,6 @@ mod tests { let expected_scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(180), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(50), receivable_scan_interval: Duration::from_secs(130), }; @@ -1909,7 +1907,6 @@ mod tests { configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(180), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), })) @@ -1942,7 +1939,6 @@ mod tests { }; let expected_scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(180), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), }; diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index b8eb5625f..039b1fe4f 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -5,7 +5,6 @@ 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::msgs::PricedTemplatesMessage; -use crate::accountant::scanners::scan_schedulers::DEFAULT_RETRY_INTERVAL; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ScanError, SentPayables, TxReceiptsMessage, }; @@ -78,7 +77,6 @@ pub struct DaoFactories { #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct ScanIntervals { pub payable_scan_interval: Duration, - pub retry_payable_scan_interval: Duration, pub pending_payable_scan_interval: Duration, pub receivable_scan_interval: Duration, } @@ -87,7 +85,6 @@ impl ScanIntervals { pub fn compute_default(chain: Chain) -> Self { Self { payable_scan_interval: Duration::from_secs(600), - retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs( chain.rec().default_pending_payable_interval_sec, ), @@ -207,7 +204,6 @@ pub enum DetailedScanType { #[cfg(test)] mod tests { - use crate::accountant::scanners::scan_schedulers::DEFAULT_RETRY_INTERVAL; use crate::accountant::test_utils::AccountantBuilder; use crate::accountant::{checked_conversion, Accountant}; use crate::sub_lib::accountant::{ @@ -323,7 +319,6 @@ mod tests { result_a, ScanIntervals { payable_scan_interval: Duration::from_secs(600), - retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs( chain_a.rec().default_pending_payable_interval_sec ), @@ -334,7 +329,6 @@ mod tests { result_b, ScanIntervals { payable_scan_interval: Duration::from_secs(600), - retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs( chain_b.rec().default_pending_payable_interval_sec ), diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index 2d1faebe7..bd26eb627 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -178,7 +178,6 @@ impl CombinedParams { &parsed_values, Duration::from_secs, "payable_scan_interval", - "retry_payable_scan_interval", "pending_payable_scan_interval", "receivable_scan_interval" ))) @@ -306,7 +305,6 @@ fn unreachable() -> ! { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::scan_schedulers::DEFAULT_RETRY_INTERVAL; use crate::sub_lib::combined_parameters::CombinedParamsDataTypes::U128; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; @@ -561,7 +559,6 @@ mod tests { result, ScanIntervals { payable_scan_interval: Duration::from_secs(115), - retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs(55), receivable_scan_interval: Duration::from_secs(113) } @@ -572,7 +569,6 @@ mod tests { fn scan_intervals_to_combined_params() { let scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(90), - retry_payable_scan_interval: DEFAULT_RETRY_INTERVAL, pending_payable_scan_interval: Duration::from_secs(40), receivable_scan_interval: Duration::from_secs(100), }; diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 7605d2238..eeb2c9560 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -627,7 +627,6 @@ pub mod unshared_test_utils { lazy_static! { pub static ref TEST_SCAN_INTERVALS: ScanIntervals = ScanIntervals { payable_scan_interval: Duration::from_secs(600), - retry_payable_scan_interval: Duration::from_millis(1), pending_payable_scan_interval: Duration::from_secs(360), receivable_scan_interval: Duration::from_secs(600), }; From 77a1d1c6e690bc2de9d2f09e8ec61645f1406240 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Mon, 20 Oct 2025 13:17:34 +0530 Subject: [PATCH 34/37] GH-598: add public rpc url for eth mainnet --- node/tests/contract_test.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/node/tests/contract_test.rs b/node/tests/contract_test.rs index 42d6fce37..35b7e0f85 100644 --- a/node/tests/contract_test.rs +++ b/node/tests/contract_test.rs @@ -133,7 +133,10 @@ fn masq_erc20_contract_exists_on_polygon_mainnet_integration() { #[test] fn masq_erc20_contract_exists_on_ethereum_mainnet_integration() { - let blockchain_urls = vec!["https://mainnet.infura.io/v3/0ead23143b174f6983c76f69ddcf4026"]; + let blockchain_urls = vec![ + "https://eth.llamarpc.com", + "https://mainnet.infura.io/v3/0ead23143b174f6983c76f69ddcf4026", + ]; let chain = Chain::EthMainnet; let assertion_body = |url, chain| assert_contract_existence(url, chain, "MASQ", 18); @@ -207,7 +210,10 @@ fn assert_total_supply( #[test] fn max_token_supply_matches_corresponding_constant_integration() { - let blockchain_urls = vec!["https://mainnet.infura.io/v3/0ead23143b174f6983c76f69ddcf4026"]; + let blockchain_urls = vec![ + "https://eth.llamarpc.com", + "https://mainnet.infura.io/v3/0ead23143b174f6983c76f69ddcf4026", + ]; let chain = Chain::EthMainnet; let assertion_body = |url, chain| assert_total_supply(url, chain, MASQ_TOTAL_SUPPLY); From 25af8cba16e8ef2f748e764d6abecbc8b17ea500 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 2 Nov 2025 13:51:43 +0100 Subject: [PATCH 35/37] GH-598: transaction count param from pending to latest --- .../blockchain_interface_web3/lower_level_interface_web3.rs | 4 ++-- .../blockchain_interface/blockchain_interface_web3/mod.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index 7a4d6ddfb..e2c6f4dcc 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -9,7 +9,7 @@ use futures::Future; use serde_json::Value; use web3::contract::{Contract, Options}; use web3::transports::{Batch, Http}; -use web3::types::{Address, BlockNumber, Filter, Log}; +use web3::types::{Address, Filter, Log}; use web3::{Error, Web3}; pub struct LowBlockchainIntWeb3 { @@ -68,7 +68,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { Box::new( self.web3 .eth() - .transaction_count(address, Some(BlockNumber::Pending)) + .transaction_count(address, None) .map_err(move |e| QueryFailed(format!("{} for wallet {}", e, address))), ) } 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 9249c6ee0..18e95ee29 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -279,6 +279,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { get_transaction_id .map_err(LocalPayableError::TransactionID) .and_then(move |latest_nonce| { + debug!(logger, "Latest nonce: {:?}", latest_nonce); + let templates = SignableTxTemplates::new(priced_templates, latest_nonce.as_u64()); From 858adfa8b4e8b64c21637783b425b600d3f99d76 Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:28:26 +0100 Subject: [PATCH 36/37] GH-598-hot-fix-panic-bug: fixed (#746) --- node/src/blockchain/blockchain_bridge.rs | 83 +++++++++++++----------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 183f58659..d892409dc 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -312,28 +312,32 @@ impl BlockchainBridge { Box::new( 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 |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"); - + .then(move |result| { + match result { + Ok(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"); + } + Err(e) => { + sent_payable_subs_err + .try_send(SentPayables { + payment_procedure_result: + Self::payment_procedure_result_from_error(e), + payable_scan_type, + response_skeleton_opt: skeleton_opt, + }) + .expect("Accountant is dead"); + } + } + // TODO Temporary workaround: prevents the Accountant from receiving two messages + // describing the same error. Duplicate notifications could previously trigger + // a panic in the scanners, because the substitution call for a given scanner + // was executed twice and tripped the guard that enforces a single concurrent scan. Ok(()) }), ) @@ -1021,7 +1025,7 @@ mod tests { // 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 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( @@ -1048,22 +1052,23 @@ mod tests { // amount: account.balance_wei // }] // ); - assert_eq!(scan_error_msg.scan_type, DetailedScanType::NewPayables); - assert_eq!( - scan_error_msg.response_skeleton_opt, - Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321 - }) - ); - 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); + // assert_eq!(scan_error_msg.scan_type, DetailedScanType::NewPayables); + // assert_eq!( + // scan_error_msg.response_skeleton_opt, + // Some(ResponseSkeleton { + // client_id: 1234, + // context_id: 4321 + // }) + // ); + // 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); + assert_eq!(accountant_recording.len(), 1); } #[test] From 6dab7987314caf1deb17ddf24a5723fbf146a154 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 20 Nov 2025 13:44:37 +0100 Subject: [PATCH 37/37] GH-598: fixed a flaw from last merge --- node/src/blockchain/blockchain_bridge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index cf0590f36..8ced8b33e 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -2205,7 +2205,7 @@ mod tests { #[test] fn extract_max_block_range_for_nodies_error_response_v2() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32001), message: \"Block range too large: maximum allowed is 20000 blocks\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32001), message: \"Block range too large: maximum allowed is 20000 blocks\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result);