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/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/masq_lib/src/blockchains/blockchain_records.rs b/masq_lib/src/blockchains/blockchain_records.rs index cc1198afa..821b0d7f1 100644 --- a/masq_lib/src/blockchains/blockchain_records.rs +++ b/masq_lib/src/blockchains/blockchain_records.rs @@ -2,63 +2,82 @@ 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, 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, - 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}; -pub const CHAINS: [BlockchainRecord; 7] = [ +pub static 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, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, 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, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, 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, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, 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, + 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, }, 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, + 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, }, 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, + 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, }, 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, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, }, @@ -69,6 +88,8 @@ 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 default_pending_payable_interval_sec: u64, pub contract: Address, pub contract_creation_block: u64, } @@ -115,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; + 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; @@ -195,6 +220,8 @@ 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, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -211,6 +238,8 @@ 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, + 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, } @@ -227,6 +256,8 @@ 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, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -243,6 +274,8 @@ 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, + 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, } @@ -259,6 +292,8 @@ 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, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -275,6 +310,8 @@ 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, + 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, } @@ -291,6 +328,8 @@ mod tests { num_chain_id: 2, 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 b7733b842..cedde32d1 100644 --- a/masq_lib/src/blockchains/chains.rs +++ b/masq_lib/src/blockchains/chains.rs @@ -141,6 +141,8 @@ mod tests { num_chain_id: 0, 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 e1e4c0fe4..fcbec6826 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; @@ -18,23 +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 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 //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -87,13 +80,20 @@ 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; +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"; @@ -107,6 +107,24 @@ pub const BASE_MAINNET_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, MAIN 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::*; @@ -124,6 +142,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 +188,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 +206,14 @@ 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!(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/lib.rs b/masq_lib/src/lib.rs index 1fc5eb68d..ae638163e 100644 --- a/masq_lib/src/lib.rs +++ b/masq_lib/src/lib.rs @@ -25,6 +25,5 @@ pub mod data_version; pub mod exit_locations; pub mod shared_schema; pub mod test_utils; -pub mod type_obfuscation; pub mod ui_gateway; pub mod ui_traffic_converter; diff --git a/masq_lib/src/messages.rs b/masq_lib/src/messages.rs index 9641c5086..d93332c3d 100644 --- a/masq_lib/src/messages.rs +++ b/masq_lib/src/messages.rs @@ -527,10 +527,10 @@ pub struct UiRatePack { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct UiScanIntervals { - #[serde(rename = "pendingPayableSec")] - pub pending_payable_sec: u64, #[serde(rename = "payableSec")] pub payable_sec: u64, + #[serde(rename = "pendingPayableSec")] + pub pending_payable_sec: u64, #[serde(rename = "receivableSec")] pub receivable_sec: u64, } @@ -783,8 +783,8 @@ conversation_message!(UiRecoverWalletsResponse, "recoverWallets"); #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum ScanType { Payables, - Receivables, PendingPayables, + Receivables, } impl FromStr for ScanType { @@ -793,8 +793,8 @@ impl FromStr for ScanType { fn from_str(s: &str) -> Result { match s { s if &s.to_lowercase() == "payables" => Ok(ScanType::Payables), - s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), s if &s.to_lowercase() == "pendingpayables" => Ok(ScanType::PendingPayables), + s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), s => Err(format!("Unrecognized ScanType: '{}'", s)), } } @@ -1225,10 +1225,10 @@ mod tests { let result: Vec = vec![ "Payables", "pAYABLES", - "Receivables", - "rECEIVABLES", "PendingPayables", "pENDINGpAYABLES", + "Receivables", + "rECEIVABLES", ] .into_iter() .map(|s| ScanType::from_str(s).unwrap()) @@ -1239,10 +1239,10 @@ mod tests { vec![ ScanType::Payables, ScanType::Payables, - ScanType::Receivables, - ScanType::Receivables, ScanType::PendingPayables, ScanType::PendingPayables, + ScanType::Receivables, + ScanType::Receivables, ] ) } diff --git a/masq_lib/src/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/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/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/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/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 d02b69416..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." @@ -15,7 +15,7 @@ automap = { path = "../automap"} backtrace = "0.3.57" base64 = "0.13.0" bytes = "0.4.12" -time = {version = "0.3.11", features = [ "macros" ]} +time = {version = "0.3.11", features = [ "macros", "parsing" ]} clap = "2.33.3" crossbeam-channel = "0.5.1" dirs = "4.0.0" diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs new file mode 100644 index 000000000..7a6e509bc --- /dev/null +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -0,0 +1,1008 @@ +// 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, 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::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::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(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum FailureReason { + Submission(AppRpcErrorKind), + Reverted, + PendingTooLong, +} + +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, ""), + } + } +} + +impl FromStr for FailureReason { + type Err = String; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(|e| format!("{} in '{}'", e, s)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum FailureStatus { + RetryRequired, + 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 { + serde_json::from_str(s).map_err(|e| format!("{} in '{}'", e, s)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FailedTx { + pub hash: TxHash, + pub receiver_address: Address, + pub amount_minor: u128, + pub timestamp: i64, + 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, + ) -> Result<(), FailedPayableDaoError>; + //TODO potentially atomically + 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, + ))); + } + + 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, \ + 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 + ) + }) + ); + + 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, \ + status \ + 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_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_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"); + let reason = + FailureReason::from_str(&reason_str).expect("Failed to parse FailureReason"); + let status_str: String = row.get(9).expectv("status"); + let status = + FailureStatus::from_str(&status_str).expect("Failed to parse FailureStatus"); + + Ok(FailedTx { + hash, + receiver_address, + amount_minor, + timestamp, + gas_price_minor, + nonce, + reason, + status, + }) + }) + .expect("Failed to execute query") + .vigilant_flatten() + .collect() + } + + 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 \ + 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(FailedPayableDaoError::PartialExecution(format!( + "Only {} of {} records had their status updated.", + rows_changed, + status_updates.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())), + } + } +} + +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::{ + PendingTooLong, Reverted, + }; + 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, 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::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::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::ops::Add; + use std::str::FromStr; + use std::time::{Duration, SystemTime}; + + #[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(Reverted) + .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) + .status(RetryRequired) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(hash) + .status(RecheckRequired(ValidationStatus::Waiting)) + .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_minor: 0, timestamp: 0, gas_price_minor: 0, \ + nonce: 0, 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) }]" + .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) + .status(RetryRequired) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(hash) + .status(RecheckRequired(ValidationStatus::Waiting)) + .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_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 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 + assert_eq!( + FailureReason::from_str(r#"{"Submission":{"Local":"Decoder"}}"#).unwrap(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Decoder)) + ); + + // Reverted + assert_eq!( + FailureReason::from_str("\"Reverted\"").unwrap(), + FailureReason::Reverted + ); + + // PendingTooLong + assert_eq!( + FailureReason::from_str("\"PendingTooLong\"").unwrap(), + FailureReason::PendingTooLong + ); + + // Invalid Variant + assert_eq!( + FailureReason::from_str("\"UnknownReason\"").unwrap_err(), + "unknown variant `UnknownReason`, \ + expected one of `Submission`, `Reverted`, `PendingTooLong` \ + at line 1 column 15 in '\"UnknownReason\"'" + ); + + // Invalid Input + assert_eq!( + 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() { + 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 + ); + + assert_eq!( + FailureStatus::from_str(r#"{"RecheckRequired":"Waiting"}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Waiting) + ); + + assert_eq!( + 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!( + 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'" + ); + } + + #[test] + fn retrieve_condition_display_works() { + assert_eq!( + FailureRetrieveCondition::ByStatus(RetryRequired).to_string(), + "WHERE status = '\"RetryRequired\"'" + ); + } + + #[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) + .status(RetryRequired) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(Reverted) + .timestamp(now - 3600) + .status(RetryRequired) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .reason(PendingTooLong) + .status(RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + ))) + .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.clone(), tx3, tx4]) + .unwrap(); + + let result = subject.retrieve_txs(Some(FailureRetrieveCondition::ByStatus(RetryRequired))); + + assert_eq!(result, vec![tx1, tx2]); + } + + #[test] + fn update_statuses_works() { + let home_dir = + ensure_node_home_directory_exists("failed_payable_dao", "update_statuses_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(Reverted) + .status(RetryRequired) + .build(); + let tx2 = FailedTxBuilder::default() + .hash(make_tx_hash(2)) + .reason(PendingTooLong) + .status(RecheckRequired(ValidationStatus::Waiting)) + .build(); + let tx3 = FailedTxBuilder::default() + .hash(make_tx_hash(3)) + .reason(PendingTooLong) + .status(RetryRequired) + .build(); + let tx4 = FailedTxBuilder::default() + .hash(make_tx_hash(4)) + .reason(PendingTooLong) + .status(RecheckRequired(ValidationStatus::Waiting)) + .build(); + 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::Remote( + RemoteErrorKind::Unreachable, + )), + &clock, + ))), + ), + (tx3.hash, Concluded), + ]); + + let result = subject.update_statuses(&hashmap); + + let updated_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(tx1.status, RetryRequired); + assert_eq!(updated_txs[0].status, Concluded); + assert_eq!(tx2.status, RecheckRequired(ValidationStatus::Waiting)); + assert_eq!( + updated_txs[1].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!(tx4.status, RecheckRequired(ValidationStatus::Waiting)); + assert_eq!( + updated_txs[3].status, + RecheckRequired(ValidationStatus::Waiting) + ); + assert_eq!(updated_txs.len(), 4); + } + + #[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 update_statuses_handles_sql_error() { + let home_dir = ensure_node_home_directory_exists( + "failed_payable_dao", + "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.update_statuses(&HashMap::from([(make_tx_hash(1), Concluded)])); + + 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() + )) + ) + } + + #[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 a350148ab..0141e8796 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -1,7 +1,10 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod banned_dao; +pub mod failed_payable_dao; pub mod payable_dao; -pub mod pending_payable_dao; pub mod receivable_dao; +pub mod sent_payable_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 88897281b..cff264a58 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -1,32 +1,30 @@ // 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_time_t, AssemblerFeeder, CustomQuery, DaoFactoryReal, - RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten, + sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, + 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; 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 +46,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 +76,11 @@ impl PayableDaoFactory for DaoFactoryReal { } } +pub struct MarkPendingPayableID { + pub receiver_wallet: Address, + pub rowid: RowId, +} + #[derive(Debug)] pub struct PayableDaoReal { conn: Box, @@ -92,7 +92,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 \ @@ -100,12 +100,12 @@ 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( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( @@ -123,46 +123,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_time_t(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( @@ -196,7 +192,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 +278,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(), @@ -316,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_time_t(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 @@ -362,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 {}, @@ -389,175 +364,181 @@ 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_time_t, now_time_t, to_time_t}; + 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() { @@ -577,7 +558,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 +600,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 +637,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) ); } @@ -701,260 +685,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_time_t(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), - 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_time_t(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, } - fn make_fingerprint_pair_and_insert_initial_payable_records( + struct TxWalletAndTimestamp { + pending_payable: SentTx, + previous_timestamp: SystemTime, + } + + 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_time_t(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 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_time_t(previous_timestamp_1_s); - let previous_timestamp_2 = from_time_t(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, } } @@ -965,7 +960,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, ) @@ -978,77 +973,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)) } @@ -1060,22 +1058,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 + ))) ) } @@ -1083,26 +1079,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] @@ -1114,38 +1105,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); } @@ -1203,13 +1193,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 }, ] @@ -1301,10 +1291,10 @@ 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. - let now = now_time_t(); + // 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( "custom_query_in_top_records_mode_with_default_ordering", @@ -1324,25 +1314,19 @@ 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), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + 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 }, ] @@ -1351,10 +1335,10 @@ 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. - let now = now_time_t(); + // 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( "custom_query_in_top_records_mode_ordered_by_age", @@ -1374,25 +1358,19 @@ mod tests { PayableAccount { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, - last_paid_timestamp: from_time_t(now - 86_401), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + last_paid_timestamp: from_unix_timestamp(now - 86_401), + pending_payable_opt: None }, 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 }, ] @@ -1420,9 +1398,9 @@ 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. - let now = now_time_t(); + // 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( conn, @@ -1482,7 +1460,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,26 +1470,20 @@ 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), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + last_paid_timestamp: from_unix_timestamp(now - 55_120), + pending_payable_opt: None } ] ); @@ -1519,7 +1491,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 +1530,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 +1542,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", @@ -1677,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 67c779ce0..000000000 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ /dev/null @@ -1,950 +0,0 @@ -// 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, -}; -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; -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(PendingPayableFingerprint { - rowid, - timestamp: from_time_t(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 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_time_t(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 fingerprint, 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(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, -} - -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::pending_payable_dao::{ - PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, - }; - use crate::accountant::db_access_objects::utils::from_time_t; - 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::{ - 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( - "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_time_t(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_time_t(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_time_t(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_time_t(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_time_t(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_time_t(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_time_t(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_time_t(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_time_t(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]); - } -} diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index 9b71a3939..9d100c633 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; @@ -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 \ @@ -120,12 +120,12 @@ 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( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( @@ -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_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 new file mode 100644 index 000000000..a82bafdce --- /dev/null +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -0,0 +1,1490 @@ +// 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, +}; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +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 std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use web3::types::Address; + +#[derive(Debug, PartialEq, Eq)] +pub enum SentPayableDaoError { + EmptyInput, + NoChange, + InvalidInput(String), + PartialExecution(String), + SqlExecutionFailed(String), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SentTx { + pub hash: TxHash, + pub receiver_address: Address, + pub amount_minor: u128, + pub timestamp: i64, + 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), + 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 for TxStatus { + fn from(tx_block: TxBlock) -> Self { + TxStatus::Confirmed { + 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(Debug, PartialEq, Eq)] +pub enum RetrieveCondition { + IsPending, + ByHash(Vec), + ByNonce(Vec), +} + +impl Display for RetrieveCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RetrieveCondition::IsPending => { + write!(f, r#"WHERE status LIKE '%"Pending":%'"#) + } + RetrieveCondition::ByHash(tx_hashes) => { + write!( + f, + "WHERE tx_hash IN ({})", + 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: &[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, + ) -> Result<(), SentPayableDaoError>; + //TODO potentially atomically + 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: &[SentTx]) -> Result<(), SentPayableDaoError> { + if txs.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let unique_hashes: HashSet = txs.iter().map(|tx| tx.hash).collect(); + if unique_hashes.len() != txs.len() { + return Err(SentPayableDaoError::InvalidInput(format!( + "Duplicate hashes found in the input. Input Transactions: {:?}", + txs + ))); + } + + let duplicates = self.get_tx_identifiers(&unique_hashes); + if !duplicates.is_empty() { + return Err(SentPayableDaoError::InvalidInput(format!( + "Duplicates detected in the database: {:?}", + duplicates, + ))); + } + + let sql = format!( + "INSERT INTO sent_payable (\ + tx_hash, \ + receiver_address, \ + amount_high_b, \ + amount_low_b, \ + timestamp, \ + gas_price_wei_high_b, \ + gas_price_wei_low_b, \ + nonce, \ + 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 + ) + }) + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(inserted_rows) => { + if inserted_rows == txs.len() { + Ok(()) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} out of {} records inserted", + inserted_rows, + txs.len() + ))) + } + } + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + + fn retrieve_txs(&self, condition_opt: Option) -> Vec { + let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ + timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, 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_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_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(SentTx { + hash, + receiver_address, + amount_minor, + timestamp, + gas_price_minor, + nonce, + status, + }) + }) + .expect("Failed to execute query") + .filter_map(Result::ok) + .collect() + } + + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { + if hash_map.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + for (hash, tx_block) in hash_map { + let sql = format!( + "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", + TxStatus::from(*tx_block), + hash + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(updated_rows) => { + if updated_rows == 1 { + continue; + } else { + return Err(SentPayableDaoError::PartialExecution(format!( + "Failed to update status for hash {:?}", + hash + ))); + } + } + Err(e) => { + return Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())); + } + } + } + + Ok(()) + } + + fn replace_records(&self, new_txs: &[SentTx]) -> 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(" ") + }; + + 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_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_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_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_minor); + let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); + low.to_string() + }); + let status_cases = build_case(|tx| format!("'{}'", tx.status)); + + 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, \ + status = CASE \ + {status_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 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); + } + + 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())), + } + } +} + +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 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 masq_lib::test_utils::utils::ensure_node_home_directory_exists; + 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() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "insert_new_records_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(2)) + .status(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + ))) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let txs = vec![tx1, tx2]; + + let result = subject.insert_new_records(&txs); + + let retrieved_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(retrieved_txs, txs); + } + + #[test] + fn insert_new_records_throws_err_for_empty_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_err_for_empty_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let empty_input = vec![]; + + let result = subject.insert_new_records(&empty_input); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_error_when_two_txs_with_same_hash_are_present_in_the_input", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(1234); + let tx1 = TxBuilder::default() + .hash(hash) + .timestamp(1749204017) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); + let tx2 = TxBuilder::default() + .hash(hash) + .timestamp(1749204020) + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(456)), + block_number: 7890123, + detection: Detection::Reclaim, + }) + .build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.insert_new_records(&vec![tx1, tx2]); + + assert_eq!( + result, + Err(SentPayableDaoError::InvalidInput( + "Duplicate hashes found in the input. Input Transactions: \ + [SentTx { \ + hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ + nonce: 0, status: Pending(Waiting) }, \ + SentTx { \ + hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount_minor: 0, timestamp: 1749204020, gas_price_minor: 0, \ + nonce: 0, status: Confirmed { block_hash: \ + \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ + block_number: 7890123, detection: Reclaim } }]" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_throws_error_when_input_tx_hash_is_already_present_in_the_db", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let hash = make_tx_hash(1234); + let tx1 = TxBuilder::default().hash(hash).build(); + let tx2 = TxBuilder::default().hash(hash).build(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let initial_insertion_result = subject.insert_new_records(&vec![tx1]); + + let result = subject.insert_new_records(&vec![tx2]); + + assert_eq!(initial_insertion_result, Ok(())); + assert_eq!( + result, + Err(SentPayableDaoError::InvalidInput( + "Duplicates detected in the database: \ + {0x00000000000000000000000000000000000000000000000000000000000004d2: 1}" + .to_string() + )) + ); + } + + #[test] + fn insert_new_records_returns_err_if_partially_executed() { + let setup_conn = Connection::open_in_memory().unwrap(); + setup_conn + .execute("CREATE TABLE example (id integer)", []) + .unwrap(); + let get_tx_identifiers_stmt = setup_conn.prepare("SELECT id FROM example").unwrap(); + let faulty_insert_stmt = { setup_conn.prepare("SELECT id FROM example").unwrap() }; + let wrapped_conn = ConnectionWrapperMock::default() + .prepare_result(Ok(get_tx_identifiers_stmt)) + .prepare_result(Ok(faulty_insert_stmt)); + let tx = TxBuilder::default().build(); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution( + "Only 0 out of 1 records inserted".to_string() + )) + ); + } + + #[test] + fn insert_new_records_can_throw_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "insert_new_records_can_throw_error", + ); + let tx = TxBuilder::default().build(); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.insert_new_records(&vec![tx]); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn get_tx_identifiers_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "get_tx_identifiers_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let another_present_hash = make_tx_hash(3); + let hashset = HashSet::from([present_hash, absent_hash, another_present_hash]); + let present_tx = TxBuilder::default().hash(present_hash).build(); + let another_present_tx = TxBuilder::default().hash(another_present_hash).build(); + subject + .insert_new_records(&vec![present_tx, another_present_tx]) + .unwrap(); + + let result = subject.get_tx_identifiers(&hashset); + + assert_eq!(result.get(&present_hash), Some(&1u64)); + assert_eq!(result.get(&absent_hash), None); + assert_eq!(result.get(&another_present_hash), Some(&2u64)); + } + + #[test] + fn retrieve_condition_display_works() { + assert_eq!(IsPending.to_string(), "WHERE status LIKE '%\"Pending\":%'"); + assert_eq!( + ByHash(vec![ + H256::from_low_u64_be(0x123456789), + H256::from_low_u64_be(0x987654321), + ]) + .to_string(), + "WHERE tx_hash IN (\ + '0x0000000000000000000000000000000000000000000000000000000123456789', \ + '0x0000000000000000000000000000000000000000000000000000000987654321'\ + )" + ); + assert_eq!(ByNonce(vec![45, 47]).to_string(), "WHERE nonce IN (45, 47)") + } + + #[test] + fn can_retrieve_all_txs() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_all_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone()]) + .unwrap(); + subject.insert_new_records(&vec![tx3.clone()]).unwrap(); + + let result = subject.retrieve_txs(None); + + assert_eq!(result, vec![tx1, tx2, tx3]); + } + + #[test] + fn can_retrieve_pending_txs() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "can_retrieve_pending_txs"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(make_tx_hash(1)) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(2)) + .status(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + ))) + .build(); + let tx3 = TxBuilder::default() + .hash(make_tx_hash(3)) + .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]) + .unwrap(); + + let result = subject.retrieve_txs(Some(RetrieveCondition::IsPending)); + + assert_eq!(result, vec![tx1, tx2]); + } + + #[test] + fn tx_can_be_retrieved_by_hash() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_hash"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx3.hash]))); + + assert_eq!(result, vec![tx1, tx3]); + } + + #[test] + 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"); + 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(); + subject + .insert_new_records(&vec![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 confirmed_tx_block_1 = TxBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), + }; + let confirmed_tx_block_2 = TxBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), + }; + let hash_map = HashMap::from([ + (tx1.hash, confirmed_tx_block_1.clone()), + (tx2.hash, confirmed_tx_block_2.clone()), + ]); + + let result = subject.confirm_txs(&hash_map); + + let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); + assert_eq!(result, Ok(())); + assert_eq!( + pre_assert_status_tx1, + TxStatus::Pending(ValidationStatus::Waiting) + ); + assert_eq!( + updated_txs[0].status, + TxStatus::Confirmed { + block_hash: format!("{:?}", confirmed_tx_block_1.block_hash), + block_number: confirmed_tx_block_1.block_number.as_u64(), + detection: Detection::Normal + } + ); + assert_eq!( + pre_assert_status_tx2, + TxStatus::Pending(ValidationStatus::Waiting) + ); + assert_eq!( + updated_txs[1].status, + TxStatus::Confirmed { + block_hash: format!("{:?}", confirmed_tx_block_2.block_hash), + block_number: confirmed_tx_block_2.block_number.as_u64(), + detection: Detection::Normal + } + ); + } + + #[test] + fn confirm_tx_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "confirm_tx_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let existent_hash = make_tx_hash(1); + let tx = TxBuilder::default().hash(existent_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hash_map = HashMap::new(); + + let result = subject.confirm_txs(&hash_map); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn confirm_tx_returns_error_during_partial_execution() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "confirm_tx_returns_error_during_partial_execution", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let existent_hash = make_tx_hash(1); + let non_existent_hash = make_tx_hash(999); + let tx = TxBuilder::default().hash(existent_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hash_map = HashMap::from([ + ( + existent_hash, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), + }, + ), + ( + non_existent_hash, + TxBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), + }, + ), + ]); + + let result = subject.confirm_txs(&hash_map); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution(format!( + "Failed to update status for hash {:?}", + non_existent_hash + ))) + ); + } + + #[test] + fn confirm_tx_returns_error_when_an_error_occurs_while_executing_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "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, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), + }, + )]); + + let result = subject.confirm_txs(&hash_map); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn txs_can_be_deleted() { + let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "txs_can_be_deleted"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); + let tx2 = TxBuilder::default().hash(make_tx_hash(2)).build(); + let tx3 = TxBuilder::default().hash(make_tx_hash(3)).build(); + let tx4 = TxBuilder::default().hash(make_tx_hash(4)).build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) + .unwrap(); + let hashset = HashSet::from([tx1.hash, tx3.hash]); + + let result = subject.delete_records(&hashset); + + let remaining_records = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!(remaining_records, vec![tx2, tx4]); + } + + #[test] + fn delete_records_returns_error_when_input_is_empty() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_input_is_empty", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.delete_records(&HashSet::new()); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn delete_records_returns_error_when_no_records_are_deleted() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_no_records_are_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let non_existent_hash = make_tx_hash(999); + let hashset = HashSet::from([non_existent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!(result, Err(SentPayableDaoError::NoChange)); + } + + #[test] + fn delete_records_returns_error_when_not_all_input_records_were_deleted() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_error_when_not_all_input_records_were_deleted", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let present_hash = make_tx_hash(1); + let absent_hash = make_tx_hash(2); + let tx = TxBuilder::default().hash(present_hash).build(); + subject.insert_new_records(&vec![tx]).unwrap(); + let hashset = HashSet::from([present_hash, absent_hash]); + + let result = subject.delete_records(&hashset); + + assert_eq!( + result, + Err(SentPayableDaoError::PartialExecution( + "Only 1 of the 2 hashes has been deleted.".to_string() + )) + ); + } + + #[test] + fn delete_records_returns_a_general_error_from_sql() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "delete_records_returns_a_general_error_from_sql", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + let hashes = HashSet::from([make_tx_hash(1)]); + + let result = subject.delete_records(&hashes); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ) + } + + #[test] + fn 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( + "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)) + .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)) + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(789)), + block_number: 45454566, + detection: Detection::Reclaim, + }) + .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("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'")); + 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)) + .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)) + .status(TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(66666)), + block_number: 353535, + detection: Detection::Reclaim, + }) + .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() + )) + ) + } + + #[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":[{"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!( + 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_made_from_transaction_block() { + let tx_block = TxBlock { + block_hash: make_block_hash(6), + block_number: 456789_u64.into(), + }; + + assert_eq!( + TxStatus::from(tx_block), + TxStatus::Confirmed { + 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 new file mode 100644 index 000000000..e395aa2de --- /dev/null +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -0,0 +1,142 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +#![cfg(test)] + +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, +}; +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, +}; +use crate::database::rusqlite_wrappers::ConnectionWrapperReal; +use rusqlite::{Connection, OpenFlags}; +use std::path::PathBuf; +use web3::types::Address; + +#[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: 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 status(mut self, status: TxStatus) -> Self { + self.status_opt = Some(status); + self + } + + pub fn build(self) -> SentTx { + SentTx { + 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_else(current_unix_timestamp), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), + nonce: self.nonce_opt.unwrap_or_default(), + status: self + .status_opt + .unwrap_or(TxStatus::Pending(ValidationStatus::Waiting)), + } + } +} + +#[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, + status_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 status(mut self, failure_status: FailureStatus) -> Self { + self.status_opt = Some(failure_status); + self + } + + pub fn build(self) -> FailedTx { + FailedTx { + 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(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), + nonce: self.nonce_opt.unwrap_or_default(), + reason: self + .reason_opt + .unwrap_or_else(|| FailureReason::PendingTooLong), + status: self + .status_opt + .unwrap_or_else(|| FailureStatus::RetryRequired), + } + } +} + +pub fn make_read_only_db_connection(home_dir: PathBuf) -> ConnectionWrapperReal { + { + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + } + let read_only_conn = Connection::open_with_flags( + home_dir.join(DATABASE_FILE), + OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .unwrap(); + + ConnectionWrapperReal::new(read_only_conn) +} diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 8b78bb5f4..21c9cdc83 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -9,11 +9,13 @@ use crate::database::db_initializer::{ }; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::accountant::PaymentThresholds; +use ethereum_types::H256; use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::messages::{ RangeQuery, TopRecordsConfig, TopRecordsOrdering, UiPayableAccount, UiReceivableAccount, }; use rusqlite::{Row, Statement, ToSql}; +use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::iter::FlatMap; use std::path::{Path, PathBuf}; @@ -21,7 +23,11 @@ use std::string::ToString; use std::time::Duration; use std::time::SystemTime; -pub fn to_time_t(system_time: SystemTime) -> i64 { +pub type TxHash = H256; +pub type RowId = u64; +pub type TxIdentifiers = HashMap; + +pub fn to_unix_timestamp(system_time: SystemTime) -> i64 { match system_time.duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => sign_conversion::(d.as_secs()).expect("MASQNode has expired"), Err(e) => panic!( @@ -31,15 +37,19 @@ 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 } +pub trait TxRecordWithHash { + fn hash(&self) -> TxHash; +} + pub struct DaoFactoryReal { pub data_directory: PathBuf, pub init_config: DbInitializationConfig, @@ -193,11 +203,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 +309,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 +476,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 +618,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/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 0a74d076c..b9a3d093b 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -14,32 +14,41 @@ 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, }; -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::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; 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}; @@ -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::UiFinancialsResponse; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, ScanType, 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}; +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,18 +85,16 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -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, receivable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, crashable: bool, scanners: Scanners, scan_schedulers: ScanSchedulers, @@ -95,7 +102,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,30 +141,43 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } -#[derive(Debug, Message, PartialEq)] +pub type TxReceiptResult = Result; + +#[derive(Debug, PartialEq, Eq, Message, Clone)] +pub struct TxReceiptsMessage { + pub results: HashMap, + 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, } #[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, } @@ -183,134 +203,265 @@ 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 { + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + 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 + ), + ScanReschedulingAfterEarlyStop::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 { + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + 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 + ), + ScanReschedulingAfterEarlyStop::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: 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 { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + // 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(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"), + }, + }; } } -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 { +impl Handler for Accountant { type Result = (); - 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); + 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. } - }; - 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); + } + } +} + +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 not bound") - .try_send(error_msg) - .expect("UiGateway is dead"); + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, 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 { + 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"); + } } } } @@ -357,7 +508,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable: Vec, + pub tx_hashes: Vec, pub response_skeleton_opt: Option, } @@ -367,34 +518,14 @@ 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 { +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) } } @@ -427,32 +558,31 @@ 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), ); Accountant { - suppress_initial_scans: config.suppress_initial_scans, consuming_wallet_opt: config.consuming_wallet_opt.clone(), earning_wallet, payable_dao, receivable_dao, - pending_payable_dao, + sent_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"), @@ -469,8 +599,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), @@ -504,12 +634,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 ); } @@ -571,7 +701,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 +730,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 +813,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 +959,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(), + ) -> ScanReschedulingAfterEarlyStop { + 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"); + ScanReschedulingAfterEarlyStop::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 +1015,127 @@ 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) => { + // 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, + 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), + ) -> ScanReschedulingAfterEarlyStop { + 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: 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"); + ScanReschedulingAfterEarlyStop::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, + ) -> ScanReschedulingAfterEarlyStop { + let is_externally_triggered = response_skeleton_opt.is_some(); + + e.log_error(&self.logger, scanner.into(), is_externally_triggered); + + if let Some(skeleton) = response_skeleton_opt { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }; + + self.scan_schedulers + .reschedule_on_error_resolver + .resolve_rescheduling_on_error(scanner, &e, is_externally_triggered, &self.logger) } fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, ) { - match self.scanners.receivable.begin_scan( - self.earning_wallet.clone(), - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ) { + let result: Result = + self.scanners.start_receivable_scan_guarded( + &self.earning_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + self.scan_schedulers.automatic_scans_enabled, + ); + + match result { Ok(scan_message) => self .retrieve_transactions_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"), - Err(e) => e.handle_error( - &self.logger, - ScanType::Receivables, - response_skeleton_opt.is_some(), - ), - }; + Err(e) => { + e.log_error( + &self.logger, + ScanType::Receivables, + response_skeleton_opt.is_some(), + ); + + if let Some(skeleton) = response_skeleton_opt { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }; + } + } } fn handle_externally_triggered_scan( @@ -928,38 +1144,38 @@ 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)); } } } - 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 ), } } @@ -969,32 +1185,30 @@ impl Accountant { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct PendingPayableId { - pub rowid: u64, - pub hash: H256, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingPayable { + pub recipient_wallet: Wallet, + pub hash: TxHash, } -impl PendingPayableId { - pub fn new(rowid: u64, hash: H256) -> Self { - Self { rowid, hash } - } - - fn rowids(ids: &[Self]) -> Vec { - ids.iter().map(|id| id.rowid).collect() +impl PendingPayable { + pub fn new(recipient_wallet: Wallet, hash: TxHash) -> Self { + Self { + recipient_wallet, + hash, + } } +} - fn serialize_hashes_to_string(ids: &[Self]) -> String { - comma_joined_stringifiable(ids, |id| format!("{:?}", id.hash)) - } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct PendingPayableId { + pub rowid: u64, + pub hash: TxHash, } -impl From for PendingPayableId { - fn from(pending_payable_fingerprint: PendingPayableFingerprint) -> Self { - Self { - hash: pending_payable_fingerprint.hash, - rowid: pending_payable_fingerprint.rowid, - } +impl PendingPayableId { + pub fn new(rowid: u64, hash: TxHash) -> Self { + Self { rowid, hash } } } @@ -1036,40 +1250,58 @@ pub fn wei_to_gwei, S: Display + Copy + Div + From> for Accountant { type Result = (); @@ -1139,9 +1370,10 @@ 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 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![])); @@ -1150,11 +1382,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 @@ -1170,7 +1405,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), @@ -1182,9 +1418,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![(), ()] @@ -1195,19 +1435,24 @@ 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 .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 @@ -1222,7 +1467,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, @@ -1230,33 +1476,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, + let default_scan_intervals = ScanIntervals::compute_default(chain); + 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() @@ -1320,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"); @@ -1330,100 +1569,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 +1582,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 +1610,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: UnpricedQualifiedPayables::from(vec![payable_account]), consuming_wallet, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1477,33 +1628,33 @@ 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 subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + let mut subject = AccountantBuilder::default() + .sent_payable_daos(vec![ForPayableScanner(sent_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"), - hash: make_tx_hash(123), + hash: tx_hash, })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, }), }; + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); subject_addr.try_send(sent_payable).unwrap(); @@ -1520,17 +1671,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 +1690,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(); @@ -1549,9 +1703,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 { - protected_qualified_payables: protect_payables_in_test(accounts.clone()), + qualified_payables: qualified_payables.clone(), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1566,8 +1723,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, + qualified_payables.clone() ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -1586,7 +1743,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 { @@ -1599,31 +1759,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 - // because otherwise this test would've panicked + assert_using_the_same_logger(&logger_clone, test_name, None) + // 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 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 +1810,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 = protect_payables_in_test(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 { - protected_qualified_payables: initial_unadjusted_accounts.clone(), + qualified_payables: initial_unadjusted_accounts.clone(), agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; @@ -1658,7 +1824,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), @@ -1671,7 +1840,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 +1862,7 @@ mod tests { assert_eq!( actual_prepared_adjustment .original_setup_msg - .protected_qualified_payables, + .qualified_payables, initial_unadjusted_accounts ); assert_eq!( @@ -1701,7 +1874,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 @@ -1723,40 +1896,40 @@ 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), 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 subject = AccountantBuilder::default() + 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 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 +1940,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], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1783,46 +1955,117 @@ 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 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)); + 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_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(tx_receipts_msg).unwrap(); + + system.run(); + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint]); + 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), + &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 +2075,231 @@ 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)); } - #[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), - }); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .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 report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 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 + ); - subject_addr.try_send(report_transaction_receipts).unwrap(); + test_externally_triggered_scan_is_prevented_if( + true, + true, + test_name, + scan_type, + &expected_log_msg, + ) + } - 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), - } + 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 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 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)]) + 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 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)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_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, + 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 first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Reverted + ),], + 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 pending_payable_request = ScanForPendingPayables { + response_skeleton_opt, }; - let subject = accountant.start(); - subject - .try_send(sent_payable) - .expect("unexpected actix error"); + subject_addr.try_send(pending_payable_request).unwrap(); - 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(); + 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!( - *mark_pending_payables_rowids_params, - vec![vec![(expected_wallet, expected_rowid)]] + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + 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_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 +2307,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 +2321,191 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + qualified_payables: UnpricedQualifiedPayables::from(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: 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, + }; + 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 +2516,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 +2531,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 +2542,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] @@ -2060,99 +2692,765 @@ mod tests { system.run(); let more_money_received_params = more_money_received_params_arc.lock().unwrap(); assert_eq!( - *more_money_received_params, - vec![(now, vec![expected_receivable_1, expected_receivable_2])] + *more_money_received_params, + vec![(now, vec![expected_receivable_1, expected_receivable_2])] + ); + let commit_params = commit_params_arc.lock().unwrap(); + assert_eq!(*commit_params, vec![()]); + let set_by_guest_transaction_params = set_by_guest_transaction_params_arc.lock().unwrap(); + assert_eq!( + *set_by_guest_transaction_params, + vec![( + transaction_id, + "start_block".to_string(), + Some("123456789".to_string()) + )] + ) + } + + #[test] + 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 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 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); + + // The system is stopped by the NotifyLaterHandleMock for the Receivable scanner + let before = SystemTime::now(); + system.run(); + 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 tx_hash = make_tx_hash(456); + 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, + )]), + 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 { + 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( + Either::Left(Retry::RetryPayments), + )); + 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_tx_receipts_msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok( + StatusReadFromReceiptCheck::Reverted, + )], + 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_tx_receipts_msg.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_tx_receipts_msg, + 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, + ); + // 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)] + 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)>>>, + } + + #[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, + TxReceiptsMessage, + 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, + TxReceiptsMessage, + 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, + TxReceiptsMessage, + 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!( + 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_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_tx_receipts_msg: TxReceiptsMessage, + 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_tx_receipts_msg, pp_finish_scan_logger) = + pending_payable_finish_scan_params.remove(0); + 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 + .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 + }], ); - let commit_params = commit_params_arc.lock().unwrap(); - assert_eq!(*commit_params, vec![()]); - let set_by_guest_transaction_params = set_by_guest_transaction_params_arc.lock().unwrap(); + } + + 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!( - *set_by_guest_transaction_params, + *scan_for_receivables_notify_later_params, vec![( - transaction_id, - "start_block".to_string(), - Some("123456789".to_string()) + ScanForReceivables { + response_skeleton_opt: None + }, + receivable_scan_interval )] - ) + ); } #[test] - fn accountant_scans_after_startup() { - 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 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) + 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![]); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); - let subject_addr: Addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); - send_bind_message!(subject_subs, peer_actors); + 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(); - send_start_message!(subject_subs); + let hint = subject.handle_request_of_scan_for_pending_payable(None); System::current().stop(); 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()); - assert!( - captured_timestamp < SystemTime::now() - && captured_timestamp >= from_time_t(to_time_t(SystemTime::now()) - 5) + let flag_after = subject.scanners.initial_pending_payable_scan(); + 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(); + let _ = blockchain_bridge_recording.get_record::(0); + } + + #[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 mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) + .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(); + + let hint = subject.handle_request_of_scan_for_pending_payable(None); + + let flag_after = subject.scanners.initial_pending_payable_scan(); + assert_eq!( + hint, + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); - 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_eq!(flag_before, true); + assert_eq!(flag_after, false); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ + cross_scan_cause_opt: None, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ + should be impossible with PendingPayableScanner in automatic mode" + )] + fn initial_pending_payable_scan_hits_unexpected_error() { + init_test_logging(); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("abc")) + .build(); + let pending_payable_scanner = + ScannerMock::default().scan_started_at_result(Some(UNIX_EPOCH)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + + let _ = subject.handle_request_of_scan_for_pending_payable(None); } #[test] fn periodical_scanning_for_receivables_and_delinquencies_works() { init_test_logging(); let test_name = "periodical_scanning_for_receivables_and_delinquencies_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_receivable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions let receivable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(RetrieveTransactions { + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Err(StartScanError::NothingToProcess)) + .start_scan_result(Ok(RetrieveTransactions { recipient: make_wallet("some_recipient"), response_skeleton_opt: None, })) @@ -2161,64 +3459,70 @@ mod tests { let mut config = bc_from_earning_wallet(earning_wallet.clone()); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(100), + pending_payable_scan_interval: Duration::from_secs(10), receivable_scan_interval: Duration::from_millis(99), - pending_payable_scan_interval: Duration::from_secs(100), }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .logger(Logger::new(test_name)) .build(); - subject.scanners.payable = Box::new(NullScanner::new()); // Skipping - subject.scanners.pending_payable = Box::new(NullScanner::new()); // Skipping - subject.scanners.receivable = Box::new(receivable_scanner); - subject.scan_schedulers.update_scheduler( - ScanType::Receivables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_receivable_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject.scan_schedulers.receivable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_later_receivable_params_arc) + .capture_msg_and_let_it_fly_on(), ); let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); - send_start_message!(subject_subs); + subject_addr + .try_send(ScanForReceivables { + response_skeleton_opt: None, + }) + .unwrap(); let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); let notify_later_receivable_params = notify_later_receivable_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Receivables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); + let tlh = TestLogHandler::new(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); let ( first_attempt_wallet, first_attempt_timestamp, first_attempt_response_skeleton_opt, first_attempt_logger, - ) = begin_scan_params.remove(0); + _, + ) = start_scan_params.remove(0); let ( second_attempt_wallet, second_attempt_timestamp, second_attempt_response_skeleton_opt, second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); + _, + ) = start_scan_params.remove(0); + assert_eq!(first_attempt_wallet, earning_wallet); assert_eq!(second_attempt_wallet, earning_wallet); assert!(time_before <= first_attempt_timestamp); assert!(first_attempt_timestamp <= second_attempt_timestamp); assert!(second_attempt_timestamp <= time_after); assert_eq!(first_attempt_response_skeleton_opt, None); assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); + assert!(start_scan_params.is_empty()); + debug!( + first_attempt_logger, + "first attempt verifying receivable scanner" + ); + debug!( + second_attempt_logger, + "second attempt verifying receivable scanner" + ); assert_eq!( *notify_later_receivable_params, vec![ @@ -2235,208 +3539,238 @@ 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 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 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 counter_msg_1 = BlockchainAgentWithContextMessage { + qualified_payables: priced_qualified_payables.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 counter_msg_2 = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable::new(creditor_wallet, transaction_hash), + )]), + response_skeleton_opt: None, + }; + let tx_status = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), }); - 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_3 = TxReceiptsMessage { + results: hashmap![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, + 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")); + 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!( - *notify_later_pending_payable_params, - vec![ - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ), - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(98) - ), - ] - ) + actual_outbound_payment_instructions_msg.affordable_accounts, + priced_qualified_payables + ); + let actual_requested_receipts_1 = + blockchain_bridge_recording.get_record::(2); + assert_eq!( + actual_requested_receipts_1, + &request_transaction_receipts_msg + ); + let notify_later_pending_payables_params = + notify_later_pending_payables_params_arc.lock().unwrap(); + assert_eq!( + *notify_later_pending_payables_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_millis(50) + ),] + ); + let notify_payables_params = notify_payable_params_arc.lock().unwrap(); + assert_eq!( + *notify_payables_params, + vec![ScanForNewPayables { + response_skeleton_opt: None + },] + ); } - #[test] - fn periodical_scanning_for_payable_works() { - init_test_logging(); - let test_name = "periodical_scanning_for_payable_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_payables_params_arc = Arc::new(Mutex::new(vec![])); - let system = System::new(test_name); - SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions - let consuming_wallet = make_paying_wallet(b"consuming"); + fn set_up_subject_to_prove_periodical_payable_scan( + test_name: &str, + blockchain_bridge_addr: &Addr, + consuming_wallet: &Wallet, + qualified_payables_msg: &QualifiedPayablesMessage, + request_transaction_receipts: &RequestTransactionReceipts, + start_scan_pending_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + start_scan_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + notify_later_pending_payables_params_arc: &Arc< + Mutex>, + >, + notify_payable_params_arc: &Arc>>, + ) -> Accountant { + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_pending_payable_params_arc) + .start_scan_result(Ok(request_transaction_receipts.clone())) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); let payable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( - 123, - )]), - consuming_wallet: consuming_wallet.clone(), - response_skeleton_opt: None, - })) - .stop_the_system_after_last_msg(); + .scan_started_at_result(None) + // Always checking also on the payable scanner when handling ScanForPendingPayable + .scan_started_at_result(None) + .start_scan_params(&start_scan_payable_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(97), + // This simply means that we're gonna surplus this value (it abides by how many pending + // payable cycles have to go in between before the lastly submitted txs are confirmed), + payable_scan_interval: Duration::from_millis(10), + pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - pending_payable_scan_interval: Duration::from_secs(100), // We'll never run this scanner }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(consuming_wallet.clone()) .logger(Logger::new(test_name)) .build(); - subject.scanners.payable = Box::new(payable_scanner); - subject.scanners.pending_payable = Box::new(NullScanner::new()); //skipping - subject.scanners.receivable = Box::new(NullScanner::new()); //skipping - subject.scan_schedulers.update_scheduler( - ScanType::Payables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_payables_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::::default() + .notify_later_params(¬ify_later_pending_payables_params_arc) + .capture_msg_and_let_it_fly_on(), ); - let subject_addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); - let peer_actors = peer_actors_builder().build(); - send_bind_message!(subject_subs, peer_actors); - - send_start_message!(subject_subs); - - let time_before = SystemTime::now(); - system.run(); - let time_after = SystemTime::now(); - //the second attempt is the one where the queue is empty and System::current.stop() ends the cycle - let notify_later_payables_params = notify_later_payables_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Payables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = begin_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, consuming_wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); - assert_eq!( - *notify_later_payables_params, - vec![ - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ] - ) + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::::default() + .notify_params(¬ify_payable_params_arc) + // This should stop the system. If anything goes wrong, the SystemKillerActor will. + .stop_system_on_count_received(1), + ); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); + subject.outbound_payments_instructions_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject.request_transaction_receipts_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject } #[test] @@ -2447,12 +3781,15 @@ mod tests { subject.consuming_wallet_opt = None; subject.logger = Logger::new(test_name); - subject.handle_request_of_scan_for_payable(None); + subject.handle_request_of_scan_for_new_payable(None); - let has_scan_started = subject.scanners.payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::Payables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." )); } @@ -2466,10 +3803,13 @@ mod tests { subject.handle_request_of_scan_for_pending_payable(None); - let has_scan_started = subject.scanners.pending_payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::PendingPayables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." )); } @@ -2481,10 +3821,10 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(100), + pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_millis(100), - pending_payable_scan_interval: Duration::from_millis(100), }); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; let peer_actors = peer_actors_builder().build(); let subject = AccountantBuilder::default() .bootstrapper_config(config) @@ -2498,14 +3838,14 @@ mod tests { System::current().stop(); assert_eq!(system.run(), 0); - // no panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes + // No panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes TestLogHandler::new().exists_log_containing( &format!("{test_name}: Started with --scans off; declining to begin database and blockchain scans"), ); } #[test] - fn scan_for_payables_message_does_not_trigger_payment_for_balances_below_the_curve() { + fn scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve() { init_test_logging(); let consuming_wallet = make_paying_wallet(b"consuming wallet"); let payment_thresholds = PaymentThresholds { @@ -2517,15 +3857,16 @@ mod tests { unban_below_gwei: 10_000_000, }; let config = bc_from_earning_wallet(make_wallet("mine")); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let accounts = vec![ // below minimum balance, to the right of time intersection (inside buffer zone) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei - 1), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( - payment_thresholds.threshold_interval_sec + 10, + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec, ), ), pending_payable_opt: None, @@ -2533,22 +3874,22 @@ mod tests { // above balance intersection, to the left of minimum time (outside buffer zone) PayableAccount { wallet: make_wallet("wallet1"), - balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec - 10, - ), + 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) + + 1, ), pending_payable_opt: None, }, // above minimum balance, to the right of minimum time (not in buffer zone, below the curve) PayableAccount { wallet: make_wallet("wallet2"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 55), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec + 15, - ), + balance_wei: gwei_to_wei::( + payment_thresholds.permanent_debt_allowed_gwei, + ) + 1, + last_paid_timestamp: from_unix_timestamp( + now - checked_conversion::(payment_thresholds.threshold_interval_sec) + + 1, ), pending_payable_opt: None, }, @@ -2558,49 +3899,62 @@ mod tests { .non_pending_payables_result(vec![]); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); let system = System::new( - "scan_for_payable_message_does_not_trigger_payment_for_balances_below_the_curve", + "scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve", ); let blockchain_bridge_addr: Addr = blockchain_bridge.start(); - let outbound_payments_instructions_sub = - blockchain_bridge_addr.recipient::(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_daos(vec![ForPayableScanner(payable_dao)]) + .consuming_wallet(consuming_wallet.clone()) + .build(); + let payable_scanner = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .payable_dao(payable_dao) .build(); - subject.outbound_payments_instructions_sub_opt = Some(outbound_payments_instructions_sub); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + bind_ui_gateway_unasserted(&mut subject); - let _result = subject.scanners.payable.begin_scan( - consuming_wallet, - SystemTime::now(), - None, - &subject.logger, - ); + let result = subject.handle_request_of_scan_for_new_payable(None); System::current().stop(); system.run(); + assert_eq!( + result, + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) + ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); } #[test] - fn scan_for_payable_message_triggers_payment_for_balances_over_the_curve() { + fn scan_for_new_payables_triggers_payment_for_balances_over_the_curve() { init_test_logging(); let mut config = bc_from_earning_wallet(make_wallet("mine")); let consuming_wallet = make_paying_wallet(b"consuming"); config.scan_intervals_opt = Some(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(50_000), payable_scan_interval: Duration::from_secs(50_000), + pending_payable_scan_interval: Duration::from_secs(10_000), receivable_scan_interval: Duration::from_secs(50_000), }); - let now = to_time_t(SystemTime::now()); + let now = to_unix_timestamp(SystemTime::now()); let qualified_payables = vec![ - // slightly above minimum balance, to the right of the curve (time intersection) + // Slightly above the minimum balance, to the right of the curve (time intersection) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei( DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei + 1, ), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( DEFAULT_PAYMENT_THRESHOLDS.threshold_interval_sec + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec @@ -2609,11 +3963,11 @@ 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), - last_paid_timestamp: from_time_t( + last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 10, ), @@ -2624,65 +3978,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: UnpricedQualifiedPayables::from(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(ScanReschedulingAfterEarlyStop::Schedule( + ScanType::Receivables, + )); + subject.scan_schedulers.reschedule_on_error_resolver = + Box::new(reschedule_on_error_resolver); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + } + #[test] fn accountant_does_not_initiate_another_scan_if_one_is_already_running() { init_test_logging(); let test_name = "accountant_does_not_initiate_another_scan_if_one_is_already_running"; - let payable_dao = PayableDaoMock::default(); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( QualifiedPayablesMessage, QualifiedPayablesMessage )) .start(); - let pps_for_blockchain_bridge_sub = blockchain_bridge_addr.clone().recipient(); - let last_paid_timestamp = to_time_t(SystemTime::now()) - - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec as i64 - - 1; - let payable_account = PayableAccount { - wallet: make_wallet("scan_for_payables"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t(last_paid_timestamp), - pending_payable_opt: None, - }; - let payable_dao = payable_dao - .non_pending_payables_result(vec![payable_account.clone()]) - .non_pending_payables_result(vec![payable_account]); - let config = bc_from_earning_wallet(make_wallet("mine")); + let qualified_payables_sub = blockchain_bridge_addr.clone().recipient(); + let (mut qualified_payables, _, _) = + make_qualified_and_unqualified_payables(now, &payment_thresholds); + let payable_1 = qualified_payables.remove(0); + let payable_2 = qualified_payables.remove(0); + let payable_dao = PayableDaoMock::new() + .non_pending_payables_result(vec![payable_1.clone()]) + .non_pending_payables_result(vec![payable_2.clone()]); + let mut config = bc_from_earning_wallet(make_wallet("mine")); + config.payment_thresholds_opt = Some(payment_thresholds); let system = System::new(test_name); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) @@ -2690,97 +4066,103 @@ 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("blah".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 )); } #[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_every_type_id!(RequestTransactionReceipts)) + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_time_t(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_time_t(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.request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); + 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(); let _ = account_addr @@ -2789,19 +4171,94 @@ 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(); - 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], + assert_eq!(received_msg, &expected_composed_msg_for_blockchain_bridge); + assert_eq!(blockchain_bridge_recording.len(), 1); + } + + #[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" ); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); + } + + #[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(ScanReschedulingAfterEarlyStop::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] @@ -2891,7 +4348,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, )); } @@ -2936,7 +4393,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, )); } @@ -3028,7 +4485,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 )); } @@ -3073,7 +4530,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, )); } @@ -3081,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()) @@ -3342,8 +4799,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(); @@ -3423,522 +4880,701 @@ 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) .build(); - prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); + prove_that_crash_request_handler_is_hooked_up(accountant, CRASH_KEY); + } + + #[test] + fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { + 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 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"))) + .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), + ); + 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 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!( + *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 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: hashset![make_tx_hash(456)], + }), + response_skeleton_opt: None, + }; + let addr = subject.start(); + + 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] + 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( + 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_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, + }); + msg.response_skeleton_opt = response_skeleton_opt; + 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 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 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 pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { + fn accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely() { 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![])); - 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 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 system = System::new("new_payable_scanner_timely"); + 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::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(); + 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), ); - 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() - .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); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let subject_addr = subject.start(); + 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), + }), + }, + ]); - send_start_message!(accountant_subs); + subject_addr.try_send(msg.clone()).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); + System::current().stop(); + system.run(); + 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!( - 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(); + 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); 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], - ] + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp ); - 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(); + 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!( - *transaction_confirmed_params, - vec![vec![fingerprint_2_fourth_round.clone()]] + *new_payable_notify_later, + vec![(ScanForNewPayables::default(), expected_computed_interval)] ); - let expected_scan_pending_payable_msg_and_interval = ( - ScanForPendingPayables { - response_skeleton_opt: None, + 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_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { + 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 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::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(); + 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 tx_block_1 = make_transaction_block(4567); + let tx_block_2 = make_transaction_block(1234); + let subject_addr = subject.start(); + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_1), }, - Duration::from_millis(pending_payable_scan_interval), + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_2), + }, + ]); + + subject_addr.try_send(msg.clone()).unwrap(); + + let system = System::new(test_name); + System::current().stop(); + system.run(); + 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 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(); + 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!( - 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, - ] + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp ); - 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", + 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 accountant_receives_reported_transaction_receipts_and_processes_them_all() { - let transactions_confirmed_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_result(Ok(())); - let subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + fn scheduler_for_new_payables_operates_with_proper_now_timestamp() { + 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() + .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(); + 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 system = System::new(test_name); 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, _) = 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), }), - }; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_time_t(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, - }; - let transaction_hash_2 = make_tx_hash(3333333); - let transaction_receipt_2 = TxReceipt { - transaction_hash: transaction_hash_2, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(200), - }), - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 10, - timestamp: from_time_t(199_780_000), - hash: Default::default(), - attempt: 15, - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_1), - fingerprint_1.clone(), - ), - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_2), - fingerprint_2.clone(), - ), - ], - response_skeleton_opt: None, - }; + }]); subject_addr.try_send(msg).unwrap(); - let system = System::new("processing reported receipts"); + let before = SystemTime::now(); System::current().stop(); system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!( - *transactions_confirmed_params, - vec![vec![fingerprint_1, fingerprint_2]] + 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 + ); + } + + 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 = TxReceiptsMessage { + results: tx_receipt_results, + response_skeleton_opt: None, + }; + + (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 { @@ -3948,75 +5584,270 @@ 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() { - assert_scan_error_is_handled_properly( - "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_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() { - assert_scan_error_is_handled_properly( + 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(), }, + do_setup_and_prepare_assertions_for_pending_payables(), ); } #[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, + 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() { - assert_scan_error_is_handled_properly( - "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_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() { - assert_scan_error_is_handled_properly( + 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(), }, + do_setup_and_prepare_assertions_for_pending_payables(), ); } #[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, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_receivables(), ); } @@ -4106,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(); @@ -4765,23 +6596,29 @@ mod tests { let _: u64 = wei_to_gwei(u128::MAX); } - fn assert_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + type RunSchedulersAssertions = Box)>; + + fn test_scan_error_is_handled_properly( + test_name: &str, + message: ScanError, + 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(); - 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.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(); @@ -4792,19 +6629,15 @@ 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.into()); assert_eq!(scan_started_at_opt, None); }), }) .unwrap(); System::current().stop(); - system.run(); + 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) => { @@ -4860,6 +6693,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..5062fc1ab 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,22 +71,20 @@ 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::test_utils::make_payable_account; + 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 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 { - protected_qualified_payables: protect_payables_in_test(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/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/mid_scan_msg_handling/payable_scanner/agent_null.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs deleted file mode 100644 index e95673002..000000000 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/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::mid_scan_msg_handling::payable_scanner::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::mid_scan_msg_handling::payable_scanner::agent_null::BlockchainAgentNull; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::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/mid_scan_msg_handling/payable_scanner/agent_web3.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs deleted file mode 100644 index 725e14f00..000000000 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/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::mid_scan_msg_handling::payable_scanner::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::mid_scan_msg_handling::payable_scanner::agent_web3::{ - BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, - }; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::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/mid_scan_msg_handling/payable_scanner/msgs.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs deleted file mode 100644 index 41a1b3940..000000000 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs +++ /dev/null @@ -1,76 +0,0 @@ -// 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::{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 consuming_wallet: Wallet, - pub response_skeleton_opt: Option, -} - -impl QualifiedPayablesMessage { - pub(in crate::accountant) fn new( - protected_qualified_payables: Obfuscated, - consuming_wallet: Wallet, - response_skeleton_opt: Option, - ) -> Self { - Self { - protected_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 protected_qualified_payables: Obfuscated, - pub agent: Box, - pub response_skeleton_opt: Option, -} - -impl BlockchainAgentWithContextMessage { - pub fn new( - qualified_payables: Obfuscated, - blockchain_agent: Box, - response_skeleton_opt: Option, - ) -> Self { - Self { - protected_qualified_payables: qualified_payables, - agent: blockchain_agent, - response_skeleton_opt, - } - } -} - -#[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; - - 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 { - protected_qualified_payables: self.protected_qualified_payables.clone(), - agent: Box::new(cloned_agent), - response_skeleton_opt: self.response_skeleton_opt, - } - } - } -} diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 1307cb006..1d69ab3c9 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,41 +1,32 @@ // 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 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::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, 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::receivable_scanner_utils::balance_and_age; -use crate::accountant::PendingPayableId; + 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, Accountant, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, - ScanForPendingPayables, 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, + comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, + TxReceiptsMessage, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, + ScanForReceivables, SentPayables, }; -use crate::sub_lib::blockchain_bridge::{ - OutboundPaymentsInstructions, -}; -use crate::sub_lib::utils::{NotifyLaterHandle, NotifyLaterHandleReal}; +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::{Context, Message}; +use actix::{Message}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; @@ -44,48 +35,69 @@ 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 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::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +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::{PersistentConfiguration, PersistentConfigurationReal}; +use crate::db_config::persistent_configuration::{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, + TxReceiptsMessage, + PendingPayableScanResult, + >, + >, + receivable: Box< + dyn PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + >, + >, } 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), )); 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,306 @@ 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( + ManulTriggerError::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( + ManulTriggerError::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: TxReceiptsMessage, + 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 { + DetailedScanType::NewPayables | DetailedScanType::RetryPayables => { + self.payable.mark_as_ended(logger) + } + DetailedScanType::PendingPayables => { + self.empty_caches(logger); + self.pending_payable.mark_as_ended(logger); + } + DetailedScanType::Receivables => { + self.receivable.mark_as_ended(logger); + } + }; + } + + 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, + 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 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>( + 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( + ManulTriggerError::AutomaticScanConflict, + )); + } + if self.initial_pending_payable_scan { + return Ok(()); + } + if triggered_manually && !self.aware_of_unresolved_pending_payable { + return Err(StartScanError::ManualTriggerError( + ManulTriggerError::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 +418,7 @@ where pub struct ScannerCommon { initiated_at_opt: Option, - pub payment_thresholds: Rc, + payment_thresholds: Rc, } impl ScannerCommon { @@ -151,7 +444,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 ); } @@ -159,6 +452,7 @@ impl ScannerCommon { } } +#[macro_export] macro_rules! time_marking_methods { ($scan_type_variant: ident) => { fn scan_started_at(&self) -> Option { @@ -180,26 +474,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 sent_payable_dao: 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 +507,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 +515,35 @@ impl Scanner for PayableScanner { "Chose {} qualified debts to pay", qualified_payables.len() ); - let protected_payables = self.protect_payables(qualified_payables); + let qualified_payables = UnpricedQualifiedPayables::from(qualified_payables); let outgoing_msg = QualifiedPayablesMessage::new( - protected_payables, - consuming_wallet, + 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 + } +} - fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { +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, @@ -242,17 +552,31 @@ 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); - 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 +594,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,19 +614,17 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { } } -impl MultistagePayableScanner 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, } @@ -374,169 +692,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 !PayableScanner::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) - } - - fn is_symmetrical( - sent_payables_hashes: HashSet, - fingerptint_hashes: HashSet, - ) -> bool { - sent_payables_hashes == fingerptint_hashes + .collect() } - 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 ) @@ -546,558 +895,185 @@ 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 { - pub common: ScannerCommon, - pub payable_dao: Box, - pub pending_payable_dao: Box, - pub when_pending_too_long_sec: u64, - pub financial_statistics: Rc>, +#[derive(Debug, PartialEq, Eq, Clone, VariantCount)] +pub enum StartScanError { + NothingToProcess, + NoConsumingWalletFound, + ScanAlreadyRunning { + cross_scan_cause_opt: Option, + started_at: SystemTime, + }, + CalledFromNullScanner, // Exclusive for tests + ManualTriggerError(ManulTriggerError), } -impl Scanner for PendingPayableScanner { - fn begin_scan( - &mut self, - _irrelevant_wallet: Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } - self.mark_as_started(timestamp); - info!(logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); - match filtered_pending_payable.is_empty() { - true => { - self.mark_as_ended(logger); - Err(BeginScanError::NothingToProcess) - } - false => { - debug!( - logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - Ok(RequestTransactionReceipts { - pending_payable: filtered_pending_payable, - response_skeleton_opt, - }) - } +impl StartScanError { + pub fn log_error(&self, logger: &Logger, scan_type: ScanType, is_externally_triggered: bool) { + enum ErrorType { + Temporary(String), + Permanent(String), } - } - fn finish_scan( - &mut self, - message: ReportTransactionReceipts, - logger: &Logger, - ) -> Option { - let response_skeleton_opt = message.response_skeleton_opt; + let log_message = match self { + StartScanError::NothingToProcess => ErrorType::Temporary(format!( + "There was nothing to process during {:?} scan.", + scan_type + )), + StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt, + started_at, + } => ErrorType::Temporary(Self::scan_already_running_msg( + scan_type, + *cross_scan_cause_opt, + *started_at, + )), + StartScanError::NoConsumingWalletFound => ErrorType::Permanent(format!( + "Cannot initiate {:?} scan because no consuming wallet was found.", + scan_type + )), + 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 { + ManulTriggerError::AutomaticScanConflict => ErrorType::Permanent(format!( + "User requested {:?} scan was denied. Automatic mode prevents manual triggers.", + scan_type + )), + 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(), + } + )) + } + }, + }; - match message.fingerprints_with_receipts.is_empty() { - true => debug!(logger, "No transaction receipts found."), - false => { - debug!( - logger, - "Processing receipts for {} transactions", - 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); - } + match log_message { + ErrorType::Temporary(msg) => match is_externally_triggered { + true => info!(logger, "{}", msg), + false => debug!(logger, "{}", msg), + }, + ErrorType::Permanent(msg) => warning!(logger, "{}", msg), } + } - 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), - }) + fn timestamp_as_string(timestamp: SystemTime) -> String { + let offset_date_time = OffsetDateTime::from(timestamp); + offset_date_time + .format( + &parse(TIME_FORMATTING_STRING) + .expect("Error while parsing the time formatting string."), + ) + .expect("Error while formatting timestamp as string.") } - time_marking_methods!(PendingPayables); + 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()) + }; - as_any_ref_in_trait_impl!(); + format!( + "{:?} scan was already initiated at {}. Hence, {} scan request will be ignored.", + blocking_scanner, + StartScanError::timestamp_as_string(scan_started), + request_spec + ) + } } -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, - ) { - 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 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) - ); - } +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ManulTriggerError { + AutomaticScanConflict, + UnnecessaryRequest { hint_opt: Option }, } -pub struct ReceivableScanner { - pub common: ScannerCommon, - pub receivable_dao: Box, - pub banned_dao: Box, - pub persistent_configuration: Box, - pub financial_statistics: Rc>, -} - -impl Scanner for ReceivableScanner { - fn begin_scan( - &mut self, - 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)); - } - self.mark_as_started(timestamp); - info!(logger, "Scanning for receivables to {}", earning_wallet); - self.scan_for_delinquencies(timestamp, logger); - - Ok(RetrieveTransactions { - recipient: earning_wallet, - response_skeleton_opt, - }) - } +pub trait RealScannerMarker {} - 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), - }) +macro_rules! impl_real_scanner_marker { + ($($t:ty),*) => { + $(impl RealScannerMarker for $t {})* } - - 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, - } - } +impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScanner); - 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)] -pub enum BeginScanError { - NothingToProcess, - NoConsumingWalletFound, - ScanAlreadyRunning(SystemTime), - CalledFromNullScanner, // Exclusive for tests -} - -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!( - "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.", - scan_type, - BeginScanError::timestamp_as_string(timestamp) - )), - BeginScanError::NoConsumingWalletFound => Some(format!( - "Cannot initiate {:?} scan because no consuming wallet was found.", - scan_type - )), - BeginScanError::CalledFromNullScanner => match cfg!(test) { - true => None, - false => panic!("Null Scanner shouldn't be running inside production code."), - }, - }; - - if let Some(log_message) = log_message_opt { - match is_externally_triggered { - true => info!(logger, "{}", log_message), - false => debug!(logger, "{}", log_message), - } - } - } - - fn timestamp_as_string(timestamp: &SystemTime) -> String { - let offset_date_time = OffsetDateTime::from(*timestamp); - offset_date_time - .format( - &parse(TIME_FORMATTING_STRING) - .expect("Error while parsing the time formatting string."), - ) - .expect("Error while formatting timestamp as string.") - } -} - -pub struct ScanSchedulers { - pub schedulers: HashMap>, -} - -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 } - } -} - -pub struct PeriodicalScanScheduler { - pub handle: Box>, - pub interval: Duration, -} - -pub trait ScanScheduler { - fn schedule(&self, ctx: &mut Context); - fn interval(&self) -> Duration { - intentionally_blank!() - } - - as_any_ref_in_trait!(); - as_any_mut_in_trait!(); -} - -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 - } - - as_any_ref_in_trait_impl!(); - as_any_mut_in_trait_impl!(); -} #[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::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::db_access_objects::utils::{from_time_t, to_time_t}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; - use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::scanners::test_utils::protect_payables_in_test; use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, ScanSchedulers, - Scanner, ScannerCommon, Scanners, + ManulTriggerError, PayableScanner, PendingPayableScanner, ReceivableScanner, Scanner, + ScannerCommon, Scanners, StartScanError, StartableScanner, }; 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, + 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::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; - use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; + 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::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, + DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; @@ -1105,39 +1081,116 @@ mod tests { 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 masq_lib::ui_gateway::NodeToUiMessage; 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::Error; - 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() .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, @@ -1149,13 +1202,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())), ); @@ -1166,8 +1219,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 @@ -1179,10 +1232,8 @@ mod tests { &payment_thresholds ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); - assert_eq!( - pending_payable_scanner.when_pending_too_long_sec, - when_pending_too_long_sec - ); + assert_eq!(scanners.aware_of_unresolved_pending_payable, false); + assert_eq!(scanners.initial_pending_payable_scan, true); assert_eq!( *pending_payable_scanner.financial_statistics.borrow(), financial_statistics @@ -1195,6 +1246,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 @@ -1220,108 +1284,289 @@ mod tests { } #[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(); - - let protected = subject.protect_payables(initial_unprotected.clone()); - let again_unprotected: Vec = subject.expose_payables(protected); - - assert_eq!(initial_unprotected, again_unprotected) - } - - #[test] - fn payable_scanner_can_initiate_a_scan() { + fn new_payable_scanner_can_initiate_a_scan() { init_test_logging(); - let test_name = "payable_scanner_can_initiate_a_scan"; + 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_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_new_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + true, + ); - let timestamp = subject.scan_started_at(); + 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 { - protected_qualified_payables: protect_payables_in_test( - qualified_payable_accounts.clone() - ), + qualified_payables: expected_unpriced_qualified_payables, 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 new payables"), &format!( "INFO: {test_name}: Chose {} qualified debts to pay", - qualified_payable_accounts.len() + qualified_payables_count ), ]) } #[test] - fn payable_scanner_throws_error_when_a_scan_is_already_running() { + fn new_payable_scanner_cannot_be_initiated_if_it_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")); + 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 run_again_result = subject.begin_scan( - consuming_wallet, + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, SystemTime::now(), None, &Logger::new("test"), + true, ); - let is_scan_running = subject.scan_started_at().is_some(); + let is_scan_running = subject.payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( - run_again_result, - Err(BeginScanError::ScanAlreadyRunning(now)) + result, + Err(StartScanError::ScanAlreadyRunning { + cross_scan_cause_opt: None, + started_at: previous_scan_started_at + }) ); } #[test] - fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + 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_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() + let mut subject = make_dull_subject(); + subject.payable = Box::new( + PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(), + ); + + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); + + 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 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() + // ), + // ]) + } + + #[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( + 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 before = SystemTime::now(); + let _ = subject.start_retry_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + ); - let result = subject.begin_scan(consuming_wallet, now, None, &Logger::new("test")); + 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 is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(is_scan_running, false); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + 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] + #[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"), + ); } #[test] 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"); @@ -1332,7 +1577,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, @@ -1342,28 +1587,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 subject = PayableScannerBuilder::new() + 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 { @@ -1374,48 +1612,50 @@ 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); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); + 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!( - *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(); + let delete_records_params = delete_records_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(); - 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" ), ]); @@ -1425,7 +1665,7 @@ mod tests { } #[test] - fn entries_must_be_kept_consistent_and_aligned() { + fn no_missing_records() { let wallet_1 = make_wallet("abc"); let hash_1 = make_tx_hash(123); let wallet_2 = make_wallet("def"); @@ -1440,212 +1680,70 @@ mod tests { 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 + let pending_payables_ref = pending_payables_owned .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, + .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_eq!(result, false) + assert!( + missing_records.is_empty(), + "We thought the vec would be empty but contained: {:?}", + missing_records + ); } #[test] - fn symmetry_check_indifferent_to_wrong_order_on_the_input() { + #[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_sent_from_blockchain_bridge = vec![ + 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), + PendingPayable::new(make_wallet("ghi"), hash_2), + PendingPayable::new(make_wallet("jkl"), 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, \ - to wallet: 0x000000000000000000000000000000626f6f6761), (tx: 0x000000000000000000000000000000000000000000000000000000000000007b, \ - to wallet: 0x00000000000000000000000000000061676f6f62) were not found; system unreliable" - )] - 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) + 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( @@ -1653,7 +1751,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![ @@ -1663,128 +1761,77 @@ 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}:")); - } - - #[test] - fn payable_scanner_mark_pending_payable_panics_nonexistent_fingerprints_also_found() { - 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")); + subject.finish_scan(sent_payables, &Logger::new(test_name)); } #[test] - fn payable_scanner_is_facing_failed_transactions_and_their_fingerprints_exist() { + fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_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 mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + 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() + .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, }; + 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); - 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]] + 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 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 } @@ -1798,24 +1845,31 @@ 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" )); 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; @@ -1823,20 +1877,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(|| { @@ -1847,37 +1898,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, }; @@ -1888,41 +1936,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, @@ -1950,20 +1999,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] @@ -2027,7 +2089,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] @@ -2103,11 +2165,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 +2198,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() @@ -2152,9 +2214,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 )); } @@ -2168,8 +2230,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, }]; @@ -2178,723 +2240,415 @@ mod tests { .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(); - 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_time_t(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_time_t(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 mut pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - let result = pending_payable_scanner.begin_scan( - consuming_wallet, - now, - None, - &Logger::new(test_name), - ); - - let no_of_pending_payables = fingerprints.len(); - let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); - assert_eq!(is_scan_running, true); - assert_eq!( - result, - Ok(RequestTransactionReceipts { - pending_payable: fingerprints, - response_skeleton_opt: None - }) - ); - TestLogHandler::new().assert_logs_match_in_order(vec![ - &format!("INFO: {test_name}: Scanning for pending payable"), - &format!( - "DEBUG: {test_name}: Found {no_of_pending_payables} pending payables to process" - ), - ]) - } - - #[test] - fn pending_payable_scanner_throws_error_in_case_scan_is_already_running() { - let now = SystemTime::now(); - let consuming_wallet = make_paying_wallet(b"consuming"); - 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() - .pending_payable_dao(pending_payable_dao) - .build(); - let logger = Logger::new("test"); - let _ = subject.begin_scan(consuming_wallet.clone(), now, None, &logger); - - let result = subject.begin_scan(consuming_wallet, SystemTime::now(), None, &logger); - - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(is_scan_running, true); - assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); - } - - #[test] - fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { - 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.begin_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!(is_scan_running, false); - } - - 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")) + let result = subject + .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); - //mocked pending payable DAO didn't panic which means we skipped the actual process + assert_eq!(result, vec![]); + TestLogHandler::new() + .exists_no_log_containing(&format!("DEBUG: {test_name}: Paying qualified debts")); } #[test] - fn cancel_failed_transactions_works() { + fn pending_payable_scanner_can_initiate_a_scan() { 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) + let test_name = "pending_payable_scanner_can_initiate_a_scan"; + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + 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() + .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 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)); + // 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 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", - )); + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Ok(RequestTransactionReceipts { + 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 1 pending payables and 1 unfinalized failures to process"), + ]) } #[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) + 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 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() + .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 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 - } + // 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.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger, true); - #[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); + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + true, + ); - subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); + let is_scan_running = subject.pending_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: now + }) + ); } #[test] - fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { - let mut subject = PendingPayableScannerBuilder::new().build(); + 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); - subject.confirm_transactions(vec![], &Logger::new("test")) + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + true, + ); - //mocked payable DAO didn't panic which means we skipped the actual process + 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 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_time_t(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_time_t(200_000_000), - hash: make_tx_hash(0x567), - attempt: 1, - amount: 5555, - process_error: None, - }; + 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); - subject.confirm_transactions( - vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), - ], - &Logger::new("confirm_transactions_works"), - ); + 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 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", + 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 ); - log_handler.exists_log_containing( - "INFO: confirm_transactions_works: \ - Transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567 \ - completed their confirmation process succeeding", + 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 = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ - 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ - (\"record change not successful\")" + expected = "internal error: entered unreachable code: Automatic pending payable \ + scan should never start if there are no pending payables to process." )] - 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; + 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; - subject.confirm_transactions(vec![fingerprint], &Logger::new("test")); + let _ = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); } #[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_time_t(189_999_888), - hash: make_tx_hash(56789), - attempt: 1, - amount: 5478, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 6, - timestamp: from_time_t(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); + 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; - subject.confirm_transactions( - vec![fingerprint_1.clone(), fingerprint_2.clone()], - &Logger::new(test_name), - ); + let result = subject.check_general_conditions_for_pending_payable_scan(false, true); - let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; - assert_eq!(total_paid_payable, 1111 + 5478 + 6543); + assert_eq!(result, Ok(())); + assert_eq!(subject.initial_pending_payable_scan, true); } #[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 mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) - .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_time_t(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, + 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 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_time_t(199_780_000), - hash: transaction_hash_2, - attempt: 15, - amount: 1212, - process_error: None, + 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 msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_1), - fingerprint_1.clone(), - ), - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_2), - fingerprint_2.clone(), - ), + 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) + .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 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, }; - 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)); + assert_eq!( + result, + PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) + ); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(message_opt, None); + 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]] ); - assert_eq!(subject.scan_started_at(), 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 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); + 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 subject = PendingPayableScannerBuilder::new().build(); - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], + let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let msg = TxReceiptsMessage { + results: hashmap![], 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 is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(message_opt, None); - assert_eq!(is_scan_running, false); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!( - "DEBUG: {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] @@ -2906,18 +2660,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 +2695,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 +2757,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 +2811,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 +2820,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!( @@ -3061,8 +2834,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"; @@ -3084,7 +2859,6 @@ mod tests { response_skeleton_opt: None, transactions: vec![], }; - // Not necessary, rather for preciseness subject.mark_as_started(SystemTime::now()); @@ -3112,13 +2886,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 +2913,23 @@ mod tests { response_skeleton_opt: None, transactions: receivables.clone(), }; - subject.mark_as_started(SystemTime::now()); + receivable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.receivable = Box::new(receivable_scanner); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_receivable_scan(msg, &Logger::new(test_name)); - let total_paid_receivable = subject + let scanner_after = subject + .receivable + .as_any() + .downcast_ref::() + .unwrap(); + let total_paid_receivable = scanner_after .financial_statistics .borrow() .total_paid_receivable_wei; - assert_eq!(message_opt, None); - assert_eq!(subject.scan_started_at(), None); + assert_eq!(ui_msg_opt, None); + assert_eq!(scanner_after.scan_started_at(), None); assert_eq!(total_paid_receivable, 2_222_123_123 + 45_780 + 3_333_345); let more_money_received_params = more_money_received_params_arc.lock().unwrap(); assert_eq!(*more_money_received_params, vec![(now, receivables)]); @@ -3277,7 +3060,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); @@ -3299,12 +3082,12 @@ 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" )); } - 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 +3126,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 +3150,234 @@ 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: DetailedScanType) -> ScanError { + ScanError { + scan_type, + response_skeleton_opt: None, + msg: "blah".to_string(), + } + } + + init_test_logging(); + let test_name = "acknowledge_scan_error_works"; + let inputs: Vec<( + DetailedScanType, + Box, + Box Option>, + )> = vec![ + ( + 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()), + ), + ( + DetailedScanType::PendingPayables, + Box::new(|subject| subject.pending_payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.pending_payable.scan_started_at()), + ), + ( + DetailedScanType::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", + ScanType::from(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(ManulTriggerError::AutomaticScanConflict), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") + }), + "WARN", + "WARN", + ), + ( + StartScanError::ManualTriggerError(ManulTriggerError::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(ManulTriggerError::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/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs similarity index 61% 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..1d1e8cb0b 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -1,28 +1,28 @@ // 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; 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 +55,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/payable_scanner_extension/msgs.rs b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs new file mode 100644 index 000000000..1e9dbe59d --- /dev/null +++ b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs @@ -0,0 +1,139 @@ +// 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/mid_scan_msg_handling/payable_scanner/test_utils.rs b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs similarity index 75% 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..b8e83b78d 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,10 @@ #![cfg(test)] -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::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/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs new file mode 100644 index 000000000..70c043909 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -0,0 +1,2053 @@ +// 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 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 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, + TxReceiptsMessage, + 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 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 { + fn finish_scan( + &mut self, + message: TxReceiptsMessage, + logger: &Logger, + ) -> PendingPayableScanResult { + let response_skeleton_opt = message.response_skeleton_opt; + + let scan_report = self.interpret_tx_receipts(message, logger); + + let retry_opt = scan_report.requires_payments_retry(); + + self.process_txs_by_state(scan_report, logger); + + 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, + sent_payable_dao: Box, + failed_payable_dao: Box, + payment_thresholds: Rc, + financial_statistics: Rc>, + ) -> Self { + Self { + common: ScannerCommon::new(payment_thresholds), + payable_dao, + 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_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, + ) -> 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 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_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, + confirmed_txs: DetectedConfirmations, + logger: &Logger, + ) { + 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() + } + + 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() + } + + 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 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, + "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 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, + "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 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, + ) { + 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 + ) + } + } + } + } + } + + 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 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, + ) { + fn resolve_optional_vec(vec_opt: Option<&Vec>) -> usize { + vec_opt.map(|hashes| hashes.len()).unwrap_or_default() + } + + debug!( + logger, + "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}; + + #[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(); + + let result = subject.start_scan(&make_wallet("blah"), SystemTime::now(), None, &logger); + + 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 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 = subject.finish_scan(msg, &logger); + + assert_eq!( + result, + 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 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 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 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 + ); + } + + #[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 + ); + } + + #[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 mut subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + + let result = subject.start_scan(&consuming_wallet, now, None, &Logger::new("test")); + + 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 handle_failed_transactions_can_process_standard_tx_failures() { + init_test_logging(); + 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![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + + 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]] + ); + 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_failed_transactions_can_process_receipt_retrieval_rpc_failures() { + init_test_logging(); + 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), + )), + ], + }; + + 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!( + *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 + ); + 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 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(); + + 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 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 = "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(\"blah\")" + )] + 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("blah".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) + .build(); + + let _ = subject + .update_validation_status_for_sent_txs(vec![failed_validation], &Logger::new("test")); + } + + #[test] + #[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(\"blah\")" + )] + 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("blah".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.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]]); + 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] + #[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() + .failed_payable_dao(failed_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 = "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] + 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() + .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")); + } + + #[test] + 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.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &logger, + ); + + 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 proceed in a reclaim as the replacement of sent tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + failed due to: NoChange" + )] + 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() + .sent_payable_dao(sent_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.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); + } + + #[test] + #[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.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); + } + + #[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 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 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 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) + .sent_payable_dao(sent_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!("{:?}", 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 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_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(&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, + ); + + 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 complete the tx confirmation by the adjustment of the payable accounts \ + 0x000000000000000000000077616c6c6574343536 due to: \ + RusqliteError(\"record change not successful\")" + )] + fn handle_confirmed_transactions_panics_on_unchecking_payable_table() { + let hash = make_tx_hash(0x315); + 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 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); + + 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 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 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) + .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.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 + 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..e01425d69 --- /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("blah".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(\"blah\")). \ + 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 new file mode 100644 index 000000000..d08808d75 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -0,0 +1,1160 @@ +// Copyright (c) 2025, 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::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::collections::HashMap; + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct ReceiptScanReport { + pub failures: DetectedFailures, + pub confirmations: DetectedConfirmations, +} + +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(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_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() { + 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 new file mode 100644 index 000000000..eff0df95e --- /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 advance the 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 new file mode 100644 index 000000000..dd23e05bd --- /dev/null +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -0,0 +1,942 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::scanners::StartScanError; +use crate::accountant::{ + Accountant, ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, + ScanForRetryPayables, +}; +use crate::sub_lib::accountant::ScanIntervals; +use crate::sub_lib::utils::{ + NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal, +}; +use actix::{Actor, Context, Handler}; +use masq_lib::logger::Logger; +use masq_lib::messages::ScanType; +use std::fmt::{Debug, Display, Formatter}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub struct ScanSchedulers { + pub payable: PayableScanScheduler, + pub pending_payable: SimplePeriodicalScanScheduler, + pub receivable: SimplePeriodicalScanScheduler, + pub reschedule_on_error_resolver: Box, + pub automatic_scans_enabled: bool, +} + +impl ScanSchedulers { + pub fn new(scan_intervals: ScanIntervals, automatic_scans_enabled: bool) -> Self { + Self { + payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), + pending_payable: SimplePeriodicalScanScheduler::new( + scan_intervals.pending_payable_scan_interval, + ), + receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), + reschedule_on_error_resolver: Box::new(RescheduleScanOnErrorResolverReal::default()), + automatic_scans_enabled, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PayableScanSchedulerError { + ScanForNewPayableAlreadyScheduled, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ScanReschedulingAfterEarlyStop { + Schedule(ScanType), + DoNotSchedule, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum PayableSequenceScanner { + NewPayables, + RetryPayables, + PendingPayables { initial_pending_payable_scan: bool }, +} + +impl Display for PayableSequenceScanner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PayableSequenceScanner::NewPayables => write!(f, "NewPayables"), + PayableSequenceScanner::RetryPayables => write!(f, "RetryPayables"), + PayableSequenceScanner::PendingPayables { .. } => write!(f, "PendingPayables"), + } + } +} + +impl From for ScanType { + fn from(scanner: PayableSequenceScanner) -> Self { + match scanner { + PayableSequenceScanner::NewPayables => ScanType::Payables, + PayableSequenceScanner::RetryPayables => ScanType::Payables, + PayableSequenceScanner::PendingPayables { .. } => ScanType::PendingPayables, + } + } +} + +pub struct PayableScanScheduler { + pub new_payable_notify_later: Box>, + pub dyn_interval_computer: Box, + pub inner: Arc>, + pub new_payable_interval: Duration, + pub new_payable_notify: Box>, + pub retry_payable_notify: Box>, +} + +impl PayableScanScheduler { + fn new(new_payable_interval: Duration) -> Self { + Self { + new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), + dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::default()), + inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), + new_payable_interval, + new_payable_notify: Box::new(NotifyHandleReal::default()), + retry_payable_notify: Box::new(NotifyHandleReal::default()), + } + } + + pub fn schedule_new_payable_scan(&self, ctx: &mut Context, logger: &Logger) { + let inner = self.inner.lock().expect("couldn't acquire inner"); + let last_new_payable_scan_timestamp = inner.last_new_payable_scan_timestamp; + let new_payable_interval = self.new_payable_interval; + let now = SystemTime::now(); + if let Some(interval) = self.dyn_interval_computer.compute_interval( + now, + last_new_payable_scan_timestamp, + new_payable_interval, + ) { + debug!( + logger, + "Scheduling a new-payable scan in {}ms", + interval.as_millis() + ); + + let _ = self.new_payable_notify_later.notify_later( + ScanForNewPayables { + response_skeleton_opt: None, + }, + interval, + ctx, + ); + } else { + debug!(logger, "Scheduling a new-payable scan asap"); + + self.new_payable_notify.notify( + ScanForNewPayables { + response_skeleton_opt: None, + }, + ctx, + ); + } + } + + // This message ships into the Accountant's mailbox with no delay. + // Can also be triggered by command, following up after the PendingPayableScanner + // that requests it. That's why the response skeleton is possible to be used. + pub fn schedule_retry_payable_scan( + &self, + ctx: &mut Context, + response_skeleton_opt: Option, + logger: &Logger, + ) { + debug!(logger, "Scheduling a retry-payable scan asap"); + + self.retry_payable_notify.notify( + ScanForRetryPayables { + response_skeleton_opt, + }, + ctx, + ) + } +} + +pub struct PayableScanSchedulerInner { + pub last_new_payable_scan_timestamp: SystemTime, +} + +impl Default for PayableScanSchedulerInner { + fn default() -> Self { + Self { + last_new_payable_scan_timestamp: UNIX_EPOCH, + } + } +} + +pub trait NewPayableScanDynIntervalComputer { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option; +} + +#[derive(Default)] +pub struct NewPayableScanDynIntervalComputerReal {} + +impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option { + let elapsed = now + .duration_since(last_new_payable_scan_timestamp) + .unwrap_or_else(|_| { + panic!( + "Unexpected now ({:?}) earlier than past timestamp ({:?})", + now, last_new_payable_scan_timestamp + ) + }); + if elapsed >= interval { + None + } else { + Some(interval - elapsed) + } + } +} + +pub struct SimplePeriodicalScanScheduler { + pub handle: Box>, + pub interval: Duration, +} + +impl SimplePeriodicalScanScheduler +where + Message: actix::Message + Default + Debug + Send + 'static, + Accountant: Actor + Handler, +{ + fn new(interval: Duration) -> Self { + Self { + handle: Box::new(NotifyLaterHandleReal::default()), + interval, + } + } + pub fn schedule(&self, ctx: &mut Context, logger: &Logger) { + // The default of the message implies response_skeleton_opt to be None because scheduled + // scans don't respond + let msg = Message::default(); + + debug!( + logger, + "Scheduling a scan via {:?} in {}ms", + msg, + self.interval.as_millis() + ); + + let _ = self.handle.notify_later(msg, self.interval, ctx); + } +} + +// Scanners that take part in a scan sequence composed of different scanners must handle +// StartScanErrors delicately to maintain the continuity and periodicity of this process. Where +// possible, either the same, some other, but traditional, or even a totally unrelated scan chosen +// just in the event of emergency, may be scheduled. The intention is to prevent a full panic while +// ensuring no harmful, toxic issues are left behind for the future scans. Following that philosophy, +// panic is justified only if the error was thought to be impossible by design and contextual +// things but still happened. +pub trait RescheduleScanOnErrorResolver { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanReschedulingAfterEarlyStop; +} + +#[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, + ) -> ScanReschedulingAfterEarlyStop { + 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, + ) -> ScanReschedulingAfterEarlyStop { + if is_externally_triggered { + ScanReschedulingAfterEarlyStop::DoNotSchedule + } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { + unreachable!( + "an automatic scan of NewPayableScanner should never interfere with itself {:?}", + err + ) + } else { + ScanReschedulingAfterEarlyStop::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, + ) -> ScanReschedulingAfterEarlyStop { + if is_externally_triggered { + ScanReschedulingAfterEarlyStop::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, + ) -> ScanReschedulingAfterEarlyStop { + if is_externally_triggered { + ScanReschedulingAfterEarlyStop::DoNotSchedule + } else if err == &StartScanError::NothingToProcess { + if initial_pending_payable_scan { + ScanReschedulingAfterEarlyStop::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 + ScanReschedulingAfterEarlyStop::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: &ScanReschedulingAfterEarlyStop, + ) { + 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, ScanReschedulingAfterEarlyStop, ScanSchedulers, + }; + 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; + 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(ManulTriggerError::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(*TEST_SCAN_INTERVALS, 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, + ScanReschedulingAfterEarlyStop::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(*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); + + 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, + ScanReschedulingAfterEarlyStop::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(*TEST_SCAN_INTERVALS, 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(*TEST_SCAN_INTERVALS, 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, + ScanReschedulingAfterEarlyStop::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(*TEST_SCAN_INTERVALS, 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(*TEST_SCAN_INTERVALS, 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(*TEST_SCAN_INTERVALS, 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(*TEST_SCAN_INTERVALS, 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(*TEST_SCAN_INTERVALS, 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(*TEST_SCAN_INTERVALS, 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(*TEST_SCAN_INTERVALS, 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, + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got '{:?}'", + result, + ); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Automatic NewPayables scan failed - rescheduling strategy: \ + \"Schedule(Payables)\"", + )); + }) + } + + #[test] + fn conversion_between_hintable_scanner_and_scan_type_works() { + assert_eq!( + ScanType::from(PayableSequenceScanner::NewPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::RetryPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false + }), + ScanType::PendingPayables + ); + assert_eq!( + ScanType::from(PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true + }), + ScanType::PendingPayables + ); + } +} diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 30b3a3d2d..c459f7226 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -1,29 +1,41 @@ // 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 crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; + 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(Vec), + 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 @@ -89,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, @@ -98,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 ); @@ -115,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 } } } @@ -181,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") }) } @@ -221,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), )) } @@ -308,141 +300,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 std::time::SystemTime; - - #[derive(Debug, Default, PartialEq, Eq, Clone)] - pub struct PendingPayableScanReport { - pub still_pending: Vec, - pub failures: Vec, - pub confirmed: Vec, - } - - 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_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::{ LocallyCausedError, RemotelyCausedErrors, }; @@ -451,37 +312,35 @@ 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::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 crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; + 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_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 @@ -504,22 +363,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_time_t(to_time_t(now) - offset), - }; - - let (balance, age) = balance_and_age(now, &receivable_account); - - assert_eq!(balance, "10"); - assert_eq!(age.as_secs(), offset as u64); - } - #[test] fn separate_errors_works_for_no_errs_just_oks() { let correct_payment_1 = PendingPayable { @@ -549,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()), @@ -562,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", ); } @@ -590,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] @@ -614,7 +460,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 +474,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 +488,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, ), @@ -657,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] @@ -780,11 +626,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()), @@ -800,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)); @@ -811,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 c43d6f71b..ecd0781fe 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,9 +2,559 @@ #![cfg(test)] -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use masq_lib::type_obfuscation::Obfuscated; +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::{ + PendingPayableCache, PendingPayableScanResult, +}; +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, +}; +use crate::accountant::{ + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, SentPayables, TxReceiptsMessage, +}; +use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, 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::collections::HashMap; +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, + >, + ), + Receivable( + ReplacementType< + ReceivableScanner, + ScannerMock>, + >, + ), +} + +pub enum MarkScanner<'a> { + Ended(&'a Logger), + Started(SystemTime), +} + +// 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); + 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, + ) -> ScanReschedulingAfterEarlyStop { + 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: ScanReschedulingAfterEarlyStop, + ) -> Self { + self.resolve_rescheduling_on_error_results + .borrow_mut() + .push(result); + self + } +} + +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 5c9d6f14a..2f777e57b 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -3,35 +3,42 @@ #![cfg(test)] use crate::accountant::db_access_objects::banned_dao::{BannedDao, BannedDaoFactory}; -use crate::accountant::db_access_objects::payable_dao::{ - PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureReason, + FailureRetrieveCondition, FailureStatus, }; -use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayableDao, PendingPayableDaoError, PendingPayableDaoFactory, TransactionHashes, +use crate::accountant::db_access_objects::payable_dao::{ + MarkPendingPayableID, PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, }; use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; -use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; -use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDaoError, SentTx, }; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{ - MultistagePayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, +use crate::accountant::db_access_objects::sent_payable_dao::{ + SentPayableDao, SentPayableDaoFactory, TxStatus, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; -use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, PeriodicalScanScheduler, - ReceivableScanner, ScanSchedulers, Scanner, +use crate::accountant::db_access_objects::utils::{ + from_unix_timestamp, to_unix_timestamp, CustomQuery, TxHash, TxIdentifiers, }; -use crate::accountant::{ - gwei_to_wei, Accountant, ResponseSkeleton, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC, +use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayableWithGasPrice, + QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, }; -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::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}; +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}; @@ -39,28 +46,26 @@ 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 ethereum_types::H256; -use itertools::Either; +use ethereum_types::U64; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; -use masq_lib::ui_gateway::NodeToUiMessage; +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::fmt::Debug; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime}; +use std::time::SystemTime; +use web3::types::Address; 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 +73,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), @@ -95,13 +100,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, } @@ -114,7 +179,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, } @@ -252,7 +318,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, @@ -279,16 +349,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 ) } @@ -307,6 +377,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>, @@ -341,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()) @@ -353,12 +446,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())); @@ -369,7 +465,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), @@ -395,7 +492,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(()); @@ -431,7 +529,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(()); @@ -531,7 +630,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>>>, @@ -543,37 +642,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() @@ -644,10 +742,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 } @@ -695,12 +790,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) } @@ -874,191 +970,182 @@ 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 } #[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 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 fingerprints_rowids_params(mut self, params: &Arc>>>) -> Self { - self.fingerprints_rowids_params = params.clone(); + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + self.insert_new_records_params = params.clone(); self } - pub fn fingerprints_rowids_result(self, result: TransactionHashes) -> Self { - self.fingerprints_rowids_results.borrow_mut().push(result); + pub fn insert_new_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.insert_new_records_results.borrow_mut().push(result); self } - pub fn insert_fingerprints_params( + 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(()); @@ -1066,7 +1153,7 @@ impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { } } -impl PendingPayableDaoFactoryMock { +impl SentPayableDaoFactoryMock { pub fn new() -> Self { Self { make_params: Arc::new(Mutex::new(vec![])), @@ -1079,7 +1166,156 @@ 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 + } +} + +#[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.clone()); + 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 { + 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 } @@ -1087,7 +1323,7 @@ impl PendingPayableDaoFactoryMock { pub struct PayableScannerBuilder { payable_dao: PayableDaoMock, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, payment_thresholds: PaymentThresholds, payment_adjuster: PaymentAdjusterMock, } @@ -1096,7 +1332,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(), } @@ -1120,18 +1356,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), ) @@ -1140,20 +1376,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()), } } @@ -1162,24 +1404,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 failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { + self.failed_payable_dao = failed_payable_dao; self } - pub fn when_pending_too_long_sec(mut self, interval: u64) -> Self { - self.when_pending_too_long_sec = interval; + 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 } } @@ -1247,18 +1511,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), - hash: make_tx_hash(456), - attempt: 1, - amount: 12345, - process_error: None, - } -} - -pub fn make_payables( +pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, ) -> ( @@ -1269,8 +1522,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 +1533,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 +1543,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, }, @@ -1339,10 +1592,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(); } @@ -1509,207 +1762,32 @@ impl PaymentAdjusterMock { } } -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() +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(), } } -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 - } +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 8ab3426cf..61c5ff9c0 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, @@ -1165,8 +1167,8 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - scan_intervals_opt: Some(ScanIntervals::default()), - suppress_initial_scans: false, + 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 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1241,7 +1243,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 +1546,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 +1732,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_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/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs b/node/src/blockchain/blockchain_agent/mod.rs similarity index 74% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs rename to node/src/blockchain/blockchain_agent/mod.rs index 2f2af4015..fb8030a09 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/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 e4275b036..3458a4140 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,19 +1,21 @@ // 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, QualifiedPayablesMessage, +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::{ - BlockchainError, PayableTransactionError, + BlockchainInterfaceError, PayableTransactionError, +}; +use crate::blockchain::blockchain_interface::data_structures::{ + ProcessedPayableFallible, StatusReadFromReceiptCheck, }; -use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible; use crate::blockchain::blockchain_interface::BlockchainInterface; use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal}; @@ -21,12 +23,11 @@ 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::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}; -use crate::sub_lib::wallet::{Wallet}; +use crate::sub_lib::wallet::Wallet; use actix::Actor; use actix::Context; use actix::Handler; @@ -35,19 +36,15 @@ 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 crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::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"; @@ -61,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)] @@ -90,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); @@ -127,7 +123,7 @@ impl Handler for BlockchainBridge { ) -> >::Result { self.handle_scan_future( Self::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg, ) } @@ -139,7 +135,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, ) } @@ -149,7 +145,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, + ); } } @@ -159,28 +161,23 @@ 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, ) } } #[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 { @@ -206,9 +203,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, }, } } @@ -263,11 +260,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.protected_qualified_payables, + priced_qualified_payables, agent, incoming_message.response_skeleton_opt, ); @@ -394,21 +393,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 +424,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 - .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.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"); @@ -458,7 +448,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, @@ -485,32 +475,33 @@ 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(); + 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") } - 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), @@ -535,6 +526,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 { @@ -547,43 +542,49 @@ 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_time_t; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; + 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::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, }; 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, + make_accountant_subs_from_recorder, make_blockchain_bridge_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::{ assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, - prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, ZERO, + 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; - use masq_lib::messages::ScanType; + 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,8 +598,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::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt}; impl Handler> for BlockchainBridge { type Result = (); @@ -612,6 +611,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"); @@ -679,15 +689,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, @@ -724,9 +733,10 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(qualified_payables.clone()); + let unpriced_qualified_payables = + UnpricedQualifiedPayables::from(qualified_payables.clone()); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables.clone(), + qualified_payables: unpriced_qualified_payables.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -741,42 +751,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.protected_qualified_payables, - qualified_payables - ); - assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .consuming_wallet(), - &consuming_wallet - ); - assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .agreed_fee_per_computation_unit(), - 0x230000000 + blockchain_agent_with_context_msg_actual.qualified_payables, + 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 - .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, @@ -789,9 +790,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) @@ -808,9 +810,9 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(vec![]); + let qualified_payables = UnpricedQualifiedPayables::from(vec![make_payable_account(123)]); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables, + qualified_payables, consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -825,12 +827,11 @@ 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( consuming_wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Api error: Transport error: Error(IncompleteMessage)".to_string(), ), ); @@ -844,10 +845,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) @@ -858,7 +859,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"); @@ -873,16 +874,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_time_t(150_000_000), + 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); @@ -890,7 +890,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, @@ -903,18 +906,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: accounts[0].wallet.clone(), - hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" - ) - .unwrap() + recipient_wallet: account.wallet.clone(), + hash: expected_hash })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -922,17 +925,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( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" - ) - .unwrap(), - amount: accounts[0].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); } @@ -949,9 +961,9 @@ 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 account_wallet = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); let persistent_configuration_mock = PersistentConfigurationMock::default(); let subject = BlockchainBridge::new( @@ -963,22 +975,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 { - wallet: wallet_account, + let account = PayableAccount { + wallet: account_wallet.clone(), 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"); 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, @@ -989,8 +1004,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( @@ -1001,25 +1016,35 @@ mod tests { "Transport error: Error(IncompleteMessage)", ); assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" - ) - .unwrap(), - amount: accounts[0].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, ScanError { - scan_type: ScanType::Payables, + 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 transactions: 0x36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". \ + Signed and hashed txs: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) } ); @@ -1041,13 +1066,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), @@ -1057,7 +1086,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) @@ -1071,7 +1100,7 @@ mod tests { Correct(PendingPayable { recipient_wallet: accounts_1.wallet, hash: H256::from_str( - "cc73f3d5fe9fc3dac28b510ddeb157b0f8030b201e809014967396cdf365488a" + "c0756e8da662cee896ed979456c77931668b7f8456b9f978fc3305671f8f82ad" ) .unwrap() }) @@ -1081,7 +1110,7 @@ mod tests { Correct(PendingPayable { recipient_wallet: accounts_2.wallet, hash: H256::from_str( - "891d9ffa838aedc0bb2f6f7e9737128ce98bb33d07b4c8aa5645871e20d6cd13" + "9ba19f88ce43297d700b1f57ed8bc6274d01a5c366b78dd05167f9874c867ba0" ) .unwrap() }) @@ -1103,8 +1132,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), @@ -1114,7 +1147,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) @@ -1125,7 +1158,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() )) ); @@ -1149,21 +1182,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_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) @@ -1184,9 +1209,9 @@ 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_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, @@ -1200,26 +1225,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, @@ -1239,7 +1258,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(); @@ -1269,7 +1288,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() } @@ -1281,8 +1300,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); @@ -1297,59 +1315,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_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_time_t(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( @@ -1359,14 +1344,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: 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, @@ -1380,15 +1365,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, @@ -1402,32 +1390,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_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 = + 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); @@ -1438,17 +1411,20 @@ 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: 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"); let _ = subject.handle_scan_future( BlockchainBridge::handle_request_transaction_receipts, - ScanType::PendingPayables, + DetailedScanType::PendingPayables, msg, ); @@ -1457,7 +1433,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() @@ -1615,7 +1591,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 +1684,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 +1768,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; @@ -1825,7 +1802,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 @@ -1849,7 +1826,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![])); @@ -1885,7 +1862,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 @@ -2024,19 +2001,22 @@ 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!(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, + DetailedScanType::Receivables, retrieve_transactions, ); 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); } @@ -2075,14 +2055,14 @@ 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()); subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg.clone(), ); @@ -2092,7 +2072,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() @@ -2119,7 +2099,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); @@ -2128,7 +2108,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); @@ -2144,7 +2124,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); @@ -2157,7 +2137,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); @@ -2170,7 +2150,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); @@ -2185,7 +2165,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); @@ -2194,7 +2174,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); @@ -2203,7 +2183,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(), ); @@ -2225,22 +2205,10 @@ mod tests { assert_on_initialization_with_panic_on_migration(&data_dir, &act); } -} -#[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, - ) - } + #[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); } } 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..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 @@ -1,62 +1,17 @@ // 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; use serde_json::Value; 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), -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct TxReceipt { - pub transaction_hash: H256, - pub status: TxStatus, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -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>, @@ -68,7 +23,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_fee_balance( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.web3 .eth() @@ -80,7 +35,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) @@ -88,7 +43,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { ) } - fn get_gas_price(&self) -> Box> { + fn get_gas_price(&self) -> Box> { Box::new( self.web3 .eth() @@ -97,7 +52,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { ) } - fn get_block_number(&self) -> Box> { + fn get_block_number(&self) -> Box> { Box::new( self.web3 .eth() @@ -109,7 +64,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_id( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.web3 .eth() @@ -121,7 +76,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); }); @@ -141,7 +96,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_logs( &self, filter: Filter, - ) -> Box, Error = BlockchainError>> { + ) -> Box, Error = BlockchainInterfaceError>> { Box::new( self.web3 .eth() @@ -173,9 +128,9 @@ 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::test_utils::make_blockchain_interface_web3; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; + use crate::blockchain::blockchain_interface::{BlockchainInterfaceError, BlockchainInterface}; + 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}; @@ -183,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::{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() { @@ -222,7 +177,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), @@ -330,7 +287,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), @@ -383,8 +342,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), @@ -547,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"), @@ -566,71 +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), - ); + 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); - } - - 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 92f8e9145..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,9 +4,9 @@ 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::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; +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 crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainInterface}; @@ -21,10 +21,20 @@ 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::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::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; +use crate::blockchain::blockchain_agent::BlockchainAgent; +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 +// 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#"[{ @@ -98,7 +108,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(); @@ -156,7 +167,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { ) } - fn build_blockchain_agent( + fn introduce_blockchain_agent( &self, consuming_wallet: Wallet, ) -> Box, Error = BlockchainAgentBuildError>> { @@ -175,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) @@ -188,13 +199,13 @@ 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, }; Ok(create_blockchain_agent_web3( - gas_limit_const_part, blockchain_agent_future_result, + gas_limit_const_part, consuming_wallet, chain, )) @@ -206,37 +217,44 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainError>> { + 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::>()) }), ) } @@ -245,8 +263,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, - affordable_accounts: Vec, + new_pending_payables_recipient: Recipient, + affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { let consuming_wallet = agent.consuming_wallet().clone(); @@ -254,7 +272,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,9 +283,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { chain, &web3_batch, consuming_wallet, - gas_price_wei, pending_nonce, - fingerprints_recipient, + new_pending_payables_recipient, affordable_accounts, ) }), @@ -279,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 { @@ -362,7 +378,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) { @@ -394,9 +410,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 @@ -408,7 +424,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); @@ -425,24 +441,42 @@ 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::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + 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::BlockchainError::QueryFailed; - use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; + use crate::blockchain::blockchain_interface::data_structures::{ + BlockchainTransaction, TxBlock, + }; use crate::blockchain::blockchain_interface::{ - BlockchainAgentBuildError, BlockchainError, BlockchainInterface, + BlockchainAgentBuildError, BlockchainInterfaceError, BlockchainInterface, RetrievedBlockchainTransactions, }; use crate::blockchain::test_utils::{ - all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder, + all_chains, make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; @@ -459,7 +493,7 @@ mod tests { use std::str::FromStr; use web3::transports::Http; use web3::types::{H256, U256}; - 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() { @@ -729,7 +763,7 @@ mod tests { assert_eq!( result.expect_err("Expected an Err, got Ok"), - BlockchainError::InvalidResponse + BlockchainInterfaceError::InvalidResponse ); } @@ -753,7 +787,7 @@ mod tests { ) .wait(); - assert_eq!(result, Err(BlockchainError::InvalidResponse)); + assert_eq!(result, Err(BlockchainInterfaceError::InvalidResponse)); } #[test] @@ -831,11 +865,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 +963,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 +981,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 +1001,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 +1016,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] @@ -919,7 +1037,7 @@ mod tests { let expected_err_factory = |wallet: &Wallet| { BlockchainAgentBuildError::TransactionFeeBalance( wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Transport error: Error(IncompleteMessage)".to_string(), ), ) @@ -941,7 +1059,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(), ), ) @@ -956,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(); @@ -1018,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] @@ -1067,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) @@ -1119,7 +1225,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Uninitialized, BlockScanRange::NoLimit, - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1137,7 +1243,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Uninitialized, BlockScanRange::Range(100), - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1155,7 +1261,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Value(50), BlockScanRange::NoLimit, - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1173,7 +1279,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Value(50), BlockScanRange::Range(100), - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Value(150) @@ -1256,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 03ed4150b..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,126 +1,137 @@ // 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::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::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +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::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; 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)] 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() } 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$} {: [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 } @@ -137,13 +148,14 @@ 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). + // 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), @@ -187,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, @@ -199,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) { @@ -211,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, - gas_price_in_wei: u128, - mut pending_nonce: U256, - accounts: &[PayableAccount], -) -> Vec { - let mut hash_and_amount_list = vec![]; - accounts.iter().for_each(|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, - gas_price_in_wei, - ); - - pending_nonce = advance_used_nonce(pending_nonce); - hash_and_amount_list.push(hash_and_amount); - }); - hash_and_amount_list + initial_pending_nonce: U256, + accounts: &PricedQualifiedPayables, +) -> 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)] @@ -250,79 +273,85 @@ 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, + new_pending_payables_recipient: Recipient, + 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( + let common_timestamp = SystemTime::now(); + + let prepared_sent_txs_records = sign_and_append_multiple_payments( + common_timestamp, logger, chain, web3_batch, consuming_wallet, - gas_price_in_wei, pending_nonce, &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()); - // 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, - 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!( logger, "{}", - transmission_log(chain, &accounts, gas_price_in_wei) + transmission_log(chain, &accounts, pending_nonce) ); Box::new( 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, - accounts, + sent_txs_hashes, + 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(), + blockchain_agent_future_result.gas_price_minor.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, )) } @@ -330,13 +359,14 @@ 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::{ 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 +390,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; @@ -404,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(), @@ -421,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, @@ -431,88 +456,178 @@ 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.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, consuming_wallet, - gwei_to_wei(gas_price_in_gwei), pending_nonce.into(), &accounts, ); - assert_eq!( - result, - vec![ - HashAndAmount { - hash: H256::from_str( - "94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2" - ) - .unwrap(), - amount: 1000000000 - }, - HashAndAmount { - hash: H256::from_str( - "3811874d2b73cecd51234c94af46bcce918d0cb4de7d946c01d7da606fe761b5" - ) - .unwrap(), - amount: 2000000000 - } - ] + 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, ); } - #[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, + 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() ); - 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, + 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) ); - 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, + assert_eq!(actual_sent_tx.timestamp, to_unix_timestamp(now)); + } + + #[test] + 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 ); } @@ -522,26 +637,17 @@ 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, }, ]; - 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 { @@ -551,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, @@ -573,9 +679,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,14 +691,13 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let gas_price = 1_000_000_000; - 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(); @@ -601,9 +706,8 @@ mod tests { chain, &web3_batch, consuming_wallet.clone(), - gas_price, pending_nonce, - new_fingerprints_recipient, + new_pending_payables_recipient, accounts.clone(), ) .wait(); @@ -611,31 +715,48 @@ 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: {}, 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 +767,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", + "0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3", ) .unwrap(), }), Correct(PendingPayable { - recipient_wallet: accounts[1].wallet.clone(), + recipient_wallet: account_2.wallet.clone(), hash: H256::from_str( - "7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3", + "6b485dbd4d769b5a19fa57058d612fad99cdd78769db6b3be129f981c42657ac", ) .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 +795,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() + hashes: hashset![ + H256::from_str("5bbe90ad19d86b69ee49879cec4b3f8b769223e6a872aae0be88773de2fc3beb").unwrap(), + H256::from_str("a1b609dbe9cc77ad586dbe4e5c1079d6ad76020a353c960928d6daeafd43f366").unwrap() ], }); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_fails_on_submit_batch_call", accounts, expected_result, @@ -693,7 +820,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 +846,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("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -727,14 +855,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("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").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 +873,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 +889,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("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -766,28 +898,22 @@ 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("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").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, ); } - #[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 3084accfb..1d01532ec 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -1,34 +1,34 @@ // 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"; #[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 +37,15 @@ 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, + Sending { + msg: String, + hashes: HashSet, + }, + UninitializedInterface, } impl Display for PayableTransactionError { @@ -63,13 +66,16 @@ 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::UninitializedBlockchainInterface => { + 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) } } @@ -78,10 +84,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 +104,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 +125,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 +144,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 +176,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(), ), @@ -180,9 +188,9 @@ 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::UninitializedBlockchainInterface, + PayableTransactionError::UninitializedInterface, ]; let actual_error_msgs = original_errors.iter().map(to_string).collect::>(); @@ -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 ]) @@ -213,16 +221,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/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/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 ef1e8d373..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::accountant::scanners::mid_scan_msg_handling::payable_scanner::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::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::db_access_objects::payable_dao::PayableAccount; -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; @@ -31,24 +38,29 @@ pub trait BlockchainInterface { start_block: BlockMarker, scan_range: BlockScanRange, recipient: Address, - ) -> Box>; + ) -> Box>; - fn build_blockchain_agent( + fn introduce_blockchain_agent( &self, consuming_wallet: Wallet, ) -> Box, Error = BlockchainAgentBuildError>>; fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainError>>; + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = HashMap, + Error = BlockchainInterfaceError, + >, + >; fn submit_payables_in_batch( &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, - affordable_accounts: Vec, + new_pending_payables_recipient: Recipient, + 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/errors/internal_errors.rs b/node/src/blockchain/errors/internal_errors.rs new file mode 100644 index 000000000..9982d0667 --- /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, Copy, 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..e406a96b1 --- /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, Copy, 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, LocalErrorKind}; + use crate::blockchain::errors::BlockchainErrorKind; + + #[test] + fn blockchain_error_serialization_deserialization() { + vec![ + ( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + r#"{"AppRpc":{"Local":"Decoder"}}"#, + ), + ( + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + r#"{"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..bf78fa53b --- /dev/null +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -0,0 +1,221 @@ +// 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, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AppRpcErrorKind { + Local(LocalErrorKind), + Remote(RemoteErrorKind), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum LocalErrorKind { + Decoder, + Internal, + IO, + Signing, + Transport, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RemoteErrorKind { + InvalidResponse, + Unreachable, + 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::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::Remote(RemoteErrorKind::InvalidResponse), + RemoteError::Unreachable => Self::Remote(RemoteErrorKind::Unreachable), + RemoteError::Web3RpcError { code, .. } => { + Self::Remote(RemoteErrorKind::Web3RpcError(*code)) + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, RemoteErrorKind, + }; + 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::Local(LocalErrorKind::Decoder) + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Internal)), + AppRpcErrorKind::Local(LocalErrorKind::Internal) + ); + assert_eq!( + 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::Local(LocalErrorKind::Signing) + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Transport( + "Transport error".to_string() + ))), + AppRpcErrorKind::Local(LocalErrorKind::Transport) + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::InvalidResponse( + "Invalid response".to_string() + ))), + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse) + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Unreachable)), + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable) + ); + assert_eq!( + AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Web3RpcError { + code: 55, + message: "Booga".to_string() + })), + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(55)) + ); + } + + #[test] + fn app_rpc_error_kind_serialization_deserialization() { + let errors = vec![ + 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| { + 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..34cb2c5e3 --- /dev/null +++ b/node/src/blockchain/errors/validation_status.rs @@ -0,0 +1,327 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +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)] +pub enum ValidationStatus { + Waiting, + Reattempting(PreviousAttempts), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreviousAttempts { + 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 { + 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; + 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() { + let validation_failure_clock = ValidationFailureClockReal::default(); + // new() + let timestamp_a = SystemTime::now(); + let subject = PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::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::Local(LocalErrorKind::IO)), + &validation_failure_clock, + ); + let timestamp_d = SystemTime::now(); + let subject = subject.add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &validation_failure_clock, + ); + let subject = subject.add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), + &validation_failure_clock, + ); + + let decoder_error_stats = subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::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::Local( + LocalErrorKind::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::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/blockchain/mod.rs b/node/src/blockchain/mod.rs index 4c51e726e..f3ef3d323 100644 --- a/node/src/blockchain/mod.rs +++ b/node/src/blockchain/mod.rs @@ -1,9 +1,11 @@ // 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; +pub mod errors; pub mod payer; pub mod signature; #[cfg(test)] diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 4124e283a..2ce57f261 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}; @@ -185,10 +188,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, @@ -217,3 +228,70 @@ pub fn transport_error_message() -> String { "Connection refused".to_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>, +} + +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/bootstrapper.rs b/node/src/bootstrapper.rs index cf82499b6..aa3943183 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -336,7 +336,7 @@ pub struct BootstrapperConfig { pub log_level: LevelFilter, pub dns_servers: Vec, pub scan_intervals_opt: Option, - pub suppress_initial_scans: bool, + pub automatic_scans_enabled: bool, pub when_pending_too_long_sec: u64, pub crash_point: CrashPoint, pub clandestine_discriminator_factories: Vec>, @@ -372,7 +372,7 @@ impl BootstrapperConfig { log_level: LevelFilter::Off, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, crash_point: CrashPoint::None, clandestine_discriminator_factories: vec![], ui_gateway_config: UiGatewayConfig { @@ -416,7 +416,7 @@ impl BootstrapperConfig { self.consuming_wallet_opt = unprivileged.consuming_wallet_opt; self.db_password_opt = unprivileged.db_password_opt; self.scan_intervals_opt = unprivileged.scan_intervals_opt; - self.suppress_initial_scans = unprivileged.suppress_initial_scans; + self.automatic_scans_enabled = unprivileged.automatic_scans_enabled; self.payment_thresholds_opt = unprivileged.payment_thresholds_opt; self.when_pending_too_long_sec = unprivileged.when_pending_too_long_sec; } @@ -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,8 +1253,8 @@ 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.suppress_initial_scans = false; + 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; privileged_config.merge_unprivileged(unprivileged_config); @@ -1276,9 +1277,9 @@ 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.suppress_initial_scans, false); + assert_eq!(privileged_config.automatic_scans_enabled, true); assert_eq!( privileged_config.when_pending_too_long_sec, DEFAULT_PENDING_TOO_LONG_SEC diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 4d31683a0..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,19 +3448,26 @@ 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; - scan_intervals.pending_payable_scan_interval = scan_intervals - .pending_payable_scan_interval + let mut scan_intervals = *TEST_SCAN_INTERVALS; + 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)); @@ -3466,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(), ) } @@ -3488,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 be5547576..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; @@ -135,7 +136,8 @@ impl DbInitializerReal { Self::create_config_table(conn); Self::initialize_config(conn, external_params); Self::create_payable_table(conn); - Self::create_pending_payable_table(conn); + Self::create_sent_payable_table(conn); + Self::create_failed_payable_table(conn); Self::create_receivable_table(conn); Self::create_banned_table(conn); } @@ -251,32 +253,62 @@ 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", ); Self::set_config_value(conn, "max_block_count", None, false, "maximum block count"); } - pub fn create_pending_payable_table(conn: &Connection) { + pub fn create_sent_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 + "create table if not exists sent_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + status text not null )", [], ) - .expect("Can't create pending_payable table"); + .expect("Can't create sent_payable table"); + + conn.execute( + "CREATE UNIQUE INDEX sent_payable_tx_hash_idx ON sent_payable (tx_hash)", + [], + ) + .expect("Can't create transaction hash index in sent payments"); + } + + pub fn create_failed_payable_table(conn: &Connection) { + conn.execute( + "create table if not exists failed_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + reason text not null, + status text not null + )", + [], + ) + .expect("Can't create failed_payable table"); + conn.execute( - "CREATE UNIQUE INDEX pending_payable_hash_idx ON pending_payable (transaction_hash)", + "CREATE UNIQUE INDEX failed_payable_tx_hash_idx ON sent_payable (tx_hash)", [], ) - .expect("Can't create transaction hash index in pending payments"); + .expect("Can't create transaction hash index in failed payments"); } pub fn create_payable_table(conn: &Connection) { @@ -621,6 +653,9 @@ impl Debug for DbInitializationConfig { mod tests { use super::*; use crate::database::db_initializer::InitializationError::SqliteError; + use crate::database::test_utils::{ + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + }; use crate::db_config::config_dao::{ConfigDao, ConfigDaoReal}; use crate::test_utils::database_utils::{ assert_create_table_stm_contains_all_parts, @@ -652,7 +687,7 @@ mod tests { #[test] fn constants_have_correct_values() { assert_eq!(DATABASE_FILE, "node-data.db"); - assert_eq!(CURRENT_SCHEMA_VERSION, 10); + assert_eq!(CURRENT_SCHEMA_VERSION, 11); } #[test] @@ -670,7 +705,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"], @@ -681,34 +716,84 @@ mod tests { } #[test] - fn db_initialize_creates_pending_payable_table() { + fn db_initialize_creates_sent_payable_table() { let home_dir = ensure_node_home_directory_does_not_exist( "db_initializer", - "db_initialize_creates_pending_payable_table", + "db_initialize_creates_sent_payable_table", ); let subject = DbInitializerReal::default(); let conn = subject .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); + let mut stmt = conn + .prepare( + "SELECT rowid, + tx_hash, + receiver_address, + amount_high_b, + amount_low_b, + timestamp, + gas_price_wei_high_b, + gas_price_wei_low_b, + nonce, + status + FROM sent_payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); + assert_create_table_stm_contains_all_parts( + &*conn, + "sent_payable", + SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + ); + let expected_key_words: &[&[&str]] = &[&["tx_hash"]]; + assert_index_stm_is_coupled_with_right_parameter( + conn.as_ref(), + "sent_payable_tx_hash_idx", + expected_key_words, + ) + } - 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 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"]]; + #[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, + status + 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(), - "pending_payable_hash_idx", + "failed_payable_tx_hash_idx", expected_key_words, ) } @@ -725,9 +810,18 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let mut stmt = conn.prepare ("select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid from payable").unwrap (); - let mut payable_contents = stmt.query_map([], |_| Ok(42)).unwrap(); - assert!(payable_contents.next().is_none()); + let mut stmt = conn + .prepare( + "SELECT wallet_address, + balance_high_b, + balance_low_b, + last_paid_timestamp, + pending_payable_rowid + FROM payable", + ) + .unwrap(); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); assert_table_created_as_strict(&*conn, "payable"); let expected_key_words: &[&[&str]] = &[ &["wallet_address", "text", "primary", "key"], @@ -753,10 +847,16 @@ mod tests { .unwrap(); let mut stmt = conn - .prepare("select wallet_address, balance_high_b, balance_low_b, last_received_timestamp from receivable") + .prepare( + "SELECT wallet_address, + balance_high_b, + balance_low_b, + last_received_timestamp + FROM receivable", + ) .unwrap(); - let mut receivable_contents = stmt.query_map([], |_| Ok(())).unwrap(); - assert!(receivable_contents.next().is_none()); + let result = stmt.execute([]).unwrap(); + assert_eq!(result, 1); assert_table_created_as_strict(&*conn, "receivable"); let expected_key_words: &[&[&str]] = &[ &["wallet_address", "text", "primary", "key"], @@ -782,8 +882,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") @@ -952,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/db_migrator.rs b/node/src/database/db_migrations/db_migrator.rs index 7d1ec4f8c..6cae9599a 100644 --- a/node/src/database/db_migrations/db_migrator.rs +++ b/node/src/database/db_migrations/db_migrator.rs @@ -2,6 +2,7 @@ use crate::database::db_initializer::ExternalData; use crate::database::db_migrations::migrations::migration_0_to_1::Migrate_0_to_1; +use crate::database::db_migrations::migrations::migration_10_to_11::Migrate_10_to_11; use crate::database::db_migrations::migrations::migration_1_to_2::Migrate_1_to_2; use crate::database::db_migrations::migrations::migration_2_to_3::Migrate_2_to_3; use crate::database::db_migrations::migrations::migration_3_to_4::Migrate_3_to_4; @@ -80,6 +81,7 @@ impl DbMigratorReal { &Migrate_7_to_8, &Migrate_8_to_9, &Migrate_9_to_10, + &Migrate_10_to_11, // TODO: GH-598: Make this one as null migration and yours as 12 ] } diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs new file mode 100644 index 000000000..b3f2a157a --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -0,0 +1,111 @@ +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; + +#[allow(non_camel_case_types)] +pub struct Migrate_10_to_11; + +impl DatabaseMigration for Migrate_10_to_11 { + fn migrate<'a>( + &self, + declaration_utils: Box, + ) -> rusqlite::Result<()> { + let sql_statement_for_sent_payable = "create table if not exists sent_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + status text not null + )"; + + let sql_statement_for_failed_payable = "create table if not exists failed_payable ( + rowid integer primary key, + tx_hash text not null, + receiver_address text not null, + amount_high_b integer not null, + amount_low_b integer not null, + timestamp integer not null, + gas_price_wei_high_b integer not null, + gas_price_wei_low_b integer not null, + nonce integer not null, + reason text not null, + 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, + ]) + } + + fn old_version(&self) -> usize { + 10 + } +} + +#[cfg(test)] +mod tests { + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::database::test_utils::{ + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + }; + use crate::test_utils::database_utils::{assert_create_table_stm_contains_all_parts, assert_table_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; + + #[test] + fn migration_from_10_to_11_is_applied_correctly() { + init_test_logging(); + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_10_to_11_is_properly_set", + ); + create_dir_all(&dir_path).unwrap(); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + + let _prev_connection = subject + .initialize_to_version( + &dir_path, + 10, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + + let connection = subject + .initialize_to_version( + &dir_path, + 11, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + + assert_table_exists(connection.as_ref(), "sent_payable"); + assert_table_exists(connection.as_ref(), "failed_payable"); + assert_create_table_stm_contains_all_parts( + &*connection, + "sent_payable", + SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, + ); + assert_create_table_stm_contains_all_parts( + &*connection, + "failed_payable", + SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, + ); + 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_4_to_5.rs b/node/src/database/db_migrations/migrations/migration_4_to_5.rs index 06deb809f..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 @@ -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()); @@ -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,8 +159,8 @@ 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, - &to_time_t(timestamp), + &amount_minor, + &to_unix_timestamp(timestamp), if !hash_str.is_empty() { &hash_str } else { @@ -208,7 +208,7 @@ mod tests { Some(transaction_hash_2), &wallet_2, 1111111, - from_time_t(200_000_000), + from_unix_timestamp(200_000_000), ); let config_table_before = fetch_all_from_config_table(&conn); diff --git a/node/src/database/db_migrations/migrations/migration_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/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/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/mod.rs b/node/src/database/test_utils/mod.rs index e8b9060f1..7b415b95b 100644 --- a/node/src/database/test_utils/mod.rs +++ b/node/src/database/test_utils/mod.rs @@ -12,6 +12,33 @@ use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +pub const SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE: &[&[&str]] = &[ + &["rowid", "integer", "primary", "key"], + &["tx_hash", "text", "not", "null"], + &["receiver_address", "text", "not", "null"], + &["amount_high_b", "integer", "not", "null"], + &["amount_low_b", "integer", "not", "null"], + &["timestamp", "integer", "not", "null"], + &["gas_price_wei_high_b", "integer", "not", "null"], + &["gas_price_wei_low_b", "integer", "not", "null"], + &["nonce", "integer", "not", "null"], + &["status", "text", "not", "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"], + &["status", "text", "not", "null"], +]; + #[derive(Debug, Default)] pub struct ConnectionWrapperMock<'conn> { prepare_params: Arc>>, 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/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/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/hopper/routing_service.rs b/node/src/hopper/routing_service.rs index 05d8af9d6..264ec29dc 100644 --- a/node/src/hopper/routing_service.rs +++ b/node/src/hopper/routing_service.rs @@ -593,6 +593,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 +611,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 +625,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 +660,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 +677,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 +813,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 +843,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 +860,7 @@ mod tests { 0, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -862,7 +868,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 +1287,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 +1321,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 +1334,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 +1351,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 +1373,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 +1422,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 +1435,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 +1452,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 +1474,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 +1520,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 +1533,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 +1550,7 @@ mod tests { 200, true, ); + subject.logger = Logger::new(test_name); subject.route_data_externally( lcp, @@ -1554,7 +1561,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 +1575,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 +1612,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 +1629,7 @@ mod tests { rate_pack_routing_byte(103), false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -1630,7 +1640,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 +1648,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 +1689,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 +1706,7 @@ mod tests { rate_pack_routing_byte(103), false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -1704,12 +1717,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 +1745,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 +1759,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 +1777,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 +1805,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 +1822,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 +1840,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 +1856,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 +1870,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 +2005,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 +2022,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 +2030,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 +2039,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 +2085,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 +2102,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 +2110,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 +2119,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 +2143,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 +2151,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 +2160,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 +2184,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 +2201,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/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..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", @@ -504,22 +504,26 @@ 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"; + + validate_scan_intervals(&scan_intervals)?; + + 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(()) } -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)); @@ -533,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, @@ -1818,7 +1837,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 +1846,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 +1874,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 +1891,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 +1913,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 +1922,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 +1954,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 +1966,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, @@ -2098,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", @@ -2115,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( @@ -2126,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( @@ -2144,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( @@ -2162,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; @@ -2170,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!( @@ -2185,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(); @@ -2578,7 +2623,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, true); + assert_eq!(bootstrapper_config.automatic_scans_enabled, false); } #[test] @@ -2599,7 +2644,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } #[test] @@ -2620,7 +2665,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } fn make_persistent_config( diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index fc0334834..d18a7ceba 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -1353,7 +1353,7 @@ impl Hostname { #[cfg(test)] mod tests { use super::*; - use crate::match_every_type_id; + use crate::match_lazily_every_type_id; use crate::proxy_server::protocol_pack::ServerImpersonator; use crate::proxy_server::server_impersonator_http::ServerImpersonatorHttp; use crate::proxy_server::server_impersonator_tls::ServerImpersonatorTls; @@ -1380,7 +1380,7 @@ mod tests { use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::{ make_request_payload, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; @@ -2627,8 +2627,8 @@ mod tests { let cryptde = main_cryptde(); let http_request = b"GET /index.html HTTP/1.1\r\nHost: nowhere.com\r\n\r\n"; let (proxy_server_mock, _, proxy_server_recording_arc) = make_recorder(); - let proxy_server_mock = - proxy_server_mock.system_stop_conditions(match_every_type_id!(AddRouteResultMessage)); + let proxy_server_mock = proxy_server_mock + .system_stop_conditions(match_lazily_every_type_id!(AddRouteResultMessage)); let route_query_response = None; let (neighborhood_mock, _, _) = make_recorder(); let neighborhood_mock = @@ -5230,7 +5230,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!(RouteQueryMessage)) + .system_stop_conditions(match_lazily_every_type_id!(RouteQueryMessage)) .route_query_response(Some(route_query_response_expected.clone())); let cryptde = main_cryptde(); let mut subject = ProxyServer::new( @@ -5400,7 +5400,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( RouteQueryMessage, RouteQueryMessage, RouteQueryMessage diff --git a/node/src/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 4b005f713..317070c09 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::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; +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}; @@ -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 { - pending_payable_scan_interval: Duration::from_secs(600), - payable_scan_interval: Duration::from_secs(600), - receivable_scan_interval: Duration::from_secs(600) - }; } //please, alphabetical order @@ -71,7 +67,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, @@ -84,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), + } } } @@ -100,8 +103,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, @@ -191,23 +194,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, 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; 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 { @@ -230,12 +254,6 @@ mod tests { threshold_interval_sec: 21600, 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), - 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!( @@ -288,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/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 2ec840cc9..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::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::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 { diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index f70f60f0f..bd26eb627 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() ) } @@ -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] @@ -400,8 +401,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), ] ); @@ -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 ) ); } @@ -550,15 +551,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 +568,14 @@ mod tests { #[test] fn scan_intervals_to_combined_params() { let scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(60), - payable_scan_interval: Duration::from_secs(70), + payable_scan_interval: Duration::from_secs(90), + pending_payable_scan_interval: Duration::from_secs(40), receivable_scan_interval: Duration::from_secs(100), }; let result = scan_intervals.to_string(); - assert_eq!(result, "60|70|100".to_string()); + assert_eq!(result, "90|40|100".to_string()); } #[test] diff --git a/node/src/sub_lib/peer_actors.rs b/node/src/sub_lib/peer_actors.rs index 3a51be868..571eca3fe 100644 --- a/node/src/sub_lib/peer_actors.rs +++ b/node/src/sub_lib/peer_actors.rs @@ -14,6 +14,8 @@ use std::fmt::Debug; use std::fmt::Formatter; use std::net::IpAddr; +// TODO This file should be test only + #[derive(Clone, PartialEq, Eq)] pub struct PeerActors { pub proxy_server: ProxyServerSubs, diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index d68d721bb..5bd7a655a 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -222,6 +222,7 @@ impl NLSpawnHandleHolder for NLSpawnHandleHolderReal { } } +#[derive(Default)] pub struct NotifyHandleReal { phantom: PhantomData, } diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index 02ba441a4..a2b6d9ee1 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -103,10 +103,16 @@ 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)) - .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 1bf32b4b5..601ee7bd1 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -12,7 +12,9 @@ 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 serde_serializer_mock; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; pub mod tokio_wrapper_mocks; @@ -539,13 +541,14 @@ 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}; 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; @@ -568,6 +571,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, @@ -628,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; @@ -672,17 +695,17 @@ 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.suppress_initial_scans = false; + 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()); config @@ -698,9 +721,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 +894,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 +921,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 +953,25 @@ 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 +992,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 +1001,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 +1013,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 +1078,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..ed35378c2 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -1,14 +1,14 @@ // 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::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; @@ -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>() } } @@ -149,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); @@ -158,11 +162,12 @@ 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); -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(); @@ -465,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), @@ -576,8 +640,9 @@ impl PeerActorsBuilder { self } - // This must be called after System.new and before System.run - pub fn build(self) -> PeerActors { + // This must be called after System.new and before System.run. + // These addresses may be helpful for setting up the Counter Messages. + pub fn build_and_provide_addresses(self) -> (PeerActors, PeerActorAddrs) { let proxy_server_addr = self.proxy_server.start(); let dispatcher_addr = self.dispatcher.start(); let hopper_addr = self.hopper.start(); @@ -588,27 +653,73 @@ impl PeerActorsBuilder { let blockchain_bridge_addr = self.blockchain_bridge.start(); let configurator_addr = self.configurator.start(); - PeerActors { - proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), - dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), - hopper: make_hopper_subs_from_recorder(&hopper_addr), - proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), - neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), - accountant: make_accountant_subs_from_recorder(&accountant_addr), - ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), - blockchain_bridge: make_blockchain_bridge_subs_from_recorder(&blockchain_bridge_addr), - configurator: make_configurator_subs_from_recorder(&configurator_addr), - } + ( + PeerActors { + proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), + dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), + hopper: make_hopper_subs_from_recorder(&hopper_addr), + proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), + neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), + accountant: make_accountant_subs_from_recorder(&accountant_addr), + ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), + blockchain_bridge: make_blockchain_bridge_subs_from_recorder( + &blockchain_bridge_addr, + ), + configurator: make_configurator_subs_from_recorder(&configurator_addr), + }, + PeerActorAddrs { + proxy_server_addr, + dispatcher_addr, + hopper_addr, + proxy_client_addr, + neighborhood_addr, + accountant_addr, + ui_gateway_addr, + blockchain_bridge_addr, + configurator_addr, + }, + ) + } + + // This must be called after System.new and before System.run + pub fn build(self) -> PeerActors { + let (peer_actors, _) = self.build_and_provide_addresses(); + peer_actors } } +pub struct PeerActorAddrs { + pub proxy_server_addr: Addr, + pub dispatcher_addr: Addr, + pub hopper_addr: Addr, + pub proxy_client_addr: Addr, + pub neighborhood_addr: Addr, + pub accountant_addr: Addr, + pub ui_gateway_addr: Addr, + pub blockchain_bridge_addr: Addr, + pub configurator_addr: Addr, +} + #[cfg(test)] mod tests { use super::*; - use crate::match_every_type_id; + use crate::blockchain::blockchain_bridge::BlockchainBridge; + use crate::sub_lib::neighborhood::{ConfigChange, Hops, WalletPair}; + use crate::test_utils::make_wallet; + use crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient; + use crate::{ + match_lazily_every_type_id, setup_for_counter_msg_triggered_via_specific_msg_id_method, + setup_for_counter_msg_triggered_via_type_id, + }; use actix::Message; use actix::System; + use masq_lib::messages::{ + SerializableLogLevel, ToMessageBody, UiLogBroadcast, UiUnmarshalError, + }; + use masq_lib::ui_gateway::MessageTarget; use std::any::TypeId; + use std::net::{IpAddr, Ipv4Addr}; + use std::vec; #[derive(Debug, PartialEq, Eq, Message)] struct FirstMessageType { @@ -669,7 +780,7 @@ mod tests { fn recorder_can_be_stopped_on_a_particular_message() { let system = System::new("recorder_can_be_stopped_on_a_particular_message"); let recorder = - Recorder::new().system_stop_conditions(match_every_type_id!(FirstMessageType)); + Recorder::new().system_stop_conditions(match_lazily_every_type_id!(FirstMessageType)); let recording_arc = recorder.get_recording(); let rec_addr: Addr = recorder.start(); @@ -705,4 +816,324 @@ mod tests { TypeId::of::>() ) } + + #[test] + fn counter_msgs_with_diff_id_methods_are_used_together_and_one_was_not_triggered() { + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( + ScanForReceivables, + NodeToUiMessage + )); + let respondent_addr = respondent.start(); + // Case 1 + // This msg will trigger as the recorder will detect the arrival of StartMessage (no more + // requirement). + let (trigger_message_1, cm_setup_1) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + // Taking an opportunity to test a setup via the macro for the simplest identification, + // by the TypeId. + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) + }; + // Case two + // This msg will not trigger as it is declared with a wrong TypeId of the supposed trigger + // msg. The supplied ID does not even belong to an Actor msg type. + let cm_setup_2 = { + let counter_msg_strayed = StartMessage {}; + let screwed_id = TypeId::of::(); + let id_method = MsgIdentification::ByType(screwed_id); + SingleTypeCounterMsgSetup::new( + screwed_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg_strayed, + respondent_addr.clone().recipient(), + ))], + ) + }; + // Case three + // This msg will not trigger as it is declared to have to be matched entirely (The message + // type, plus the data of the message). The expected msg and the actual sent msg bear + // different IP addresses. + let (trigger_msg_3_unmatching, cm_setup_3) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), + }; + let type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(4), + body: UiUnmarshalError { + message: "abc".to_string(), + bad_data: "456".to_string(), + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 6, 5, 4)), + }), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg, + respondent_addr.clone().recipient(), + ))], + ), + ) + }; + // Case four + // This msg will trigger as the performed msg is an exact match of the expected msg. + let (trigger_msg_4_matching, cm_setup_4, counter_msg_4) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + }; + let msg_type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(234), + body: UiLogBroadcast { + msg: "Good one".to_string(), + log_level: SerializableLogLevel::Error, + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(trigger_msg.clone()), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + msg_type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg.clone(), + respondent_addr.clone().recipient(), + ))], + ), + counter_msg, + ) + }; + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + // Supplying messages deliberately in a tangled manner to express that the mechanism is + // robust enough to compensate for it. + // This works because we don't supply overlapping setups, such as that could apply to + // a single trigger msg. + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![cm_setup_3, cm_setup_1, cm_setup_2, cm_setup_4], + }) + .unwrap(); + + subject_addr.try_send(trigger_message_1).unwrap(); + subject_addr + .try_send(trigger_msg_3_unmatching.clone()) + .unwrap(); + subject_addr + .try_send(trigger_msg_4_matching.clone()) + .unwrap(); + + system.run(); + // Actual counter-messages that flew in this test + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let _first_counter_msg_recorded = respondent_recording.get_record::(0); + let second_counter_msg_recorded = respondent_recording.get_record::(1); + assert_eq!(second_counter_msg_recorded, &counter_msg_4); + assert_eq!(respondent_recording.len(), 2); + // Recorded trigger messages + let subject_recording = subject_recording_arc.lock().unwrap(); + let _first_recorded_trigger_msg = subject_recording.get_record::(0); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_3_unmatching); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_4_matching); + assert_eq!(subject_recording.len(), 3) + } + + #[test] + fn counter_msgs_evaluate_lazily_so_the_msgs_with_the_same_triggers_are_eliminated_sequentially() + { + // This test demonstrates the need for caution in setups where multiple messages are sent + // at different times and should be responded to by different counter-messages. However, + // the trigger methods of these setups also apply to each other. Which setup gets + // triggered depends purely on the order used to supply them to the recorder + // in SetUpCounterMsgs. + + // Notice that three of the messages share the same data type, with one additional message + // serving a special purpose in assertions. Two of the three use only TypeId for + // identification. This already requires greater caution since you probably need the three + // messages to be dispatched in a specific sequence. However, this wasn't considered + // properly and, as you can see in the test, the trigger messages aren't sent in the same + // order as the counter-message setups were supplied. + + // This results in an inevitable mismatch. The first counter-message that was sent should + // have belonged to the second trigger message, but was triggered by the third trigger + // message (which actually introduces the test). Similarly, the second trigger message + // activates a message rightfully meant for the first trigger message. To complete + // the picture, even the first trigger message is matched with the third counter-message. + + // This shows how important it is to avoid ambiguous setups. When operating with multiple + // calls of the same typed message as triggers, it is highly recommended not to use + // MsgIdentification::ByTypeId but to use more specific, unmistakable settings instead: + // MsgIdentification::ByMatch or MsgIdentification::ByPredicate. + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( + ConfigChangeMsg, + ConfigChangeMsg, + ConfigChangeMsg + )); + let respondent_addr = respondent.start(); + // Case 1 + let (trigger_msg_1, cm_setup_1) = { + let trigger_msg = CrashNotification { + process_id: 7777777, + exit_code: None, + stderr: Some("blah".to_string()), + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateMinHops(Hops::SixHops), + }; + let id_method = MsgIdentification::ByPredicate { + predicate: Box::new(|msg_boxed| { + let msg = msg_boxed.downcast_ref::().unwrap(); + msg.process_id == 1010 + }), + }; + ( + trigger_msg, + // Taking an opportunity to test a setup via the macro allowing more specific + // identification methods. + setup_for_counter_msg_triggered_via_specific_msg_id_method!( + CrashNotification, + id_method, + counter_msg, + &respondent_addr + ), + ) + }; + // Case two + let (trigger_msg_2, cm_setup_2) = { + let trigger_msg = CrashNotification { + process_id: 1010, + exit_code: Some(11), + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdatePassword("betterPassword".to_string()), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) + }; + // Case three + let (trigger_msg_3, cm_setup_3) = { + let trigger_msg = CrashNotification { + process_id: 9999999, + exit_code: None, + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def"), + }), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) + }; + // Case four + let (trigger_msg_4, cm_setup_4) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) + }; + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + // Adding messages in standard order + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![cm_setup_1, cm_setup_2, cm_setup_3, cm_setup_4], + }) + .unwrap(); + + // Now the fun begins, the trigger messages are shuffled + subject_addr.try_send(trigger_msg_3.clone()).unwrap(); + // The fourth message demonstrates that the previous trigger didn't activate two messages + // at once, even though this trigger actually matches two different setups. This shows + // that each trigger can only be matched with one setup at a time, consuming it. If you + // want to trigger multiple messages in response, you must configure that setup with + // multiple counter-messages (a one-to-many scenario). + subject_addr.try_send(trigger_msg_4.clone()).unwrap(); + subject_addr.try_send(trigger_msg_2.clone()).unwrap(); + subject_addr.try_send(trigger_msg_1.clone()).unwrap(); + + system.run(); + // Actual counter-messages that flew in this test + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let first_counter_msg_recorded = respondent_recording.get_record::(0); + assert_eq!( + first_counter_msg_recorded.change, + ConfigChange::UpdatePassword("betterPassword".to_string()) + ); + let _ = respondent_recording.get_record::(1); + let third_counter_msg_recorded = respondent_recording.get_record::(2); + assert_eq!( + third_counter_msg_recorded.change, + ConfigChange::UpdateMinHops(Hops::SixHops) + ); + let fourth_counter_msg_recorded = respondent_recording.get_record::(3); + assert_eq!( + fourth_counter_msg_recorded.change, + ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def") + }) + ); + assert_eq!(respondent_recording.len(), 4); + // Recorded trigger messages + let subject_recording = subject_recording_arc.lock().unwrap(); + let first_recorded_trigger_msg = subject_recording.get_record::(0); + assert_eq!(first_recorded_trigger_msg, &trigger_msg_3); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_4); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_2); + let fourth_recorded_trigger_msg = subject_recording.get_record::(3); + assert_eq!(fourth_recorded_trigger_msg, &trigger_msg_1); + assert_eq!(subject_recording.len(), 4) + } } diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs new file mode 100644 index 000000000..ee56936f6 --- /dev/null +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -0,0 +1,172 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use crate::test_utils::recorder_stop_conditions::{ForcedMatchable, MsgIdentification}; +use actix::{Message, Recipient}; +use std::any::TypeId; +use std::cell::RefCell; +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +// Counter-messages are a powerful tool that allows you to actively simulate communication within +// a system. They enable sending either a single message or multiple messages in response to +// a specific trigger, which is just another Actor message arriving at the Recorder. +// By trigger, we mean the moment when an incoming message is tested sequentially against collected +// identification methods and matches. Each counter-message must have its ID method attached when +// it is being prepared for storage in the Recorder. This bundle is called a setup. Each setup has +// one ID method but can contain multiple counter-messages that are all sent when triggered. + +// Counter-messages can be independently customized and targeted at different actors by +// providing their addresses, supporting complex interaction patterns. This design facilitates +// sophisticated testing scenarios by mimicking real communication flows between multiple Actors. +// The actual preparation of the Recorder needs to be carried out somewhat specifically during the +// late stage of configuring the test, when all participating Actors are already started and their +// addresses are known. The setup for counter-messages must be registered with the appropriate +// Recorder using a specially designated Actor message SetUpCounterMsgs. + +// If a trigger message matches multiple counter-message setups, the triggered setup depends +// on the order in which setups are provided. Consider using MsgIdentification::ByMatch +// or MsgIdentification::ByPredicate instead of MsgIdentification::ByTypeId to avoid confusion +// about setup ordering. + +pub trait CounterMsgGear: Send { + fn try_send(&self); +} + +pub struct SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + msg_opt: RefCell>, + recipient: Recipient, +} + +impl CounterMsgGear for SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + fn try_send(&self) { + let msg = self.msg_opt.take().unwrap(); + self.recipient.try_send(msg).unwrap() + } +} + +impl SendableCounterMsgWithRecipient +where + Msg: Message + Send + 'static, + Msg::Result: Send, +{ + pub fn new(msg: Msg, recipient: Recipient) -> SendableCounterMsgWithRecipient { + Self { + msg_opt: RefCell::new(Some(msg)), + recipient, + } + } +} + +pub struct SingleTypeCounterMsgSetup { + // Leave them private + trigger_msg_type_id: TriggerMsgTypeId, + trigger_msg_id_method: MsgIdentification, + // Responding by multiple outbound messages to a single incoming (trigger) message is supported. + // (Imitates a message handler whose execution implies a couple of message dispatches) + msg_gears: Vec>, +} + +impl SingleTypeCounterMsgSetup { + pub fn new( + trigger_msg_type_id: TriggerMsgTypeId, + trigger_msg_id_method: MsgIdentification, + msg_gears: Vec>, + ) -> Self { + Self { + trigger_msg_type_id, + trigger_msg_id_method, + msg_gears, + } + } +} + +pub type TriggerMsgTypeId = TypeId; + +#[derive(Default)] +pub struct CounterMessages { + msgs: HashMap>, +} + +impl CounterMessages { + pub fn search_for_msg_gear( + &mut self, + trigger_msg: &Msg, + ) -> Option>> + where + Msg: ForcedMatchable + 'static, + { + let type_id = trigger_msg.trigger_msg_type_id(); + if let Some(msgs_vec) = self.msgs.get_mut(&type_id) { + msgs_vec + .iter_mut() + .position(|cm_setup| { + cm_setup + .trigger_msg_id_method + .resolve_condition(trigger_msg) + }) + .map(|idx| msgs_vec.remove(idx).msg_gears) + } else { + None + } + } + + pub fn add_msg(&mut self, counter_msg_setup: SingleTypeCounterMsgSetup) { + let type_id = counter_msg_setup.trigger_msg_type_id; + match self.msgs.entry(type_id) { + Entry::Occupied(mut existing_vec) => existing_vec.get_mut().push(counter_msg_setup), + Entry::Vacant(vacancy) => { + vacancy.insert(vec![counter_msg_setup]); + } + } + } +} + +// Note that you're not limited to triggering only one message at a time, but you can supply more +// messages to this macro, all triggered by the same type id. +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_type_id{ + ($trigger_msg_type: ty, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + + crate::setup_for_counter_msg_triggered_via_specific_msg_id_method!( + $trigger_msg_type, + MsgIdentification::ByType(TypeId::of::<$trigger_msg_type>()), + $($owned_counter_msg, $respondent_actor_addr_ref),+ + ) + }; +} + +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_specific_msg_id_method{ + ($trigger_msg_type: ty, $msg_id_method: expr, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + // This macro returns a block of operations. That's why it begins with these curly brackets + { + let msg_gears: Vec< + Box + > = vec![ + // This part can be repeated as long as there are more expression pairs suplied + $(Box::new( + crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient::new( + $owned_counter_msg, + $respondent_actor_addr_ref.clone().recipient() + ) + )),+ + ]; + + SingleTypeCounterMsgSetup::new( + TypeId::of::<$trigger_msg_type>(), + $msg_id_method, + msg_gears + ) + } + }; +} diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index b3dca287d..f10e0e4a6 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::accountant::DetailedScanType; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; - use masq_lib::messages::ScanType; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; 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,19 +246,19 @@ 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 + 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(), }; @@ -249,12 +276,12 @@ mod tests { #[test] fn match_any_works_with_every_matching_condition_and_no_need_to_take_elements_out() { let mut cond_set = StopConditions::Any(vec![ - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnMatch { + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }, ]); - let first_msg = ScanForPayables { + let first_msg = ScanForNewPayables { response_skeleton_opt: None, }; let second_msg = StartMessage {}; @@ -265,11 +292,16 @@ mod tests { }; let inspect_len_of_any = |cond_set: &StopConditions, msg_number: usize| match cond_set { StopConditions::Any(conditions) => conditions.len(), - StopConditions::All(_) => panic!("stage {}: expected Any but got All", msg_number), + StopConditions::AllGreedily(_) => { + panic!("stage {}: expected Any but got AllGreedily", msg_number) + } + StopConditions::AllLazily(_) => { + panic!("stage {}: expected Any but got AllLazily", msg_number) + } }; assert_eq!( - cond_set.resolve_stop_conditions::(&first_msg), + cond_set.resolve_stop_conditions::(&first_msg), false ); let len_after_stage_1 = inspect_len_of_any(&cond_set, 1); @@ -289,9 +321,9 @@ mod tests { } #[test] - fn match_all_with_conditions_gradually_eliminated_until_vector_is_emptied_and_it_is_match() { - let mut cond_set = StopConditions::All(vec![ - StopCondition::StopOnPredicate { + fn match_all_with_conditions_gradually_eliminated_greedily_until_empty() { + let mut cond_set = StopConditions::AllGreedily(vec![ + MsgIdentification::ByPredicate { predicate: Box::new(|msg| { if let Some(ip_msg) = msg.downcast_ref::() { ip_msg.new_ip.is_ipv4() @@ -300,34 +332,31 @@ mod tests { } }), }, - StopCondition::StopOnMatch { - exemplar: Box::new(ScanForPayables { + MsgIdentification::ByMatch { + exemplar: Box::new(ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, }), }), }, - StopCondition::StopOnType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), ]); - let tested_msg_1 = ScanForPayables { + let tested_msg_1 = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, }), }; - let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); assert_eq!(kill_system, false); - match &cond_set { - StopConditions::All(conds) => { - assert_eq!(conds.len(), 2); - assert!(matches!(conds[0], StopCondition::StopOnPredicate { .. })); - assert!(matches!(conds[1], StopCondition::StopOnType(_))); - } - StopConditions::Any(_) => panic!("Stage 1: expected StopConditions::All, not ...Any"), - } + assert_state_after_greedily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); let tested_msg_2 = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 4, 1)), }; @@ -335,11 +364,109 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); assert_eq!(kill_system, true); + assert_state_after_greedily_matched(2, &cond_set, |conds| assert!(conds.is_empty())) + } + + fn assert_state_after_greedily_matched( + stage: usize, + cond_set: &StopConditions, + apply_assertions: fn(&[MsgIdentification]), + ) { match cond_set { - StopConditions::All(conds) => { - assert!(conds.is_empty()) + StopConditions::AllGreedily(conds) => apply_assertions(conds), + StopConditions::Any(_) => { + panic!("Stage {stage}: expected StopConditions::AllGreedily, not Any") + } + StopConditions::AllLazily(_) => { + panic!("Stage {stage}: expected StopConditions::AllGreedily, not AllLazily") + } + } + } + + #[test] + fn match_all_with_conditions_gradually_eliminated_lazily_until_empty() { + let mut cond_set = StopConditions::AllLazily(vec![ + MsgIdentification::ByPredicate { + predicate: Box::new(|msg| { + if let Some(ip_msg) = msg.downcast_ref::() { + ip_msg.new_ip.is_ipv6() + } else { + false + } + }), + }, + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + ]); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage one + let tested_msg_1 = ScanForNewPayables { + response_skeleton_opt: None, + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 3); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + assert!(matches!(conds[2], MsgIdentification::ByType(_))); + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage two + let tested_msg_2 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(6, 7, 8, 9)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(2, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage three + let tested_msg_3 = NewPublicIp { + new_ip: IpAddr::V6(Ipv6Addr::new(1, 2, 4, 1, 4, 3, 2, 1)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_3); + + assert_eq!(kill_system, false); + assert_state_after_lazily_matched(3, &cond_set, |conds| { + assert_eq!(conds.len(), 1); + assert!(matches!(conds[0], MsgIdentification::ByType(_))) + }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage four + let tested_msg_4 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(45, 45, 45, 45)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_4); + + assert_eq!(kill_system, true); + assert_state_after_lazily_matched(4, &cond_set, |conds| { + assert!(conds.is_empty()); + }); + } + + fn assert_state_after_lazily_matched( + stage: usize, + cond_set: &StopConditions, + apply_assertions: fn(&[MsgIdentification]), + ) { + match &cond_set { + StopConditions::AllLazily(conds) => apply_assertions(conds), + StopConditions::Any(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not Any") + } + StopConditions::AllGreedily(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not AllGreedily") } - StopConditions::Any(_) => panic!("Stage 2: expected StopConditions::All, not ...Any"), } } } diff --git a/node/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") + } +} 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"); 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."