diff --git a/USER-INTERFACE-INTERFACE.md b/USER-INTERFACE-INTERFACE.md index 443abc65a6..3a5cacffa2 100644 --- a/USER-INTERFACE-INTERFACE.md +++ b/USER-INTERFACE-INTERFACE.md @@ -358,8 +358,8 @@ Another reason the secrets might be missing is that there are not yet any secret "exitServiceRate: " }, "scanIntervals": { - "pendingPayableSec": , "payableSec": , + "pendingPayableSec": , "receivableSec": }, } @@ -453,20 +453,21 @@ database password. If you want to know whether the password you have is the corr * `scanIntervals`: These three intervals describe the length of three different scan cycles running automatically in the background since the Node has connected to a qualified neighborhood that consists of neighbors enabling a complete - 3-hop route. Each parameter can be set independently, but by default are all the same which currently is most desirable - for the consistency of service payments to and from your Node. Technically, there doesn't have to be any lower limit - for the minimum of time you can set; two scans of the same sort would never run at the same time but the next one is + 3-hop route. Each parameter can be set independently. Technically, there doesn't have to be any lower limit for +* the minimum of time you can set; two scans of the same sort would never run at the same time but the next one is always scheduled not earlier than the end of the previous one. These are ever present values, no matter if the user's set any value, because defaults are prepared. -* `pendingPayableSec`: Amount of seconds between two sequential cycles of scanning for payments that are marked as currently - pending; the payments were sent to pay our debts, the payable. The purpose of this process is to confirm the status of - the pending payment; either the payment transaction was written on blockchain as successful or failed. - -* `payableSec`: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts of that meet +* `payableSec`: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question. +* `pendingPayableSec`: The time elapsed since the last payable transaction was processed. This scan operates + on an irregular schedule and is triggered after new transactions are sent or when failed transactions need + to be replaced. The scanner monitors pending transactions and verifies their blockchain status, determining whether + each payment was successfully recorded or failed. Any failed transaction is automatically resubmitted as soon + as the failure is detected. + * `receivableSec`: Amount of seconds between two sequential cycles of scanning for payments on the blockchain that have been sent by our creditors to us, which are credited against receivables recorded for services provided. diff --git a/masq_lib/src/blockchains/blockchain_records.rs b/masq_lib/src/blockchains/blockchain_records.rs index 00ac1bb664..821b0d7f17 100644 --- a/masq_lib/src/blockchains/blockchain_records.rs +++ b/masq_lib/src/blockchains/blockchain_records.rs @@ -4,7 +4,9 @@ use crate::blockchains::chains::Chain; use crate::constants::{ BASE_GAS_PRICE_CEILING_WEI, BASE_MAINNET_CHAIN_ID, BASE_MAINNET_CONTRACT_CREATION_BLOCK, BASE_MAINNET_FULL_IDENTIFIER, BASE_SEPOLIA_CHAIN_ID, BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, - BASE_SEPOLIA_FULL_IDENTIFIER, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, + BASE_SEPOLIA_FULL_IDENTIFIER, DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, DEV_GAS_PRICE_CEILING_WEI, ETH_GAS_PRICE_CEILING_WEI, ETH_MAINNET_CHAIN_ID, ETH_MAINNET_CONTRACT_CREATION_BLOCK, ETH_MAINNET_FULL_IDENTIFIER, ETH_ROPSTEN_CHAIN_ID, ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, ETH_ROPSTEN_FULL_IDENTIFIER, @@ -15,14 +17,13 @@ use crate::constants::{ }; use ethereum_types::{Address, H160}; -// TODO these should probably be a static (it's a shame that we construct the data every time anew -// when we ask for the chain specs), and dynamic initialization should be allowed as well -pub const CHAINS: [BlockchainRecord; 7] = [ +pub static CHAINS: [BlockchainRecord; 7] = [ BlockchainRecord { self_id: Chain::PolyMainnet, num_chain_id: POLYGON_MAINNET_CHAIN_ID, literal_identifier: POLYGON_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -31,6 +32,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: ETH_MAINNET_CHAIN_ID, literal_identifier: ETH_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -39,6 +41,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: BASE_MAINNET_CHAIN_ID, literal_identifier: BASE_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -47,6 +50,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: BASE_SEPOLIA_CHAIN_ID, literal_identifier: BASE_SEPOLIA_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, }, @@ -55,6 +59,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: POLYGON_AMOY_CHAIN_ID, literal_identifier: POLYGON_AMOY_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, }, @@ -63,6 +68,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: ETH_ROPSTEN_CHAIN_ID, literal_identifier: ETH_ROPSTEN_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, }, @@ -71,6 +77,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: DEV_CHAIN_ID, literal_identifier: DEV_CHAIN_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: DEV_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, }, @@ -82,6 +89,7 @@ pub struct BlockchainRecord { pub num_chain_id: u64, pub literal_identifier: &'static str, pub gas_price_safe_ceiling_minor: u128, + pub default_pending_payable_interval_sec: u64, pub contract: Address, pub contract_creation_block: u64, } @@ -128,7 +136,11 @@ const POLYGON_MAINNET_CONTRACT_ADDRESS: Address = H160([ mod tests { use super::*; use crate::blockchains::chains::chain_from_chain_identifier_opt; - use crate::constants::{BASE_MAINNET_CONTRACT_CREATION_BLOCK, WEIS_IN_GWEI}; + use crate::constants::{ + BASE_MAINNET_CONTRACT_CREATION_BLOCK, DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, WEIS_IN_GWEI, + }; use std::collections::HashSet; use std::iter::FromIterator; @@ -209,6 +221,7 @@ mod tests { self_id: examined_chain, literal_identifier: "eth-mainnet", gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -226,6 +239,7 @@ mod tests { self_id: examined_chain, literal_identifier: "eth-ropsten", gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, } @@ -243,6 +257,7 @@ mod tests { self_id: examined_chain, literal_identifier: "polygon-mainnet", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -260,6 +275,7 @@ mod tests { self_id: examined_chain, literal_identifier: "polygon-amoy", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, } @@ -277,6 +293,7 @@ mod tests { self_id: examined_chain, literal_identifier: "base-mainnet", gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -294,6 +311,7 @@ mod tests { self_id: examined_chain, literal_identifier: "base-sepolia", gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, } @@ -311,6 +329,7 @@ mod tests { self_id: examined_chain, literal_identifier: "dev", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, } diff --git a/masq_lib/src/blockchains/chains.rs b/masq_lib/src/blockchains/chains.rs index b7061d8990..cedde32d11 100644 --- a/masq_lib/src/blockchains/chains.rs +++ b/masq_lib/src/blockchains/chains.rs @@ -142,6 +142,7 @@ mod tests { self_id: Chain::PolyMainnet, literal_identifier: "", gas_price_safe_ceiling_minor: 0, + default_pending_payable_interval_sec: 0, contract: Default::default(), contract_creation_block: 0, } diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index 67338f5a30..fcbec68263 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -18,24 +18,16 @@ pub const MASQ_URL_PREFIX: &str = "masq://"; pub const CURRENT_LOGFILE_NAME: &str = "MASQNode_rCURRENT.log"; pub const MASQ_PROMPT: &str = "masq> "; -pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really -pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; - pub const WALLET_ADDRESS_LENGTH: usize = 42; -pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; pub const WEIS_IN_GWEI: i128 = 1_000_000_000; -pub const DEFAULT_MAX_BLOCK_COUNT: u64 = 100_000; +pub const COMBINED_PARAMETERS_DELIMITER: char = '|'; pub const PAYLOAD_ZERO_SIZE: usize = 0usize; -pub const ETH_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 11_170_708; -pub const ETH_ROPSTEN_CONTRACT_CREATION_BLOCK: u64 = 8_688_171; -pub const POLYGON_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 14_863_650; -pub const POLYGON_AMOY_CONTRACT_CREATION_BLOCK: u64 = 5_323_366; -pub const BASE_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 19_711_235; -pub const BASE_SEPOLIA_CONTRACT_CREATION_BLOCK: u64 = 14_732_730; -pub const MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK: u64 = 0; +//descriptor +pub const CENTRAL_DELIMITER: char = '@'; +pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; //Migration versions //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -88,11 +80,11 @@ pub const VALUE_EXCEEDS_ALLOWED_LIMIT: u64 = ACCOUNTANT_PREFIX | 3; //////////////////////////////////////////////////////////////////////////////////////////////////// -pub const COMBINED_PARAMETERS_DELIMITER: char = '|'; +pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; -//descriptor -pub const CENTRAL_DELIMITER: char = '@'; -pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; +pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really +pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; +pub const DEFAULT_MAX_BLOCK_COUNT: u64 = 100_000; //chains pub const POLYGON_MAINNET_CHAIN_ID: u64 = 137; @@ -114,11 +106,25 @@ pub const ETH_ROPSTEN_FULL_IDENTIFIER: &str = concatcp!(ETH_FAMILY, LINK, "ropst pub const BASE_MAINNET_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, MAINNET); pub const BASE_SEPOLIA_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, "sepolia"); pub const DEV_CHAIN_FULL_IDENTIFIER: &str = "dev"; + +pub const ETH_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 11_170_708; +pub const ETH_ROPSTEN_CONTRACT_CREATION_BLOCK: u64 = 8_688_171; +pub const POLYGON_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 14_863_650; +pub const POLYGON_AMOY_CONTRACT_CREATION_BLOCK: u64 = 5_323_366; +pub const BASE_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 19_711_235; +pub const BASE_SEPOLIA_CONTRACT_CREATION_BLOCK: u64 = 14_732_730; +pub const MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK: u64 = 0; + pub const POLYGON_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; pub const ETH_GAS_PRICE_CEILING_WEI: u128 = 100_000_000_000; pub const BASE_GAS_PRICE_CEILING_WEI: u128 = 50_000_000_000; pub const DEV_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC: u64 = 600; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC: u64 = 120; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC: u64 = 180; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC: u64 = 120; + #[cfg(test)] mod tests { use super::*; @@ -204,6 +210,10 @@ mod tests { assert_eq!(ETH_GAS_PRICE_CEILING_WEI, 100_000_000_000); assert_eq!(BASE_GAS_PRICE_CEILING_WEI, 50_000_000_000); assert_eq!(DEV_GAS_PRICE_CEILING_WEI, 200_000_000_000); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, 600); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, 120); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, 180); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, 120); assert_eq!( CLIENT_REQUEST_PAYLOAD_CURRENT_VERSION, DataVersion { major: 0, minor: 1 } diff --git a/masq_lib/src/shared_schema.rs b/masq_lib/src/shared_schema.rs index 11dfb865fc..3a8b194da2 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/utils.rs b/masq_lib/src/utils.rs index 8d563ef374..ad4197aad4 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,50 @@ 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 + } + }; +} + +#[macro_export(local_inner_macros)] +macro_rules! btreeset { + () => { + ::std::collections::BTreeSet::new() + }; + ($($val:expr,)+) => { + btreeset!($($val),+) + }; + ($($value:expr),+) => { + { + let mut _bts = ::std::collections::BTreeSet::new(); + $( + let _ = _bts.insert($value); + )* + _bts + } + }; +} + #[cfg(test)] mod tests { use super::*; - use std::collections::HashMap; + use itertools::Itertools; + use std::collections::BTreeSet; + 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 +873,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 +887,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 +902,119 @@ 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); + } + + #[test] + fn btreeset_macro_works() { + let empty_btreeset: BTreeSet = btreeset!(); + let btreeset_with_one_element = btreeset!(2); + let btreeset_with_multiple_elements = btreeset!(2, 20, 42); + let btreeset_with_trailing_comma = btreeset!(2, 20,); + let btreeset_of_string = btreeset!("val_a", "val_b"); + let btreeset_with_duplicate = btreeset!(2, 2); + + let expected_empty_btreeset: BTreeSet = BTreeSet::new(); + let mut expected_btreeset_with_one_element = BTreeSet::new(); + expected_btreeset_with_one_element.insert(2); + let mut expected_btreeset_with_multiple_elements = BTreeSet::new(); + expected_btreeset_with_multiple_elements.insert(2); + expected_btreeset_with_multiple_elements.insert(20); + expected_btreeset_with_multiple_elements.insert(42); + let mut expected_btreeset_with_trailing_comma = BTreeSet::new(); + expected_btreeset_with_trailing_comma.insert(2); + expected_btreeset_with_trailing_comma.insert(20); + let mut expected_btreeset_of_string = BTreeSet::new(); + expected_btreeset_of_string.insert("val_a"); + expected_btreeset_of_string.insert("val_b"); + let mut expected_btreeset_with_duplicate = BTreeSet::new(); + expected_btreeset_with_duplicate.insert(2); + assert_eq!(empty_btreeset, expected_empty_btreeset); + assert_eq!( + btreeset_with_one_element, + expected_btreeset_with_one_element + ); + assert_eq!( + btreeset_with_multiple_elements, + expected_btreeset_with_multiple_elements + ); + assert_eq!( + btreeset_with_trailing_comma, + expected_btreeset_with_trailing_comma + ); + assert_eq!(btreeset_of_string, expected_btreeset_of_string); + assert_eq!(btreeset_with_duplicate, expected_btreeset_with_duplicate); } } diff --git a/multinode_integration_tests/tests/bookkeeping_test.rs b/multinode_integration_tests/tests/bookkeeping_test.rs index dd5e2bca41..ea5c8ae907 100644 --- a/multinode_integration_tests/tests/bookkeeping_test.rs +++ b/multinode_integration_tests/tests/bookkeeping_test.rs @@ -81,7 +81,7 @@ fn provided_and_consumed_services_are_recorded_in_databases() { fn retrieve_payables(node: &MASQRealNode) -> Vec { let payable_dao = payable_dao(node.name()); - payable_dao.retrieve_payables() + payable_dao.retrieve_payables(None) } fn receivables(node: &MASQRealNode) -> Vec { diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index 0202609a08..9b369e192e 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -225,7 +225,7 @@ fn verify_bill_payment() { } let now = Instant::now(); - while !consuming_payable_dao.retrieve_payables().is_empty() + while !consuming_payable_dao.retrieve_payables(None).is_empty() && now.elapsed() < Duration::from_secs(10) { thread::sleep(Duration::from_millis(400)); @@ -400,7 +400,7 @@ fn verify_pending_payables() { ); let now = Instant::now(); - while !consuming_payable_dao.retrieve_payables().is_empty() + while !consuming_payable_dao.retrieve_payables(None).is_empty() && now.elapsed() < Duration::from_secs(10) { thread::sleep(Duration::from_millis(400)); @@ -437,7 +437,7 @@ fn verify_pending_payables() { .tmb(0), ); - assert!(consuming_payable_dao.retrieve_payables().is_empty()); + assert!(consuming_payable_dao.retrieve_payables(None).is_empty()); MASQNodeUtils::assert_node_wrote_log_containing( real_consuming_node.name(), "Found 3 pending payables to process", diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index e2deb6125c..7d4644ffa6 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -1,17 +1,16 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_access_objects::sent_payable_dao::Tx; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_access_objects::utils::{ sql_values_of_failed_tx, DaoFactoryReal, TxHash, TxIdentifiers, VigilantRusqliteFlatten, }; use crate::accountant::db_access_objects::Transaction; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, join_with_separator}; +use crate::accountant::{join_with_commas, join_with_separator}; use crate::blockchain::errors::rpc_errors::{AppRpcError, AppRpcErrorKind}; -use crate::blockchain::errors::validation_status::PreviousAttempts; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; -use std::cmp::Ordering; use std::collections::{BTreeSet, HashMap}; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -27,9 +26,9 @@ pub enum FailedPayableDaoError { SqlExecutionFailed(String), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum FailureReason { - Submission(AppRpcError), + Submission(AppRpcErrorKind), Reverted, PendingTooLong, } @@ -52,7 +51,7 @@ impl FromStr for FailureReason { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] pub enum FailureStatus { RetryRequired, RecheckRequired(ValidationStatus), @@ -76,19 +75,13 @@ impl FromStr for FailureStatus { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] -pub enum ValidationStatus { - Waiting, - Reattempting(PreviousAttempts), -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct FailedTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub reason: FailureReason, pub status: FailureStatus, @@ -104,7 +97,7 @@ impl Transaction for FailedTx { } fn amount(&self) -> u128 { - self.amount + self.amount_minor } fn timestamp(&self) -> i64 { @@ -112,7 +105,7 @@ impl Transaction for FailedTx { } fn gas_price_wei(&self) -> u128 { - self.gas_price_wei + self.gas_price_minor } fn nonce(&self) -> u64 { @@ -124,34 +117,18 @@ impl Transaction for FailedTx { } } -// PartialOrd and Ord are used to create BTreeSet -impl PartialOrd for FailedTx { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for FailedTx { - fn cmp(&self, other: &Self) -> Ordering { - // Descending Order - other - .timestamp - .cmp(&self.timestamp) - .then_with(|| other.nonce.cmp(&self.nonce)) - .then_with(|| other.amount.cmp(&self.amount)) - } -} - -impl From<(&Tx, &Web3Error)> for FailedTx { - fn from((sent_tx, error): (&Tx, &Web3Error)) -> Self { +impl From<(&SentTx, &Web3Error)> for FailedTx { + fn from((sent_tx, error): (&SentTx, &Web3Error)) -> Self { + let app_rpc_error = AppRpcError::from(error.clone()); + let error_kind = AppRpcErrorKind::from(&app_rpc_error); Self { hash: sent_tx.hash, receiver_address: sent_tx.receiver_address, - amount: sent_tx.amount, + amount_minor: sent_tx.amount_minor, timestamp: sent_tx.timestamp, - gas_price_wei: sent_tx.gas_price_wei, + gas_price_minor: sent_tx.gas_price_minor, nonce: sent_tx.nonce, - reason: FailureReason::Submission(error.clone().into()), + reason: FailureReason::Submission(error_kind), status: FailureStatus::RetryRequired, } } @@ -159,13 +136,22 @@ impl From<(&Tx, &Web3Error)> for FailedTx { #[derive(Debug, Clone, PartialEq, Eq)] pub enum FailureRetrieveCondition { + ByTxHash(Vec), ByStatus(FailureStatus), ByReceiverAddresses(BTreeSet
), + 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 ({})", + join_with_commas(hashes, |hash| format!("'{:?}'", hash)) + ) + } FailureRetrieveCondition::ByStatus(status) => { write!(f, "WHERE status = '{}'", status) } @@ -173,21 +159,26 @@ impl Display for FailureRetrieveCondition { write!( f, "WHERE receiver_address IN ({})", - join_with_separator(addresses, |address| format!("'{:?}'", address), ", ") + join_with_commas(addresses, |address| format!("'{:?}'", address)) ) } + FailureRetrieveCondition::EveryRecheckRequiredRecord => { + write!(f, "WHERE status LIKE 'RecheckRequired%'") + } } } } pub trait FailedPayableDao { fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers; + //TODO potentially atomically fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), FailedPayableDaoError>; fn retrieve_txs(&self, condition: Option) -> BTreeSet; fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &BTreeSet) -> Result<(), FailedPayableDaoError>; } @@ -206,7 +197,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers { let sql = format!( "SELECT tx_hash, rowid FROM failed_payable WHERE tx_hash IN ({})", - join_with_separator(hashes, |hash| format!("'{:?}'", hash), ", ") + join_with_commas(hashes, |hash| format!("'{:?}'", hash)) ); let mut stmt = self @@ -260,7 +251,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { reason, \ status ) VALUES {}", - join_with_separator(txs, |tx| sql_values_of_failed_tx(tx), ", ") + join_with_commas(txs, |tx| sql_values_of_failed_tx(tx)) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -310,11 +301,11 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { Address::from_str(&receiver_address_str[2..]).expect("Failed to parse Address"); let amount_high_b = row.get(2).expectv("amount_high_b"); let amount_low_b = row.get(3).expectv("amount_low_b"); - let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let amount_minor = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; let timestamp = row.get(4).expectv("timestamp"); let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); - let gas_price_wei = + let gas_price_minor = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); let reason_str: String = row.get(8).expectv("reason"); @@ -327,9 +318,9 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { Ok(FailedTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, reason, status, @@ -342,19 +333,18 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { if status_updates.is_empty() { return Err(FailedPayableDaoError::EmptyInput); } let case_statements = join_with_separator( - &status_updates, + status_updates, |(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status), " ", ); - let tx_hashes = - join_with_separator(status_updates.keys(), |hash| format!("'{:?}'", hash), ", "); + let tx_hashes = join_with_commas(status_updates.keys(), |hash| format!("'{:?}'", hash)); let sql = format!( "UPDATE failed_payable \ @@ -388,7 +378,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { let sql = format!( "DELETE FROM failed_payable WHERE tx_hash IN ({})", - join_with_separator(hashes, |hash| { format!("'{:?}'", hash) }, ", ") + join_with_commas(hashes, |hash| { format!("'{:?}'", hash) }) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -430,20 +420,20 @@ mod tests { }; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailedTx, FailureReason, - FailureRetrieveCondition, FailureStatus, ValidationStatus, + FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::test_utils::{ make_read_only_db_connection, FailedTxBuilder, }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; use crate::accountant::db_access_objects::Transaction; - use crate::blockchain::errors::rpc_errors::LocalError::Decoder; - use crate::blockchain::errors::rpc_errors::{AppRpcError, AppRpcErrorKind}; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClockReal, + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, }; use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::test_utils::{make_address, make_tx_hash, ValidationFailureClockMock}; + use crate::blockchain::test_utils::{make_address, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; @@ -530,13 +520,13 @@ mod tests { {FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1719990000, gas_price_wei: 0, \ - nonce: 2, reason: PendingTooLong, status: RecheckRequired(Waiting) }, \ + amount_minor: 0, timestamp: 1719990000, gas_price_minor: 0, \ + nonce: 1, reason: PendingTooLong, status: RetryRequired }, \ FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1719990000, gas_price_wei: 0, \ - nonce: 1, reason: PendingTooLong, status: RetryRequired }}" + amount_minor: 0, timestamp: 1719990000, gas_price_minor: 0, \ + nonce: 2, reason: PendingTooLong, status: RecheckRequired(Waiting) }}" .to_string() )) ); @@ -646,18 +636,40 @@ mod tests { let result = subject.get_tx_identifiers(&hashset); - assert_eq!(result.get(&present_hash), Some(&2u64)); + assert_eq!(result.get(&present_hash), Some(&1u64)); assert_eq!(result.get(&absent_hash), None); - assert_eq!(result.get(&another_present_hash), Some(&1u64)); + 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":"am i alive?"}}}"#) - .unwrap(), - FailureReason::Submission(AppRpcError::Local(Decoder("am i alive?".to_string()))) + FailureReason::from_str(r#"{"Submission":{"Local":"Decoder"}}"#).unwrap(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Decoder)) ); // Reverted @@ -705,8 +717,8 @@ mod tests { ); assert_eq!( - FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"AppRpc":{"Unreachable":{"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}}}}}"#).unwrap(), - FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Unreachable), &validation_failure_clock))) + FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":[{"error":{"AppRpc":{"Remote":"Unreachable"}},"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}]}}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &validation_failure_clock))) ); assert_eq!( @@ -717,9 +729,8 @@ mod tests { // Invalid Variant assert_eq!( FailureStatus::from_str("\"UnknownStatus\"").unwrap_err(), - "unknown variant `UnknownStatus`, \ - expected one of `RetryRequired`, `RecheckRequired`, `Concluded` \ - at line 1 column 15 in '\"UnknownStatus\"'" + "unknown variant `UnknownStatus`, expected one of `RetryRequired`, `RecheckRequired`, \ + `Concluded` at line 1 column 15 in '\"UnknownStatus\"'" ); // Invalid Input @@ -738,7 +749,7 @@ mod tests { assert_eq!( FailureRetrieveCondition::ByReceiverAddresses(BTreeSet::from([make_address(1), make_address(2)])) .to_string(), - "WHERE receiver_address IN ('0x0000000000000000000000000000000000000001', '0x0000000000000000000000000000000000000002')" + "WHERE receiver_address IN ('0x0000000000000000000003000000000003000000', '0x0000000000000000000006000000000006000000')" ) } @@ -817,7 +828,9 @@ mod tests { .reason(PendingTooLong) .status(RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Unreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -936,19 +949,25 @@ mod tests { 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::Unreachable), - &ValidationFailureClockReal::default(), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &clock, ))), ), (tx3.hash, Concluded), ]); - let result = subject.update_statuses(hashmap); + let result = subject.update_statuses(&hashmap); let updated_txs = subject.retrieve_txs(None); let find_tx = |tx_hash| updated_txs.iter().find(|tx| tx.hash == tx_hash).unwrap(); let updated_tx1 = find_tx(hash1); @@ -962,8 +981,8 @@ mod tests { assert_eq!( updated_tx2.status, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Unreachable), - &ValidationFailureClockReal::default() + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &clock ))) ); assert_eq!(tx3.status, RetryRequired); @@ -973,6 +992,7 @@ mod tests { updated_tx4.status, RecheckRequired(ValidationStatus::Waiting) ); + assert_eq!(updated_txs.len(), 4); } #[test] @@ -986,7 +1006,7 @@ mod tests { .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let result = subject.update_statuses(HashMap::new()); + let result = subject.update_statuses(&HashMap::new()); assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); } @@ -1000,7 +1020,7 @@ mod tests { let wrapped_conn = make_read_only_db_connection(home_dir); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.update_statuses(HashMap::from([(make_tx_hash(1), Concluded)])); + let result = subject.update_statuses(&HashMap::from([(make_tx_hash(1), Concluded)])); assert_eq!( result, @@ -1131,43 +1151,6 @@ mod tests { ) } - #[test] - fn failed_tx_ordering_in_btree_set_works() { - let tx1 = FailedTxBuilder::default() - .hash(make_tx_hash(1)) - .timestamp(1000) - .nonce(1) - .amount(100) - .build(); - let tx2 = FailedTxBuilder::default() - .hash(make_tx_hash(2)) - .timestamp(1000) - .nonce(1) - .amount(200) - .build(); - let tx3 = FailedTxBuilder::default() - .hash(make_tx_hash(3)) - .timestamp(1000) - .nonce(2) - .amount(100) - .build(); - let tx4 = FailedTxBuilder::default() - .hash(make_tx_hash(4)) - .timestamp(2000) - .nonce(3) - .amount(100) - .build(); - - let mut set = BTreeSet::new(); - set.insert(tx1.clone()); - set.insert(tx2.clone()); - set.insert(tx3.clone()); - set.insert(tx4.clone()); - - let expected_order = vec![tx4, tx3, tx2, tx1]; - assert_eq!(set.into_iter().collect::>(), expected_order); - } - #[test] fn transaction_trait_methods_for_failed_tx() { let hash = make_tx_hash(1); @@ -1182,9 +1165,9 @@ mod tests { let failed_tx = FailedTx { hash, receiver_address, - amount, + amount_minor: amount, timestamp, - gas_price_wei, + gas_price_minor: gas_price_wei, nonce, reason, status, diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index 3c4c007b0f..a8c8e225ee 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -6,8 +6,8 @@ use web3::types::Address; 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; pub 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 011096c760..f9a723f549 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -1,32 +1,32 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use std::collections::{BTreeSet}; -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::failed_payable_dao::FailedTx; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_access_objects::utils; -use crate::accountant::db_access_objects::utils::{from_unix_timestamp, sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten}; -use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::{ - compose_case_expression, execute_command, serialize_wallets, +use crate::accountant::db_access_objects::utils::{ + from_unix_timestamp, sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, + CustomQuery, DaoFactoryReal, RangeStmConfig, RowId, TopStmConfig, VigilantRusqliteFlatten, }; -use crate::accountant::{checked_conversion, join_with_separator, sign_conversion, PendingPayableId}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +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, join_with_commas, sign_conversion, PendingPayableId}; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; +use ethabi::Address; #[cfg(test)] -use ethereum_types::{BigEndianHash, U256}; +use ethereum_types::{BigEndianHash, H256, U256}; +use itertools::Either; use masq_lib::utils::ExpectValue; #[cfg(test)] use rusqlite::OptionalExtension; use rusqlite::{Error, Row}; +use std::collections::BTreeSet; use std::fmt::{Debug, Display, Formatter}; -use std::str::FromStr; use std::time::SystemTime; -use itertools::Either; -use web3::types::{Address, H256}; -use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; #[derive(Debug, PartialEq, Eq)] pub enum PayableDaoError { @@ -46,7 +46,7 @@ impl From<&FailedTx> for PayableAccount { fn from(failed_tx: &FailedTx) -> Self { PayableAccount { wallet: Wallet::from(failed_tx.receiver_address), - balance_wei: failed_tx.amount, + balance_wei: failed_tx.amount_minor, last_paid_timestamp: from_unix_timestamp(failed_tx.timestamp), pending_payable_opt: None, } @@ -64,7 +64,7 @@ impl Display for PayableRetrieveCondition { PayableRetrieveCondition::ByAddresses(addresses) => write!( f, "AND wallet_address IN ({})", - join_with_separator(addresses, |hash| format!("'{:?}'", hash), ", ") + join_with_commas(addresses, |hash| format!("'{:?}'", hash)) ), } } @@ -75,18 +75,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 retrieve_payables( &self, @@ -111,6 +108,11 @@ impl PayableDaoFactory for DaoFactoryReal { } } +pub struct MarkPendingPayableID { + pub receiver_wallet: Address, + pub rowid: RowId, +} + #[derive(Debug)] pub struct PayableDaoReal { conn: Box, @@ -122,7 +124,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 \ @@ -135,7 +137,7 @@ impl PayableDao for PayableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( @@ -153,46 +155,42 @@ impl PayableDao for PayableDaoReal { fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], + _mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError> { - if wallets_and_rowids.is_empty() { - panic!("broken code: empty input is not permit to enter this method") - } - - let case_expr = compose_case_expression(wallets_and_rowids); - let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); - //the Wallet type is secure against SQL injections - let sql = format!( - "update payable set \ - pending_payable_rowid = {} \ - where - pending_payable_rowid is null and wallet_address in ({}) - returning - pending_payable_rowid", - case_expr, wallets, - ); - execute_command(&*self.conn, wallets_and_rowids, &sql) + todo!("Will be an object of removal in GH-662") + // if wallets_and_rowids.is_empty() { + // panic!("broken code: empty input is not permit to enter this method") + // } + // + // let case_expr = compose_case_expression(wallets_and_rowids); + // let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); + // //the Wallet type is secure against SQL injections + // let sql = format!( + // "update payable set \ + // pending_payable_rowid = {} \ + // where + // pending_payable_rowid is null and wallet_address in ({}) + // returning + // pending_payable_rowid", + // case_expr, wallets, + // ); + // execute_command(&*self.conn, wallets_and_rowids, &sql) } - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], - ) -> Result<(), PayableDaoError> { - confirmed_payables.iter().try_for_each(|pending_payable_fingerprint| { - + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError> { + confirmed_payables.iter().try_for_each(|confirmed_payable| { let main_sql = "update payable set \ balance_high_b = balance_high_b + :balance_high_b, balance_low_b = balance_low_b + :balance_low_b, \ - last_paid_timestamp = :last_paid, pending_payable_rowid = null where pending_payable_rowid = :rowid"; + last_paid_timestamp = :last_paid, pending_payable_rowid = null where wallet_address = :wallet"; let update_clause_with_compensated_overflow = "update payable set \ balance_high_b = :balance_high_b, balance_low_b = :balance_low_b, last_paid_timestamp = :last_paid, \ - pending_payable_rowid = null where pending_payable_rowid = :rowid"; + pending_payable_rowid = null where wallet_address = :wallet"; - let i64_rowid = checked_conversion::(pending_payable_fingerprint.rowid); - let last_paid = to_unix_timestamp(pending_payable_fingerprint.timestamp); + let wallet = format!("{:?}", confirmed_payable.receiver_address); let params = SQLParamsBuilder::default() - .key( PendingPayableRowid(&i64_rowid)) - .wei_change(WeiChange::new( "balance", pending_payable_fingerprint.amount, WeiChangeDirection::Subtraction)) - .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &last_paid))]) + .key( WalletAddress(&wallet)) + .wei_change(WeiChange::new("balance", confirmed_payable.amount_minor, WeiChangeDirection::Subtraction)) + .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &confirmed_payable.timestamp))]) .build(); self.big_int_db_processor.execute(Either::Left(self.conn.as_ref()), BigIntSqlConfig::new( @@ -354,39 +352,22 @@ impl PayableDaoReal { let balance_high_bytes_result = row.get(1); let balance_low_bytes_result = row.get(2); let last_paid_timestamp_result = row.get(3); - let pending_payable_rowid_result: Result, Error> = row.get(4); - let pending_payable_hash_result: Result, Error> = row.get(5); match ( wallet_result, balance_high_bytes_result, balance_low_bytes_result, last_paid_timestamp_result, - pending_payable_rowid_result, - pending_payable_hash_result, ) { - ( - Ok(wallet), - Ok(high_bytes), - Ok(low_bytes), - Ok(last_paid_timestamp), - Ok(rowid_opt), - Ok(hash_opt), - ) => Ok(PayableAccount { - wallet, - balance_wei: checked_conversion::(BigIntDivider::reconstitute( - high_bytes, low_bytes, - )), - last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), - pending_payable_opt: rowid_opt.map(|rowid| { - let hash_str = - hash_opt.expect("database corrupt; missing hash but existing rowid"); - PendingPayableId::new( - u64::try_from(rowid).unwrap(), - H256::from_str(&hash_str[2..]) - .unwrap_or_else(|_| panic!("wrong form of tx hash {}", hash_str)), - ) - }), - }), + (Ok(wallet), Ok(high_bytes), Ok(low_bytes), Ok(last_paid_timestamp)) => { + Ok(PayableAccount { + wallet, + balance_wei: checked_conversion::(BigIntDivider::reconstitute( + high_bytes, low_bytes, + )), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), + pending_payable_opt: None, + }) + } e => panic!( "Database is corrupt: PAYABLE table columns and/or types: {:?}", e @@ -400,13 +381,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 {}, @@ -427,176 +404,183 @@ 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::join_with_commas; +// use crate::accountant::db_access_objects::payable_dao::{MarkPendingPayableID, PayableDaoError}; +// use crate::accountant::db_access_objects::utils::{ +// update_rows_and_return_valid_count, VigilantRusqliteFlatten, +// }; +// 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 { +// join_with_commas(pairs, |(wallet, rowid)| { +// format!( +// "( Wallet: {}, Rowid: {} )", +// wallet, +// rowid_pretty_writer(rowid) +// ) +// }) +// } +// } #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::{from_unix_timestamp, current_unix_timestamp, to_unix_timestamp}; + use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition::ByAddresses; + use crate::accountant::db_access_objects::sent_payable_dao::SentTx; + use crate::accountant::db_access_objects::test_utils::make_sent_tx; + use crate::accountant::db_access_objects::utils::{ + current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, TxHash, + }; 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, + 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::accountant::db_access_objects::payable_dao::PayableRetrieveCondition::ByAddresses; - use crate::database::test_utils::ConnectionWrapperMock; + use std::time::Duration; #[test] fn more_money_payable_works_for_new_address() { @@ -743,260 +727,271 @@ mod tests { fn mark_pending_payables_marks_pending_transactions_for_new_addresses() { //the extra unchanged record checks the safety of right count of changed rows; //experienced serious troubles in the past - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_marks_pending_transactions_for_new_addresses", - ); - let wallet_0 = make_wallet("wallet"); - let wallet_1 = make_wallet("booga"); - let pending_payable_rowid_1 = 656; - let wallet_2 = make_wallet("bagaboo"); - let pending_payable_rowid_2 = 657; - let boxed_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ - last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; - let mut stm = boxed_conn.prepare(insert).unwrap(); - let params = [ - [&wallet_0 as &dyn ToSql, &12345, &1, &45678], - [&wallet_1, &0, &i64::MAX, &150_000_000], - [&wallet_2, &3, &0, &151_000_000], - ] - .into_iter() - .flatten() - .collect::>(); - stm.execute(params.as_slice()).unwrap(); - } - let subject = PayableDaoReal::new(boxed_conn); - - subject - .mark_pending_payables_rowids(&[ - (&wallet_1, pending_payable_rowid_1), - (&wallet_2, pending_payable_rowid_2), - ]) - .unwrap(); - - let account_statuses = [&wallet_0, &wallet_1, &wallet_2] - .iter() - .map(|wallet| subject.account_status(wallet).unwrap()) - .collect::>(); - assert_eq!( - account_statuses, - vec![ - PayableAccount { - wallet: wallet_0, - balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), - last_paid_timestamp: from_unix_timestamp(45678), - pending_payable_opt: None, - }, - PayableAccount { - wallet: wallet_1, - balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), - last_paid_timestamp: from_unix_timestamp(150_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_1, - make_tx_hash(0) - )), - }, - //notice the hashes are garbage generated by a test method not knowing doing better - PayableAccount { - wallet: wallet_2, - balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), - last_paid_timestamp: from_unix_timestamp(151_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_2, - make_tx_hash(0) - )) - } - ] - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_marks_pending_transactions_for_new_addresses", + // ); + // let wallet_0 = make_wallet("wallet"); + // let wallet_1 = make_wallet("booga"); + // let pending_payable_rowid_1 = 656; + // let wallet_2 = make_wallet("bagaboo"); + // let pending_payable_rowid_2 = 657; + // let boxed_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // { + // let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ + // last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; + // let mut stm = boxed_conn.prepare(insert).unwrap(); + // let params = [ + // [&wallet_0 as &dyn ToSql, &12345, &1, &45678], + // [&wallet_1, &0, &i64::MAX, &150_000_000], + // [&wallet_2, &3, &0, &151_000_000], + // ] + // .into_iter() + // .flatten() + // .collect::>(); + // stm.execute(params.as_slice()).unwrap(); + // } + // let subject = PayableDaoReal::new(boxed_conn); + // + // subject + // .mark_pending_payables_rowids(&[ + // (&wallet_1, pending_payable_rowid_1), + // (&wallet_2, pending_payable_rowid_2), + // ]) + // .unwrap(); + // + // let account_statuses = [&wallet_0, &wallet_1, &wallet_2] + // .iter() + // .map(|wallet| subject.account_status(wallet).unwrap()) + // .collect::>(); + // assert_eq!( + // account_statuses, + // vec![ + // PayableAccount { + // wallet: wallet_0, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(45678), + // pending_payable_opt: None, + // }, + // PayableAccount { + // wallet: wallet_1, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(150_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_1, + // make_tx_hash(0) + // )), + // }, + // //notice the hashes are garbage generated by a test method not knowing doing better + // PayableAccount { + // wallet: wallet_2, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(151_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_2, + // make_tx_hash(0) + // )) + // } + // ] + // ) } #[test] - #[should_panic(expected = "\ - Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ - 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ - The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ - ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ - points to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ - probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] + // #[should_panic(expected = "\ + // Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ + // 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ + // ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ + // points to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ + // probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] fn mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified( ) { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let first_wallet = make_wallet("booga"); - let first_rowid = 456; - insert_payable_record_fn( - &*conn, - &first_wallet.to_string(), - 123456, - 789789, - Some(first_rowid), - ); - let subject = PayableDaoReal::new(conn); - - let _ = subject.mark_pending_payables_rowids(&[ - (&first_wallet, first_rowid as u64), - (&make_wallet("yahoo"), 789), - ]); + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let first_wallet = make_wallet("booga"); + // let first_rowid = 456; + // insert_payable_record_fn( + // &*conn, + // &first_wallet.to_string(), + // 123456, + // 789789, + // Some(first_rowid), + // ); + // let subject = PayableDaoReal::new(conn); + // + // let _ = subject.mark_pending_payables_rowids(&[ + // (&first_wallet, first_rowid as u64), + // (&make_wallet("yahoo"), 789), + // ]); } #[test] fn explanatory_extension_shows_resulting_account_with_unpopulated_rowid() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", - ); - let wallet_1 = make_wallet("hooga"); - let rowid_1 = 550; - let wallet_2 = make_wallet("booga"); - let rowid_2 = 555; - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let record_seeds = [ - (&wallet_1.to_string(), 12345, 1_000_000_000, None), - (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), - ]; - record_seeds - .into_iter() - .for_each(|(wallet, balance, timestamp, rowid_opt)| { - insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) - }); - - let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); - - assert_eq!(result, "\ - The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ - ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ - ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ - Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double \ - payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be \ - suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for \ - this account\nprobably had not managed to complete successfully before another payment was \ - requested: preventive measures failed.\n".to_string()) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", + // ); + // let wallet_1 = make_wallet("hooga"); + // let rowid_1 = 550; + // let wallet_2 = make_wallet("booga"); + // let rowid_2 = 555; + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let record_seeds = [ + // (&wallet_1.to_string(), 12345, 1_000_000_000, None), + // (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), + // ]; + // record_seeds + // .into_iter() + // .for_each(|(wallet, balance, timestamp, rowid_opt)| { + // insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) + // }); + // + // let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); + // + // assert_eq!(result, "\ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ + // ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ + // ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ + // Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double \ + // payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be \ + // suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for \ + // this account\nprobably had not managed to complete successfully before another payment was \ + // requested: preventive measures failed.\n".to_string()) } #[test] fn mark_pending_payables_rowids_handles_general_sql_error() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_handles_general_sql_error", - ); - let wallet = make_wallet("booga"); - let rowid = 656; - let conn = payable_read_only_conn(&home_dir); - let conn_wrapped = ConnectionWrapperReal::new(conn); - let subject = PayableDaoReal::new(Box::new(conn_wrapped)); - - let result = subject.mark_pending_payables_rowids(&[(&wallet, rowid)]); - - assert_eq!( - result, - Err(PayableDaoError::RusqliteError( - "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ - Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ - database\"))]" - .to_string() - )) - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_handles_general_sql_error", + // ); + // let wallet = make_wallet("booga"); + // let rowid = 656; + // let single_mark_instruction = MarkPendingPayableID::new(wallet.address(), rowid); + // let conn = payable_read_only_conn(&home_dir); + // let conn_wrapped = ConnectionWrapperReal::new(conn); + // let subject = PayableDaoReal::new(Box::new(conn_wrapped)); + // + // let result = subject.mark_pending_payables_rowids(&[single_mark_instruction]); + // + // assert_eq!( + // result, + // Err(PayableDaoError::RusqliteError( + // "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ + // Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ + // database\"))]" + // .to_string() + // )) + // ) } #[test] - #[should_panic(expected = "broken code: empty input is not permit to enter this method")] + //#[should_panic(expected = "broken code: empty input is not permit to enter this method")] fn mark_pending_payables_rowids_is_strict_about_empty_input() { - let wrapped_conn = ConnectionWrapperMock::default(); - let subject = PayableDaoReal::new(Box::new(wrapped_conn)); - - let _ = subject.mark_pending_payables_rowids(&[]); + // TODO Will be an object of removal in GH-662 + // let wrapped_conn = ConnectionWrapperMock::default(); + // let subject = PayableDaoReal::new(Box::new(wrapped_conn)); + // + // let _ = subject.mark_pending_payables_rowids(&[]); } struct TestSetupValuesHolder { - fingerprint_1: PendingPayableFingerprint, - fingerprint_2: PendingPayableFingerprint, - wallet_1: Wallet, - wallet_2: Wallet, - previous_timestamp_1: SystemTime, - previous_timestamp_2: SystemTime, + account_1: TxWalletAndTimestamp, + account_2: TxWalletAndTimestamp, + } + + struct TxWalletAndTimestamp { + pending_payable: SentTx, + previous_timestamp: SystemTime, } - fn make_fingerprint_pair_and_insert_initial_payable_records( + struct TestInputs { + hash: TxHash, + previous_timestamp: SystemTime, + new_payable_timestamp: SystemTime, + receiver_wallet: Address, + initial_amount_wei: u128, + balance_change: u128, + } + + fn insert_initial_payable_records_and_return_sent_txs( conn: &dyn ConnectionWrapper, - initial_amount_1: u128, - initial_amount_2: u128, - balance_change_1: u128, - balance_change_2: u128, + (initial_amount_1, balance_change_1): (u128, u128), + (initial_amount_2, balance_change_2): (u128, u128), ) -> TestSetupValuesHolder { - let hash_1 = make_tx_hash(12345); - let rowid_1 = 789; - let previous_timestamp_1_s = 190_000_000; - let new_payable_timestamp_1 = from_unix_timestamp(199_000_000); - let wallet_1 = make_wallet("bobble"); - let hash_2 = make_tx_hash(54321); - let rowid_2 = 792; - let previous_timestamp_2_s = 187_100_000; - let new_payable_timestamp_2 = from_unix_timestamp(191_333_000); - let wallet_2 = make_wallet("booble bobble"); - { + let now = SystemTime::now(); + let (account_1, account_2) = [ + TestInputs { + hash: make_tx_hash(12345), + previous_timestamp: now.checked_sub(Duration::from_secs(45_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("bobbles").address(), + initial_amount_wei: initial_amount_1, + balance_change: balance_change_1, + }, + TestInputs { + hash: make_tx_hash(54321), + previous_timestamp: now.checked_sub(Duration::from_secs(22_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("yet more bobbles").address(), + initial_amount_wei: initial_amount_2, + balance_change: balance_change_2, + }, + ] + .into_iter() + .enumerate() + .map(|(idx, test_inputs)| { insert_payable_record_fn( conn, - &wallet_1.to_string(), - i128::try_from(initial_amount_1).unwrap(), - previous_timestamp_1_s, - Some(rowid_1 as i64), + &format!("{:?}", test_inputs.receiver_wallet), + i128::try_from(test_inputs.initial_amount_wei).unwrap(), + to_unix_timestamp(test_inputs.previous_timestamp), + // TODO argument will be eliminated in GH-662 + None, ); - insert_payable_record_fn( - conn, - &wallet_2.to_string(), - i128::try_from(initial_amount_2).unwrap(), - previous_timestamp_2_s, - Some(rowid_2 as i64), - ) - } - let fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: new_payable_timestamp_1, - hash: hash_1, - attempt: 1, - amount: balance_change_1, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: new_payable_timestamp_2, - hash: hash_2, - attempt: 1, - amount: balance_change_2, - process_error: None, - }; - let previous_timestamp_1 = from_unix_timestamp(previous_timestamp_1_s); - let previous_timestamp_2 = from_unix_timestamp(previous_timestamp_2_s); + let mut sent_tx = make_sent_tx((idx as u32 + 1) * 1234); + sent_tx.hash = test_inputs.hash; + sent_tx.amount_minor = test_inputs.balance_change; + sent_tx.receiver_address = test_inputs.receiver_wallet; + 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, } } @@ -1007,7 +1002,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, ) @@ -1020,77 +1015,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)) } @@ -1102,22 +1100,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 + ))) ) } @@ -1125,26 +1121,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] @@ -1156,38 +1147,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); } @@ -1396,9 +1386,9 @@ mod tests { #[test] fn custom_query_in_top_records_mode_with_default_ordering() { - //Accounts of balances smaller than one gwei don't qualify. - //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, - //here by balance and then by age. + // Accounts of balances smaller than one gwei don't qualify. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( @@ -1426,13 +1416,7 @@ mod tests { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), @@ -1446,9 +1430,9 @@ mod tests { #[test] fn custom_query_in_top_records_mode_ordered_by_age() { - //Accounts of balances smaller than one gwei don't qualify. - //Two accounts differ only in balance but not in the debt's age which allows to check doubled ordering, - //here by age and then by balance. + // Accounts of balances smaller than one gwei don't qualify. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( @@ -1470,13 +1454,7 @@ mod tests { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), @@ -1515,8 +1493,8 @@ mod tests { #[test] fn custom_query_in_range_mode() { - //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, - //by balance and then by age. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { insert( @@ -1600,13 +1578,7 @@ mod tests { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: gwei_to_wei(1_800_456_000_u32), last_paid_timestamp: from_unix_timestamp(now - 55_120), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None } ] ); @@ -1772,19 +1744,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 e555fcc9ae..0000000000 --- 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_unix_timestamp, to_unix_timestamp, DaoFactoryReal, VigilantRusqliteFlatten, -}; -use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::blockchain_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_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { - panic!( - "Invalid hash format (\"{}\": {:?}) - database corrupt", - transaction_hash, e - ) - }), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_bytes, - amount_low_bytes, - )), - process_error: None, - }) - }) - .expect("rusqlite failure") - .vigilant_flatten() - .collect() - } - - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError> { - fn values_clause_for_fingerprints_to_insert( - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> String { - let time_t = to_unix_timestamp(batch_wide_timestamp); - comma_joined_stringifiable(hashes_and_amounts, |hash_and_amount| { - let amount_checked = checked_conversion::(hash_and_amount.amount); - let (high_bytes, low_bytes) = BigIntDivider::deconstruct(amount_checked); - format!( - "('{:?}', {}, {}, {}, 1, null)", - hash_and_amount.hash, high_bytes, low_bytes, time_t - ) - }) - } - - let insert_sql = format!( - "insert into pending_payable (\ - transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error\ - ) values {}", - values_clause_for_fingerprints_to_insert(hashes_and_amounts, batch_wide_timestamp) - ); - match self - .conn - .prepare(&insert_sql) - .expect("Internal error") - .execute([]) - { - Ok(x) if x == hashes_and_amounts.len() => Ok(()), - Ok(x) => panic!( - "expected {} changed rows but got {}", - hashes_and_amounts.len(), - x - ), - Err(e) => Err(PendingPayableDaoError::InsertionFailed(e.to_string())), - } - } - - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - let sql = format!( - "delete from pending_payable where rowid in ({})", - Self::serialize_ids(ids) - ); - match self - .conn - .prepare(&sql) - .expect("delete command wrong") - .execute([]) - { - Ok(x) if x == ids.len() => Ok(()), - Ok(num) => panic!( - "deleting 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_unix_timestamp; - use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; - use crate::blockchain::test_utils::make_tx_hash; - use crate::database::db_initializer::{ - 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_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - let _ = subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - - let records = subject.return_all_errorless_fingerprints(); - assert_eq!( - records, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_1.hash, - attempt: 1, - amount: hash_and_amount_1.amount, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_2.hash, - attempt: 1, - amount: hash_and_amount_2.amount, - process_error: None - } - ] - ) - } - - #[test] - fn insert_new_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "insert_new_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let hash = make_tx_hash(45466); - let amount = 55556; - let timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { hash, amount }; - - let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); - - assert_eq!( - result, - Err(PendingPayableDaoError::InsertionFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic(expected = "expected 1 changed rows but got 0")] - fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { - let setup_conn = Connection::open_in_memory().unwrap(); - // injecting a by-plan failing statement into the mocked connection in order to provoke - // a reaction that would've been untestable directly on the table the act is closely coupled with - let statement = { - setup_conn - .execute("create table example (id integer)", []) - .unwrap(); - setup_conn.prepare("select id from example").unwrap() - }; - let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); - let hash_1 = make_tx_hash(4546); - let amount_1 = 55556; - let batch_wide_timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - - let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); - } - - #[test] - fn fingerprints_rowids_when_records_reachable() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_records_reachable", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_unix_timestamp(195_000_000); - // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, - // then complain about its excessive size if supplied in unquoted strings - let hash_1 = - H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") - .unwrap(); - let hash_2 = - H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") - .unwrap(); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 4567, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 6789, - }; - let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; - { - subject - .insert_new_fingerprints(&fingerprints_init_input, timestamp) - .unwrap(); - } - - let result = subject.fingerprints_rowids(&[hash_1, hash_2]); - - let first_expected_pair = &(1, hash_1); - assert!( - result.rowid_results.contains(first_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - first_expected_pair, - result.rowid_results - ); - let second_expected_pair = &(2, hash_2); - assert!( - result.rowid_results.contains(second_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - second_expected_pair, - result.rowid_results - ); - assert_eq!(result.rowid_results.len(), 2); - } - - #[test] - fn fingerprints_rowids_when_nonexistent_records() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_nonexistent_records", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let hash_1 = make_tx_hash(11119); - let hash_2 = make_tx_hash(22229); - let hash_3 = make_tx_hash(33339); - let hash_4 = make_tx_hash(44449); - // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for - // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of - // just an ambiguous 1 - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_2, - amount: 8901234, - }], - SystemTime::now(), - ) - .unwrap(); - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_3, - amount: 1234567, - }], - SystemTime::now(), - ) - .unwrap(); - subject.delete_fingerprints(&[1]).unwrap(); - - let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); - - assert_eq!(result.rowid_results, vec![(2, hash_3),]); - assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); - } - - #[test] - fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let batch_wide_timestamp = from_unix_timestamp(195_000_000); - let hash_1 = make_tx_hash(11119); - let amount_1 = 787; - let hash_2 = make_tx_hash(10000); - let amount_2 = 333; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: None - } - ] - ) - } - - #[test] - fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_unix_timestamp(198_000_000); - let hash = make_tx_hash(10000); - let amount = 333; - let hash_and_amount_1 = HashAndAmount { - hash: make_tx_hash(11119), - amount: 2000, - }; - let hash_and_amount_2 = HashAndAmount { hash, amount }; - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - subject.mark_failures(&[1]).unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![PendingPayableFingerprint { - rowid: 2, - timestamp, - hash, - attempt: 1, - amount, - process_error: None - }] - ) - } - - #[test] - #[should_panic( - expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" - )] - fn return_all_errorless_fingerprints_panics_on_malformed_hash() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_panics_on_malformed_hash", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - wrapped_conn - .prepare("insert into pending_payable \ - (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ - values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") - .unwrap() - .execute([]) - .unwrap(); - } - let subject = PendingPayableDaoReal::new(wrapped_conn); - - let _ = subject.return_all_errorless_fingerprints(); - } - - #[test] - fn delete_fingerprints_happy_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_happy_path", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[ - HashAndAmount { - hash: make_tx_hash(1234), - amount: 1111, - }, - HashAndAmount { - hash: make_tx_hash(2345), - amount: 5555, - }, - HashAndAmount { - hash: make_tx_hash(3456), - amount: 2222, - }, - ], - SystemTime::now(), - ) - .unwrap(); - } - - let result = subject.delete_fingerprints(&[2, 3]); - - assert_eq!(result, Ok(())); - let records_in_the_db = subject.return_all_errorless_fingerprints(); - let record_left_in = &records_in_the_db[0]; - assert_eq!(record_left_in.hash, make_tx_hash(1234)); - assert_eq!(record_left_in.rowid, 1); - assert_eq!(records_in_the_db.len(), 1); - } - - #[test] - fn delete_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let rowid = 45; - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.delete_fingerprints(&[rowid]); - - assert_eq!( - result, - Err(PendingPayableDaoError::RecordDeletion( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "deleting fingerprint, expected 2 rows to be changed, but the actual number is 1" - )] - fn delete_fingerprints_changed_different_number_of_rows_than_expected() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_changed_different_number_of_rows_than_expected", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let rowid_1 = 1; - let rowid_2 = 2; - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: make_tx_hash(666666), - amount: 5555, - }], - SystemTime::now(), - ) - .unwrap(); - } - - let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); - } - - #[test] - fn increment_scan_attempts_works() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(345); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(567); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 1122, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 2233, - }; - let hash_and_amount_3 = HashAndAmount { - hash: hash_3, - amount: 3344, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], - timestamp, - ) - .unwrap(); - } - - let result = subject.increment_scan_attempts(&[2, 3]); - - assert_eq!(result, Ok(())); - let mut all_records = subject.return_all_errorless_fingerprints(); - assert_eq!(all_records.len(), 3); - let record_1 = all_records.remove(0); - assert_eq!(record_1.hash, hash_1); - assert_eq!(record_1.attempt, 1); - let record_2 = all_records.remove(0); - assert_eq!(record_2.hash, hash_2); - assert_eq!(record_2.attempt, 2); - let record_3 = all_records.remove(0); - assert_eq!(record_3.hash, hash_3); - assert_eq!(record_3.attempt, 2); - } - - #[test] - fn increment_scan_attempts_works_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.increment_scan_attempts(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::UpdateFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" - )] - fn increment_scan_attempts_panics_on_unexpected_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_panics_on_unexpected_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.increment_scan_attempts(&[1, 2]); - } - - #[test] - fn mark_failures_works() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_works"); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(555); - let amount_1 = 1234; - let hash_2 = make_tx_hash(666); - let amount_2 = 2345; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - } - - let result = subject.mark_failures(&[2]); - - assert_eq!(result, Ok(())); - let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); - let mut assert_stm = assert_conn - .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") - .unwrap(); - let found_fingerprints = assert_stm - .query_map([], |row| { - let rowid: u64 = row.get(0).unwrap(); - let transaction_hash: String = row.get(1).unwrap(); - let amount_high_b: i64 = row.get(2).unwrap(); - let amount_low_b: i64 = row.get(3).unwrap(); - let timestamp: i64 = row.get(4).unwrap(); - let attempt: u16 = row.get(5).unwrap(); - let process_error: Option = row.get(6).unwrap(); - Ok(PendingPayableFingerprint { - rowid, - timestamp: from_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap(), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_b, - amount_low_b, - )), - process_error, - }) - }) - .unwrap() - .flatten() - .collect::>(); - assert_eq!( - *found_fingerprints, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: Some("ERROR".to_string()) - } - ] - ) - } - - #[test] - fn mark_failures_sad_path() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_sad_path"); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.mark_failures(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::ErrorMarkFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" - )] - fn mark_failures_panics_on_wrong_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "mark_failures_panics_on_wrong_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.mark_failures(&[10, 20]); - } -} diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index ad8f524622..9d100c6337 100644 --- a/node/src/accountant/db_access_objects/receivable_dao.rs +++ b/node/src/accountant/db_access_objects/receivable_dao.rs @@ -55,7 +55,7 @@ pub trait ReceivableDao { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError>; fn more_money_received( @@ -112,7 +112,7 @@ impl ReceivableDao for ReceivableDaoReal { &self, timestamp: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError> { let main_sql = "insert into receivable (wallet_address, balance_high_b, balance_low_b, last_received_timestamp) values \ (:wallet, :balance_high_b, :balance_low_b, :last_received_timestamp) on conflict (wallet_address) do update set \ @@ -125,7 +125,7 @@ impl ReceivableDao for ReceivableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( diff --git a/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs new file mode 100644 index 0000000000..26c7dd5fed --- /dev/null +++ b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; + +impl From<(FailedTx, TxBlock)> for SentTx { + fn from((failed_tx, confirmation_block): (FailedTx, TxBlock)) -> Self { + SentTx { + hash: failed_tx.hash, + receiver_address: failed_tx.receiver_address, + amount_minor: failed_tx.amount_minor, + timestamp: failed_tx.timestamp, + gas_price_minor: failed_tx.gas_price_minor, + nonce: failed_tx.nonce, + status: TxStatus::Confirmed { + block_hash: format!("{:?}", confirmation_block.block_hash), + block_number: confirmation_block.block_number.as_u64(), + detection: Detection::Reclaim, + }, + } + } +} + +impl From<(SentTx, FailureReason)> for FailedTx { + fn from((sent_tx, failure_reason): (SentTx, FailureReason)) -> Self { + FailedTx { + hash: sent_tx.hash, + receiver_address: sent_tx.receiver_address, + amount_minor: sent_tx.amount_minor, + timestamp: sent_tx.timestamp, + gas_price_minor: sent_tx.gas_price_minor, + nonce: sent_tx.nonce, + reason: failure_reason, + status: FailureStatus::RetryRequired, + } + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; + use crate::accountant::db_access_objects::utils::to_unix_timestamp; + use crate::accountant::gwei_to_wei; + use crate::accountant::test_utils::make_transaction_block; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; + use crate::blockchain::errors::validation_status::ValidationStatus; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::make_wallet; + use std::time::{Duration, SystemTime}; + + #[test] + fn sent_tx_record_can_be_converted_from_failed_tx_record() { + let failed_tx = FailedTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + }; + let tx_block = make_transaction_block(789); + + let result = SentTx::from((failed_tx.clone(), tx_block)); + + assert_eq!( + result, + SentTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + status: TxStatus::Confirmed { + block_hash: + "0x000000000000000000000000000000000000000000000000000000003b9acd15" + .to_string(), + block_number: 491169069, + detection: Detection::Reclaim, + }, + } + ); + } + + #[test] + fn conversion_from_sent_tx_and_failure_reason_to_failed_tx_works() { + let sent_tx = SentTx { + hash: make_tx_hash(789), + receiver_address: make_wallet("receiver").address(), + amount_minor: 123_456_789, + timestamp: to_unix_timestamp( + SystemTime::now() + .checked_sub(Duration::from_secs(10_000)) + .unwrap(), + ), + gas_price_minor: gwei_to_wei(424_u64), + nonce: 456_u64.into(), + status: TxStatus::Pending(ValidationStatus::Waiting), + }; + + let result_1 = FailedTx::from((sent_tx.clone(), FailureReason::Reverted)); + let result_2 = FailedTx::from(( + sent_tx.clone(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + )); + + assert_conversion_into_failed_tx(result_1, sent_tx.clone(), FailureReason::Reverted); + assert_conversion_into_failed_tx( + result_2, + sent_tx, + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + ); + } + + fn assert_conversion_into_failed_tx( + result: FailedTx, + original_sent_tx: SentTx, + expected_failure_reason: FailureReason, + ) { + assert_eq!(result.hash, original_sent_tx.hash); + assert_eq!(result.receiver_address, original_sent_tx.receiver_address); + assert_eq!(result.amount_minor, original_sent_tx.amount_minor); + assert_eq!(result.timestamp, original_sent_tx.timestamp); + assert_eq!(result.gas_price_minor, original_sent_tx.gas_price_minor); + assert_eq!(result.nonce, original_sent_tx.nonce); + assert_eq!(result.status, FailureStatus::RetryRequired); + assert_eq!(result.reason, expected_failure_reason); + } +} diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs index 0e72a578d1..d0edbfa34a 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -1,21 +1,23 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::utils::{ + sql_values_of_sent_tx, DaoFactoryReal, TxHash, TxIdentifiers, +}; +use crate::accountant::db_access_objects::Transaction; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::accountant::{checked_conversion, join_with_commas, join_with_separator}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; +use crate::blockchain::errors::validation_status::ValidationStatus; +use crate::database::rusqlite_wrappers::ConnectionWrapper; +use ethereum_types::H256; +use itertools::Itertools; +use masq_lib::utils::ExpectValue; +use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::{BTreeSet, HashMap}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -use ethereum_types::{H256}; use web3::types::Address; -use masq_lib::utils::ExpectValue; -use crate::accountant::{checked_conversion, join_with_separator}; -use crate::accountant::db_access_objects::utils::{sql_values_of_sent_tx, DaoFactoryReal, TxHash, TxIdentifiers}; -use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; -use crate::database::rusqlite_wrappers::ConnectionWrapper; -use serde_derive::{Deserialize, Serialize}; -use crate::accountant::db_access_objects::failed_payable_dao::{ValidationStatus}; -use crate::accountant::db_access_objects::failed_payable_dao::{FailedPayableDao}; -use crate::accountant::db_access_objects::Transaction; #[derive(Debug, PartialEq, Eq)] pub enum SentPayableDaoError { @@ -26,18 +28,18 @@ pub enum SentPayableDaoError { SqlExecutionFailed(String), } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Tx { +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SentTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub status: TxStatus, } -impl Transaction for Tx { +impl Transaction for SentTx { fn hash(&self) -> TxHash { self.hash } @@ -47,7 +49,7 @@ impl Transaction for Tx { } fn amount(&self) -> u128 { - self.amount + self.amount_minor } fn timestamp(&self) -> i64 { @@ -55,7 +57,7 @@ impl Transaction for Tx { } fn gas_price_wei(&self) -> u128 { - self.gas_price_wei + self.gas_price_minor } fn nonce(&self) -> u64 { @@ -67,24 +69,7 @@ impl Transaction for Tx { } } -impl PartialOrd for Tx { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Tx { - fn cmp(&self, other: &Self) -> Ordering { - // Descending Order - other - .timestamp - .cmp(&self.timestamp) - .then_with(|| other.nonce.cmp(&self.nonce)) - .then_with(|| other.amount.cmp(&self.amount)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TxStatus { Pending(ValidationStatus), Confirmed { @@ -94,6 +79,41 @@ pub enum TxStatus { }, } +impl PartialOrd for TxStatus { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for TxStatus { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (TxStatus::Pending(status1), TxStatus::Pending(status2)) => status1.cmp(status2), + (TxStatus::Pending(_), TxStatus::Confirmed { .. }) => Ordering::Greater, + (TxStatus::Confirmed { .. }, TxStatus::Pending(_)) => Ordering::Less, + ( + TxStatus::Confirmed { + block_hash: block_hash1, + block_number: block_num1, + detection: detection1, + }, + TxStatus::Confirmed { + block_hash: block_hash2, + block_number: block_num2, + detection: detection2, + }, + ) => block_hash1 + .cmp(block_hash2) + .then_with(|| block_num1.cmp(block_num2)) + .then_with(|| detection1.cmp(detection2)), + } + } +} + impl FromStr for TxStatus { type Err = String; @@ -118,27 +138,21 @@ pub enum Detection { Reclaim, } -impl From<&TxConfirmation> for TxStatus { - fn from(tx_confirmation: &TxConfirmation) -> Self { +impl From for TxStatus { + fn from(tx_block: TxBlock) -> Self { TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number) - .expect("block number too big"), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).expect("block number too big"), + detection: Detection::Normal, } } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TxConfirmation { - block_info: TransactionBlock, - detection: Detection, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub enum RetrieveCondition { IsPending, ByHash(BTreeSet), + ByNonce(Vec), } impl Display for RetrieveCondition { @@ -151,7 +165,14 @@ impl Display for RetrieveCondition { write!( f, "WHERE tx_hash IN ({})", - join_with_separator(tx_hashes, |hash| format!("'{:?}'", hash), ", ") + join_with_commas(tx_hashes, |hash| format!("'{:?}'", hash)) + ) + } + RetrieveCondition::ByNonce(nonces) => { + write!( + f, + "WHERE nonce IN ({})", + join_with_commas(nonces, |nonce| nonce.to_string()) ) } } @@ -160,13 +181,16 @@ impl Display for RetrieveCondition { pub trait SentPayableDao { fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers; - fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError>; - fn retrieve_txs(&self, condition: Option) -> BTreeSet; - fn confirm_tx( + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> BTreeSet; + //TODO potentially atomically + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError>; + fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError>; + fn update_statuses( &self, - hash_map: &HashMap, + hash_map: &HashMap, ) -> Result<(), SentPayableDaoError>; - fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError>; } @@ -185,7 +209,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers { let sql = format!( "SELECT tx_hash, rowid FROM sent_payable WHERE tx_hash IN ({})", - join_with_separator(hashes, |hash| format!("'{:?}'", hash), ", ") + join_with_commas(hashes, |hash| format!("'{:?}'", hash)) ); let mut stmt = self @@ -205,7 +229,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError> { + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError> { if txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } @@ -238,7 +262,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { nonce, \ status \ ) VALUES {}", - join_with_separator(txs, |tx| sql_values_of_sent_tx(tx), ", ") + join_with_commas(txs, |tx| sql_values_of_sent_tx(tx)) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -257,7 +281,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } - fn retrieve_txs(&self, condition_opt: Option) -> BTreeSet { + fn retrieve_txs(&self, condition_opt: Option) -> BTreeSet { let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, status FROM sent_payable" .to_string(); @@ -279,22 +303,22 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Address::from_str(&receiver_address_str[2..]).expect("Failed to parse H160"); let amount_high_b = row.get(2).expectv("amount_high_b"); let amount_low_b = row.get(3).expectv("amount_low_b"); - let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let amount_minor = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; let timestamp = row.get(4).expectv("timestamp"); let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); - let gas_price_wei = + let gas_price_minor = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); let status_str: String = row.get(8).expectv("status"); let status = TxStatus::from_str(&status_str).expect("Failed to parse TxStatus"); - Ok(Tx { + Ok(SentTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, status, }) @@ -304,18 +328,15 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn confirm_tx( - &self, - hash_map: &HashMap, - ) -> Result<(), SentPayableDaoError> { + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { if hash_map.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - for (hash, tx_confirmation) in hash_map { + for (hash, tx_block) in hash_map { let sql = format!( "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", - TxStatus::from(tx_confirmation), + TxStatus::from(*tx_block), hash ); @@ -339,12 +360,12 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Ok(()) } - fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError> { + fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError> { if new_txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - let build_case = |value_fn: fn(&Tx) -> String| { + let build_case = |value_fn: fn(&SentTx) -> String| { join_with_separator( new_txs, |tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx)), @@ -355,29 +376,29 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let tx_hash_cases = build_case(|tx| format!("'{:?}'", tx.hash)); let receiver_address_cases = build_case(|tx| format!("'{:?}'", tx.receiver_address)); let amount_high_b_cases = build_case(|tx| { - let amount_checked = checked_conversion::(tx.amount); + let amount_checked = checked_conversion::(tx.amount_minor); let (high, _) = BigIntDivider::deconstruct(amount_checked); high.to_string() }); let amount_low_b_cases = build_case(|tx| { - let amount_checked = checked_conversion::(tx.amount); + let amount_checked = checked_conversion::(tx.amount_minor); let (_, low) = BigIntDivider::deconstruct(amount_checked); low.to_string() }); let timestamp_cases = build_case(|tx| tx.timestamp.to_string()); let gas_price_wei_high_b_cases = build_case(|tx| { - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (high, _) = BigIntDivider::deconstruct(gas_price_wei_checked); high.to_string() }); let gas_price_wei_low_b_cases = build_case(|tx| { - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); low.to_string() }); let status_cases = build_case(|tx| format!("'{}'", tx.status)); - let nonces = join_with_separator(new_txs, |tx| tx.nonce.to_string(), ", "); + let nonces = join_with_commas(new_txs, |tx| tx.nonce.to_string()); let sql = format!( "UPDATE sent_payable \ @@ -423,6 +444,47 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } + fn update_statuses( + &self, + status_updates: &HashMap, + ) -> Result<(), SentPayableDaoError> { + if status_updates.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let case_statements = status_updates + .iter() + .map(|(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status)) + .join(" "); + let tx_hashes = join_with_commas(&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: &BTreeSet) -> Result<(), SentPayableDaoError> { if hashes.is_empty() { return Err(SentPayableDaoError::EmptyInput); @@ -430,7 +492,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let sql = format!( "DELETE FROM sent_payable WHERE tx_hash IN ({})", - join_with_separator(hashes, |hash| { format!("'{:?}'", hash) }, ", ") + join_with_commas(hashes, |hash| { format!("'{:?}'", hash) }) ); match self.conn.prepare(&sql).expect("Internal error").execute([]) { @@ -464,29 +526,42 @@ impl SentPayableDaoFactory for DaoFactoryReal { #[cfg(test)] mod tests { - use std::collections::{BTreeSet, HashMap}; - use std::ops::Add; - use std::str::FromStr; - use std::sync::{Arc, Mutex}; - use std::time::{Duration, UNIX_EPOCH}; - use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, Tx, TxConfirmation, TxStatus}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ + ByHash, ByNonce, IsPending, + }; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{ + EmptyInput, PartialExecution, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, + SentTx, TxStatus, + }; + use crate::accountant::db_access_objects::test_utils::{ + make_read_only_db_connection, make_sent_tx, TxBuilder, + }; + use crate::accountant::db_access_objects::Transaction; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::blockchain::blockchain_interface::data_structures::TxBlock; + use crate::blockchain::errors::internal_errors::InternalErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_address, make_block_hash, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; use crate::database::test_utils::ConnectionWrapperMock; - use ethereum_types::{ H256, U64}; + use ethereum_types::{H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; - use rusqlite::{Connection}; - use crate::accountant::db_access_objects::failed_payable_dao::{ValidationStatus}; - use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending}; - use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; - use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; - use crate::accountant::db_access_objects::Transaction; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; - use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; - use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; - use crate::blockchain::test_utils::{make_address, make_block_hash, make_tx_hash, ValidationFailureClockMock}; + use rusqlite::Connection; + use std::cmp::Ordering; + use std::collections::{BTreeSet, HashMap}; + 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() { @@ -500,11 +575,15 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Unreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ) .add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Unreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -569,18 +648,17 @@ mod tests { Err(SentPayableDaoError::InvalidInput( "Duplicate hashes found in the input. Input Transactions: \ {\ - Tx { \ + 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: 0, timestamp: 1749204020, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204020, gas_price_minor: 0, \ nonce: 0, status: Confirmed { block_hash: \ \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ - block_number: 7890123, detection: Reclaim } }, \ - Tx { \ - hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ - receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ - nonce: 0, status: Pending(Waiting) }\ + block_number: 7890123, detection: Reclaim } }\ }" .to_string() )) @@ -698,8 +776,8 @@ mod tests { '0x0000000000000000000000000000000000000000000000000000000123456789', \ '0x0000000000000000000000000000000000000000000000000000000987654321'\ )" - .to_string() ); + assert_eq!(ByNonce(vec![45, 47]).to_string(), "WHERE nonce IN (45, 47)") } #[test] @@ -741,7 +819,9 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Unreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -821,6 +901,35 @@ mod tests { ); } + #[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(&BTreeSet::from([tx1.clone(), tx2, tx3.clone()])) + .unwrap(); + + let result = subject.retrieve_txs(Some(ByNonce(vec![33, 35]))); + + assert_eq!(result, BTreeSet::from([tx1, tx3])); + } + #[test] fn confirm_tx_works() { let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "confirm_tx_works"); @@ -839,26 +948,20 @@ mod tests { subject.retrieve_txs(Some(ByHash(BTreeSet::from([hash1, hash2])))); let pre_assert_status_tx1 = updated_pre_assert_txs.get(&tx1).unwrap().status.clone(); let pre_assert_status_tx2 = updated_pre_assert_txs.get(&tx2).unwrap().status.clone(); - let tx_confirmation_1 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(3), - block_number: U64::from(1), - }, - detection: Detection::Normal, + let confirmed_tx_block_1 = TxBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), }; - let tx_confirmation_2 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(4), - block_number: U64::from(2), - }, - detection: Detection::Reclaim, + let confirmed_tx_block_2 = TxBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), }; let hash_map = HashMap::from([ - (tx1.hash, tx_confirmation_1.clone()), - (tx2.hash, tx_confirmation_2.clone()), + (tx1.hash, confirmed_tx_block_1.clone()), + (tx2.hash, confirmed_tx_block_2.clone()), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); let updated_txs = subject.retrieve_txs(Some(ByHash(BTreeSet::from([tx1.hash, tx2.hash])))); let updated_tx1 = updated_txs.iter().find(|tx| tx.hash == hash1).unwrap(); @@ -871,9 +974,9 @@ mod tests { assert_eq!( updated_tx1.status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_1.block_info.block_hash), - block_number: tx_confirmation_1.block_info.block_number.as_u64(), - detection: tx_confirmation_1.detection + block_hash: format!("{:?}", confirmed_tx_block_1.block_hash), + block_number: confirmed_tx_block_1.block_number.as_u64(), + detection: Detection::Normal } ); assert_eq!( @@ -883,9 +986,9 @@ mod tests { assert_eq!( updated_tx2.status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_2.block_info.block_hash), - block_number: tx_confirmation_2.block_info.block_number.as_u64(), - detection: tx_confirmation_2.detection + block_hash: format!("{:?}", confirmed_tx_block_2.block_hash), + block_number: confirmed_tx_block_2.block_number.as_u64(), + detection: Detection::Normal } ); } @@ -905,7 +1008,7 @@ mod tests { subject.insert_new_records(&BTreeSet::from([tx])).unwrap(); let hash_map = HashMap::new(); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); } @@ -927,27 +1030,21 @@ mod tests { let hash_map = HashMap::from([ ( existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), }, ), ( non_existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(2), - block_number: U64::from(2), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), }, ), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -969,16 +1066,13 @@ mod tests { let hash = make_tx_hash(1); let hash_map = HashMap::from([( hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::default(), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), }, )]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -1096,6 +1190,146 @@ mod tests { ) } + #[test] + fn update_statuses_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "update_statuses_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let timestamp_a = SystemTime::now().sub(Duration::from_millis(11)); + let timestamp_b = SystemTime::now().sub(Duration::from_millis(1234)); + let subject = SentPayableDaoReal::new(wrapped_conn); + let mut tx1 = make_sent_tx(456); + tx1.status = TxStatus::Pending(ValidationStatus::Waiting); + let mut tx2 = make_sent_tx(789); + tx2.status = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ))); + let mut tx3 = make_sent_tx(123); + tx3.status = TxStatus::Pending(ValidationStatus::Waiting); + subject + .insert_new_records(&BTreeSet::from([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: Vec<_> = subject.retrieve_txs(None).into_iter().collect(); + assert_eq!(result, Ok(())); + assert_eq!( + updated_txs[0].status, + TxStatus::Confirmed { + block_hash: "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + } + ); + assert_eq!( + updated_txs[1].status, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a) + ))) + ); + assert_eq!( + updated_txs[2].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.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( @@ -1167,7 +1401,7 @@ mod tests { 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 (3, 2, 1)")); + 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'")); @@ -1280,14 +1514,15 @@ mod tests { fn tx_status_from_str_works() { let validation_failure_clock = ValidationFailureClockMock::default() .now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); + assert_eq!( TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), TxStatus::Pending(ValidationStatus::Waiting) ); assert_eq!( - TxStatus::from_str(r#"{"Pending":{"Reattempting":{"InvalidResponse":{"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}}}}"#).unwrap(), - TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::InvalidResponse), &validation_failure_clock))) + TxStatus::from_str(r#"{"Pending":{"Reattempting":[{"error":{"AppRpc":{"Remote":"InvalidResponse"}},"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}]}}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &validation_failure_clock))) ); assert_eq!( @@ -1323,89 +1558,109 @@ mod tests { } #[test] - fn tx_status_can_be_converted_from_tx_confirmation() { - let tx_confirmation = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(6), - block_number: 456789_u64.into(), - }, - detection: Detection::Normal, + fn tx_status_can_be_made_from_transaction_block() { + let tx_block = TxBlock { + block_hash: make_block_hash(6), + block_number: 456789_u64.into(), }; assert_eq!( - TxStatus::from(&tx_confirmation), + TxStatus::from(tx_block), TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number).unwrap(), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).unwrap(), + detection: Detection::Normal, } ) } #[test] - fn tx_ordering_works() { - let tx1 = Tx { - hash: make_tx_hash(1), - receiver_address: make_address(1), - amount: 100, - timestamp: 1000, - gas_price_wei: 10, - nonce: 1, - status: TxStatus::Pending(ValidationStatus::Waiting), + fn tx_status_ordering_works() { + let tx_status_1 = TxStatus::Pending(ValidationStatus::Waiting); + let tx_status_2 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), + &ValidationFailureClockReal::default(), + ))); + let tx_status_3 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &ValidationFailureClockReal::default(), + ))); + let tx_status_4 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + &ValidationFailureClockReal::default(), + ))); + let tx_status_5 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(1)), + block_number: 123456, + detection: Detection::Normal, }; - let tx2 = Tx { - hash: make_tx_hash(2), - receiver_address: make_address(2), - amount: 200, - timestamp: 1000, - gas_price_wei: 20, - nonce: 1, - status: TxStatus::Pending(ValidationStatus::Waiting), + let tx_status_6 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(2)), + block_number: 6543, + detection: Detection::Normal, }; - let tx3 = Tx { - hash: make_tx_hash(3), - receiver_address: make_address(3), - amount: 100, - timestamp: 2000, - gas_price_wei: 30, - nonce: 2, - status: TxStatus::Pending(ValidationStatus::Waiting), + let tx_status_7 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(1)), + block_number: 123456, + detection: Detection::Reclaim, }; + let tx_status_1_identical = tx_status_1.clone(); + let tx_status_6_identical = tx_status_6.clone(); let mut set = BTreeSet::new(); - set.insert(tx1.clone()); - set.insert(tx2.clone()); - set.insert(tx3.clone()); + vec![ + tx_status_1.clone(), + tx_status_2.clone(), + tx_status_3.clone(), + tx_status_4.clone(), + tx_status_5.clone(), + tx_status_6.clone(), + tx_status_7.clone(), + ] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); - let expected_order = vec![tx3, tx2, tx1]; + let expected_order = vec![ + tx_status_5, + tx_status_7, + tx_status_6.clone(), + tx_status_3, + tx_status_2, + tx_status_4, + tx_status_1.clone(), + ]; assert_eq!(set.into_iter().collect::>(), expected_order); + assert_eq!(tx_status_1.cmp(&tx_status_1_identical), Ordering::Equal); + assert_eq!(tx_status_6.cmp(&tx_status_6_identical), Ordering::Equal); } #[test] fn transaction_trait_methods_for_tx() { let hash = make_tx_hash(1); let receiver_address = make_address(1); - let amount = 1000; + let amount_minor = 1000; let timestamp = 1625247600; - let gas_price_wei = 2000; + let gas_price_minor = 2000; let nonce = 42; let status = TxStatus::Pending(ValidationStatus::Waiting); - let tx = Tx { + let tx = SentTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, status, }; assert_eq!(tx.receiver_address(), receiver_address); assert_eq!(tx.hash(), hash); - assert_eq!(tx.amount(), amount); + assert_eq!(tx.amount(), amount_minor); assert_eq!(tx.timestamp(), timestamp); - assert_eq!(tx.gas_price_wei(), gas_price_wei); + assert_eq!(tx.gas_price_wei(), gas_price_minor); assert_eq!(tx.nonce(), nonce); assert_eq!(tx.is_failed(), false); } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index 2fa6d1a65d..fca96ed7f3 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -2,11 +2,13 @@ #![cfg(test)] use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedTx, FailureReason, FailureStatus, ValidationStatus, + FailedTx, FailureReason, FailureStatus, }; -use crate::accountant::db_access_objects::sent_payable_dao::{Tx, TxStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; -use crate::blockchain::test_utils::make_tx_hash; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplate; +use crate::blockchain::errors::validation_status::ValidationStatus; +use crate::blockchain::test_utils::{make_address, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; @@ -51,18 +53,26 @@ impl TxBuilder { self } + pub fn template(mut self, signable_tx_template: SignableTxTemplate) -> Self { + self.receiver_address_opt = Some(signable_tx_template.receiver_address); + self.amount_opt = Some(signable_tx_template.amount_in_wei); + self.gas_price_wei_opt = Some(signable_tx_template.gas_price_wei); + self.nonce_opt = Some(signable_tx_template.nonce); + self + } + pub fn status(mut self, status: TxStatus) -> Self { self.status_opt = Some(status); self } - pub fn build(self) -> Tx { - Tx { + pub fn build(self) -> SentTx { + SentTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: self.amount_opt.unwrap_or_default(), timestamp: self.timestamp_opt.unwrap_or_else(current_unix_timestamp), - gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), status: self .status_opt @@ -123,6 +133,14 @@ impl FailedTxBuilder { self } + pub fn template(mut self, signable_tx_template: SignableTxTemplate) -> Self { + self.receiver_address_opt = Some(signable_tx_template.receiver_address); + self.amount_opt = Some(signable_tx_template.amount_in_wei); + self.gas_price_wei_opt = Some(signable_tx_template.gas_price_wei); + self.nonce_opt = Some(signable_tx_template.nonce); + self + } + pub fn status(mut self, failure_status: FailureStatus) -> Self { self.status_opt = Some(failure_status); self @@ -132,9 +150,9 @@ impl FailedTxBuilder { FailedTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: self.amount_opt.unwrap_or_default(), timestamp: self.timestamp_opt.unwrap_or_else(|| 1719990000), - gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), reason: self .reason_opt @@ -147,43 +165,53 @@ impl FailedTxBuilder { } pub fn make_failed_tx(n: u32) -> FailedTx { - let n = (n * 2) + 1; // Always Odd + let n = n % 0xfff; FailedTxBuilder::default() .hash(make_tx_hash(n)) + .timestamp(((n * 12) as i64).pow(2)) + .receiver_address(make_address(n.pow(2))) + .gas_price_wei((n as u128).pow(3)) + .amount((n as u128).pow(4)) .nonce(n as u64) .build() } -pub fn make_sent_tx(n: u32) -> Tx { - let n = n * 2; // Always Even +pub fn make_sent_tx(n: u32) -> SentTx { + let n = n % 0xfff; TxBuilder::default() .hash(make_tx_hash(n)) - .nonce(n as u64) + .timestamp(((n * 12) as i64).pow(2)) + .template(SignableTxTemplate { + receiver_address: make_address(n), + amount_in_wei: (n as u128).pow(4), + gas_price_wei: (n as u128).pow(3), + nonce: n as u64, + }) .build() } -pub fn assert_on_sent_txs(left: Vec, right: Vec) { - assert_eq!(left.len(), right.len()); - - left.iter().zip(right).for_each(|(t1, t2)| { - assert_eq!(t1.hash, t2.hash); - assert_eq!(t1.receiver_address, t2.receiver_address); - assert_eq!(t1.amount, t2.amount); - assert_eq!(t1.gas_price_wei, t2.gas_price_wei); - assert_eq!(t1.nonce, t2.nonce); - assert_eq!(t1.status, t2.status); - assert!((t1.timestamp - t2.timestamp).abs() < 10); +pub fn assert_on_sent_txs(actual: Vec, expected: Vec) { + assert_eq!(actual.len(), expected.len()); + + actual.iter().zip(expected).for_each(|(st1, st2)| { + assert_eq!(st1.hash, st2.hash); + assert_eq!(st1.receiver_address, st2.receiver_address); + assert_eq!(st1.amount_minor, st2.amount_minor); + assert_eq!(st1.gas_price_minor, st2.gas_price_minor); + assert_eq!(st1.nonce, st2.nonce); + assert_eq!(st1.status, st2.status); + assert!((st1.timestamp - st2.timestamp).abs() < 10); }) } -pub fn assert_on_failed_txs(left: Vec, right: Vec) { - assert_eq!(left.len(), right.len()); +pub fn assert_on_failed_txs(actual: Vec, expected: Vec) { + assert_eq!(actual.len(), expected.len()); - left.iter().zip(right).for_each(|(f1, f2)| { + actual.iter().zip(expected).for_each(|(f1, f2)| { assert_eq!(f1.hash, f2.hash); assert_eq!(f1.receiver_address, f2.receiver_address); - assert_eq!(f1.amount, f2.amount); - assert_eq!(f1.gas_price_wei, f2.gas_price_wei); + assert_eq!(f1.amount_minor, f2.amount_minor); + assert_eq!(f1.gas_price_minor, f2.gas_price_minor); assert_eq!(f1.nonce, f2.nonce); assert_eq!(f1.reason, f2.reason); assert_eq!(f1.status, f2.status); diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index a007fed6b1..98c14ac3e0 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -3,6 +3,7 @@ use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, gwei_to_wei, sign_conversion}; use crate::database::db_initializer::{ @@ -23,7 +24,6 @@ use std::path::{Path, PathBuf}; use std::string::ToString; use std::time::Duration; use std::time::SystemTime; -use crate::accountant::db_access_objects::sent_payable_dao::Tx; pub type TxHash = H256; pub type RowId = u64; @@ -49,8 +49,8 @@ pub fn from_unix_timestamp(unix_timestamp: i64) -> SystemTime { } pub fn sql_values_of_failed_tx(failed_tx: &FailedTx) -> String { - let amount_checked = checked_conversion::(failed_tx.amount); - let gas_price_wei_checked = checked_conversion::(failed_tx.gas_price_wei); + let amount_checked = checked_conversion::(failed_tx.amount_minor); + let gas_price_wei_checked = checked_conversion::(failed_tx.gas_price_minor); let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); let (gas_price_wei_high_b, gas_price_wei_low_b) = BigIntDivider::deconstruct(gas_price_wei_checked); @@ -69,9 +69,9 @@ pub fn sql_values_of_failed_tx(failed_tx: &FailedTx) -> String { ) } -pub fn sql_values_of_sent_tx(sent_tx: &Tx) -> String { - let amount_checked = checked_conversion::(sent_tx.amount); - let gas_price_wei_checked = checked_conversion::(sent_tx.gas_price_wei); +pub fn sql_values_of_sent_tx(sent_tx: &SentTx) -> String { + let amount_checked = checked_conversion::(sent_tx.amount_minor); + let gas_price_wei_checked = checked_conversion::(sent_tx.gas_price_minor); let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); let (gas_price_wei_high_b, gas_price_wei_low_b) = BigIntDivider::deconstruct(gas_price_wei_checked); diff --git a/node/src/accountant/db_big_integer/big_int_db_processor.rs b/node/src/accountant/db_big_integer/big_int_db_processor.rs index 3ef15278d4..c362e3740f 100644 --- a/node/src/accountant/db_big_integer/big_int_db_processor.rs +++ b/node/src/accountant/db_big_integer/big_int_db_processor.rs @@ -322,6 +322,7 @@ pub trait DisplayableParamValue: ToSql + Display {} impl DisplayableParamValue for i64 {} impl DisplayableParamValue for &str {} +impl DisplayableParamValue for String {} impl DisplayableParamValue for Wallet {} #[derive(Default)] diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 77444a480a..20512a1352 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -14,27 +14,40 @@ 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::{StartScanError, Scanners}; -use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; -use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError; -use crate::blockchain::blockchain_interface::data_structures::{BatchResults, BlockchainTransaction}; +use crate::accountant::scanners::payable_scanner::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, +}; +use crate::accountant::scanners::payable_scanner::utils::NextScanToRun; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableScanResult, TxHashByTable, +}; +use crate::accountant::scanners::scan_schedulers::{ + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, +}; +use crate::accountant::scanners::{Scanners, StartScanError}; +use crate::blockchain::blockchain_bridge::{ + BlockMarker, RegisterNewPendingPayables, RetrieveTransactions, +}; +use crate::blockchain::blockchain_interface::data_structures::{ + BatchResults, BlockchainTransaction, 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}; @@ -52,31 +65,24 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, - UiScanRequest, + QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; +use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::ui_gateway::MessageTarget::ClientId; use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; +use std::collections::{BTreeMap, BTreeSet}; #[cfg(test)] use std::default::Default; use std::fmt::Display; -use std::hash::Hash; use std::ops::{Div, Mul}; use std::path::Path; use std::rc::Rc; use std::time::SystemTime; -use web3::types::H256; -use crate::accountant::scanners::payable_scanner::msgs::{PricedTemplatesMessage, InitialTemplatesMessage}; -use crate::accountant::scanners::payable_scanner::utils::NextScanToRun; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; -use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; -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 @@ -86,7 +92,7 @@ pub struct Accountant { earning_wallet: Wallet, payable_dao: Box, receivable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, crashable: bool, scanners: Scanners, scan_schedulers: ScanSchedulers, @@ -133,19 +139,21 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } +pub type TxReceiptResult = Result; + #[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, +pub struct TxReceiptsMessage { + pub results: BTreeMap, pub response_skeleton_opt: Option, } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum PayableScanType { New, Retry, } -#[derive(Debug, Message, PartialEq, Clone)] +#[derive(Debug, Message, PartialEq, Eq, Clone)] pub struct SentPayables { pub payment_procedure_result: Result, pub payable_scan_type: PayableScanType, @@ -174,7 +182,7 @@ pub struct ScanForReceivables { #[derive(Debug, Clone, Message, PartialEq, Eq)] pub struct ScanError { - pub scan_type: ScanType, + pub scan_type: DetailedScanType, pub response_skeleton_opt: Option, pub msg: String, } @@ -234,20 +242,20 @@ impl Handler for Accountant { self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) => self .scan_schedulers .pending_payable .schedule(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::Schedule(scan_type) => unreachable!( "Early stopped pending payable scan was suggested to be followed up \ by the scan for {:?}, which is not supported though", scan_type ), - ScanRescheduleAfterEarlyStop::DoNotSchedule => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the pending payable scan did find results" @@ -271,16 +279,16 @@ impl Handler for Accountant { let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(other_scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::Schedule(other_scan_type) => unreachable!( "Early stopped new payable scan was suggested to be followed up by the scan \ for {:?}, which is not supported though", other_scan_type ), - ScanRescheduleAfterEarlyStop::DoNotSchedule => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the new payable scan did find results" @@ -312,11 +320,10 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { - let response_skeleton_opt = msg.response_skeleton_opt; + fn handle(&mut self, msg: TxReceiptsMessage, ctx: &mut Self::Context) -> Self::Result { 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 { @@ -325,18 +332,37 @@ impl Handler for Accountant { .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - // Externally triggered scan should never be allowed to spark a procedure that - // would bring over payables with fresh nonces. The job's done. + // Non-automatic scan for pending payables is not permitted to spark a payable + // scan bringing over new payables with fresh nonces. The job's done here. } else { self.scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger) } } - PendingPayableScanResult::PaymentRetryRequired => self + PendingPayableScanResult::PaymentRetryRequired(response_skeleton_opt) => self .scan_schedulers .payable - .schedule_retry_payable_scan(ctx, &self.logger), + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + PendingPayableScanResult::ProcedureShouldBeRepeated(ui_msg_opt) => { + if let Some(node_to_ui_msg) = ui_msg_opt { + info!( + self.logger, + "Re-running the pending payable scan is recommended, as some \ + parts did not finish last time." + ); + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + // The repetition must be triggered by an external impulse + } else { + self.scan_schedulers + .pending_payable + .schedule(ctx, &self.logger) + } + } }; } } @@ -356,7 +382,7 @@ impl Handler for Accountant { let scan_result = self.scanners.finish_payable_scan(msg, &self.logger); match scan_result.ui_response_opt { - None => self.schedule_next_scan(scan_result.result, ctx), + None => self.schedule_next_automatic_scan(scan_result.result, ctx), Some(node_to_ui_msg) => { self.ui_message_sub_opt .as_ref() @@ -365,8 +391,8 @@ impl Handler for Accountant { .expect("UIGateway is dead"); // Externally triggered scans are not allowed to provoke an unwinding scan sequence - // with intervals. The only exception is the PendingPayableScanner and retry- - // payable scanner, which are ever meant to run in a tight tandem. + // with intervals. The only exception is the PendingPayableScanner that is always + // followed by the retry-payable scanner in a tight tandem. } } } @@ -389,31 +415,51 @@ impl Handler for Accountant { impl Handler for Accountant { type Result = (); - fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, scan_error: ScanError, ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); self.scanners .acknowledge_scan_error(&scan_error, &self.logger); - if let Some(response_skeleton) = scan_error.response_skeleton_opt { - let error_msg = NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: MessageBody { - opcode: "scan".to_string(), - path: MessagePath::Conversation(response_skeleton.context_id), - payload: Err(( - SCAN_ERROR, - format!( - "{:?} scan failed: '{}'", - scan_error.scan_type, scan_error.msg - ), - )), - }, - }; - error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway not bound") - .try_send(error_msg) - .expect("UiGateway is dead"); + + match scan_error.response_skeleton_opt { + None => match scan_error.scan_type { + DetailedScanType::NewPayables => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + DetailedScanType::RetryPayables => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, None, &self.logger), + DetailedScanType::PendingPayables => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + DetailedScanType::Receivables => { + self.scan_schedulers.receivable.schedule(ctx, &self.logger) + } + }, + Some(response_skeleton) => { + let error_msg = NodeToUiMessage { + target: ClientId(response_skeleton.client_id), + body: MessageBody { + opcode: "scan".to_string(), + path: MessagePath::Conversation(response_skeleton.context_id), + payload: Err(( + SCAN_ERROR, + format!( + "{:?} scan failed: '{}'", + scan_error.scan_type, scan_error.msg + ), + )), + }, + }; + error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway not bound") + .try_send(error_msg) + .expect("UiGateway is dead"); + } } } } @@ -460,7 +506,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable_fingerprints: Vec, + pub tx_hashes: Vec, pub response_skeleton_opt: Option, } @@ -470,14 +516,14 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); fn handle( &mut self, - msg: PendingPayableFingerprintSeeds, + msg: RegisterNewPendingPayables, _ctx: &mut Self::Context, ) -> Self::Result { - self.handle_new_pending_payable_fingerprints(msg) + self.register_new_pending_sent_tx(msg) } } @@ -510,13 +556,12 @@ impl Accountant { let earning_wallet = config.earning_wallet.clone(); let financial_statistics = Rc::new(RefCell::new(FinancialStatistics::default())); let payable_dao = dao_factories.payable_dao_factory.make(); - let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); + let sent_payable_dao = dao_factories.sent_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); let scan_schedulers = ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), - config.when_pending_too_long_sec, Rc::clone(&financial_statistics), ); @@ -525,7 +570,7 @@ impl Accountant { earning_wallet, payable_dao, receivable_dao, - pending_payable_dao, + sent_payable_dao, scanners, crashable: config.crash_point == CrashPoint::Message, scan_schedulers, @@ -552,8 +597,8 @@ impl Accountant { report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, PricedTemplatesMessage), 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), @@ -587,12 +632,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 ); } @@ -915,7 +960,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -935,7 +980,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::NewPayables, @@ -969,6 +1014,8 @@ impl Accountant { .expect("BlockchainBridge is dead"); } Err(e) => { + // It is thrown away and there is no rescheduling downstream because every error + // happening here on the start resolves into a panic by the current design let _ = self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::RetryPayables, e, @@ -981,7 +1028,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -994,14 +1041,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: ScanRescheduleAfterEarlyStop = match result { + let hint: ScanReschedulingAfterEarlyStop = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1027,7 +1074,7 @@ impl Accountant { scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error(&self.logger, scanner.into(), is_externally_triggered); @@ -1110,7 +1157,11 @@ impl Accountant { } } - fn schedule_next_scan(&self, next_scan_to_run: NextScanToRun, ctx: &mut Context) { + fn schedule_next_automatic_scan( + &self, + next_scan_to_run: NextScanToRun, + ctx: &mut Context, + ) { match next_scan_to_run { NextScanToRun::PendingPayableScan => self .scan_schedulers @@ -1123,31 +1174,29 @@ impl Accountant { NextScanToRun::RetryPayableScan => self .scan_schedulers .payable - .schedule_retry_payable_scan(ctx, &self.logger), + .schedule_retry_payable_scan(ctx, None, &self.logger), } } - 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 { + join_with_commas(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } - match self - .pending_payable_dao - .insert_new_fingerprints(&msg.hashes_and_balances, msg.batch_wide_timestamp) - { + + let sent_txs: BTreeSet = msg.new_sent_txs.iter().cloned().collect(); + + match self.sent_payable_dao.insert_new_records(&sent_txs) { Ok(_) => debug!( self.logger, - "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 ), } } @@ -1157,41 +1206,31 @@ impl Accountant { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingPayable { + pub recipient_wallet: Wallet, + pub hash: TxHash, +} + +impl PendingPayable { + pub fn new(recipient_wallet: Wallet, hash: TxHash) -> Self { + Self { + recipient_wallet, + hash, + } + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct PendingPayableId { pub rowid: u64, - pub hash: H256, + pub hash: TxHash, } impl PendingPayableId { - pub fn new(rowid: u64, hash: H256) -> Self { + pub fn new(rowid: u64, hash: TxHash) -> Self { Self { rowid, hash } } - - fn rowids(ids: &[Self]) -> Vec { - ids.iter().map(|id| id.rowid).collect() - } - - fn serialize_hashes_to_string(ids: &[Self]) -> String { - comma_joined_stringifiable(ids, |id| format!("{:?}", id.hash)) - } -} - -impl From for PendingPayableId { - fn from(pending_payable_fingerprint: PendingPayableFingerprint) -> Self { - Self { - hash: pending_payable_fingerprint.hash, - rowid: pending_payable_fingerprint.rowid, - } - } -} - -// TODO: Keep either comma_joined_stringifiable or join_with_separator after merge -pub fn comma_joined_stringifiable(collection: &[T], stringify: F) -> String -where - F: FnMut(&T) -> String, -{ - collection.iter().map(stringify).join(", ") } pub fn join_with_separator(collection: I, stringify: F, separator: &str) -> String @@ -1205,6 +1244,14 @@ where .join(separator) } +pub fn join_with_commas(collection: I, stringify: F) -> String +where + F: Fn(&T) -> String, + I: IntoIterator, +{ + join_with_separator(collection, stringify, ", ") +} + pub fn sign_conversion>(num: T) -> Result { S::try_from(num).map_err(|_| num) } @@ -1236,33 +1283,61 @@ pub fn wei_to_gwei, S: Display + Copy + Div + From> for Accountant { type Result = (); @@ -1337,9 +1408,8 @@ 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 failed_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let sent_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let receivable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); @@ -1350,16 +1420,15 @@ 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 PendingPayable Scanner - let failed_payable_dao_factory = FailedPayableDaoFactoryMock::new() - .make_params(&failed_payable_dao_factory_params_arc) - .make_result(FailedPayableDaoMock::new()); // For Payable Scanner let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() .make_params(&sent_payable_dao_factory_params_arc) - .make_result(SentPayableDaoMock::new()); // For Payable Scanner + .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()) // For Payable Scanner + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::new())); // For PendingPayableScanner; let receivable_dao_factory = ReceivableDaoFactoryMock::new() .make_params(&receivable_dao_factory_params_arc) .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1375,9 +1444,8 @@ mod tests { config, DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), - failed_payable_dao_factory: Box::new(failed_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), @@ -1389,16 +1457,12 @@ mod tests { vec![(), (), ()] ); assert_eq!( - *pending_payable_dao_factory_params_arc.lock().unwrap(), - vec![(), ()] + *sent_payable_dao_factory_params_arc.lock().unwrap(), + vec![(), (), ()] ); assert_eq!( *failed_payable_dao_factory_params_arc.lock().unwrap(), - vec![()] - ); - assert_eq!( - *sent_payable_dao_factory_params_arc.lock().unwrap(), - vec![()] + vec![(), ()] ); assert_eq!( *receivable_dao_factory_params_arc.lock().unwrap(), @@ -1410,22 +1474,25 @@ 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 PendingPayable Scanner - ); - let failed_payable_dao_factory = - Box::new(FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new())); // For Payable Scanner - let sent_payable_dao_factory = - Box::new(SentPayableDaoFactoryMock::new().make_result(SentPayableDaoMock::new())); // For Payable Scanner + let failed_payable_dao_factory = Box::new( + FailedPayableDaoFactoryMock::new() + .make_result(FailedPayableDaoMock::new()) // For Payable Scanner + .make_result(FailedPayableDaoMock::new()), + ); // For PendingPayable Scanner + let sent_payable_dao_factory = Box::new( + SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner + .make_result(SentPayableDaoMock::new()), + ); // For PendingPayable Scanner let receivable_dao_factory = Box::new( ReceivableDaoFactoryMock::new() .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1441,7 +1508,6 @@ mod tests { DaoFactories { payable_dao_factory, sent_payable_dao_factory, - pending_payable_dao_factory, failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, @@ -1450,7 +1516,7 @@ mod tests { ); let financial_statistics = result.financial_statistics().clone(); - let default_scan_intervals = ScanIntervals::default(); + let default_scan_intervals = ScanIntervals::compute_default(chain); assert_eq!( result.scan_schedulers.payable.new_payable_interval, default_scan_intervals.payable_scan_interval @@ -1533,7 +1599,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"); @@ -1601,19 +1667,15 @@ mod tests { } #[test] - fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { + fn sent_payables_with_response_skeleton_results_in_scan_response_to_ui_gateway() { let config = bc_from_earning_wallet(make_wallet("earning_wallet")); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(1, make_tx_hash(123))], - no_rowid_results: vec![], - }); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); let subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) - .sent_payable_dao(sent_payable_dao) + .sent_payable_daos(vec![DaoWithDestination::ForPayableScanner( + sent_payable_dao, + )]) .bootstrapper_config(config) .build(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); @@ -1884,20 +1946,17 @@ mod tests { receivable_scan_interval: Duration::from_millis(10_000), pending_payable_scan_interval: Duration::from_secs(100), }); - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let sent_tx = make_sent_tx(555); + let tx_hash = sent_tx.hash; + let sent_payable_dao = + SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::from([sent_tx])); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge @@ -1927,7 +1986,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable_fingerprints: vec![fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1946,12 +2005,22 @@ mod tests { let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transaction_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Ok(())); + let mut subject = AccountantBuilder::default().build(); + let mut sent_tx = make_sent_tx(123); + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let sent_payable_cache = + PendingPayableCacheMock::default().get_record_by_hash_result(Some(sent_tx.clone())); + let pending_payable_scanner = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Real( + pending_payable_scanner, + ))); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let ui_gateway = ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); @@ -1967,26 +2036,27 @@ mod tests { Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); let subject_addr = subject.start(); - let tx_fingerprint = make_pending_payable_fingerprint(); - let report_tx_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(777), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(456), - block_number: 78901234.into(), - }), - }), - tx_fingerprint.clone(), + let tx_block = TxBlock { + block_hash: make_tx_hash(456), + block_number: 78901234.into(), + }; + let tx_receipts_msg = TxReceiptsMessage { + results: btreemap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Succeeded(tx_block), )], response_skeleton_opt, }; - subject_addr.try_send(report_tx_receipts).unwrap(); + subject_addr.try_send(tx_receipts_msg).unwrap(); system.run(); let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transaction_confirmed_params, vec![vec![tx_fingerprint]]); + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, + }; + assert_eq!(*transaction_confirmed_params, vec![vec![sent_tx]]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2158,16 +2228,41 @@ mod tests { #[test] fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( ) { - // TODO when we have more logic in place with the other cards taken in, we'll need to configure these - // accordingly - let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]) - .mark_failures_result(Ok(())); + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao_for_payable_scanner = + PayableDaoMock::default().retrieve_payables_result(vec![]); + let payable_dao_for_pending_payable_scanner = + PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let sent_payable_dao_for_payable_scanner = SentPayableDaoMock::default() + // TODO should be removed with GH-701 + .insert_new_records_result(Ok(())); + let sent_payable_dao_for_pending_payable_scanner = SentPayableDaoMock::default() + .retrieve_txs_result(BTreeSet::from([sent_tx.clone()])) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let failed_tx = make_failed_tx(123); + let failed_payable_dao_for_payable_scanner = + FailedPayableDaoMock::default().retrieve_txs_result(btreeset!(failed_tx)); + let failed_payable_dao_for_pending_payable_scanner = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable)]) + .payable_daos(vec![ + ForPayableScanner(payable_dao_for_payable_scanner), + ForPendingPayableScanner(payable_dao_for_pending_payable_scanner), + ]) + .sent_payable_daos(vec![ + ForPayableScanner(sent_payable_dao_for_payable_scanner), + ForPendingPayableScanner(sent_payable_dao_for_pending_payable_scanner), + ]) + .failed_payable_daos(vec![ + ForPayableScanner(failed_payable_dao_for_payable_scanner), + ForPendingPayableScanner(failed_payable_dao_for_pending_payable_scanner), + ]) .build(); subject.scan_schedulers.automatic_scans_enabled = false; let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -2186,14 +2281,10 @@ mod tests { }); let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, - ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(234), - status: TxStatus::Failed - }), - make_pending_payable_fingerprint() - )], + TxReceiptsMessage { + results: btreemap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Reverted + ),], response_skeleton_opt }, &subject_addr @@ -2226,6 +2317,14 @@ mod tests { subject_addr.try_send(pending_payable_request).unwrap(); system.run(); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([expected_failed_tx])] + ); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![BTreeSet::from([tx_hash])]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2763,7 +2862,7 @@ mod tests { let system = System::new(test_name); let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); - let pp_fingerprint = make_pending_payable_fingerprint(); + let tx_hash = make_tx_hash(456); let retry_tx_templates = RetryTxTemplates(vec![make_retry_tx_template(1)]); let payable_scanner = ScannerMock::new() .scan_started_at_result(None) @@ -2785,11 +2884,11 @@ mod tests { .scan_started_at_result(None) .start_scan_params(&scan_params.pending_payable_start_scan) .start_scan_result(Ok(RequestTransactionReceipts { - pending_payable_fingerprints: vec![pp_fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, })) .finish_scan_params(&scan_params.pending_payable_finish_scan) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(None)); let receivable_scanner = ScannerMock::new() .scan_started_at_result(None) .start_scan_params(&scan_params.receivable_start_scan) @@ -2806,13 +2905,9 @@ mod tests { let (peer_actors, addresses) = peer_actors_builder().build_and_provide_addresses(); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); - let expected_report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(789), - status: TxStatus::Failed, - }), - make_pending_payable_fingerprint(), + let expected_tx_receipts_msg = TxReceiptsMessage { + results: btreemap![TxHashByTable::SentPayable(tx_hash) => Ok( + StatusReadFromReceiptCheck::Reverted, )], response_skeleton_opt: None, }; @@ -2830,7 +2925,7 @@ mod tests { }; let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, - expected_report_transaction_receipts.clone(), + expected_tx_receipts_msg.clone(), &subject_addr ); let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( @@ -2859,7 +2954,7 @@ mod tests { &scan_params, ¬ify_and_notify_later_params.pending_payables_notify_later, pending_payable_expected_notify_later_interval, - expected_report_transaction_receipts, + expected_tx_receipts_msg, before, after, ); @@ -2877,9 +2972,9 @@ mod tests { ¬ify_and_notify_later_params.receivables_notify_later, receivable_scan_interval, ); - // Given the assertions prove that the pending payable scanner has run multiple times - // before the new payable scanner started or was scheduled, the front position belongs to - // the one first mentioned, no doubts. + // Since the assertions proved that the pending payable scanner had run multiple times + // before the new payable scanner started or was scheduled, the front position definitely + // belonged to the one first mentioned. } #[derive(Default)] @@ -2889,10 +2984,9 @@ mod tests { payable_finish_scan: Arc>>, pending_payable_start_scan: Arc, Logger, String)>>>, - pending_payable_finish_scan: Arc>>, + pending_payable_finish_scan: Arc>>, receivable_start_scan: Arc, Logger, String)>>>, - // receivable_finish_scan ... not needed } #[derive(Default)] @@ -2911,7 +3005,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -2970,7 +3064,7 @@ mod tests { payable_scanner: ScannerMock, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -3024,7 +3118,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -3097,7 +3191,7 @@ mod tests { Mutex>, >, pending_payable_expected_notify_later_interval: Duration, - expected_report_tx_receipts_msg: ReportTransactionReceipts, + expected_tx_receipts_msg: TxReceiptsMessage, act_started_at: SystemTime, act_finished_at: SystemTime, ) { @@ -3110,12 +3204,9 @@ mod tests { assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); let mut pending_payable_finish_scan_params = scan_params.pending_payable_finish_scan.lock().unwrap(); - let (actual_report_tx_receipts_msg, pp_finish_scan_logger) = + let (actual_tx_receipts_msg, pp_finish_scan_logger) = pending_payable_finish_scan_params.remove(0); - assert_eq!( - actual_report_tx_receipts_msg, - expected_report_tx_receipts_msg - ); + assert_eq!(actual_tx_receipts_msg, expected_tx_receipts_msg); assert_using_the_same_logger(&pp_finish_scan_logger, test_name, Some("pp finish scan")); let scan_for_pending_payables_notify_later_params = scan_for_pending_payables_notify_later_params_arc @@ -3336,11 +3427,14 @@ mod tests { #[test] fn initial_pending_payable_scan_if_some_payables_found() { - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let sent_payable_dao = + SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::from([make_sent_tx(789)])); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let system = System::new("test"); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -3353,7 +3447,7 @@ mod tests { System::current().stop(); system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, ScanRescheduleAfterEarlyStop::DoNotSchedule); + assert_eq!(hint, ScanReschedulingAfterEarlyStop::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); @@ -3362,11 +3456,13 @@ mod tests { #[test] fn initial_pending_payable_scan_if_no_payables_found() { - let pending_payable_dao = - PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let flag_before = subject.scanners.initial_pending_payable_scan(); @@ -3375,7 +3471,7 @@ mod tests { let flag_after = subject.scanners.initial_pending_payable_scan(); assert_eq!( hint, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); assert_eq!(flag_before, true); assert_eq!(flag_after, false); @@ -3541,6 +3637,7 @@ mod tests { response_skeleton_opt: None, }; let transaction_hash = make_tx_hash(789); + let tx_hash = make_tx_hash(456); let creditor_wallet = make_wallet("blah"); let sent_tx = TxBuilder::default() .hash(transaction_hash) @@ -3554,23 +3651,16 @@ mod tests { payable_scan_type: PayableScanType::New, response_skeleton_opt: None, }; - let tx_receipt = TxReceipt { - transaction_hash, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(369369), - block_number: 4444444444u64.into(), - }), - }; - let pending_payable_fingerprint = make_pending_payable_fingerprint(); - let counter_msg_3 = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(tx_receipt), - pending_payable_fingerprint.clone(), - )], + let tx_status = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), + }); + let counter_msg_3 = TxReceiptsMessage { + results: btreemap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], response_skeleton_opt: None, }; let request_transaction_receipts_msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![pending_payable_fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, }; let qualified_payables_msg = InitialTemplatesMessage { @@ -3907,7 +3997,7 @@ mod tests { system.run(); assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); @@ -3997,7 +4087,7 @@ mod tests { fn start_scan_error_in_new_payables_and_unexpected_reaction_by_receivable_scan_scheduling() { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() - .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4107,41 +4197,40 @@ mod tests { } #[test] - fn scan_for_pending_payables_finds_still_pending_payables() { + fn scan_for_pending_payables_finds_various_payables() { init_test_logging(); + let test_name = "scan_for_pending_payables_finds_various_payables"; + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 2, - amount: 7999, - process_error: None, + let tx_hash_1 = make_tx_hash(456); + let tx_hash_2 = make_tx_hash(789); + let tx_hash_3 = make_tx_hash(123); + let expected_composed_msg_for_blockchain_bridge = RequestTransactionReceipts { + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + TxHashByTable::FailedPayable(tx_hash_3), + ], + response_skeleton_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![ - payable_fingerprint_1.clone(), - payable_fingerprint_2.clone(), - ]); - let config = bc_from_earning_wallet(make_wallet("mine")); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Ok(expected_composed_msg_for_blockchain_bridge.clone())); + let consuming_wallet = make_wallet("consuming"); let system = System::new("pending payable scan"); let mut subject = AccountantBuilder::default() - .consuming_wallet(make_paying_wallet(b"consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) + .logger(Logger::new(test_name)) .build(); - + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); @@ -4151,19 +4240,24 @@ mod tests { }) .unwrap(); + let before = SystemTime::now(); system.run(); + let after = SystemTime::now(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(before <= timestamp && timestamp <= after); + assert_eq!(response_skeleton_opt, None); + assert!( + start_scan_params.is_empty(), + "Should be empty but {:?}", + start_scan_params + ); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let received_msg = blockchain_bridge_recording.get_record::(0); - assert_eq!( - received_msg, - &RequestTransactionReceipts { - pending_payable_fingerprints: vec![payable_fingerprint_1, payable_fingerprint_2], - response_skeleton_opt: None, - } - ); + assert_eq!(received_msg, &expected_composed_msg_for_blockchain_bridge); assert_eq!(blockchain_bridge_recording.len(), 1); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); } #[test] @@ -4219,7 +4313,7 @@ mod tests { { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() - .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4323,7 +4417,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, )); } @@ -4368,7 +4462,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, )); } @@ -4460,7 +4554,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 )); } @@ -4505,7 +4599,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, )); } @@ -4513,7 +4607,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()) @@ -4774,8 +4868,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(); @@ -4855,7 +4949,7 @@ mod tests { expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" )] fn accountant_can_be_crashed_properly_but_not_improperly() { - let mut config = make_bc_with_defaults(); + let mut config = make_bc_with_defaults(TEST_DEFAULT_CHAIN); config.crash_point = CrashPoint::Message; let accountant = AccountantBuilder::default() .bootstrapper_config(config) @@ -4866,23 +4960,24 @@ mod tests { #[test] fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { - let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); + // let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let inserted_new_records_params_arc = Arc::new(Mutex::new(vec![])); - let expected_wallet = make_wallet("paying_you"); let expected_hash = H256::from("transaction_hash".keccak256()); - let payable_dao = PayableDaoMock::new() - .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) - .mark_pending_payables_rowids_result(Ok(())); + let payable_dao = PayableDaoMock::new(); let sent_payable_dao = SentPayableDaoMock::new() .insert_new_records_params(&inserted_new_records_params_arc) .insert_new_records_result(Ok(())); + // let expected_rowid = 45623; + // let sent_payable_dao = SentPayableDaoMock::default() + // .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + // .get_tx_identifiers_result(hashmap! (expected_hash => expected_rowid)); let system = System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .payable_daos(vec![ForPayableScanner(payable_dao)]) - .sent_payable_dao(sent_payable_dao) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .build(); let pending_payable_interval = Duration::from_millis(55); subject.scan_schedulers.pending_payable.interval = pending_payable_interval; @@ -4890,6 +4985,12 @@ mod tests { NotifyLaterHandleMock::default() .notify_later_params(&pending_payable_notify_later_params_arc), ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); let expected_tx = TxBuilder::default().hash(expected_hash.clone()).build(); let sent_payable = SentPayables { payment_procedure_result: Ok(BatchResults { @@ -4916,21 +5017,14 @@ mod tests { *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 accountant_finishes_processing_of_retry_payables_and_schedules_pending_payable_scanner() { - let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let inserted_new_records_params_arc = Arc::new(Mutex::new(vec![])); - let expected_wallet = make_wallet("paying_you"); let expected_hash = H256::from("transaction_hash".keccak256()); - let payable_dao = PayableDaoMock::new() - .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) - .mark_pending_payables_rowids_result(Ok(())); + let payable_dao = PayableDaoMock::new(); let sent_payable_dao = SentPayableDaoMock::new() .insert_new_records_params(&inserted_new_records_params_arc) .insert_new_records_result(Ok(())); @@ -4941,8 +5035,8 @@ mod tests { let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .payable_daos(vec![ForPayableScanner(payable_dao)]) - .failed_payable_dao(failed_payble_dao) - .sent_payable_dao(sent_payable_dao) + .failed_payable_daos(vec![ForPayableScanner(failed_payble_dao)]) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .build(); let pending_payable_interval = Duration::from_millis(55); subject.scan_schedulers.pending_payable.interval = pending_payable_interval; @@ -4950,6 +5044,12 @@ mod tests { NotifyLaterHandleMock::default() .notify_later_params(&pending_payable_notify_later_params_arc), ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); let expected_tx = TxBuilder::default().hash(expected_hash.clone()).build(); let sent_payable = SentPayables { payment_procedure_result: Ok(BatchResults { @@ -4976,70 +5076,6 @@ mod tests { *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: NextScanToRun::NewPayableScan, - }), - ))); - // 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("Sending error".to_string()), - payable_scan_type: PayableScanType::New, - 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] @@ -5102,10 +5138,11 @@ mod tests { } #[test] - fn accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed() { + fn accountant_in_automatic_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed() + { init_test_logging(); let test_name = - "accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed"; + "accountant_in_automatic_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default() @@ -5113,22 +5150,32 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(None)); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.retry_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); let system = System::new(test_name); - let (mut msg, _) = - make_report_transaction_receipts_msg(vec![TxStatus::Pending, TxStatus::Failed]); - let response_skeleton_opt = Some(ResponseSkeleton { - client_id: 45, - context_id: 7, - }); - msg.response_skeleton_opt = response_skeleton_opt; + 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, + }, + ]); + msg.response_skeleton_opt = None; let subject_addr = subject.start(); subject_addr.try_send(msg.clone()).unwrap(); @@ -5142,28 +5189,212 @@ mod tests { assert_eq!( *retry_payable_notify_params, vec![ScanForRetryPayables { - response_skeleton_opt + response_skeleton_opt: None }] ); assert_using_the_same_logger(&logger, test_name, None) } #[test] - fn accountant_confirms_payable_txs_and_schedules_the_new_payable_scanner_timely() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); - let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); - let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + fn accountant_reschedules_pending_p_scanner_in_automatic_mode_after_receipt_fetching_failed() { + init_test_logging(); + let test_name = + "accountant_reschedules_pending_p_scanner_in_automatic_mode_after_receipt_fetching_failed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::ProcedureShouldBeRepeated(None)); + 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: btreemap!(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_reschedules_pending_p_scanner_in_manual_mode_after_receipt_fetching_failed() { + init_test_logging(); + let test_name = + "accountant_reschedules_pending_p_scanner_in_manual_mode_after_receipt_fetching_failed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let expected_node_to_ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(1234), + body: UiScanResponse {}.tmb(54), + }; + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::ProcedureShouldBeRepeated(Some( + expected_node_to_ui_msg.clone(), + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let interval = Duration::from_secs(20); + subject.scan_schedulers.pending_payable.interval = interval; + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.ui_message_sub_opt = Some(ui_gateway.start().recipient()); + let system = System::new(test_name); + let response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 54, + }; + let msg = TxReceiptsMessage { + results: btreemap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: Some(response_skeleton), + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let node_to_ui_msg = ui_gateway_recording.get_record::(0); + assert_eq!(node_to_ui_msg, &expected_node_to_ui_msg); + assert_eq!(ui_gateway_recording.len(), 1); + assert_using_the_same_logger(&logger, test_name, None); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Re-running the pending payable scan is recommended, as some parts \ + did not finish last time." + )); + } + + #[test] + fn accountant_in_manual_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed() { + init_test_logging(); + let test_name = + "accountant_in_manual_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed"; + let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let response_skeleton = ResponseSkeleton { + client_id: 123, + context_id: 333, + }; + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(Some( + response_skeleton, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let system = System::new(test_name); + let msg = TxReceiptsMessage { + results: btreemap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: Some(response_skeleton), + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + System::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: Some(response_skeleton) + }] + ); + assert_using_the_same_logger(&logger, test_name, None) + } + + #[test] + fn accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely() { + init_test_logging(); + let test_name = + "accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); let system = System::new("new_payable_scanner_timely"); let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(3)) .unwrap(); @@ -5187,23 +5418,36 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(123), - block_number: U64::from(100), - }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(555)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); System::current().stop(); system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); let (_, last_new_payable_timestamp_actual, scan_interval_actual) = compute_interval_params.remove(0); @@ -5228,19 +5472,24 @@ mod tests { #[test] fn accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + init_test_logging(); + let test_name = + "accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(8)) .unwrap(); @@ -5262,25 +5511,34 @@ mod tests { ); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let tx_block_1 = make_transaction_block(4567); + let tx_block_2 = make_transaction_block(1234); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(123), - block_number: U64::from(100), - }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_1), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_2), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); - let system = System::new("new_payable_scanner_asap"); + let system = System::new(test_name); System::current().stop(); system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); let (_, last_new_payable_timestamp_actual, scan_interval_actual) = compute_interval_params.remove(0); @@ -5297,20 +5555,23 @@ mod tests { new_payable_notify_later ); let new_payable_notify = new_payable_notify_arc.lock().unwrap(); - assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]) + assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]); } #[test] fn scheduler_for_new_payables_operates_with_proper_now_timestamp() { let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let system = System::new("scheduler_for_new_payables_operates_with_proper_now_timestamp"); + let test_name = "scheduler_for_new_payables_operates_with_proper_now_timestamp"; let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_millis(3500)) .unwrap(); @@ -5326,17 +5587,15 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); + let system = System::new(test_name); let subject_addr = subject.start(); - let (msg, _) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { + let (msg, _) = make_tx_receipts_msg(vec![SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { block_hash: make_tx_hash(123), block_number: U64::from(100), }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), - ]); + }]); subject_addr.try_send(msg).unwrap(); @@ -5366,126 +5625,153 @@ mod tests { ); } - fn make_report_transaction_receipts_msg( - status_txs: Vec, - ) -> (ReportTransactionReceipts, Vec) { - let (receipt_result_fingerprint_pairs, fingerprints): (Vec<_>, Vec<_>) = status_txs - .into_iter() - .enumerate() - .map(|(idx, status)| { - let transaction_hash = make_tx_hash(idx as u32); - let transaction_receipt_result = TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash, - status, - }); - let fingerprint = PendingPayableFingerprint { - rowid: idx as u64, - timestamp: from_unix_timestamp(1_000_000_000 * idx as i64), - hash: transaction_hash, - attempt: 2, - amount: 1_000_000 * idx as u128 * idx as u128, - process_error: None, - }; - ( - (transaction_receipt_result, fingerprint.clone()), - fingerprint, - ) - }) - .unzip(); + pub struct SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + } - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: receipt_result_fingerprint_pairs, + fn make_tx_receipts_msg( + seeds: Vec, + ) -> (TxReceiptsMessage, Vec) { + let (tx_receipt_results, tx_record_vec) = seeds.into_iter().enumerate().fold( + (btreemap![], vec![]), + |(mut tx_receipt_results, mut record_by_table_vec), (idx, seed_params)| { + let tx_hash = seed_params.tx_hash; + let status = seed_params.status; + 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, fingerprints) + (msg, tx_record_vec) + } + + fn make_receipt_check_result_and_record( + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + idx: u64, + ) -> (TxHashByTable, TxReceiptResult, TxByTable) { + match tx_hash { + TxHashByTable::SentPayable(hash) => { + let mut sent_tx = make_sent_tx((1 + idx) as u32); + 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 as u32); + 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(); + let insert_new_records_params = insert_new_records_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", + *insert_new_records_params, + vec![BTreeSet::from([sent_tx_1, sent_tx_2])] ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Registered new pending payables for: \ + 0x000000000000000000000000000000000000000000000000000000000006c81c, \ + 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(); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); assert_eq!( - *insert_fingerprint_params, - vec![(vec![hash_and_amount], timestamp)] + *insert_new_records_params, + vec![BTreeSet::from([sent_tx_1, sent_tx_2])] ); - 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"); + 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 { @@ -5495,75 +5781,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(), ); } @@ -5653,7 +6134,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(); @@ -6312,15 +6793,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(); - subject - .scanners - .reset_scan_started(message.scan_type, MarkScanner::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(); @@ -6331,13 +6826,15 @@ mod tests { subject_addr .try_send(AssertionsMessage { assertions: Box::new(move |actor: &mut Accountant| { - let scan_started_at_opt = actor.scanners.scan_started_at(message.scan_type); + let scan_started_at_opt = + actor.scanners.scan_started_at(message.scan_type.into()); assert_eq!(scan_started_at_opt, None); }), }) .unwrap(); System::current().stop(); assert_eq!(system.run(), 0); + run_schedulers_assertions(message.response_skeleton_opt); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); match message.response_skeleton_opt { Some(response_skeleton) => { @@ -6417,7 +6914,7 @@ pub mod exportable_test_parts { check_if_source_code_is_attached, ensure_node_home_directory_exists, ShouldWeRunTheTest, }; use regex::Regex; - use std::collections::{BTreeSet, HashSet}; + use std::collections::BTreeSet; use std::env::current_dir; use std::fs::File; use std::io::{BufRead, BufReader}; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index d58f213689..a11813615e 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -6,18 +6,7 @@ pub mod receivable_scanner; pub mod scan_schedulers; pub mod test_utils; -use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::Submission; -use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedPayableDao, FailedPayableDaoError, FailedTx, FailureReason, FailureStatus, -}; -use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayable, PendingPayableDao, -}; -use crate::accountant::db_access_objects::receivable_dao::ReceivableDao; -use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::ByHash; -use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, Tx}; -use crate::accountant::db_access_objects::utils::{RowId, TxHash, TxIdentifiers}; -use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; +use crate::accountant::payment_adjuster::PaymentAdjusterReal; use crate::accountant::scanners::payable_scanner::msgs::{ InitialTemplatesMessage, PricedTemplatesMessage, }; @@ -25,51 +14,41 @@ use crate::accountant::scanners::payable_scanner::payment_adjuster_integration:: use crate::accountant::scanners::payable_scanner::utils::{NextScanToRun, PayableScanResult}; use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PayableScanner}; use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; -use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; -use crate::accountant::scanners::receivable_scanner::ReceivableScanner; -use crate::accountant::{ - PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables, +use crate::accountant::scanners::pending_payable_scanner::{ + ExtendedPendingPayablePrivateScanner, PendingPayableScanner, }; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::{ - ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, - ScanForNewPayables, ScanForReceivables, SentPayables, + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, ScanError, ScanForNewPayables, + ScanForReceivables, ScanForRetryPayables, SentPayables, TxReceiptsMessage, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; -use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError; use crate::db_config::persistent_configuration::PersistentConfigurationReal; -use crate::sub_lib::accountant::{DaoFactories, FinancialStatistics, PaymentThresholds}; +use crate::sub_lib::accountant::{ + DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, +}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; use actix::Message; -use itertools::{Either, Itertools}; +use itertools::Either; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; -use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; -use masq_lib::utils::ExpectValue; +use masq_lib::messages::ScanType; +use masq_lib::ui_gateway::NodeToUiMessage; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::rc::Rc; use std::time::SystemTime; use time::format_description::parse; use time::OffsetDateTime; use variant_count::VariantCount; -use web3::types::H256; // Leave the individual scanner objects private! pub struct Scanners { payable: Box, aware_of_unresolved_pending_payable: bool, initial_pending_payable_scan: bool, - pending_payable: Box< - dyn PrivateScanner< - ScanForPendingPayables, - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - >, - >, + pending_payable: Box, receivable: Box< dyn PrivateScanner< ScanForReceivables, @@ -84,7 +63,6 @@ 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( @@ -97,9 +75,9 @@ impl Scanners { 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), )); @@ -223,6 +201,7 @@ impl Scanners { } (None, None) => (), } + self.pending_payable .start_scan(wallet, timestamp, response_skeleton_opt, logger) } @@ -254,16 +233,15 @@ impl Scanners { pub fn finish_payable_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { let scan_result = self.payable.finish_scan(msg, logger); - match scan_result.result { - NextScanToRun::PendingPayableScan => self.aware_of_unresolved_pending_payable = true, - _ => (), - }; + if scan_result.result == NextScanToRun::PendingPayableScan { + self.aware_of_unresolved_pending_payable = true + } scan_result } pub fn finish_pending_payable_scan( &mut self, - msg: ReportTransactionReceipts, + msg: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { self.pending_payable.finish_scan(msg, logger) @@ -279,18 +257,23 @@ impl Scanners { pub fn acknowledge_scan_error(&mut self, error: &ScanError, logger: &Logger) { match error.scan_type { - ScanType::Payables => { - self.payable.mark_as_ended(logger); + DetailedScanType::NewPayables | DetailedScanType::RetryPayables => { + self.payable.mark_as_ended(logger) } - ScanType::PendingPayables => { + DetailedScanType::PendingPayables => { + self.empty_caches(logger); self.pending_payable.mark_as_ended(logger); } - ScanType::Receivables => { + DetailedScanType::Receivables => { self.receivable.mark_as_ended(logger); } }; } + fn empty_caches(&mut self, logger: &Logger) { + self.pending_payable.empty_caches(logger) + } + pub fn try_skipping_payable_adjustment( &self, msg: PricedTemplatesMessage, @@ -316,7 +299,7 @@ impl Scanners { } // This is a helper function reducing a boilerplate of complex trait resolving where - // the compiler requires to specify which trigger message distinguish the scan to run. + // the compiler requires to specify which trigger message distinguishes the scan to run. // The payable scanner offers two modes through doubled implementations of StartableScanner // which uses the trigger message type as the only distinction between them. fn start_correct_payable_scanner<'a, TriggerMessage>( @@ -403,7 +386,6 @@ where fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); fn mark_as_ended(&mut self, logger: &Logger); - as_any_ref_in_trait!(); as_any_mut_in_trait!(); } @@ -436,7 +418,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 ); } @@ -584,9 +566,12 @@ impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScann #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::payable_dao::PayableAccount; + 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::test_utils::{make_failed_tx, make_sent_tx}; - use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; @@ -595,47 +580,53 @@ mod tests { }; use crate::accountant::scanners::payable_scanner::utils::PayableScanResult; use crate::accountant::scanners::payable_scanner::PayableScanner; - use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, TxHashByTable, + }; + use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; + use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::scanners::test_utils::{ assert_timestamps_from_str, parse_system_time_from_str, - trim_expected_timestamp_to_three_digits_nanos, MarkScanner, NullScanner, ReplacementType, - ScannerReplacement, + trim_expected_timestamp_to_three_digits_nanos, MarkScanner, NullScanner, + PendingPayableCacheMock, ReplacementType, ScannerReplacement, }; use crate::accountant::scanners::{ - ManulTriggerError, PendingPayableScanner, ReceivableScanner, Scanner, ScannerCommon, - Scanners, StartScanError, StartableScanner, + ManulTriggerError, Scanner, ScannerCommon, Scanners, StartScanError, StartableScanner, }; use crate::accountant::test_utils::{ - make_custom_payment_thresholds, make_payable_account, make_pending_payable_fingerprint, - make_qualified_and_unqualified_payables, make_receivable_account, BannedDaoFactoryMock, - BannedDaoMock, ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, FailedPayableDaoMock, - PayableDaoFactoryMock, PayableDaoMock, PayableThresholdsGaugeMock, - PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, - ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder, - SentPayableDaoFactoryMock, SentPayableDaoMock, + make_custom_payment_thresholds, make_qualified_and_unqualified_payables, + make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, + FailedPayableDaoFactoryMock, FailedPayableDaoMock, PayableDaoFactoryMock, PayableDaoMock, + PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, + ReceivableScannerBuilder, SentPayableDaoFactoryMock, SentPayableDaoMock, }; use crate::accountant::{ - gwei_to_wei, PayableScanType, ReceivedPayments, ReportTransactionReceipts, - RequestTransactionReceipts, ResponseSkeleton, ScanError, SentPayables, - }; - use crate::blockchain::blockchain_bridge::{ - BlockMarker, PendingPayableFingerprint, RetrieveTransactions, + PayableScanType, ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, ScanError, + SentPayables, TxReceiptsMessage, }; + use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::{ - BatchResults, BlockchainTransaction, + BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck, TxBlock, + }; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, RemoteError, RemoteErrorKind, }; - use crate::blockchain::test_utils::make_tx_hash; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::mocks::ConfigDaoMock; use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, + DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::Message; + use ethereum_types::U64; use itertools::Either; use masq_lib::logger::Logger; use masq_lib::messages::ScanType; @@ -645,6 +636,7 @@ mod tests { use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; use std::collections::BTreeSet; + use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; use std::sync::{Arc, Mutex}; @@ -732,9 +724,6 @@ mod tests { let payable_dao_factory = PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()); - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()); let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() .make_result(SentPayableDaoMock::new()) .make_result(SentPayableDaoMock::new()); @@ -749,7 +738,6 @@ mod tests { .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, @@ -761,7 +749,6 @@ 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), @@ -769,7 +756,6 @@ mod tests { 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())), ); @@ -780,8 +766,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 @@ -795,10 +781,6 @@ mod tests { assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!(scanners.aware_of_unresolved_pending_payable, false); assert_eq!(scanners.initial_pending_payable_scan, true); - assert_eq!( - pending_payable_scanner.when_pending_too_long_sec, - when_pending_too_long_sec - ); assert_eq!( *pending_payable_scanner.financial_statistics.borrow(), financial_statistics @@ -811,6 +793,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 @@ -1077,15 +1072,30 @@ mod tests { #[test] fn finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err( + ) { + test_finish_payable_scan_keeps_aware_flag_false_on_error(PayableScanType::New, "new_scan"); + test_finish_payable_scan_keeps_aware_flag_false_on_error( + PayableScanType::Retry, + "retry_scan", + ); + } + + fn test_finish_payable_scan_keeps_aware_flag_false_on_error( + payable_scan_type: PayableScanType, + test_name_str: &str, ) { init_test_logging(); - let test_name = "finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err"; + let test_name = format!( + "finish_payable_scan_keeps_the_aware_of_unresolved_\ + pending_payable_flag_as_false_in_case_of_err_for_\ + {test_name_str}" + ); let sent_payable = SentPayables { payment_procedure_result: Err("Some error".to_string()), - payable_scan_type: PayableScanType::New, + payable_scan_type, response_skeleton_opt: None, }; - let logger = Logger::new(test_name); + let logger = Logger::new(&test_name); let payable_scanner = PayableScannerBuilder::new().build(); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); @@ -1103,41 +1113,6 @@ mod tests { )); } - #[test] - fn finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found( - ) { - init_test_logging(); - let test_name = "finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found"; - let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); - let payable_scanner = PayableScannerBuilder::new() - .sent_payable_dao(sent_payable_dao) - .build(); - let logger = Logger::new(test_name); - let mut subject = make_dull_subject(); - subject.payable = Box::new(payable_scanner); - let sent_payables = SentPayables { - payment_procedure_result: Ok(BatchResults { - sent_txs: vec![make_sent_tx(1)], - failed_txs: vec![], - }), - payable_scan_type: PayableScanType::New, - response_skeleton_opt: None, - }; - let aware_of_unresolved_pending_payable_before = - subject.aware_of_unresolved_pending_payable; - - subject.finish_payable_scan(sent_payables, &logger); - - let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; - assert_eq!(aware_of_unresolved_pending_payable_before, false); - assert_eq!(aware_of_unresolved_pending_payable_after, true); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!( - "DEBUG: {test_name}: Processed new txs while sending to RPC: \ - Total: 1, Sent to RPC: 1, Failed to send: 0." - )); - } - #[test] fn finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found_in_retry_mode( ) { @@ -1176,254 +1151,25 @@ mod tests { )); } - #[test] - fn payable_is_found_innocent_by_age_and_returns() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result(true); - let mut subject = PayableScannerBuilder::new().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - let now = SystemTime::now(); - let debt_age_s = 111_222; - let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); - let mut payable = make_payable_account(111); - payable.last_paid_timestamp = last_paid_timestamp; - - let result = subject.payable_exceeded_threshold(&payable, now); - - assert_eq!(result, None); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age_returned, threshold_value) = is_innocent_age_params.remove(0); - assert!(is_innocent_age_params.is_empty()); - assert_eq!(debt_age_returned, debt_age_s); - assert_eq!( - threshold_value, - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec - ) - // No panic and so no other method was called, which means an early return - } - - #[test] - fn payable_is_found_innocent_by_balance_and_returns() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result(false) - .is_innocent_balance_params(&is_innocent_balance_params_arc) - .is_innocent_balance_result(true); - let mut subject = PayableScannerBuilder::new().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - let now = SystemTime::now(); - let debt_age_s = 3_456; - let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); - let mut payable = make_payable_account(222); - payable.last_paid_timestamp = last_paid_timestamp; - payable.balance_wei = 123456; - - let result = subject.payable_exceeded_threshold(&payable, now); - - assert_eq!(result, None); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age_returned, _) = is_innocent_age_params.remove(0); - assert!(is_innocent_age_params.is_empty()); - assert_eq!(debt_age_returned, debt_age_s); - let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); - assert_eq!( - *is_innocent_balance_params, - vec![( - 123456_u128, - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) - )] - ) - //no other method was called (absence of panic) and that means we returned early - } - - #[test] - fn threshold_calculation_depends_on_user_defined_payment_thresholds() { - let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); - let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); - let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); - let balance = gwei_to_wei(5555_u64); - let now = SystemTime::now(); - let debt_age_s = 1111 + 1; - let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); - let payable_account = PayableAccount { - wallet: make_wallet("hi"), - balance_wei: balance, - last_paid_timestamp, - pending_payable_opt: None, - }; - let custom_payment_thresholds = PaymentThresholds { - maturity_threshold_sec: 1111, - payment_grace_period_sec: 2222, - permanent_debt_allowed_gwei: 3333, - debt_threshold_gwei: 4444, - threshold_interval_sec: 5555, - unban_below_gwei: 5555, - }; - let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() - .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result( - debt_age_s <= custom_payment_thresholds.maturity_threshold_sec as u64, - ) - .is_innocent_balance_params(&is_innocent_balance_params_arc) - .is_innocent_balance_result( - balance <= gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei), - ) - .calculate_payout_threshold_in_gwei_params(&calculate_payable_threshold_params_arc) - .calculate_payout_threshold_in_gwei_result(4567898); //made up value - let mut subject = PayableScannerBuilder::new() - .payment_thresholds(custom_payment_thresholds) - .build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); - - let result = subject.payable_exceeded_threshold(&payable_account, now); - - assert_eq!(result, Some(4567898)); - let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); - let (debt_age_returned_innocent, curve_derived_time) = is_innocent_age_params.remove(0); - assert_eq!(*is_innocent_age_params, vec![]); - assert_eq!(debt_age_returned_innocent, debt_age_s); - assert_eq!( - curve_derived_time, - custom_payment_thresholds.maturity_threshold_sec as u64 - ); - let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); - assert_eq!( - *is_innocent_balance_params, - vec![( - payable_account.balance_wei, - gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei) - )] - ); - let mut calculate_payable_curves_params = - calculate_payable_threshold_params_arc.lock().unwrap(); - let (payment_thresholds, debt_age_returned_curves) = - calculate_payable_curves_params.remove(0); - assert_eq!(*calculate_payable_curves_params, vec![]); - assert_eq!(debt_age_returned_curves, debt_age_s); - assert_eq!(payment_thresholds, custom_payment_thresholds) - } - - #[test] - fn payable_with_debt_under_the_slope_is_marked_unqualified() { - init_test_logging(); - let now = SystemTime::now(); - let payment_thresholds = PaymentThresholds::default(); - let debt = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); - let time = to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; - let unqualified_payable_account = vec![PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: debt, - last_paid_timestamp: from_unix_timestamp(time), - pending_payable_opt: None, - }]; - let subject = PayableScannerBuilder::new() - .payment_thresholds(payment_thresholds) - .build(); - let test_name = - "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; - let logger = Logger::new(test_name); - - let result = subject - .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); - - assert_eq!(result, vec![]); - TestLogHandler::new() - .exists_no_log_containing(&format!("DEBUG: {}: Paying qualified debts", test_name)); - } - - #[test] - fn payable_with_debt_above_the_slope_is_qualified() { - init_test_logging(); - let payment_thresholds = PaymentThresholds::default(); - let debt = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); - let time = (payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec - - 1) as i64; - let qualified_payable = PayableAccount { - wallet: make_wallet("wallet0"), - balance_wei: debt, - last_paid_timestamp: from_unix_timestamp(time), - pending_payable_opt: None, - }; - let subject = PayableScannerBuilder::new() - .payment_thresholds(payment_thresholds) - .build(); - let test_name = "payable_with_debt_above_the_slope_is_qualified"; - let logger = Logger::new(test_name); - - let result = subject.sniff_out_alarming_payables_and_maybe_log_them( - vec![qualified_payable.clone()], - &logger, - ); - - assert_eq!(result, vec![qualified_payable]); - TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {}: Paying qualified debts:\n999,999,999,000,000,\ - 000 wei owed for \\d+ sec exceeds threshold: 500,000,000,000,000,000 wei; creditor: \ - 0x0000000000000000000000000077616c6c657430", - test_name - )); - } - - #[test] - fn retrieved_payables_turn_into_an_empty_vector_if_all_unqualified() { - init_test_logging(); - let test_name = "retrieved_payables_turn_into_an_empty_vector_if_all_unqualified"; - let now = SystemTime::now(); - let payment_thresholds = PaymentThresholds::default(); - let unqualified_payable_account = vec![PayableAccount { - wallet: make_wallet("wallet1"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), - last_paid_timestamp: from_unix_timestamp( - to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, - ), - pending_payable_opt: None, - }]; - let subject = PayableScannerBuilder::new() - .payment_thresholds(payment_thresholds) - .build(); - let logger = Logger::new(test_name); - - let result = subject - .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); - - assert_eq!(result, vec![]); - TestLogHandler::new() - .exists_no_log_containing(&format!("DEBUG: {test_name}: Paying qualified debts")); - } - #[test] fn 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_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 1, - amount: 7999, - process_error: None, - }; - let fingerprints = vec![payable_fingerprint_1, payable_fingerprint_2]; - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(fingerprints.clone()); + let sent_tx = make_sent_tx(456); + let sent_tx_hash = sent_tx.hash; + let failed_tx = make_failed_tx(789); + let sent_payable_dao = + SentPayableDaoMock::new().retrieve_txs_result(btreeset![sent_tx.clone()]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([failed_tx.clone()])); let mut subject = make_dull_subject(); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -1439,21 +1185,21 @@ mod tests { true, ); - let no_of_pending_payables = fingerprints.len(); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, Ok(RequestTransactionReceipts { - pending_payable_fingerprints: fingerprints, + tx_hashes: vec![ + TxHashByTable::SentPayable(sent_tx_hash), + TxHashByTable::FailedPayable(failed_tx.hash) + ], response_skeleton_opt: None }) ); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!("INFO: {test_name}: Scanning for pending payable"), - &format!( - "DEBUG: {test_name}: Found {no_of_pending_payables} pending payables to process" - ), + &format!("DEBUG: {test_name}: Found 1 pending payables and 1 unfinalized failures to process"), ]) } @@ -1462,10 +1208,15 @@ mod tests { let now = SystemTime::now(); let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = make_dull_subject(); - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let sent_payable_dao = + SentPayableDaoMock::new().retrieve_txs_result(btreeset![make_sent_tx(123)]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([make_failed_tx(456)])); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -1608,24 +1359,6 @@ mod tests { ); } - #[test] - fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { - let now = SystemTime::now(); - let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let pending_payable_dao = - PendingPayableDaoMock::new().return_all_errorless_fingerprints_result(vec![]); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - let result = - pending_payable_scanner.start_scan(&consuming_wallet, now, None, &Logger::new("test")); - - let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); - assert_eq!(result, Err(StartScanError::NothingToProcess)); - assert_eq!(is_scan_running, false); - } - #[test] fn check_general_conditions_for_pending_payable_scan_if_it_is_initial_pending_payable_scan() { let mut subject = make_dull_subject(); @@ -1638,119 +1371,188 @@ mod tests { } #[test] - fn pending_payable_scanner_handles_report_transaction_receipts_message() { - // TODO: Bert would like to fight this one - // init_test_logging(); - // let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message"; - // let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - // 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 pending_payable_scanner = 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_unix_timestamp(200_000_000), - // hash: transaction_hash_1, - // attempt: 2, - // amount: 444, - // process_error: None, - // }; - // let transaction_hash_2 = make_tx_hash(1234); - // let transaction_receipt_2 = TxReceipt { - // transaction_hash: transaction_hash_2, - // status: TxStatus::Succeeded(TransactionBlock { - // block_hash: Default::default(), - // block_number: U64::from(2345), - // }), - // }; - // let fingerprint_2 = PendingPayableFingerprint { - // rowid: 10, - // timestamp: from_unix_timestamp(199_780_000), - // hash: transaction_hash_2, - // attempt: 15, - // amount: 1212, - // process_error: None, - // }; - // let msg = ReportTransactionReceipts { - // fingerprints_with_receipts: vec![ - // ( - // TransactionReceiptResult::RpcResponse(transaction_receipt_1), - // fingerprint_1.clone(), - // ), - // ( - // TransactionReceiptResult::RpcResponse(transaction_receipt_2), - // fingerprint_2.clone(), - // ), - // ], - // response_skeleton_opt: None, - // }; - // pending_payable_scanner.mark_as_started(SystemTime::now()); - // let mut subject = make_dull_subject(); - // subject.pending_payable = Box::new(pending_payable_scanner); - // - // let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - // - // let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - // assert_eq!( - // result, - // PendingPayableScanResult::NoPendingPayablesLeft(None) - // ); - // assert_eq!( - // *transactions_confirmed_params, - // vec![vec![fingerprint_1, fingerprint_2]] - // ); - // assert_eq!(subject.scan_started_at(ScanType::PendingPayables), None); - // TestLogHandler::new().assert_logs_match_in_order(vec![ - // &format!( - // "INFO: {}: Transactions {:?}, {:?} completed their confirmation process succeeding", - // test_name, transaction_hash_1, transaction_hash_2 - // ), - // &format!("INFO: {test_name}: The PendingPayables scan ended in \\d+ms."), - // ]); + 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_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 sent_payable_dao = SentPayableDaoMock::new() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .update_statuses_params(&update_statuses_pending_payable_params_arc) + .update_statuses_result(Ok(())) + .replace_records_result(Ok(())) + .delete_records_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::new() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())) + .update_statuses_params(&update_statuses_failed_payable_params_arc) + .update_statuses_result(Ok(())) + .delete_records_result(Ok(())); + let tx_hash_1 = make_tx_hash(0x111); + let mut sent_tx_1 = make_sent_tx(123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(333), + block_number: U64::from(1234), + }; + let tx_status_1 = StatusReadFromReceiptCheck::Succeeded(tx_block_1); + let tx_hash_2 = make_tx_hash(0x222); + let mut failed_tx_2 = make_failed_tx(789); + failed_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(222), + block_number: U64::from(2345), + }; + let tx_status_2 = StatusReadFromReceiptCheck::Succeeded(tx_block_2); + let tx_hash_3 = make_tx_hash(0x333); + let mut sent_tx_3 = make_sent_tx(456); + sent_tx_3.hash = tx_hash_3; + let tx_status_3 = StatusReadFromReceiptCheck::Pending; + let tx_hash_4 = make_tx_hash(0x444); + let mut sent_tx_4 = make_sent_tx(4567); + sent_tx_4.hash = tx_hash_4; + sent_tx_4.status = TxStatus::Pending(ValidationStatus::Waiting); + let tx_receipt_rpc_error_4 = AppRpcError::Remote(RemoteError::Unreachable); + let tx_hash_5 = make_tx_hash(0x555); + let mut failed_tx_5 = make_failed_tx(888); + failed_tx_5.hash = tx_hash_5; + failed_tx_5.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ))); + let tx_receipt_rpc_error_5 = + AppRpcError::Remote(RemoteError::InvalidResponse("game over".to_string())); + let tx_hash_6 = make_tx_hash(0x666); + let mut sent_tx_6 = make_sent_tx(789); + sent_tx_6.hash = tx_hash_6; + let tx_status_6 = StatusReadFromReceiptCheck::Reverted; + let sent_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(sent_tx_1.clone())) + .get_record_by_hash_result(Some(sent_tx_3.clone())) + .get_record_by_hash_result(Some(sent_tx_4)) + .get_record_by_hash_result(Some(sent_tx_6.clone())); + let failed_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(failed_tx_2.clone())) + .get_record_by_hash_result(Some(failed_tx_5)); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .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: btreemap![ + TxHashByTable::SentPayable(tx_hash_1) => Ok(tx_status_1), + TxHashByTable::FailedPayable(tx_hash_2) => Ok(tx_status_2), + TxHashByTable::SentPayable(tx_hash_3) => Ok(tx_status_3), + 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, + }; + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); + + let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); + + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + 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![btreeset![sent_tx_2]]); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failure_for_tx_3 = FailedTx::from((sent_tx_3, FailureReason::PendingTooLong)); + let expected_failure_for_tx_6 = FailedTx::from((sent_tx_6, FailureReason::Reverted)); + assert_eq!( + *insert_new_records_params, + vec![btreeset![ + expected_failure_for_tx_3, + expected_failure_for_tx_6 + ]] + ); + let update_statuses_pending_payable_params = + update_statuses_pending_payable_params_arc.lock().unwrap(); + 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() { - // TODO: Bert would like to fight this one - // init_test_logging(); - // let test_name = - // "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; - // let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); - // let msg = ReportTransactionReceipts { - // fingerprints_with_receipts: vec![], - // response_skeleton_opt: None, - // }; - // pending_payable_scanner.mark_as_started(SystemTime::now()); - // let mut subject = make_dull_subject(); - // subject.pending_payable = Box::new(pending_payable_scanner); - // - // let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - // - // let is_scan_running = subject.scan_started_at(ScanType::PendingPayables).is_some(); - // assert_eq!( - // result, - // PendingPayableScanResult::NoPendingPayablesLeft(None) - // ); - // assert_eq!(is_scan_running, false); - // let tlh = TestLogHandler::new(); - // tlh.exists_log_containing(&format!( - // "WARN: {test_name}: No transaction receipts found." - // )); - // tlh.exists_log_matching(&format!( - // "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." - // )); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let msg = TxReceiptsMessage { + results: btreemap![], + response_skeleton_opt: None, + }; + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); + + let _ = subject.finish_pending_payable_scan(msg, &Logger::new("test")); } #[test] @@ -1936,8 +1738,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"; @@ -2182,7 +1986,7 @@ mod tests { subject.signal_scanner_completion(ScanType::Receivables, SystemTime::now(), &logger); TestLogHandler::new().exists_log_containing(&format!( - "ERROR: {test_name}: Called scan_finished() for Receivables scanner but timestamp was not found" + "ERROR: {test_name}: Called scan_finished() for Receivables scanner but could not find any timestamp" )); } @@ -2233,7 +2037,7 @@ mod tests { &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PendingPayableScannerBuilder::new().build(), "PendingPayables", test_name, @@ -2301,7 +2105,7 @@ mod tests { #[test] fn acknowledge_scan_error_works() { - fn scan_error(scan_type: ScanType) -> ScanError { + fn scan_error(scan_type: DetailedScanType) -> ScanError { ScanError { scan_type, response_skeleton_opt: None, @@ -2312,22 +2116,27 @@ mod tests { init_test_logging(); let test_name = "acknowledge_scan_error_works"; let inputs: Vec<( - ScanType, + DetailedScanType, Box, Box Option>, )> = vec![ ( - ScanType::Payables, + DetailedScanType::NewPayables, + Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.payable.scan_started_at()), + ), + ( + DetailedScanType::RetryPayables, Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.payable.scan_started_at()), ), ( - ScanType::PendingPayables, + DetailedScanType::PendingPayables, Box::new(|subject| subject.pending_payable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.pending_payable.scan_started_at()), ), ( - ScanType::Receivables, + DetailedScanType::Receivables, Box::new(|subject| subject.receivable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.receivable.scan_started_at()), ), @@ -2360,7 +2169,7 @@ mod tests { ); test_log_handler.exists_log_containing(&format!( "INFO: {test_name}: The {:?} scan ended in", - scan_type + ScanType::from(scan_type) )); }) } diff --git a/node/src/accountant/scanners/payable_scanner/finish_scan.rs b/node/src/accountant/scanners/payable_scanner/finish_scan.rs index 9dd1e27e32..900bf9b561 100644 --- a/node/src/accountant/scanners/payable_scanner/finish_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/finish_scan.rs @@ -10,6 +10,13 @@ use std::time::SystemTime; impl Scanner for PayableScanner { fn finish_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { + // TODO as for GH-701, here there should be this check, but later on, when it comes to + // GH-655, the need for this check passes and it will go away. Until then it should be + // present, though. + // if !sent_payables.is_empty() { + // self.check_on_missing_sent_tx_records(&sent_payables); + // } + self.process_message(&msg, logger); self.mark_as_ended(logger); @@ -27,7 +34,6 @@ impl Scanner for PayableScanner { #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::failed_payable_dao::ValidationStatus::Waiting; use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; use crate::accountant::db_access_objects::test_utils::{ make_failed_tx, make_sent_tx, FailedTxBuilder, @@ -38,6 +44,7 @@ mod tests { use crate::accountant::test_utils::{FailedPayableDaoMock, SentPayableDaoMock}; use crate::accountant::{join_with_separator, PayableScanType, ResponseSkeleton, SentPayables}; use crate::blockchain::blockchain_interface::data_structures::BatchResults; + use crate::blockchain::errors::validation_status::ValidationStatus::Waiting; use crate::blockchain::test_utils::make_tx_hash; use masq_lib::logger::Logger; use masq_lib::messages::{ToMessageBody, UiScanResponse}; @@ -202,13 +209,16 @@ mod tests { #[test] fn payable_scanner_with_error_works_as_expected() { - test_execute_payable_scanner_finish_scan_with_an_error(PayableScanType::New); - test_execute_payable_scanner_finish_scan_with_an_error(PayableScanType::Retry); + test_execute_payable_scanner_finish_scan_with_an_error(PayableScanType::New, "new"); + test_execute_payable_scanner_finish_scan_with_an_error(PayableScanType::Retry, "retry"); } - fn test_execute_payable_scanner_finish_scan_with_an_error(payable_scan_type: PayableScanType) { + fn test_execute_payable_scanner_finish_scan_with_an_error( + payable_scan_type: PayableScanType, + suffix: &str, + ) { init_test_logging(); - let test_name = "test_execute_payable_scanner_finish_scan_with_an_error"; + let test_name = &format!("test_execute_payable_scanner_finish_scan_with_an_error_{suffix}"); let response_skeleton = ResponseSkeleton { client_id: 1234, context_id: 5678, @@ -217,7 +227,7 @@ mod tests { subject.mark_as_started(SystemTime::now()); let sent_payables = SentPayables { payment_procedure_result: Err("Any error".to_string()), - payable_scan_type: PayableScanType::New, + payable_scan_type, response_skeleton_opt: Some(response_skeleton), }; let logger = Logger::new(test_name); @@ -231,7 +241,10 @@ mod tests { target: MessageTarget::ClientId(response_skeleton.client_id), body: UiScanResponse {}.tmb(response_skeleton.context_id), }), - result: NextScanToRun::NewPayableScan, + result: match payable_scan_type { + PayableScanType::New => NextScanToRun::NewPayableScan, + PayableScanType::Retry => NextScanToRun::RetryPayableScan, + }, } ); let tlh = TestLogHandler::new(); diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index c91b3c765a..b82d2c46ef 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -12,35 +12,46 @@ use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCon use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedTx, FailureReason, FailureRetrieveCondition, FailureStatus, - ValidationStatus, }; use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition::ByAddresses; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; -use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, Tx}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, SentTx}; +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::payment_adjuster::PaymentAdjuster; use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::SolvencySensitivePaymentInstructor; use crate::accountant::scanners::payable_scanner::utils::{ - batch_stats, calculate_lengths, filter_receiver_addresses_from_txs, generate_status_updates, + batch_stats, calculate_occurences, filter_receiver_addresses_from_txs, generate_status_updates, payables_debug_summary, NextScanToRun, PayableScanResult, PayableThresholdsGauge, - PayableThresholdsGaugeReal, + PayableThresholdsGaugeReal, PendingPayableMissingInDb, }; use crate::accountant::scanners::{Scanner, ScannerCommon, StartableScanner}; use crate::accountant::{ - gwei_to_wei, join_with_separator, PayableScanType, ResponseSkeleton, ScanForNewPayables, - ScanForRetryPayables, SentPayables, + gwei_to_wei, join_with_commas, join_with_separator, PayableScanType, PendingPayable, + ResponseSkeleton, ScanForNewPayables, ScanForRetryPayables, SentPayables, }; use crate::blockchain::blockchain_interface::data_structures::BatchResults; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::sub_lib::accountant::PaymentThresholds; +use crate::sub_lib::wallet::Wallet; use itertools::Itertools; use masq_lib::logger::Logger; use masq_lib::messages::{ToMessageBody, UiScanResponse}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use masq_lib::utils::ExpectValue; use std::collections::{BTreeSet, HashMap}; use std::rc::Rc; use std::time::SystemTime; use web3::types::Address; +pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: + StartableScanner + + StartableScanner + + SolvencySensitivePaymentInstructor + + Scanner +{ +} + pub struct PayableScanner { pub payable_threshold_gauge: Box, pub common: ScannerCommon, @@ -50,14 +61,6 @@ pub struct PayableScanner { pub payment_adjuster: Box, } -pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: - StartableScanner - + StartableScanner - + SolvencySensitivePaymentInstructor - + Scanner -{ -} - impl MultistageDualPayableScanner for PayableScanner {} impl PayableScanner { @@ -106,7 +109,7 @@ impl PayableScanner { } } - pub fn payable_exceeded_threshold( + fn payable_exceeded_threshold( &self, payable: &PayableAccount, now: SystemTime, @@ -140,6 +143,69 @@ impl PayableScanner { } } + fn check_for_missing_records( + &self, + just_baked_sent_payables: &[&PendingPayable], + ) -> Vec { + let actual_sent_payables_len = just_baked_sent_payables.len(); + let hashset_with_hashes_to_eliminate_duplicates = just_baked_sent_payables + .iter() + .map(|pending_payable| pending_payable.hash) + .collect::>(); + + if hashset_with_hashes_to_eliminate_duplicates.len() != actual_sent_payables_len { + panic!( + "Found duplicates in the recent sent txs: {:?}", + just_baked_sent_payables + ); + } + + let transaction_hashes_and_rowids_from_db = self + .sent_payable_dao + .get_tx_identifiers(&hashset_with_hashes_to_eliminate_duplicates); + let hashes_from_db = transaction_hashes_and_rowids_from_db + .keys() + .copied() + .collect::>(); + + let missing_sent_payables_hashes = hashset_with_hashes_to_eliminate_duplicates + .difference(&hashes_from_db) + .copied(); + + let mut sent_payables_hashmap = just_baked_sent_payables + .iter() + .map(|payable| (payable.hash, &payable.recipient_wallet)) + .collect::>(); + missing_sent_payables_hashes + .map(|hash| { + let wallet_address = sent_payables_hashmap + .remove(&hash) + .expectv("wallet") + .address(); + PendingPayableMissingInDb::new(wallet_address, hash) + }) + .collect() + } + + // TODO this should be used when Utkarsh picks the card GH-701 where he postponed the fix of saving the SentTxs + #[allow(dead_code)] + fn check_on_missing_sent_tx_records(&self, sent_payments: &[&PendingPayable]) { + fn missing_record_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { + format!( + "Expected sent-payable records for {} were not found. The system has become unreliable", + join_with_commas(nonexistent, |missing_sent_tx_ids| format!( + "(tx: {:?}, to wallet: {:?})", + missing_sent_tx_ids.hash, missing_sent_tx_ids.recipient + )) + ) + } + + let missing_sent_tx_records = self.check_for_missing_records(sent_payments); + if !missing_sent_tx_records.is_empty() { + panic!("{}", missing_record_msg(&missing_sent_tx_records)) + } + } + fn determine_next_scan_to_run(msg: &SentPayables) -> NextScanToRun { match &msg.payment_procedure_result { Ok(batch_results) => { @@ -163,15 +229,19 @@ impl PayableScanner { fn process_message(&self, msg: &SentPayables, logger: &Logger) { match &msg.payment_procedure_result { Ok(batch_results) => match msg.payable_scan_type { - PayableScanType::New => self.handle_new(batch_results, logger), - PayableScanType::Retry => self.handle_retry(batch_results, logger), + PayableScanType::New => { + self.handle_batch_results_for_new_scan(batch_results, logger) + } + PayableScanType::Retry => { + self.handle_batch_results_for_retry_scan(batch_results, logger) + } }, Err(local_error) => Self::log_local_error(local_error, logger), } } - fn handle_new(&self, batch_results: &BatchResults, logger: &Logger) { - let (sent, failed) = calculate_lengths(&batch_results); + fn handle_batch_results_for_new_scan(&self, batch_results: &BatchResults, logger: &Logger) { + let (sent, failed) = calculate_occurences(batch_results); debug!( logger, "Processed new txs while sending to RPC: {}", @@ -185,8 +255,8 @@ impl PayableScanner { } } - fn handle_retry(&self, batch_results: &BatchResults, logger: &Logger) { - let (sent, failed) = calculate_lengths(&batch_results); + fn handle_batch_results_for_retry_scan(&self, batch_results: &BatchResults, logger: &Logger) { + let (sent, failed) = calculate_occurences(batch_results); debug!( logger, "Processed retried txs while sending to RPC: {}", @@ -203,10 +273,10 @@ impl PayableScanner { } } - fn update_statuses_of_prev_txs(&self, sent_txs: &Vec) { + fn update_statuses_of_prev_txs(&self, sent_txs: &[SentTx]) { // TODO: We can do better here, possibly by creating a relationship between failed and sent txs // Also, consider the fact that some txs will be with PendingTooLong status, what should we do with them? - let retrieved_txs = self.retrieve_failed_txs_by_receiver_addresses(&sent_txs); + let retrieved_txs = self.retrieve_failed_txs_by_receiver_addresses(sent_txs); let (pending_too_long, other_reasons): (BTreeSet<_>, BTreeSet<_>) = retrieved_txs .into_iter() .partition(|tx| matches!(tx.reason, FailureReason::PendingTooLong)); @@ -221,7 +291,7 @@ impl PayableScanner { } } - fn retrieve_failed_txs_by_receiver_addresses(&self, sent_txs: &Vec) -> BTreeSet { + fn retrieve_failed_txs_by_receiver_addresses(&self, sent_txs: &[SentTx]) -> BTreeSet { let receiver_addresses = filter_receiver_addresses_from_txs(sent_txs.iter()); self.failed_payable_dao .retrieve_txs(Some(FailureRetrieveCondition::ByReceiverAddresses( @@ -232,7 +302,7 @@ impl PayableScanner { fn update_failed_txs(&self, failed_txs: &BTreeSet, status: FailureStatus) { let status_updates = generate_status_updates(failed_txs, status); self.failed_payable_dao - .update_statuses(status_updates) + .update_statuses(&status_updates) .unwrap_or_else(|e| panic!("Failed to conclude txs in database: {:?}", e)); } @@ -253,7 +323,7 @@ impl PayableScanner { ) } - fn insert_records_in_sent_payables(&self, sent_txs: &Vec) { + fn insert_records_in_sent_payables(&self, sent_txs: &[SentTx]) { self.sent_payable_dao .insert_new_records(&sent_txs.iter().cloned().collect()) .unwrap_or_else(|e| { @@ -264,7 +334,7 @@ impl PayableScanner { }); } - fn insert_records_in_failed_payables(&self, failed_txs: &Vec) { + fn insert_records_in_failed_payables(&self, failed_txs: &[FailedTx]) { self.failed_payable_dao .insert_new_records(&failed_txs.iter().cloned().collect()) .unwrap_or_else(|e| { @@ -310,12 +380,18 @@ mod tests { use crate::accountant::db_access_objects::test_utils::{ make_failed_tx, make_sent_tx, FailedTxBuilder, TxBuilder, }; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; - use crate::accountant::test_utils::{FailedPayableDaoMock, SentPayableDaoMock}; + use crate::accountant::test_utils::{ + make_payable_account, FailedPayableDaoMock, PayableThresholdsGaugeMock, SentPayableDaoMock, + }; use crate::blockchain::test_utils::make_tx_hash; + use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; + use crate::test_utils::make_wallet; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; + use std::time::Duration; #[test] fn generate_ui_response_works_correctly() { @@ -489,6 +565,294 @@ mod tests { ); } + #[test] + fn no_missing_records() { + let wallet_1 = make_wallet("abc"); + let hash_1 = make_tx_hash(123); + let wallet_2 = make_wallet("def"); + let hash_2 = make_tx_hash(345); + let wallet_3 = make_wallet("ghi"); + let hash_3 = make_tx_hash(546); + let wallet_4 = make_wallet("jkl"); + let hash_4 = make_tx_hash(678); + let pending_payables_owned = vec![ + PendingPayable::new(wallet_1.clone(), hash_1), + PendingPayable::new(wallet_2.clone(), hash_2), + PendingPayable::new(wallet_3.clone(), hash_3), + PendingPayable::new(wallet_4.clone(), hash_4), + ]; + let pending_payables_ref = pending_payables_owned + .iter() + .collect::>(); + let sent_payable_dao = SentPayableDaoMock::new().get_tx_identifiers_result( + hashmap!(hash_4 => 4, hash_1 => 1, hash_3 => 3, hash_2 => 2), + ); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .build(); + + let missing_records = subject.check_for_missing_records(&pending_payables_ref); + + assert!( + missing_records.is_empty(), + "We thought the vec would be empty but contained: {:?}", + missing_records + ); + } + + #[test] + #[should_panic( + expected = "Found duplicates in the recent sent txs: [PendingPayable { recipient_wallet: \ + Wallet { kind: Address(0x0000000000000000000000000000000000616263) }, hash: \ + 0x000000000000000000000000000000000000000000000000000000000000007b }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000646566) }, \ + hash: 0x00000000000000000000000000000000000000000000000000000000000001c8 }, \ + PendingPayable { recipient_wallet: Wallet { kind: \ + Address(0x0000000000000000000000000000000000676869) }, hash: \ + 0x00000000000000000000000000000000000000000000000000000000000001c8 }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x00000000000000000000000000000000006a6b6c) }, \ + hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }]" + )] + fn just_baked_pending_payables_contain_duplicates() { + let hash_1 = make_tx_hash(123); + let hash_2 = make_tx_hash(456); + let hash_3 = make_tx_hash(789); + let pending_payables = vec![ + PendingPayable::new(make_wallet("abc"), hash_1), + PendingPayable::new(make_wallet("def"), hash_2), + PendingPayable::new(make_wallet("ghi"), hash_2), + PendingPayable::new(make_wallet("jkl"), hash_3), + ]; + let pending_payables_ref = pending_payables.iter().collect::>(); + let sent_payable_dao = SentPayableDaoMock::new() + .get_tx_identifiers_result(hashmap!(hash_1 => 1, hash_2 => 3, hash_3 => 5)); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .build(); + + subject.check_for_missing_records(&pending_payables_ref); + } + + #[test] + fn payable_is_found_innocent_by_age_and_returns() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result(true); + let mut subject = PayableScannerBuilder::new().build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + let now = SystemTime::now(); + let debt_age_s = 111_222; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let mut payable = make_payable_account(111); + payable.last_paid_timestamp = last_paid_timestamp; + + let result = subject.payable_exceeded_threshold(&payable, now); + + assert_eq!(result, None); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned, threshold_value) = is_innocent_age_params.remove(0); + assert!(is_innocent_age_params.is_empty()); + assert_eq!(debt_age_returned, debt_age_s); + assert_eq!( + threshold_value, + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + ) + // No panic and so no other method was called, which means an early return + } + + #[test] + fn payable_is_found_innocent_by_balance_and_returns() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result(false) + .is_innocent_balance_params(&is_innocent_balance_params_arc) + .is_innocent_balance_result(true); + let mut subject = PayableScannerBuilder::new().build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + let now = SystemTime::now(); + let debt_age_s = 3_456; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let mut payable = make_payable_account(222); + payable.last_paid_timestamp = last_paid_timestamp; + payable.balance_wei = 123456; + + let result = subject.payable_exceeded_threshold(&payable, now); + + assert_eq!(result, None); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned, _) = is_innocent_age_params.remove(0); + assert!(is_innocent_age_params.is_empty()); + assert_eq!(debt_age_returned, debt_age_s); + let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); + assert_eq!( + *is_innocent_balance_params, + vec![( + 123456_u128, + gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) + )] + ) + //no other method was called (absence of panic), and that means we returned early + } + + #[test] + fn threshold_calculation_depends_on_user_defined_payment_thresholds() { + let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); + let is_innocent_balance_params_arc = Arc::new(Mutex::new(vec![])); + let calculate_payable_threshold_params_arc = Arc::new(Mutex::new(vec![])); + let balance = gwei_to_wei(5555_u64); + let now = SystemTime::now(); + let debt_age_s = 1111 + 1; + let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); + let payable_account = PayableAccount { + wallet: make_wallet("hi"), + balance_wei: balance, + last_paid_timestamp, + pending_payable_opt: None, + }; + let custom_payment_thresholds = PaymentThresholds { + maturity_threshold_sec: 1111, + payment_grace_period_sec: 2222, + permanent_debt_allowed_gwei: 3333, + debt_threshold_gwei: 4444, + threshold_interval_sec: 5555, + unban_below_gwei: 5555, + }; + let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() + .is_innocent_age_params(&is_innocent_age_params_arc) + .is_innocent_age_result( + debt_age_s <= custom_payment_thresholds.maturity_threshold_sec as u64, + ) + .is_innocent_balance_params(&is_innocent_balance_params_arc) + .is_innocent_balance_result( + balance <= gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei), + ) + .calculate_payout_threshold_in_gwei_params(&calculate_payable_threshold_params_arc) + .calculate_payout_threshold_in_gwei_result(4567898); //made up value + let mut subject = PayableScannerBuilder::new() + .payment_thresholds(custom_payment_thresholds) + .build(); + subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + + let result = subject.payable_exceeded_threshold(&payable_account, now); + + assert_eq!(result, Some(4567898)); + let mut is_innocent_age_params = is_innocent_age_params_arc.lock().unwrap(); + let (debt_age_returned_innocent, curve_derived_time) = is_innocent_age_params.remove(0); + assert_eq!(*is_innocent_age_params, vec![]); + assert_eq!(debt_age_returned_innocent, debt_age_s); + assert_eq!( + curve_derived_time, + custom_payment_thresholds.maturity_threshold_sec as u64 + ); + let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); + assert_eq!( + *is_innocent_balance_params, + vec![( + payable_account.balance_wei, + gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei) + )] + ); + let mut calculate_payable_curves_params = + calculate_payable_threshold_params_arc.lock().unwrap(); + let (payment_thresholds, debt_age_returned_curves) = + calculate_payable_curves_params.remove(0); + assert_eq!(*calculate_payable_curves_params, vec![]); + assert_eq!(debt_age_returned_curves, debt_age_s); + assert_eq!(payment_thresholds, custom_payment_thresholds) + } + + #[test] + fn payable_with_debt_under_the_slope_is_marked_unqualified() { + init_test_logging(); + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let debt = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); + let time = to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1; + let unqualified_payable_account = vec![PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: debt, + last_paid_timestamp: from_unix_timestamp(time), + pending_payable_opt: None, + }]; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let test_name = + "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; + let logger = Logger::new(test_name); + + let result = subject + .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); + + assert_eq!(result, vec![]); + TestLogHandler::new() + .exists_no_log_containing(&format!("DEBUG: {}: Paying qualified debts", test_name)); + } + + #[test] + fn payable_with_debt_above_the_slope_is_qualified() { + init_test_logging(); + let payment_thresholds = PaymentThresholds::default(); + let debt = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); + let time = (payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec + - 1) as i64; + let qualified_payable = PayableAccount { + wallet: make_wallet("wallet0"), + balance_wei: debt, + last_paid_timestamp: from_unix_timestamp(time), + pending_payable_opt: None, + }; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let test_name = "payable_with_debt_above_the_slope_is_qualified"; + let logger = Logger::new(test_name); + + let result = subject.sniff_out_alarming_payables_and_maybe_log_them( + vec![qualified_payable.clone()], + &logger, + ); + + assert_eq!(result, vec![qualified_payable]); + TestLogHandler::new().exists_log_matching(&format!( + "DEBUG: {}: Paying qualified debts:\n\ + 999,999,999,000,000,000 wei owed for \\d+ sec exceeds the threshold \ + 500,000,000,000,000,000 wei for creditor 0x0000000000000000000000000077616c6c657430", + test_name + )); + } + + #[test] + fn retrieved_payables_turn_into_an_empty_vector_if_all_unqualified() { + init_test_logging(); + let test_name = "retrieved_payables_turn_into_an_empty_vector_if_all_unqualified"; + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); + let unqualified_payable_account = vec![PayableAccount { + wallet: make_wallet("wallet1"), + balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1), + last_paid_timestamp: from_unix_timestamp( + to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, + ), + pending_payable_opt: None, + }]; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .build(); + let logger = Logger::new(test_name); + + let result = subject + .sniff_out_alarming_payables_and_maybe_log_them(unqualified_payable_account, &logger); + + assert_eq!(result, vec![]); + TestLogHandler::new() + .exists_no_log_containing(&format!("DEBUG: {test_name}: Paying qualified debts")); + } + #[test] fn insert_records_in_sent_payables_inserts_records_successfully() { let insert_new_records_params = Arc::new(Mutex::new(vec![])); @@ -572,11 +936,14 @@ mod tests { } #[test] - fn handle_new_does_not_perform_any_operation_when_sent_txs_is_empty() { - let insert_new_records_params_sent = Arc::new(Mutex::new(vec![])); + fn handle_batch_results_for_new_scan_does_not_perform_any_operation_when_sent_txs_is_empty() { + let insert_new_records_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let insert_new_records_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); let sent_payable_dao = SentPayableDaoMock::default() - .insert_new_records_params(&insert_new_records_params_sent); - let failed_payable_dao = FailedPayableDaoMock::default().insert_new_records_result(Ok(())); + .insert_new_records_params(&insert_new_records_sent_tx_params_arc); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_failed_tx_params_arc) + .insert_new_records_result(Ok(())); let subject = PayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) .failed_payable_dao(failed_payable_dao) @@ -586,13 +953,23 @@ mod tests { failed_txs: vec![make_failed_tx(1)], }; - subject.handle_new(&batch_results, &Logger::new("test")); + subject.handle_batch_results_for_new_scan(&batch_results, &Logger::new("test")); - assert!(insert_new_records_params_sent.lock().unwrap().is_empty()); + assert_eq!( + insert_new_records_failed_tx_params_arc + .lock() + .unwrap() + .len(), + 1 + ); + assert!(insert_new_records_sent_tx_params_arc + .lock() + .unwrap() + .is_empty()); } #[test] - fn handle_new_does_not_perform_any_operation_when_failed_txs_is_empty() { + fn handle_batch_results_for_new_scan_does_not_perform_any_operation_when_failed_txs_is_empty() { let insert_new_records_params_failed = Arc::new(Mutex::new(vec![])); let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); let failed_payable_dao = FailedPayableDaoMock::default() @@ -606,18 +983,18 @@ mod tests { failed_txs: vec![], }; - subject.handle_new(&batch_results, &Logger::new("test")); + subject.handle_batch_results_for_new_scan(&batch_results, &Logger::new("test")); assert!(insert_new_records_params_failed.lock().unwrap().is_empty()); } #[test] - fn handle_retry_does_not_perform_any_operation_when_sent_txs_is_empty() { - let insert_new_records_params_sent = Arc::new(Mutex::new(vec![])); + fn handle_batch_results_for_retry_scan_does_not_perform_any_operation_when_sent_txs_is_empty() { + let insert_new_records_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); let retrieve_txs_params = Arc::new(Mutex::new(vec![])); let update_statuses_params = Arc::new(Mutex::new(vec![])); let sent_payable_dao = SentPayableDaoMock::default() - .insert_new_records_params(&insert_new_records_params_sent); + .insert_new_records_params(&insert_new_records_sent_tx_params_arc); let failed_payable_dao = FailedPayableDaoMock::default() .retrieve_txs_params(&retrieve_txs_params) .update_statuses_params(&update_statuses_params); @@ -630,9 +1007,12 @@ mod tests { failed_txs: vec![make_failed_tx(1)], }; - subject.handle_retry(&batch_results, &Logger::new("test")); + subject.handle_batch_results_for_retry_scan(&batch_results, &Logger::new("test")); - assert!(insert_new_records_params_sent.lock().unwrap().is_empty()); + assert!(insert_new_records_sent_tx_params_arc + .lock() + .unwrap() + .is_empty()); assert!(retrieve_txs_params.lock().unwrap().is_empty()); assert!(update_statuses_params.lock().unwrap().is_empty()); } @@ -655,7 +1035,7 @@ mod tests { failed_txs: vec![], }; - subject.handle_retry(&batch_results, &Logger::new(test_name)); + subject.handle_batch_results_for_retry_scan(&batch_results, &Logger::new(test_name)); let tlh = TestLogHandler::new(); tlh.exists_no_log_containing(&format!("WARN: {test_name}")); diff --git a/node/src/accountant/scanners/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner/msgs.rs index b905bd1336..5379d26f5b 100644 --- a/node/src/accountant/scanners/payable_scanner/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner/msgs.rs @@ -5,6 +5,8 @@ use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::Pri use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_bridge::MsgInterpretableAsDetailedScanType; +use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::wallet::Wallet; use actix::Message; use itertools::Either; @@ -16,6 +18,15 @@ pub struct InitialTemplatesMessage { pub response_skeleton_opt: Option, } +impl MsgInterpretableAsDetailedScanType for InitialTemplatesMessage { + fn detailed_scan_type(&self) -> DetailedScanType { + match self.initial_templates { + Either::Left(_) => DetailedScanType::NewPayables, + Either::Right(_) => DetailedScanType::RetryPayables, + } + } +} + #[derive(Message)] pub struct PricedTemplatesMessage { pub priced_templates: Either, @@ -28,3 +39,31 @@ impl SkeletonOptHolder for InitialTemplatesMessage { self.response_skeleton_opt } } + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; + use crate::blockchain::blockchain_bridge::MsgInterpretableAsDetailedScanType; + use crate::sub_lib::accountant::DetailedScanType; + use crate::test_utils::make_wallet; + use itertools::Either; + + #[test] + fn detailed_scan_type_is_implemented_for_initial_templates_message() { + let msg_a = InitialTemplatesMessage { + initial_templates: Either::Left(NewTxTemplates(vec![])), + consuming_wallet: make_wallet("abc"), + response_skeleton_opt: None, + }; + let msg_b = InitialTemplatesMessage { + initial_templates: Either::Right(RetryTxTemplates(vec![])), + consuming_wallet: make_wallet("abc"), + response_skeleton_opt: None, + }; + + assert_eq!(msg_a.detailed_scan_type(), DetailedScanType::NewPayables); + assert_eq!(msg_b.detailed_scan_type(), DetailedScanType::RetryPayables); + } +} diff --git a/node/src/accountant/scanners/payable_scanner/start_scan.rs b/node/src/accountant/scanners/payable_scanner/start_scan.rs index cb1d4ae5b6..35cbd3ab20 100644 --- a/node/src/accountant/scanners/payable_scanner/start_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/start_scan.rs @@ -1,6 +1,4 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCondition::ByStatus; -use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; @@ -82,7 +80,9 @@ impl StartableScanner for Payable mod tests { use super::*; use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::PendingTooLong; + use crate::accountant::db_access_objects::failed_payable_dao::FailureRetrieveCondition::ByStatus; use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition; use crate::accountant::db_access_objects::test_utils::FailedTxBuilder; use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; @@ -162,7 +162,7 @@ mod tests { let tx_template_2 = RetryTxTemplate::from(&failed_tx_2); - RetryTxTemplates(vec![tx_template_2, tx_template_1]) + RetryTxTemplates(vec![tx_template_1, tx_template_2]) }; assert_eq!( result, diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs index 21a8fd31e6..aceb532b05 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/new.rs @@ -56,12 +56,7 @@ impl FromIterator for NewTxTemplates { impl From<&Vec> for NewTxTemplates { fn from(payable_accounts: &Vec) -> Self { - Self( - payable_accounts - .iter() - .map(|payable_account| NewTxTemplate::from(payable_account)) - .collect(), - ) + Self(payable_accounts.iter().map(NewTxTemplate::from).collect()) } } diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs index 3d40e2dfd8..9990635cd9 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/initial/retry.rs @@ -17,8 +17,7 @@ impl RetryTxTemplate { let mut retry_template = RetryTxTemplate::from(failed_tx); if let Some(payable_scan_amount) = payable_scan_amount_opt { - retry_template.base.amount_in_wei = - retry_template.base.amount_in_wei + payable_scan_amount; + retry_template.base.amount_in_wei += payable_scan_amount; } retry_template @@ -30,9 +29,9 @@ impl From<&FailedTx> for RetryTxTemplate { RetryTxTemplate { base: BaseTxTemplate { receiver_address: failed_tx.receiver_address, - amount_in_wei: failed_tx.amount, + amount_in_wei: failed_tx.amount_minor, }, - prev_gas_price_wei: failed_tx.gas_price_wei, + prev_gas_price_wei: failed_tx.gas_price_minor, prev_nonce: failed_tx.nonce, } } @@ -110,8 +109,8 @@ mod tests { let failed_tx = FailedTx { hash: tx_hash, receiver_address, - amount: amount_in_wei, - gas_price_wei: gas_price, + amount_minor: amount_in_wei, + gas_price_minor: gas_price, nonce, timestamp: 1234567, reason: FailureReason::PendingTooLong, diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs index 255ba4b7b8..6de54e4c97 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/new.rs @@ -86,7 +86,7 @@ impl PricedNewTxTemplates { ceil: u128, ) -> String { format!( - "The computed gas price {} wei is above the ceil value of {} wei set by the Node.\n\ + "The computed gas price {} wei is above the ceil value of {} wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ {}", computed_gas_price_wei.separate_with_commas(), diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs index 97db24bf00..48e41f4b9c 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/priced/retry.rs @@ -99,9 +99,9 @@ impl PricedRetryTxTemplates { }) .collect(); - log_builder.build().map(|log_msg| { - warning!(logger, "{}", log_msg); - }); + if let Some(log_msg) = log_builder.build() { + warning!(logger, "{}", log_msg) + } templates } @@ -150,7 +150,7 @@ impl RetryLogBuilder { } else { Some(format!( "The computed gas price(s) in wei is \ - above the ceil value of {} wei set by the Node.\n\ + above the ceil value of {} wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ {}", self.ceil.separate_with_commas(), diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs index f4a05e85f7..d1ae97ebee 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/signable/mod.rs @@ -1,7 +1,10 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; -use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; -use bytes::Buf; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ + PricedNewTxTemplate, PricedNewTxTemplates, +}; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::{ + PricedRetryTxTemplate, PricedRetryTxTemplates, +}; use itertools::{Either, Itertools}; use std::ops::Deref; use web3::types::Address; @@ -14,6 +17,28 @@ pub struct SignableTxTemplate { pub nonce: u64, } +impl From<(&PricedNewTxTemplate, u64)> for SignableTxTemplate { + fn from((priced_new_tx_template, nonce): (&PricedNewTxTemplate, u64)) -> Self { + SignableTxTemplate { + receiver_address: priced_new_tx_template.base.receiver_address, + amount_in_wei: priced_new_tx_template.base.amount_in_wei, + gas_price_wei: priced_new_tx_template.computed_gas_price_wei, + nonce, + } + } +} + +impl From<(&PricedRetryTxTemplate, u64)> for SignableTxTemplate { + fn from((priced_retry_tx_template, nonce): (&PricedRetryTxTemplate, u64)) -> Self { + SignableTxTemplate { + receiver_address: priced_retry_tx_template.base.receiver_address, + amount_in_wei: priced_retry_tx_template.base.amount_in_wei, + gas_price_wei: priced_retry_tx_template.computed_gas_price_wei, + nonce, + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct SignableTxTemplates(pub Vec); @@ -23,6 +48,27 @@ impl FromIterator for SignableTxTemplates { } } +impl From<(PricedNewTxTemplates, u64)> for SignableTxTemplates { + fn from((priced_new_tx_templates, latest_nonce): (PricedNewTxTemplates, u64)) -> Self { + priced_new_tx_templates + .iter() + .enumerate() + .map(|(i, template)| SignableTxTemplate::from((template, latest_nonce + i as u64))) + .collect() + } +} + +impl From<(PricedRetryTxTemplates, u64)> for SignableTxTemplates { + fn from((priced_retry_tx_templates, latest_nonce): (PricedRetryTxTemplates, u64)) -> Self { + priced_retry_tx_templates + .reorder_by_nonces(latest_nonce) + .iter() + .enumerate() + .map(|(i, template)| SignableTxTemplate::from((template, latest_nonce + i as u64))) + .collect() + } +} + impl SignableTxTemplates { pub fn new( priced_tx_templates: Either, @@ -30,41 +76,14 @@ impl SignableTxTemplates { ) -> Self { match priced_tx_templates { Either::Left(priced_new_tx_templates) => { - Self::from_new_txs(priced_new_tx_templates, latest_nonce) + Self::from((priced_new_tx_templates, latest_nonce)) } Either::Right(priced_retry_tx_templates) => { - Self::from_retry_txs(priced_retry_tx_templates, latest_nonce) + Self::from((priced_retry_tx_templates, latest_nonce)) } } } - fn from_new_txs(templates: PricedNewTxTemplates, latest_nonce: u64) -> Self { - templates - .iter() - .enumerate() - .map(|(i, template)| SignableTxTemplate { - receiver_address: template.base.receiver_address, - amount_in_wei: template.base.amount_in_wei, - gas_price_wei: template.computed_gas_price_wei, - nonce: latest_nonce + i as u64, - }) - .collect() - } - - fn from_retry_txs(templates: PricedRetryTxTemplates, latest_nonce: u64) -> Self { - templates - .reorder_by_nonces(latest_nonce) - .iter() - .enumerate() - .map(|(i, template)| SignableTxTemplate { - receiver_address: template.base.receiver_address, - amount_in_wei: template.base.amount_in_wei, - gas_price_wei: template.computed_gas_price_wei, - nonce: latest_nonce + i as u64, - }) - .collect() - } - pub fn nonce_range(&self) -> (u64, u64) { let sorted: Vec<&SignableTxTemplate> = self .iter() @@ -121,18 +140,30 @@ mod tests { .iter() .zip(result.iter()) .enumerate() - .for_each(|(index, (priced, signable))| { - assert_eq!(signable.receiver_address, priced.base.receiver_address); - assert_eq!(signable.amount_in_wei, priced.base.amount_in_wei); - assert_eq!(signable.gas_price_wei, priced.computed_gas_price_wei); - assert_eq!(signable.nonce, nonce + index as u64); + .for_each(|(i, (priced, signable))| { + assert_eq!( + signable.receiver_address, priced.base.receiver_address, + "Element {i}: receiver_address mismatch", + ); + assert_eq!( + signable.amount_in_wei, priced.base.amount_in_wei, + "Element {i}: amount_in_wei mismatch", + ); + assert_eq!( + signable.gas_price_wei, priced.computed_gas_price_wei, + "Element {i}: gas_price_wei mismatch", + ); + assert_eq!( + signable.nonce, + nonce + i as u64, + "Element {i}: nonce mismatch", + ); }); } #[test] fn signable_tx_templates_can_be_created_from_priced_retry_tx_templates() { let nonce = 10; - // n is same as prev_nonce here let retries = PricedRetryTxTemplates(vec![ make_priced_retry_tx_template(12), make_priced_retry_tx_template(6), @@ -146,18 +177,30 @@ mod tests { let expected_order = vec![2, 4, 0, 1, 3]; result .iter() - .enumerate() .zip(expected_order.into_iter()) - .for_each(|((i, signable), index)| { + .enumerate() + .for_each(|(i, (signable, tx_order))| { + assert_eq!( + signable.receiver_address, retries[tx_order].base.receiver_address, + "Element {} (tx_order {}): receiver_address mismatch", + i, tx_order + ); + assert_eq!( + signable.nonce, + nonce + i as u64, + "Element {} (tx_order {}): nonce mismatch", + i, + tx_order + ); assert_eq!( - signable.receiver_address, - retries[index].base.receiver_address + signable.amount_in_wei, retries[tx_order].base.amount_in_wei, + "Element {} (tx_order {}): amount_in_wei mismatch", + i, tx_order ); - assert_eq!(signable.nonce, nonce + i as u64); - assert_eq!(signable.amount_in_wei, retries[index].base.amount_in_wei); assert_eq!( - signable.gas_price_wei, - retries[index].computed_gas_price_wei + signable.gas_price_wei, retries[tx_order].computed_gas_price_wei, + "Element {} (tx_order {}): gas_price_wei mismatch", + i, tx_order ); }); } diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs index 9dc3fb4134..b91eaed764 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/test_utils.rs @@ -30,28 +30,28 @@ pub fn make_priced_new_tx_template(n: u64) -> PricedNewTxTemplate { } } -pub fn make_priced_retry_tx_template(n: u64) -> PricedRetryTxTemplate { +pub fn make_priced_retry_tx_template(prev_nonce: u64) -> PricedRetryTxTemplate { PricedRetryTxTemplate { - base: BaseTxTemplate::from(&make_payable_account(n)), - prev_nonce: n, + base: BaseTxTemplate::from(&make_payable_account(prev_nonce)), + prev_nonce, computed_gas_price_wei: DEFAULT_GAS_PRICE as u128, } } -pub fn make_signable_tx_template(n: u64) -> SignableTxTemplate { +pub fn make_signable_tx_template(nonce: u64) -> SignableTxTemplate { SignableTxTemplate { receiver_address: make_address(1), - amount_in_wei: n as u128 * 1000, - gas_price_wei: n as u128 * 100, - nonce: n, + amount_in_wei: nonce as u128 * 1_000, + gas_price_wei: nonce as u128 * 1_000_000, + nonce, } } pub fn make_retry_tx_template(n: u32) -> RetryTxTemplate { RetryTxTemplateBuilder::new() .receiver_address(make_address(n)) - .amount_in_wei(n as u128 * 1000) - .prev_gas_price_wei(n as u128 * 100) + .amount_in_wei(n as u128 * 1_000) + .prev_gas_price_wei(n as u128 * 1_000_000) .prev_nonce(n as u64) .build() } diff --git a/node/src/accountant/scanners/payable_scanner/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index 4b1bac799e..3ace3b8b62 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -1,19 +1,14 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::comma_joined_stringifiable; use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; -use crate::accountant::db_access_objects::sent_payable_dao::Tx; use crate::accountant::db_access_objects::utils::{ThresholdUtils, TxHash}; use crate::accountant::db_access_objects::Transaction; use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; -use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; -use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; +use crate::accountant::{join_with_commas, PendingPayable}; use crate::blockchain::blockchain_interface::data_structures::BatchResults; use crate::sub_lib::accountant::PaymentThresholds; use crate::sub_lib::wallet::Wallet; -use bytes::Buf; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; @@ -24,7 +19,7 @@ use std::time::SystemTime; use thousands::Separable; use web3::types::{Address, H256}; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub struct PayableScanResult { pub ui_response_opt: Option, pub result: NextScanToRun, @@ -151,7 +146,7 @@ pub fn payables_debug_summary(qualified_accounts: &[(PayableAccount, u128)], log .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(), @@ -162,6 +157,18 @@ pub fn payables_debug_summary(qualified_accounts: &[(PayableAccount, u128)], log }) } +#[derive(Debug, PartialEq, Eq)] +pub struct PendingPayableMissingInDb { + pub recipient: Address, + pub hash: H256, +} + +impl PendingPayableMissingInDb { + pub fn new(recipient: Address, hash: H256) -> Self { + PendingPayableMissingInDb { recipient, hash } + } +} + #[derive(Debug, PartialEq, Eq)] pub struct PendingPayableMetadata<'a> { pub recipient: &'a Wallet, @@ -195,7 +202,7 @@ pub fn mark_pending_payable_fatal_error( }; panic!( "Unable to create a mark in the payable table for wallets {} due to {:?}", - comma_joined_stringifiable(sent_payments, |pending_p| pending_p + join_with_commas(sent_payments, |pending_p| pending_p .recipient_wallet .to_string()), error @@ -383,10 +390,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] @@ -502,64 +509,4 @@ mod tests { assert_eq!(result, false) } - - #[test] - fn requires_payments_retry_says_yes() { - todo!("complete this test with GH-604") - // let cases = vec![ - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // ]; - // - // cases.into_iter().enumerate().for_each(|(idx, case)| { - // let result = case.requires_payments_retry(); - // assert_eq!( - // result, true, - // "We expected true, but got false for case of idx {}", - // idx - // ) - // }) - } - - #[test] - fn requires_payments_retry_says_no() { - todo!("complete this test with GH-604") - // let report = PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }; - // - // let result = report.requires_payments_retry(); - // - // assert_eq!(result, false) - } } diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index cfb874f197..7e179ac9d8 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -1,37 +1,84 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod test_utils; +mod tx_receipt_interpreter; pub mod utils; -use std::cell::RefCell; -use std::rc::Rc; -use std::time::SystemTime; -use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; -use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; -use crate::accountant::db_access_objects::payable_dao::PayableDao; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; -use crate::accountant::{comma_joined_stringifiable, PendingPayableId, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables}; -use crate::accountant::scanners::{PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner}; -use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedTx, FailureRetrieveCondition, FailureStatus, +}; +use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDao, SentPayableDaoError, SentTx, TxStatus, +}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::db_access_objects::Transaction; +use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PendingPayableScanResult, PresortedTxFailure, + ReceiptScanReport, RecheckRequiringFailures, Retry, TxByTable, TxCaseToBeInterpreted, + TxHashByTable, UpdatableValidationStatus, +}; +use crate::accountant::scanners::{ + PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, +}; +use crate::accountant::{ + join_with_commas, 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::{BTreeSet, HashMap}; +use std::fmt::Display; +use std::rc::Rc; +use std::str::FromStr; +use std::time::SystemTime; +use thousands::Separable; +use web3::types::H256; + +pub(in crate::accountant::scanners) trait ExtendedPendingPayablePrivateScanner: + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + TxReceiptsMessage, + PendingPayableScanResult, + > + CachesEmptiableScanner +{ +} + +pub trait CachesEmptiableScanner { + fn empty_caches(&mut self, logger: &Logger); +} pub struct PendingPayableScanner { pub common: ScannerCommon, pub payable_dao: Box, - pub pending_payable_dao: Box, - pub when_pending_too_long_sec: u64, + pub sent_payable_dao: Box, + pub failed_payable_dao: Box, pub financial_statistics: Rc>, + pub current_sent_payables: Box>, + pub yet_unproven_failed_payables: Box>, + pub clock: Box, } +impl ExtendedPendingPayablePrivateScanner for PendingPayableScanner {} + impl PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, > for PendingPayableScanner { @@ -48,757 +95,2083 @@ impl StartableScanner logger: &Logger, ) -> Result { self.mark_as_started(timestamp); + info!(logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); - match filtered_pending_payable.is_empty() { - true => { - self.mark_as_ended(logger); - Err(StartScanError::NothingToProcess) - } - false => { - debug!( - logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - Ok(RequestTransactionReceipts { - pending_payable_fingerprints: filtered_pending_payable, - response_skeleton_opt, - }) - } - } + + let tx_hashes = self.harvest_tables(logger).map_err(|e| { + self.mark_as_ended(logger); + e + })?; + + Ok(RequestTransactionReceipts { + tx_hashes, + response_skeleton_opt, + }) } } -impl Scanner for PendingPayableScanner { +impl Scanner for PendingPayableScanner { fn finish_scan( &mut self, - message: ReportTransactionReceipts, + message: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { let response_skeleton_opt = message.response_skeleton_opt; - let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { - true => { - warning!(logger, "No transaction receipts found."); - todo!("This requires the payment retry. GH-631 must be completed first"); - } - false => { - debug!( - logger, - "Processing receipts for {} transactions", - message.fingerprints_with_receipts.len() - ); - let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - let requires_payment_retry = - self.process_transactions_by_reported_state(scan_report, logger); + let scan_report = self.interpret_tx_receipts(message, logger); - self.mark_as_ended(logger); + let retry_opt = scan_report.requires_payments_retry(); - requires_payment_retry - } - }; + self.process_txs_by_state(scan_report, logger); - if requires_payment_retry { - PendingPayableScanResult::PaymentRetryRequired - } else { - let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }); - PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) - } + self.mark_as_ended(logger); + + Self::compose_scan_result(retry_opt, response_skeleton_opt) } time_marking_methods!(PendingPayables); as_any_ref_in_trait_impl!(); + + as_any_mut_in_trait_impl!(); +} + +impl CachesEmptiableScanner for PendingPayableScanner { + fn empty_caches(&mut self, logger: &Logger) { + self.current_sent_payables.ensure_empty_cache(logger); + self.yet_unproven_failed_payables.ensure_empty_cache(logger); + } } impl PendingPayableScanner { pub fn new( payable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, + failed_payable_dao: Box, payment_thresholds: Rc, - when_pending_too_long_sec: u64, financial_statistics: Rc>, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, - pending_payable_dao, - when_pending_too_long_sec, + sent_payable_dao, + failed_payable_dao, financial_statistics, + current_sent_payables: Box::new(CurrentPendingPayables::default()), + yet_unproven_failed_payables: Box::new(RecheckRequiringFailures::default()), + clock: Box::new(ValidationFailureClockReal::default()), } } - fn handle_receipts_for_pending_transactions( - &self, - msg: ReportTransactionReceipts, + fn harvest_tables(&mut self, logger: &Logger) -> Result, StartScanError> { + let pending_tx_hashes_opt = self.harvest_pending_payables(); + let failure_hashes_opt = self.harvest_unproven_failures(); + + if Self::is_there_nothing_to_process( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + ) { + return Err(StartScanError::NothingToProcess); + } + + Self::log_records_for_receipt_check( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + logger, + ); + + Ok(Self::merge_hashes( + pending_tx_hashes_opt, + failure_hashes_opt, + )) + } + + fn harvest_pending_payables(&mut self) -> Option> { + let pending_txs = self + .sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::IsPending)) + .into_iter() + .collect_vec(); + + if pending_txs.is_empty() { + return None; + } + + let pending_tx_hashes = Self::wrap_hashes(&pending_txs, TxHashByTable::SentPayable); + self.current_sent_payables.load_cache(pending_txs); + Some(pending_tx_hashes) + } + + fn harvest_unproven_failures(&mut self) -> Option> { + let failures = self + .failed_payable_dao + .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)) + .into_iter() + .collect_vec(); + + if failures.is_empty() { + return None; + } + + let failure_hashes = Self::wrap_hashes(&failures, TxHashByTable::FailedPayable); + self.yet_unproven_failed_payables.load_cache(failures); + Some(failure_hashes) + } + + fn is_there_nothing_to_process( + pending_tx_hashes_opt: Option<&Vec>, + failure_hashes_opt: Option<&Vec>, + ) -> bool { + pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() + } + + fn merge_hashes( + pending_tx_hashes_opt: Option>, + failure_hashes_opt: Option>, + ) -> Vec { + let failures = failure_hashes_opt.unwrap_or_default(); + pending_tx_hashes_opt + .unwrap_or_default() + .into_iter() + .chain(failures) + .collect() + } + + fn wrap_hashes( + records: &[Record], + wrap_the_hash: fn(TxHash) -> TxHashByTable, + ) -> Vec + where + Record: Transaction, + { + 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 { + match retry { + Retry::RetryPayments => { + PendingPayableScanResult::PaymentRetryRequired(response_skeleton_opt) + } + Retry::RetryTxStatusCheckOnly => { + let ui_msg_opt = + response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::ProcedureShouldBeRepeated(ui_msg_opt) + } + } + } else { + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + 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, - ) -> 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) + ) -> 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, TxHashByTable> = Either::Left(vec![]); + let either = + msg.results + .into_iter() + .fold( + init, + |acc, (tx_hash_by_table, tx_receipt_result)| match acc { + Either::Left(cases) => { + self.resolve_real_query(cases, tx_receipt_result, tx_hash_by_table) + } + Either::Right(missing_entry) => Either::Right(missing_entry), + }, + ); + + let cases = match either { + Either::Left(cases) => cases, + Either::Right(missing_entry) => self.panic_dump(missing_entry), + }; + + 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, TxHashByTable> { + 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) } - TxStatus::Succeeded(_) => { - handle_status_with_success(scan_report_so_far, fingerprint, logger) + None => Either::Right(looked_up_hash), + } + } + 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) } - }, - TransactionReceiptResult::LocalError(e) => handle_none_receipt( - scan_report_so_far, - fingerprint, - &format!("failed due to {}", e), - logger, - ), - }, + None => Either::Right(looked_up_hash), + } + } + } + } + + fn panic_dump(&mut self, missing_entry: TxHashByTable) -> ! { + 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: {:?}.", + missing_entry, + rearrange(self.current_sent_payables.dump_cache()), + rearrange(self.yet_unproven_failed_payables.dump_cache()), ) } - fn process_transactions_by_reported_state( + fn process_txs_by_state(&mut self, scan_report: ReceiptScanReport, logger: &Logger) { + self.handle_confirmed_transactions(scan_report.confirmations, logger); + self.handle_failed_transactions(scan_report.failures, logger); + } + + fn handle_confirmed_transactions( &mut self, - scan_report: PendingPayableScanReport, + confirmed_txs: DetectedConfirmations, logger: &Logger, - ) -> bool { - let requires_payments_retry = scan_report.requires_payments_retry(); + ) { + self.handle_tx_failure_reclaims(confirmed_txs.reclaims, logger); + self.handle_normal_confirmations(confirmed_txs.normal_confirmations, logger); + } + + fn handle_tx_failure_reclaims(&mut self, reclaimed: Vec, logger: &Logger) { + if reclaimed.is_empty() { + return; + } + + let hashes_and_blocks = Self::collect_and_sort_hashes_and_blocks(&reclaimed); + + self.replace_sent_tx_records(&reclaimed, &hashes_and_blocks, logger); + + self.delete_failed_tx_records(&hashes_and_blocks, logger); + + self.add_to_the_total_of_paid_payable(&reclaimed, logger) + } - 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 isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> BTreeSet { + 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() + } - requires_payments_retry + 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 update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.increment_scan_attempts(&rowids) { - Ok(_) => trace!( + fn replace_sent_tx_records( + &self, + sent_txs_to_reclaim: &[SentTx], + hashes_and_blocks: &[(TxHash, TxBlock)], + logger: &Logger, + ) { + let btreeset: BTreeSet = sent_txs_to_reclaim.iter().cloned().collect(); + + match self.sent_payable_dao.replace_records(&btreeset) { + Ok(_) => { + debug!(logger, "Replaced records for txs being reclaimed") + } + Err(e) => { + panic!( + "Unable to proceed in a reclaim as the replacement of sent tx records \ + {} failed due to: {:?}", + join_with_commas(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, - "Updated records for rowids: {} ", - comma_joined_stringifiable(&rowids, |id| id.to_string()) - ), - Err(e) => panic!( - "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", - PendingPayableId::serialize_hashes_to_string(&ids), + "Reclaimed txs {} as confirmed on-chain", + join_with_commas(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: {:?}", + join_with_commas(hashes_and_blocks, |(tx_hash, _)| { + format!("{:?}", tx_hash) + }), e - ), + ) } } } - fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - //TODO this function is imperfect. It waits for GH-663 - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.mark_failures(&rowids) { - Ok(_) => warning!( + fn 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: {:?}", + join_with_commas( + &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: {:?}", + join_with_commas( + &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_btreeset(failures: &[FailedTx]) -> BTreeSet { + failures.iter().map(|failure| failure.hash).collect() + } + fn log_procedure_finished(logger: &Logger, new_failures: &[FailedTx]) { + info!( + logger, + "Failed txs {} were processed in the db", + join_with_commas(new_failures, |failure| format!("{:?}", failure.hash)) + ) + } + + if new_failures.is_empty() { + return; + } + + let new_failures_btree_set: BTreeSet = new_failures.iter().cloned().collect(); + + if let Err(e) = self + .failed_payable_dao + .insert_new_records(&new_failures_btree_set) + { + panic!( + "Unable to persist failed txs {} due to: {:?}", + join_with_commas(&new_failures, |failure| format!("{:?}", failure.hash)), + e + ) + } + + match self + .sent_payable_dao + .delete_records(&prepare_btreeset(&new_failures)) + { + Ok(_) => { + log_procedure_finished(logger, &new_failures); + } + Err(e) => { + panic!( + "Unable to purge sent payable records for failed txs {} due to: {:?}", + join_with_commas(&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, FailureStatus::Concluded)) + .collect() + } + + if rechecks_completed.is_empty() { + return; + } + + match self + .failed_payable_dao + .update_statuses(&prepare_hashmap(&rechecks_completed)) + { + Ok(_) => { + debug!( logger, - "Broken transactions {} marked as an error. You should take over the care \ - of those to make sure your debts are going to be settled properly. At the moment, \ - there is no automated process fixing that without your assistance", - PendingPayableId::serialize_hashes_to_string(&ids) - ), - Err(e) => panic!( - "Unsuccessful attempt for transactions {} \ - to mark fatal error at payable fingerprint due to {:?}; database unreliable", - PendingPayableId::serialize_hashes_to_string(&ids), + "Concluded failures that had required rechecks: {}.", + join_with_commas(&rechecks_completed, |tx_hash| format!("{:?}", tx_hash)) + ); + } + Err(e) => { + panic!( + "Unable to conclude rechecks for failed txs {} due to: {:?}", + join_with_commas(&rechecks_completed, |tx_hash| format!("{:?}", tx_hash)), e - ), + ) } } } - fn confirm_transactions( - &mut self, - fingerprints: Vec, + fn handle_rpc_failures(&self, failures: Vec, logger: &Logger) { + if failures.is_empty() { + return; + } + + let (sent_payable_failures, failed_payable_failures): ( + Vec>, + Vec>, + ) = failures.into_iter().partition_map(|failure| match failure { + FailedValidationByTable::SentPayable(failed_validation) => { + Either::Left(failed_validation) + } + FailedValidationByTable::FailedPayable(failed_validation) => { + Either::Right(failed_validation) + } + }); + + self.update_validation_status_for_sent_txs(sent_payable_failures, logger); + + self.update_validation_status_for_failed_txs(failed_payable_failures, logger); + } + + fn update_validation_status_for_sent_txs( + &self, + sent_payable_failures: Vec>, logger: &Logger, ) { - fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { - comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) + if !sent_payable_failures.is_empty() { + let updatable = + Self::prepare_statuses_for_update(&sent_payable_failures, &*self.clock, logger); + if !updatable.is_empty() { + match self.sent_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Pending-tx statuses were processed in the db for validation failure \ + of txs {}", + join_with_commas(&sent_payable_failures, |failure| { + format!("{:?}", failure.tx_hash) + }) + ) + } + Err(e) => { + panic!( + "Unable to update pending-tx statuses for validation failures '{:?}' \ + due to: {:?}", + sent_payable_failures, e + ) + } + } + } } + } - if !fingerprints.is_empty() { - if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { - panic!( - "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ - records due to {:?}", serialize_hashes(&fingerprints), e - ) - } else { - self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); - let rowids = fingerprints - .iter() - .map(|fingerprint| fingerprint.rowid) - .collect::>(); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { - panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", - serialize_hashes(&fingerprints), e) - } else { - info!( - logger, - "Transactions {} completed their confirmation process succeeding", - serialize_hashes(&fingerprints) - ) + fn update_validation_status_for_failed_txs( + &self, + failed_txs_validation_failures: Vec>, + logger: &Logger, + ) { + if !failed_txs_validation_failures.is_empty() { + let updatable = Self::prepare_statuses_for_update( + &failed_txs_validation_failures, + &*self.clock, + logger, + ); + if !updatable.is_empty() { + match self.failed_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Failed-tx statuses were processed in the db for validation failure \ + of txs {}", + join_with_commas(&failed_txs_validation_failures, |failure| { + format!("{:?}", failure.tx_hash) + }) + ) + } + Err(e) => { + panic!( + "Unable to update failed-tx statuses for validation failures '{:?}' \ + due to: {:?}", + failed_txs_validation_failures, e + ) + } } } } } - fn add_to_the_total_of_paid_payable( - &mut self, - fingerprints: &[PendingPayableFingerprint], - serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, + fn prepare_statuses_for_update( + failures: &[FailedValidation], + clock: &dyn ValidationFailureClock, + logger: &Logger, + ) -> HashMap { + failures + .iter() + .flat_map(|failure| { + failure + .new_status(clock) + .map(|tx_status| (failure.tx_hash, tx_status)) + .or_else(|| { + debug!( + logger, + "{}", + PendingPayableScanner::status_not_updatable_log_msg( + &failure.current_status + ) + ); + None + }) + }) + .collect() + } + + fn status_not_updatable_log_msg(status: &dyn Display) -> String { + format!( + "Handling a validation failure, but the status {} cannot be updated.", + status + ) + } + + fn log_records_for_receipt_check( + pending_tx_hashes_opt: Option<&Vec>, + failure_hashes_opt: Option<&Vec>, logger: &Logger, ) { - fingerprints.iter().for_each(|fingerprint| { - self.financial_statistics - .borrow_mut() - .total_paid_payable_wei += fingerprint.amount - }); + fn resolve_optional_vec(vec_opt: Option<&Vec>) -> usize { + vec_opt.map(|hashes| hashes.len()).unwrap_or_default() + } + debug!( logger, - "Confirmation of transactions {}; record for total paid payable was modified", - serialize_hashes(fingerprints) + "Found {} pending payables and {} unfinalized failures to process", + resolve_optional_vec(pending_tx_hashes_opt), + resolve_optional_vec(failure_hashes_opt) ); } } #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDaoError, FailureStatus, + }; + use crate::accountant::db_access_objects::payable_dao::PayableDaoError; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, SentPayableDaoError, TxStatus, + }; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PendingPayableScanResult, PresortedTxFailure, + 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_transaction_block, FailedPayableDaoMock, PayableDaoMock, PendingPayableScannerBuilder, + SentPayableDaoMock, + }; + use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, 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::Itertools; + use masq_lib::logger::Logger; + use masq_lib::messages::{ToMessageBody, UiScanResponse}; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; + use std::collections::{BTreeSet, HashMap}; use std::ops::Sub; + use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use ethereum_types::{H256, U64}; - use regex::Regex; - use web3::types::TransactionReceipt; - use masq_lib::logger::Logger; - use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use crate::accountant::{PendingPayableId, ReportTransactionReceipts, DEFAULT_PENDING_TOO_LONG_SEC}; - use crate::accountant::db_access_objects::payable_dao::PayableDaoError; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoError; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::test_utils::{make_pending_payable_fingerprint, PayableDaoMock, PendingPayableDaoMock, PendingPayableScannerBuilder}; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxReceipt, TxStatus}; - use crate::blockchain::test_utils::make_tx_hash; - - fn assert_interpreting_none_status_for_pending_payable( - test_name: &str, - when_pending_too_long_sec: u64, - pending_payable_age_sec: u64, - rowid: u64, - hash: H256, - ) -> PendingPayableScanReport { - init_test_logging(); - let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: when_sent, - hash, - attempt: 1, - amount: 123, - process_error: None, + + #[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(btreeset![sent_tx_1.clone(), sent_tx_2.clone()]); + let failed_payable_dao = FailedPayableDaoMock::new() + .retrieve_txs_result(btreeset![failed_tx_1.clone(), failed_tx_2.clone()]); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .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: btreemap![ + TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_sent_tx)), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + 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(None)); + 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() { + let sent_tx_hash_1 = make_tx_hash(0x890); + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.hash = sent_tx_hash_1; + let sent_tx_hash_2 = make_tx_hash(0x123); + let failed_tx_hash_1 = make_tx_hash(0x987); + let mut failed_tx_1 = make_failed_tx(567); + failed_tx_1.hash = failed_tx_hash_1; + 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: btreemap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok( + StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected = "Looking up 'SentPayable(0x00000000000000000000000000000000000000000000\ + 00000000000000000123)' in the cache, the record could not be found. Dumping the remaining \ + values. Pending payables: [SentTx { hash: 0x0000000000000000000000000000000000000000000000\ + 000000000000000890, receiver_address: 0x0000000000000000000558000000000558000000, \ + amount_minor: 43237380096, timestamp: 29942784, gas_price_minor: 94818816, nonce: 456, \ + status: Pending(Waiting) }]. Unproven failures: []."; + assert_eq!(panic_msg, expected); } - fn assert_log_msg_and_elapsed_time_in_log_makes_sense( - expected_msg: &str, - elapsed_after: u64, - capture_regex: &str, - ) { - let log_handler = TestLogHandler::default(); - let log_idx = log_handler.exists_log_matching(expected_msg); - let log = log_handler.get_log_at(log_idx); - let capture = captures_for_regex_time_in_sec(&log, capture_regex); - assert!(capture <= elapsed_after) - } + #[test] + fn 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(987); + let mut pending_payable_cache = CurrentPendingPayables::default(); + pending_payable_cache.load_cache(vec![sent_tx_1, sent_tx_2]); + let mut failed_payable_cache = RecheckRequiringFailures::default(); + 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: btreemap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, + }; - fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { - let capture_regex = Regex::new(capture_regex).unwrap(); - let time_str = capture_regex - .captures(stack) - .unwrap() - .get(1) - .unwrap() - .as_str(); - time_str.parse().unwrap() + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected = "Looking up 'FailedPayable(0x000000000000000000000000000000000000000000\ + 00000000000000000003db)' in the cache, the record could not be found. Dumping the remaining \ + values. Pending payables: [SentTx { hash: 0x000000000000000000000000000000000000000000000000\ + 00000000000001c8, receiver_address: 0x0000000000000000000558000000000558000000, amount_minor: \ + 43237380096, timestamp: 29942784, gas_price_minor: 94818816, nonce: 456, status: \ + Pending(Waiting) }, SentTx { hash: 0x0000000000000000000000000000000000000000000000000000000\ + 000000315, receiver_address: 0x000000000000000000093f00000000093f000000, amount_minor: \ + 387532395441, timestamp: 89643024, gas_price_minor: 491169069, nonce: 789, status: \ + Pending(Waiting) }]. Unproven failures: []."; + assert_eq!(panic_msg, expected); } - fn elapsed_since_secs_back(sec: u64) -> u64 { - SystemTime::now() - .sub(Duration::from_secs(sec)) - .elapsed() - .unwrap() - .as_secs() + #[test] + fn compose_scan_result_all_payments_resolved_in_automatic_mode() { + let result = PendingPayableScanner::compose_scan_result(None, None); + + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ) } #[test] - fn 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, + fn compose_scan_result_all_payments_resolved_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + None, + Some(ResponseSkeleton { + client_id: 2222, + context_id: 22, + }), ); - 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) + PendingPayableScanResult::NoPendingPayablesLeft(Some(NodeToUiMessage { + target: MessageTarget::ClientId(2222), + body: UiScanResponse {}.tmb(22) + })) + ) } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; - let hash = make_tx_hash(0x7b); - let rowid = 333; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; + fn compose_scan_result_payments_retry_required_in_automatic_mode() { + let result = PendingPayableScanner::compose_scan_result(Some(Retry::RetryPayments), None); + + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)) + } - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, + #[test] + fn compose_scan_result_payments_retry_required_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + Some(Retry::RetryPayments), + Some(ResponseSkeleton { + client_id: 1234, + context_id: 21, + }), ); - 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); + PendingPayableScanResult::PaymentRetryRequired(Some(ResponseSkeleton { + client_id: 1234, + context_id: 21 + })) + ) } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; - let hash = make_tx_hash(0x237); - let rowid = 466; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; + fn compose_scan_result_only_scan_procedure_should_be_repeated_in_automatic_mode() { + let result = + PendingPayableScanner::compose_scan_result(Some(Retry::RetryTxStatusCheckOnly), None); + + assert_eq!( + result, + PendingPayableScanResult::ProcedureShouldBeRepeated(None) + ) + } - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, + #[test] + fn compose_scan_result_only_scan_procedure_should_be_repeated_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + Some(Retry::RetryTxStatusCheckOnly), + Some(ResponseSkeleton { + client_id: 4455, + context_id: 12, + }), ); - 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); + PendingPayableScanResult::ProcedureShouldBeRepeated(Some(NodeToUiMessage { + target: MessageTarget::ClientId(4455), + body: UiScanResponse {}.tmb(12) + })) + ) + } + + #[test] + fn throws_an_error_when_no_records_to_process_were_found() { + let now = SystemTime::now(); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(btreeset![]); + let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(btreeset![]); + let mut subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .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 interpret_transaction_receipt_when_transaction_status_is_a_failure() { + fn handle_failed_transactions_can_process_standard_tx_failures() { init_test_logging(); - let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = make_tx_hash(0xd7); - let fingerprint = PendingPayableFingerprint { - rowid: 777777, - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt: 5, - amount: 2222, - process_error: None, + let test_name = "handle_failed_transactions_can_process_standard_tx_failures"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1.clone()), + PresortedTxFailure::NewEntry(failed_tx_2.clone()), + ], + tx_receipt_rpc_failures: vec![], }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - let result = handle_status_with_failure(scan_report, fingerprint, &logger); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(777777, hash,)], - confirmed: vec![] - } + *insert_new_records_params, + vec![btreeset![failed_tx_1, failed_tx_2]] ); - TestLogHandler::new().exists_log_matching(&format!( - "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ - 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ - 1500\\d\\dms from the sending" + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![btreeset![hash_1, hash_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 were processed in the db" )); } #[test] - fn handle_pending_txs_with_receipts_handles_none_for_receipt() { + fn handle_failed_transactions_can_process_receipt_retrieval_rpc_failures() { init_test_logging(); - let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; - let subject = PendingPayableScannerBuilder::new().build(); - let rowid = 455; - let hash = make_tx_hash(0x913); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt: 3, - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }), - fingerprint.clone(), - )], - response_skeleton_opt: None, + let test_name = "handle_failed_transactions_can_process_receipt_retrieval_rpc_failures"; + let retrieve_failed_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let retrieve_sent_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let hash_3 = make_tx_hash(0x987); + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(1)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(2)); + let timestamp_d = SystemTime::now().sub(Duration::from_secs(3)); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + failed_tx_1.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + failed_tx_2.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ))); + let failed_payable_dao = FailedPayableDaoMock::default() + .retrieve_txs_params(&retrieve_failed_txs_params_arc) + .retrieve_txs_result(btreeset![failed_tx_1, failed_tx_2]) + .update_statuses_params(&update_statuses_failed_tx_params_arc) + .update_statuses_result(Ok(())); + let mut sent_tx = make_sent_tx(789); + 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(btreeset![sent_tx.clone()]) + .update_statuses_params(&update_statuses_sent_tx_params_arc) + .update_statuses_result(Ok(())); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b) + .now_result(timestamp_c); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new(validation_failure_clock)) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + )), + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_d), + ), + )), + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_3, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Pending(ValidationStatus::Waiting), + )), + ], }; - let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let update_statuses_sent_tx_params = update_statuses_sent_tx_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } + *update_statuses_sent_tx_params, + vec![ + hashmap![hash_3 => TxStatus::Pending(ValidationStatus::Reattempting (PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_a))))] + ] + ); + let mut update_statuses_failed_tx_params = + update_statuses_failed_tx_params_arc.lock().unwrap(); + let actual_params = update_statuses_failed_tx_params + .remove(0) + .into_iter() + .sorted_by_key(|(key, _)| *key) + .collect::>(); + let expected_params = hashmap!( + hash_1 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_b))) + ), + hash_2 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp_d)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockReal::default()))) + ).into_iter().sorted_by_key(|(key,_)|*key).collect::>(); + assert_eq!(actual_params, expected_params); + assert!( + update_statuses_failed_tx_params.is_empty(), + "Should be empty but: {:?}", + update_statuses_sent_tx_params ); - TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {test_name}: Interpreting a receipt for transaction \ - 0x0000000000000000000000000000000000000000000000000000000000000913 \ - but none was given; attempt 3, 100\\d\\dms since sending" + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Pending-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000987" + )); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Failed-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654" )); + let expectedly_missing_log_msg_fragment = "Handling a validation failure, but the status"; + let otherwise_possible_log_msg = + PendingPayableScanner::status_not_updatable_log_msg(&"Something"); + assert!( + otherwise_possible_log_msg.contains(expectedly_missing_log_msg_fragment), + "We expected to select a true log fragment '{}', but it is not included in '{}'", + expectedly_missing_log_msg_fragment, + otherwise_possible_log_msg + ); + test_log_handler.exists_no_log_containing(&format!( + "DEBUG: {test_name}: {}", + expectedly_missing_log_msg_fragment + )) } #[test] - fn increment_scan_attempts_happy_path() { - let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let hash_1 = make_tx_hash(444888); - let rowid_1 = 3456; - let hash_2 = make_tx_hash(444888); - let rowid_2 = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) - .increment_scan_attempts_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); - let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); + fn handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated() { + init_test_logging(); + let test_name = "handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated"; + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let subject = PendingPayableScannerBuilder::new().build(); - let _ = subject.update_remaining_fingerprints( - vec![transaction_id_1, transaction_id_2], - &Logger::new("test"), + subject.handle_rpc_failures( + vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RetryRequired, + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Confirmed { + block_hash: "abc".to_string(), + block_number: 0, + detection: Detection::Normal, + }, + )), + ], + &Logger::new(test_name), ); - let update_remaining_fingerprints_params = - update_remaining_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *update_remaining_fingerprints_params, - vec![vec![rowid_1, rowid_2]] - ) + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_no_log_containing(&format!("INFO: {test_name}: ")); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \ + {{\"Confirmed\":{{\"block_hash\":\"abc\",\"block_number\":0,\"detection\":\"Normal\"}}}} \ + cannot be updated.", + )); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \"RetryRequired\" \ + cannot be updated." + )); + // It didn't panic, which means none of the DAO methods was called because the DAOs are + // mocked in this test } #[test] #[should_panic( - expected = "Failure on incrementing scan attempts for fingerprints of \ - 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ - due to UpdateFailed(\"yeah, bad\")" + expected = "Unable to update pending-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"blah\")" )] - fn increment_scan_attempts_sad_path() { - let hash = make_tx_hash(0x6c9d8); - let rowid = 3456; - let pending_payable_dao = - PendingPayableDaoMock::default().increment_scan_attempts_result(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); + fn update_validation_status_for_sent_txs_panics_on_update_statuses() { + let failed_validation = FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ); + let sent_payable_dao = SentPayableDaoMock::default() + .update_statuses_result(Err(SentPayableDaoError::InvalidInput("blah".to_string()))); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) .build(); - let logger = Logger::new("test"); - let transaction_id = PendingPayableId::new(rowid, hash); - let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); + let _ = subject + .update_validation_status_for_sent_txs(vec![failed_validation], &Logger::new("test")); } #[test] - fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { - let subject = PendingPayableScannerBuilder::new().build(); + #[should_panic( + expected = "Unable to update failed-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"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.update_remaining_fingerprints(vec![], &Logger::new("test")) + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([failed_tx_1])] + ); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![btreeset![tx_hash_1]]); + let update_statuses_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_params, + vec![ + hashmap!(tx_hash_2 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp))))) + ] + ); } #[test] - fn cancel_failed_transactions_works() { - init_test_logging(); - let test_name = "cancel_failed_transactions_works"; - let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failures_params(&mark_failures_params_arc) - .mark_failures_result(Ok(())); + #[should_panic(expected = "Unable to persist failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: NoChange")] + fn handle_failed_transactions_panics_when_it_fails_to_insert_failed_tx_record() { + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_result(Err(FailedPayableDaoError::NoChange)); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); - let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + } - let mark_failures_params = mark_failures_params_arc.lock().unwrap(); - assert_eq!(*mark_failures_params, vec![vec![2, 3]]); - TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ - 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ - the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ - process fixing that without your assistance", - )); + #[test] + #[should_panic(expected = "Unable to purge sent payable records for failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: \ + InvalidInput(\"Booga\")")] + fn handle_failed_transactions_panics_when_it_fails_to_delete_obsolete_sent_tx_records() { + let failed_payable_dao = FailedPayableDaoMock::default().insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_result(Err(SentPayableDaoError::InvalidInput("Booga".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - #[should_panic( - expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ - 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ - 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ - database unreliable" - )] - fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { - let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), + fn handle_failed_transactions_can_conclude_rechecked_failures() { + let update_status_params_arc = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = tx_hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = tx_hash_2; + let failed_payable_dao = FailedPayableDaoMock::default() + .update_statuses_params(&update_status_params_arc) + .update_statuses_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + + let update_status_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_status_params, + vec![ + hashmap!(tx_hash_1 => FailureStatus::Concluded, tx_hash_2 => FailureStatus::Concluded), + ] + ); + } + + #[test] + #[should_panic(expected = "Unable to conclude rechecks for failed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 due to: \ + InvalidInput(\"Booga\")")] + fn concluding_rechecks_fails_on_updating_statuses() { + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let failed_payable_dao = FailedPayableDaoMock::default().update_statuses_result(Err( + FailedPayableDaoError::InvalidInput("Booga".to_string()), )); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); - let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); - let transaction_ids = vec![transaction_id_1, transaction_id_2]; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { - let subject = PendingPayableScannerBuilder::new().build(); + fn handle_confirmed_transactions_does_nothing_if_no_confirmation_found_on_the_blockchain() { + let mut subject = PendingPayableScannerBuilder::new().build(); + + subject + .handle_confirmed_transactions(DetectedConfirmations::default(), &Logger::new("test")) + + // Mocked payable DAO without prepared results didn't panic, which means none of its methods + // was used in this test + } + + #[test] + fn handles_failure_reclaims_alone() { + init_test_logging(); + let test_name = "handles_failure_reclaims_alone"; + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.cancel_failed_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &logger, + ); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!( + *replace_records_params, + vec![btreeset![sent_tx_1, sent_tx_2]] + ); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + // assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); + assert_eq!( + *delete_records_params, + vec![BTreeSet::from([tx_hash_1, tx_hash_2])] + ); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) as confirmed on-chain", + )); } #[test] #[should_panic( - expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ - 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ - 0000000000021a of verified transactions due to RecordDeletion(\"the database \ - is fooling around with us\")" + expected = "Unable to proceed in a reclaim as the replacement of sent tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + failed due to: NoChange" )] - fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { - let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Err( - PendingPayableDaoError::RecordDeletion( - "the database is fooling around with us".to_string(), - ), - )); + fn failure_reclaim_fails_on_replace_sent_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_result(Err(SentPayableDaoError::NoChange)); let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.rowid = 1; - fingerprint_1.hash = make_tx_hash(0x315); - let mut fingerprint_2 = make_pending_payable_fingerprint(); - fingerprint_2.rowid = 1; - fingerprint_2.hash = make_tx_hash(0x21a); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { - let mut subject = PendingPayableScannerBuilder::new().build(); + #[should_panic(expected = "Unable to delete failed tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + to finish the reclaims due to: EmptyInput")] + fn failure_reclaim_fails_on_delete_failed_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default().replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_result(Err(FailedPayableDaoError::EmptyInput)); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); + } - //mocked payable DAO didn't panic which means we skipped the actual process + #[test] + #[should_panic( + expected = "Processing a reclaim for tx 0x0000000000000000000000000000000000000000000000000\ + 000000000000123 which isn't filled with the confirmation details" + )] + fn handle_failure_reclaim_meets_a_record_without_confirmation_details() { + let mut subject = PendingPayableScannerBuilder::new().build(); + let tx_hash = make_tx_hash(0x123); + let mut sent_tx = make_sent_tx(123_123); + sent_tx.hash = tx_hash; + // Here, it should be confirmed already in this status + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_works() { + fn handles_normal_confirmations_alone() { init_test_logging(); + let test_name = "handles_normal_confirmations_alone"; let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())); + let logger = Logger::new(test_name); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let rowid_1 = 2; - let rowid_2 = 5; - let pending_payable_fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: from_unix_timestamp(199_000_000), - hash: make_tx_hash(0x123), - attempt: 1, - amount: 4567, - process_error: None, - }; - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: from_unix_timestamp(200_000_000), - hash: make_tx_hash(0x567), - attempt: 1, - amount: 5555, - process_error: None, - }; - - subject.confirm_transactions( - vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), - ], - &Logger::new("confirm_transactions_works"), + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_2.block_hash), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone(), sent_tx_2.clone()], + reclaims: vec![], + }, + &logger, ); - let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!( + *transactions_confirmed_params, + vec![vec![sent_tx_1, sent_tx_2]] + ); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); assert_eq!( - *confirm_transactions_params, - vec![vec![ - pending_payable_fingerprint_1, - pending_payable_fingerprint_2 - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); + *confirm_tx_params, + vec![hashmap![tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2]] + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "DEBUG: confirm_transactions_works: \ - Confirmation of transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567; \ - record for total paid payable was modified", + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) were confirmed", + )); + } + + #[test] + fn mixed_tx_confirmations_work() { + init_test_logging(); + let test_name = "mixed_tx_confirmations_work"; + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transactions_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x913); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(567_567); + sent_tx_2.hash = tx_hash_2; + let tx_block_3 = TxBlock { + block_hash: make_block_hash(78), + block_number: 7_898_989_878_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_3.block_hash), + block_number: tx_block_3.block_number.as_u64(), + detection: Detection::Reclaim, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone()], + reclaims: vec![sent_tx_2.clone()], + }, + &logger, ); - log_handler.exists_log_containing( - "INFO: confirm_transactions_works: \ - Transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567 \ - completed their confirmation process succeeding", + + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![vec![sent_tx_1]]); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); + assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![btreeset![sent_tx_2]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + // assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); + assert_eq!(*delete_records_params, vec![BTreeSet::from([tx_hash_2])]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000913 (block 7898989878) \ + as confirmed on-chain", + )); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878) was confirmed", + )); + } + + #[test] + #[should_panic( + expected = "Unable to update sent payable records 0x000000000000000000000000000000000000000\ + 000000000000000000000021a, 0x0000000000000000000000000000000000000000000000000000000000000315 \ + by their tx blocks due to: SqlExecutionFailed(\"The database manager is \ + a funny guy, he's fooling around with us\")" + )] + fn handle_confirmed_transactions_panics_while_updating_sent_payable_records_with_the_tx_blocks() + { + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Err( + SentPayableDaoError::SqlExecutionFailed( + "The database manager is a funny guy, he's fooling around with us".to_string(), + ), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let mut sent_tx_1 = make_sent_tx(456); + let block = make_transaction_block(678); + sent_tx_1.hash = make_tx_hash(0x315); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.hash = make_tx_hash(0x21a); + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![], + }, + &Logger::new("test"), ); } #[test] #[should_panic( - expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ - 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ - (\"record change not successful\")" + expected = "Unable to complete the tx confirmation by the adjustment of the payable accounts \ + 0x0000000000000000000558000000000558000000 due to: \ + RusqliteError(\"record change not successful\")" )] - fn confirm_transactions_panics_on_unchecking_payable_table() { - let hash = make_tx_hash(0x315); - let rowid = 3; + fn handle_confirmed_transactions_panics_on_unchecking_payable_table() { + let hash = make_tx_hash(315); let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( PayableDaoError::RusqliteError("record change not successful".to_string()), )); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let mut fingerprint = make_pending_payable_fingerprint(); - fingerprint.rowid = rowid; - fingerprint.hash = hash; + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = hash; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx], + reclaims: vec![], + }, + &Logger::new("test"), + ); + } + + #[test] + fn log_tx_success_is_agnostic_to_singular_or_plural_form() { + init_test_logging(); + let test_name = "log_tx_success_is_agnostic_to_singular_or_plural_form"; + let plural_case_name = format!("{}_testing_plural_case", test_name); + let singular_case_name = format!("{}_testing_singular_case", test_name); + let logger_plural = Logger::new(&plural_case_name); + let logger_singular = Logger::new(&singular_case_name); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut tx_block_1 = make_transaction_block(456); + tx_block_1.block_number = 1_234_501_u64.into(); + let mut tx_block_2 = make_transaction_block(789); + tx_block_2.block_number = 1_234_502_u64.into(); + let mut tx_hashes_and_blocks = hashmap!(tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2); + + PendingPayableScanner::log_tx_success(&logger_plural, &tx_hashes_and_blocks); + + tx_hashes_and_blocks.remove(&tx_hash_2); + + PendingPayableScanner::log_tx_success(&logger_singular, &tx_hashes_and_blocks); - subject.confirm_transactions(vec![fingerprint], &Logger::new("test")); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {plural_case_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 1234502) were confirmed", + )); + log_handler.exists_log_containing(&format!( + "INFO: {singular_case_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501) was confirmed", + )); } #[test] fn total_paid_payable_rises_with_each_bill_paid() { + init_test_logging(); let test_name = "total_paid_payable_rises_with_each_bill_paid"; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(189_999_888), - hash: make_tx_hash(56789), - attempt: 1, - amount: 5478, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 6, - timestamp: from_unix_timestamp(200_000_011), - hash: make_tx_hash(33333), - attempt: 1, - amount: 6543, - process_error: None, + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.amount_minor = 5478; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: 89898, + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.amount_minor = 3344; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(234)), + block_number: 66312, + detection: Detection::Normal, + }; + let mut sent_tx_3 = make_sent_tx(789); + sent_tx_3.amount_minor = 6543; + sent_tx_3.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(321)), + block_number: 67676, + detection: Detection::Reclaim, }; let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_result(Ok(())) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default().delete_records_result(Ok(())); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let mut financial_statistics = subject.financial_statistics.borrow().clone(); financial_statistics.total_paid_payable_wei += 1111; subject.financial_statistics.replace(financial_statistics); - subject.confirm_transactions( - vec![fingerprint_1.clone(), fingerprint_2.clone()], + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![sent_tx_3], + }, &Logger::new(test_name), ); let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; - assert_eq!(total_paid_payable, 1111 + 5478 + 6543); + assert_eq!(total_paid_payable, 1111 + 5478 + 3344 + 6543); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!("DEBUG: {test_name}: The total paid payables increased by 6,543 to 7,654 wei"), + &format!( + "DEBUG: {test_name}: The total paid payables increased by 8,822 to 16,476 wei" + ), + ]); } } diff --git a/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs new file mode 100644 index 0000000000..473fd28cb3 --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs @@ -0,0 +1,23 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::validation_status::ValidationFailureClock; +use std::cell::RefCell; +use std::time::SystemTime; + +#[derive(Default)] +pub struct ValidationFailureClockMock { + now_results: RefCell>, +} + +impl ValidationFailureClock for ValidationFailureClockMock { + fn now(&self) -> SystemTime { + self.now_results.borrow_mut().remove(0) + } +} + +impl ValidationFailureClockMock { + pub fn now_result(self, result: SystemTime) -> Self { + self.now_results.borrow_mut().push(result); + self + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs new file mode 100644 index 0000000000..6039cd711d --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -0,0 +1,707 @@ +// 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 + .iter() + .next() + .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::test_utils::{make_failed_tx, make_sent_tx}; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + DetectedConfirmations, DetectedFailures, FailedValidation, FailedValidationByTable, + PresortedTxFailure, ReceiptScanReport, TxByTable, + }; + use crate::accountant::test_utils::{make_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::collections::BTreeSet; + 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(BTreeSet::from([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(BTreeSet::from([newer_sent_tx_for_older_failed_tx])); + let tx_hash = make_tx_hash(0x913); + let mut failed_tx = make_failed_tx(789); + let failed_tx_nonce = failed_tx.nonce; + 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(BTreeSet::new()); + + 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 index f277a1c911..f86984df0f 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -1,191 +1,1204 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::PendingPayableId; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::{ResponseSkeleton, TxReceiptResult}; +use crate::blockchain::errors::rpc_errors::AppRpcError; +use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClock, ValidationStatus, +}; +use crate::blockchain::errors::BlockchainErrorKind; +use itertools::Either; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; -use std::time::SystemTime; +use std::cmp::Ordering; +use std::collections::HashMap; #[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct PendingPayableScanReport { - pub still_pending: Vec, - pub failures: Vec, - pub confirmed: Vec, +pub struct ReceiptScanReport { + pub failures: DetectedFailures, + pub confirmations: DetectedConfirmations, } -impl PendingPayableScanReport { - pub fn requires_payments_retry(&self) -> bool { - todo!("complete my within GH-642") +impl ReceiptScanReport { + pub fn requires_payments_retry(&self) -> Option { + match ( + self.failures.requires_retry(), + self.confirmations.is_empty(), + ) { + (None, true) => unreachable!("reading tx receipts gave no results, but always should"), + (None, _) => None, + (Some(retry), _) => Some(retry), + } + } + + pub(super) fn register_confirmed_tx( + &mut self, + confirmed_tx: SentTx, + confirmation_type: ConfirmationType, + ) { + match confirmation_type { + ConfirmationType::Normal => self.confirmations.normal_confirmations.push(confirmed_tx), + ConfirmationType::Reclaim => self.confirmations.reclaims.push(confirmed_tx), + } + } + + pub(super) fn register_new_failure(&mut self, failed_tx: FailedTx) { + self.failures + .tx_failures + .push(PresortedTxFailure::NewEntry(failed_tx)); + } + + pub(super) fn register_finalization_of_unproven_failure(&mut self, tx_hash: TxHash) { + self.failures + .tx_failures + .push(PresortedTxFailure::RecheckCompleted(tx_hash)); + } + + pub(super) fn register_rpc_failure(&mut self, status_update: FailedValidationByTable) { + self.failures.tx_receipt_rpc_failures.push(status_update); + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedConfirmations { + pub normal_confirmations: Vec, + pub reclaims: Vec, +} + +impl DetectedConfirmations { + pub(super) fn is_empty(&self) -> bool { + self.normal_confirmations.is_empty() && self.reclaims.is_empty() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ConfirmationType { + Normal, + Reclaim, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedFailures { + pub tx_failures: Vec, + pub tx_receipt_rpc_failures: Vec, +} + +impl DetectedFailures { + fn requires_retry(&self) -> Option { + if self.tx_failures.is_empty() && self.tx_receipt_rpc_failures.is_empty() { + None + } else if !self.tx_failures.is_empty() { + Some(Retry::RetryPayments) + } else { + Some(Retry::RetryTxStatusCheckOnly) + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PresortedTxFailure { + NewEntry(FailedTx), + RecheckCompleted(TxHash), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum FailedValidationByTable { + SentPayable(FailedValidation), + FailedPayable(FailedValidation), +} + +impl FailedValidationByTable { + pub fn new( + tx_hash: TxHash, + error: AppRpcError, + status: Either, + ) -> Self { + match status { + Either::Left(tx_status) => Self::SentPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + tx_status, + )), + Either::Right(failure_reason) => Self::FailedPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + failure_reason, + )), + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct FailedValidation { + pub tx_hash: TxHash, + pub validation_failure: BlockchainErrorKind, + pub current_status: RecordStatus, +} + +impl FailedValidation +where + RecordStatus: UpdatableValidationStatus, +{ + pub fn new( + tx_hash: TxHash, + validation_failure: BlockchainErrorKind, + current_status: RecordStatus, + ) -> Self { + Self { + tx_hash, + validation_failure, + current_status, + } + } + + pub fn new_status(&self, clock: &dyn ValidationFailureClock) -> Option { + self.current_status + .update_after_failure(self.validation_failure, clock) + } +} + +pub trait UpdatableValidationStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option + where + Self: Sized; +} + +impl UpdatableValidationStatus for TxStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + TxStatus::Pending(ValidationStatus::Waiting) => Some(TxStatus::Pending( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )), + TxStatus::Pending(ValidationStatus::Reattempting(previous_attempts)) => { + Some(TxStatus::Pending(ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error, clock), + ))) + } + TxStatus::Confirmed { .. } => None, + } + } +} + +impl UpdatableValidationStatus for FailureStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + FailureStatus::RecheckRequired(ValidationStatus::Waiting) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )) + } + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(previous_attempts)) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error, clock), + ), + )) + } + FailureStatus::RetryRequired | FailureStatus::Concluded => None, + } + } +} + +pub trait PendingPayableCache { + fn load_cache(&mut self, records: Vec); + fn get_record_by_hash(&mut self, hash: TxHash) -> Option; + fn ensure_empty_cache(&mut self, logger: &Logger); + fn dump_cache(&mut self) -> HashMap; +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct CurrentPendingPayables { + pub(super) sent_payables: HashMap, +} + +impl PendingPayableCache for CurrentPendingPayables { + fn load_cache(&mut self, records: Vec) { + self.sent_payables + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.sent_payables.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.sent_payables.is_empty() { + debug!( + logger, + "Cache misuse - some pending payables left unprocessed: {:?}. Dumping.", + self.sent_payables + ); + } + self.sent_payables.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.sent_payables.drain().collect() + } +} + +impl CurrentPendingPayables { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct RecheckRequiringFailures { + pub(super) failures: HashMap, +} + +impl PendingPayableCache for RecheckRequiringFailures { + fn load_cache(&mut self, records: Vec) { + self.failures + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.failures.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.failures.is_empty() { + debug!( + logger, + "Cache misuse - some tx failures left unprocessed: {:?}. Dumping.", self.failures + ); + } + self.failures.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.failures.drain().collect() + } +} + +impl RecheckRequiringFailures { + pub fn new() -> Self { + Self::default() } } #[derive(Debug, PartialEq, Eq)] pub enum PendingPayableScanResult { NoPendingPayablesLeft(Option), - PaymentRetryRequired, -} - -pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() -} - -pub fn handle_none_status( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Pending transaction {:?} couldn't be confirmed at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let elapsed = elapsed.as_secs(); - if elapsed > max_pending_interval { - error!( - logger, - "Pending transaction {:?} has exceeded the maximum pending time \ - ({}sec) with the age {}sec and the confirmation process is going to be aborted now \ - at the final attempt {}; manual resolution is required from the \ - user to complete the transaction.", - fingerprint.hash, - max_pending_interval, - elapsed, - fingerprint.attempt - ); - scan_report.failures.push(fingerprint.into()) - } else { - scan_report.still_pending.push(fingerprint.into()) - } - scan_report -} - -pub fn handle_status_with_success( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Transaction {:?} has been added to the blockchain; detected locally at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.confirmed.push(fingerprint); - scan_report -} - -//TODO: failures handling is going to need enhancement suggested by GH-693 -pub fn handle_status_with_failure( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - error!( - logger, - "Pending transaction {:?} announced as a failure, interpreting attempt \ - {} after {}ms from the sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.failures.push(fingerprint.into()); - scan_report -} - -pub fn handle_none_receipt( - mut scan_report: PendingPayableScanReport, - payable: PendingPayableFingerprint, - error_msg: &str, - logger: &Logger, -) -> PendingPayableScanReport { - debug!( - logger, - "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", - payable.hash, - error_msg, - payable.attempt, - elapsed_in_ms(payable.timestamp) - ); - - scan_report - .still_pending - .push(PendingPayableId::new(payable.rowid, payable.hash)); - scan_report + PaymentRetryRequired(Option), + ProcedureShouldBeRepeated(Option), +} + +#[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)] +pub enum TxHashByTable { + SentPayable(TxHash), + FailedPayable(TxHash), +} + +impl PartialOrd for TxHashByTable { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for TxHashByTable { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (TxHashByTable::FailedPayable(..), TxHashByTable::SentPayable(..)) => Ordering::Less, + (TxHashByTable::SentPayable(..), TxHashByTable::FailedPayable(..)) => Ordering::Greater, + (TxHashByTable::SentPayable(hash_1), TxHashByTable::SentPayable(hash_2)) => { + hash_1.cmp(hash_2) + } + (TxHashByTable::FailedPayable(hash_1), TxHashByTable::FailedPayable(hash_2)) => { + hash_1.cmp(hash_2) + } + } + } +} + +impl TxHashByTable { + pub fn hash(&self) -> TxHash { + match self { + 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::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PresortedTxFailure, ReceiptScanReport, + RecheckRequiringFailures, Retry, TxByTable, TxHashByTable, + }; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::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::cmp::Ordering; + use std::collections::BTreeSet; + use std::ops::Sub; + use std::time::{Duration, SystemTime}; + use std::vec; #[test] - fn requires_payments_retry_says_yes() { - todo!("complete this test with GH-604") - // let cases = vec![ - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // ]; - // - // cases.into_iter().enumerate().for_each(|(idx, case)| { - // let result = case.requires_payments_retry(); - // assert_eq!( - // result, true, - // "We expected true, but got false for case of idx {}", - // idx - // ) - // }) + fn 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() { - todo!("complete this test with GH-604") - // let report = PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }; - // - // let result = report.requires_payments_retry(); - // - // assert_eq!(result, false) + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for detected_confirmations in detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, None, + "We expected None but got {:?} for case {:?}", + result, case + ); + } + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: reading tx receipts gave no results, \ + but always should" + )] + fn requires_payments_retry_with_no_results_in_whole_summary() { + let report = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + }; + + let _ = report.requires_payments_retry(); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_single_record() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let state_before = subject.sent_payables.clone(); + subject.load_cache(records); + + let first_attempt = subject.get_record_by_hash(tx_hash); + let second_attempt = subject.get_record_by_hash(tx_hash); + + assert_eq!(state_before, hashmap!()); + assert_eq!(first_attempt, Some(sent_tx)); + assert_eq!(second_attempt, None); + assert!( + subject.sent_payables.is_empty(), + "Should be empty but was {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_multiple_records() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx_1 = make_sent_tx(123); + let tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(456); + let tx_hash_2 = sent_tx_2.hash; + let sent_tx_3 = make_sent_tx(789); + let tx_hash_3 = sent_tx_3.hash; + let sent_tx_4 = make_sent_tx(101); + let tx_hash_4 = sent_tx_4.hash; + let nonexistent_tx_hash = make_tx_hash(234); + let records = vec![ + sent_tx_1.clone(), + sent_tx_2.clone(), + sent_tx_3.clone(), + sent_tx_4.clone(), + ]; + + let first_query = subject.get_record_by_hash(tx_hash_1); + subject.load_cache(records); + let second_query = subject.get_record_by_hash(nonexistent_tx_hash); + let third_query = subject.get_record_by_hash(tx_hash_2); + let fourth_query = subject.get_record_by_hash(tx_hash_1); + let fifth_query = subject.get_record_by_hash(tx_hash_4); + let sixth_query = subject.get_record_by_hash(tx_hash_1); + let seventh_query = subject.get_record_by_hash(tx_hash_1); + let eighth_query = subject.get_record_by_hash(tx_hash_3); + + assert_eq!(first_query, None); + assert_eq!(second_query, None); + assert_eq!(third_query, Some(sent_tx_2)); + assert_eq!(fourth_query, Some(sent_tx_1)); + assert_eq!(fifth_query, Some(sent_tx_4)); + assert_eq!(sixth_query, None); + assert_eq!(seventh_query, None); + assert_eq!(eighth_query, Some(sent_tx_3)); + assert!( + subject.sent_payables.is_empty(), + "Expected empty cache, but got {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_ensure_empty_happy_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_happy_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(567); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + let _ = subject.get_record_by_hash(tx_hash); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.sent_payables.is_empty(), + "Should be empty by now but was {:?}", + subject.sent_payables + ); + TestLogHandler::default().exists_no_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some pending payables left unprocessed:" + )); + } + + #[test] + fn pending_payable_cache_ensure_empty_sad_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_sad_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(0x567); + 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: \ + {{0x0000000000000000000000000000000000000000000000000000000000000567: SentTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000567, receiver_address: \ + 0x0000000000000000001035000000001035000000, amount_minor: 3658379210721, timestamp: \ + 275427216, gas_price_minor: 2645248887, nonce: 1383, 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(0x567); + 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: \ + {{0x0000000000000000000000000000000000000000000000000000000000000567: FailedTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000567, receiver_address: \ + 0x00000000000000000003cc0000000003cc000000, amount_minor: 3658379210721, timestamp: \ + 275427216, gas_price_minor: 2645248887, nonce: 1383, 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)); + } + + #[test] + fn tx_hash_by_table_ordering_works_correctly() { + let tx_1 = TxHashByTable::SentPayable(make_tx_hash(123)); + let tx_2 = TxHashByTable::FailedPayable(make_tx_hash(333)); + let tx_3 = TxHashByTable::SentPayable(make_tx_hash(654)); + let tx_4 = TxHashByTable::FailedPayable(make_tx_hash(222)); + let tx_1_identical = tx_1; + let tx_2_identical = tx_2; + + let mut set = BTreeSet::new(); + vec![tx_1.clone(), tx_2.clone(), tx_3.clone(), tx_4.clone()] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); + + let expected_order = vec![tx_4, tx_2, tx_1, tx_3]; + assert_eq!(set.into_iter().collect::>(), expected_order); + assert_eq!(tx_1.cmp(&tx_1_identical), Ordering::Equal); + assert_eq!(tx_2.cmp(&tx_2_identical), Ordering::Equal); } } diff --git a/node/src/accountant/scanners/receivable_scanner/mod.rs b/node/src/accountant/scanners/receivable_scanner/mod.rs index b7222df0d7..eff0df95eb 100644 --- a/node/src/accountant/scanners/receivable_scanner/mod.rs +++ b/node/src/accountant/scanners/receivable_scanner/mod.rs @@ -111,7 +111,7 @@ impl ReceivableScanner { { Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", + "Attempt to advance the start block to {} failed due to: {:?}", start_block_number, e ), } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index e53510f090..dd23e05bd9 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -44,7 +44,7 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq, Eq)] -pub enum ScanRescheduleAfterEarlyStop { +pub enum ScanReschedulingAfterEarlyStop { Schedule(ScanType), DoNotSchedule, } @@ -135,12 +135,17 @@ impl PayableScanScheduler { // 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, logger: &Logger) { + 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: None, + response_skeleton_opt, }, ctx, ) @@ -240,7 +245,7 @@ pub trait RescheduleScanOnErrorResolver { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop; + ) -> ScanReschedulingAfterEarlyStop; } #[derive(Default)] @@ -253,7 +258,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let reschedule_hint = match scanner { PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) @@ -280,16 +285,16 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } } @@ -299,9 +304,9 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else { unreachable!( "{:?} should be impossible with RetryPayableScanner in automatic mode", @@ -314,12 +319,12 @@ impl RescheduleScanOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -335,7 +340,7 @@ impl RescheduleScanOnErrorResolverReal { // the user. // TODO Correctly, a check-point during the bootstrap that wouldn't allow to come // this far should be the solution. Part of the issue mentioned in GH-799 - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) } else { unreachable!( "PendingPayableScanner called later than the initial attempt, but \ @@ -354,7 +359,7 @@ impl RescheduleScanOnErrorResolverReal { scanner: PayableSequenceScanner, is_externally_triggered: bool, logger: &Logger, - reschedule_hint: &ScanRescheduleAfterEarlyStop, + reschedule_hint: &ScanReschedulingAfterEarlyStop, ) { let scan_mode = if is_externally_triggered { "Manual" @@ -376,10 +381,11 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, }; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; + use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; use itertools::Itertools; use lazy_static::lazy_static; use masq_lib::logger::Logger; @@ -591,7 +597,7 @@ mod tests { #[test] fn resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let test_name = "resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered"; @@ -629,7 +635,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::DoNotSchedule, + ScanReschedulingAfterEarlyStop::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, @@ -647,7 +653,7 @@ mod tests { fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( ) { init_test_logging(); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let test_name = "resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true"; let logger = Logger::new(test_name); @@ -664,7 +670,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); @@ -682,7 +688,7 @@ mod tests { )] fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( ) { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let _ = subject .reschedule_on_error_resolver @@ -701,7 +707,7 @@ mod tests { init_test_logging(); let test_name = "resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan"; let logger = Logger::new(test_name); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let scanner = PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, }; @@ -717,7 +723,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables), "We expected Schedule(PendingPayables) but got {:?} for {:?}", result, scanner @@ -735,7 +741,7 @@ mod tests { possible" )] fn pending_p_scan_attempt_if_no_consuming_wallet_found_mustnt_happen_if_not_initial_scan() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let scanner = PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }; @@ -790,7 +796,7 @@ mod tests { StartScanError::NothingToProcess, StartScanError::NoConsumingWalletFound, ]); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); test_forbidden_states(&subject, &inputs, false); test_forbidden_states(&subject, &inputs, true); @@ -800,7 +806,7 @@ mod tests { fn resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered() { let test_name = "resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered"; - let subject = ScanSchedulers::new(ScanIntervals::default(), false); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, false); test_what_if_externally_triggered( test_name, @@ -811,7 +817,7 @@ mod tests { #[test] fn any_automatic_scan_with_start_scan_error_is_fatal_for_retry_payables() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); ALL_START_SCAN_ERRORS.iter().for_each(|error| { let panic = catch_unwind(AssertUnwindSafe(|| { @@ -844,7 +850,7 @@ mod tests { fn resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered() { let test_name = "resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered"; - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); test_what_if_externally_triggered( test_name, @@ -859,7 +865,7 @@ mod tests { should never interfere with itself ScanAlreadyRunning { cross_scan_cause_opt: None, started_at:" )] fn resolve_hint_for_new_payables_if_scan_is_already_running_error_and_is_automatic_scan() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let _ = subject .reschedule_on_error_resolver @@ -885,7 +891,7 @@ mod tests { ]); let logger = Logger::new(test_name); let test_log_handler = TestLogHandler::new(); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); inputs.errors.iter().for_each(|error| { let result = subject @@ -899,7 +905,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, ); diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index bc6709455d..731fb508db 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,6 +2,7 @@ #![cfg(test)] +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::scanners::payable_scanner::msgs::{ InitialTemplatesMessage, PricedTemplatesMessage, }; @@ -10,18 +11,22 @@ use crate::accountant::scanners::payable_scanner::payment_adjuster_integration:: }; use crate::accountant::scanners::payable_scanner::utils::PayableScanResult; use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PayableScanner}; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableCache, PendingPayableScanResult, +}; +use crate::accountant::scanners::pending_payable_scanner::{ + CachesEmptiableScanner, ExtendedPendingPayablePrivateScanner, +}; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, - ScanRescheduleAfterEarlyStop, + ScanReschedulingAfterEarlyStop, }; use crate::accountant::scanners::{ PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, Scanner, StartScanError, StartableScanner, }; use crate::accountant::{ - ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, - SentPayables, + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, SentPayables, TxReceiptsMessage, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, OutboundPaymentsInstructions}; @@ -33,6 +38,7 @@ use masq_lib::ui_gateway::NodeToUiMessage; use regex::Regex; use std::any::type_name; use std::cell::RefCell; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use time::{format_description, PrimitiveDateTime}; @@ -107,6 +113,14 @@ impl SolvencySensitivePaymentInstructor for NullScanner { } } +impl ExtendedPendingPayablePrivateScanner for NullScanner {} + +impl CachesEmptiableScanner for NullScanner { + fn empty_caches(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + impl Default for NullScanner { fn default() -> Self { Self::new() @@ -303,6 +317,19 @@ impl SolvencySensitivePaymentInstructor } } +impl ExtendedPendingPayablePrivateScanner + for ScannerMock +{ +} + +impl CachesEmptiableScanner + for ScannerMock +{ + fn empty_caches(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + pub trait ScannerMockMarker {} impl ScannerMockMarker for ScannerMock {} @@ -368,11 +395,7 @@ pub enum ScannerReplacement { PendingPayable( ReplacementType< PendingPayableScanner, - ScannerMock< - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - >, + ScannerMock, >, ), Receivable( @@ -388,8 +411,8 @@ pub enum MarkScanner<'a> { Started(SystemTime), } -// Cautious: Don't compare to another timestamp on a full match; this timestamp is trimmed in -// nanoseconds down to three digits +// Cautious: Don't compare to another timestamp on an exact match. This timestamp is trimmed in +// nanoseconds down to three digits. Works only for the format bound by TIME_FORMATTING_STRING pub fn parse_system_time_from_str(examined_str: &str) -> Vec { let regex = Regex::new(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})").unwrap(); let captures = regex.captures_iter(examined_str); @@ -445,7 +468,7 @@ pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec>>, - resolve_rescheduling_on_error_results: RefCell>, + resolve_rescheduling_on_error_results: RefCell>, } impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { @@ -455,7 +478,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { self.resolve_rescheduling_on_error_params .lock() .unwrap() @@ -481,7 +504,7 @@ impl RescheduleScanOnErrorResolverMock { } pub fn resolve_rescheduling_on_error_result( self, - result: ScanRescheduleAfterEarlyStop, + result: ScanReschedulingAfterEarlyStop, ) -> Self { self.resolve_rescheduling_on_error_results .borrow_mut() @@ -493,3 +516,70 @@ impl RescheduleScanOnErrorResolverMock { pub fn make_zeroed_consuming_wallet_balances() -> ConsumingWalletBalances { ConsumingWalletBalances::new(0.into(), 0.into()) } + +pub struct PendingPayableCacheMock { + load_cache_params: Arc>>>, + load_cache_results: RefCell>>, + get_record_by_hash_params: Arc>>, + get_record_by_hash_results: RefCell>>, + ensure_empty_cache_params: Arc>>, +} + +impl Default for PendingPayableCacheMock { + fn default() -> Self { + Self { + load_cache_params: Arc::new(Mutex::new(vec![])), + load_cache_results: RefCell::new(vec![]), + get_record_by_hash_params: Arc::new(Mutex::new(vec![])), + get_record_by_hash_results: RefCell::new(vec![]), + ensure_empty_cache_params: Arc::new(Mutex::new(vec![])), + } + } +} + +impl PendingPayableCache for PendingPayableCacheMock { + fn load_cache(&mut self, records: Vec) { + self.load_cache_params.lock().unwrap().push(records); + self.load_cache_results.borrow_mut().remove(0); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.get_record_by_hash_params.lock().unwrap().push(hash); + self.get_record_by_hash_results.borrow_mut().remove(0) + } + + fn ensure_empty_cache(&mut self, _logger: &Logger) { + self.ensure_empty_cache_params.lock().unwrap().push(()); + } + + fn dump_cache(&mut self) -> HashMap { + unimplemented!("not needed yet") + } +} + +impl PendingPayableCacheMock { + pub fn load_cache_params(mut self, params: &Arc>>>) -> Self { + self.load_cache_params = params.clone(); + self + } + + pub fn load_cache_result(self, result: HashMap) -> Self { + self.load_cache_results.borrow_mut().push(result); + self + } + + pub fn get_record_by_hash_params(mut self, params: &Arc>>) -> Self { + self.get_record_by_hash_params = params.clone(); + self + } + + pub fn get_record_by_hash_result(self, result: Option) -> Self { + self.get_record_by_hash_results.borrow_mut().push(result); + self + } + + pub fn ensure_empty_cache_params(mut self, params: &Arc>>) -> Self { + self.ensure_empty_cache_params = params.clone(); + self + } +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index c8d9d89b50..8d6fb49ed3 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -7,23 +7,33 @@ use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureRetrieveCondition, FailureStatus, }; -use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, PayableRetrieveCondition}; -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, + PayableRetrieveCondition, }; + use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; -use crate::accountant::db_access_objects::sent_payable_dao::{RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoFactory, Tx, TxConfirmation}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoFactory, SentTx, TxStatus, +}; use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, TxHash, TxIdentifiers, }; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; -use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; -use crate::blockchain::test_utils::{make_address, make_tx_hash}; +use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; +use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::utils::PayableThresholdsGauge; +use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableCache; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; +use crate::accountant::scanners::test_utils::PendingPayableCacheMock; +use crate::accountant::{gwei_to_wei, Accountant}; +use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, TxBlock}; +use crate::blockchain::errors::validation_status::ValidationFailureClock; +use crate::blockchain::test_utils::make_block_hash; use crate::bootstrapper::BootstrapperConfig; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; @@ -35,24 +45,18 @@ use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::make_bc_with_defaults; -use actix::System; -use ethereum_types::H256; +use ethereum_types::U64; use masq_lib::logger::Logger; +use 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::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; use std::fmt::Debug; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; -use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::PreparedAdjustment; -use crate::accountant::scanners::payable_scanner::utils::PayableThresholdsGauge; -use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; -use crate::accountant::scanners::receivable_scanner::ReceivableScanner; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionBlock; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_unix_timestamp(SystemTime::now()); @@ -90,13 +94,19 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( } } +pub fn make_transaction_block(num: u64) -> TxBlock { + TxBlock { + block_hash: make_block_hash(num as u32), + block_number: U64::from(num * num * 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, @@ -111,7 +121,6 @@ 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, @@ -251,7 +260,12 @@ const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::PendingPayableScanner, ]; -const PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ +const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 2] = [ + DestinationMarker::PayableScanner, + DestinationMarker::PendingPayableScanner, +]; + +const SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, DestinationMarker::PayableScanner, DestinationMarker::PendingPayableScanner, @@ -278,16 +292,16 @@ impl AccountantBuilder { self } - pub fn pending_payable_daos( + pub fn sent_payable_daos( mut self, - specially_configured_daos: Vec>, + specially_configured_daos: Vec>, ) -> Self { create_or_update_factory!( specially_configured_daos, - PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - pending_payable_dao_factory_opt, - PendingPayableDaoFactoryMock, - PendingPayableDao, + SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, + sent_payable_dao_factory_opt, + SentPayableDaoFactoryMock, + SentPayableDao, self ) } @@ -306,6 +320,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![BTreeSet::new()]; + 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>, @@ -320,35 +355,38 @@ impl AccountantBuilder { ) } - pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { - match self.sent_payable_dao_factory_opt { - None => { - self.sent_payable_dao_factory_opt = - Some(SentPayableDaoFactoryMock::new().make_result(sent_payable_dao)) - } - Some(sent_payable_dao_factory) => { - self.sent_payable_dao_factory_opt = - Some(sent_payable_dao_factory.make_result(sent_payable_dao)) - } - } - - self - } - - pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { - match self.failed_payable_dao_factory_opt { - None => { - self.failed_payable_dao_factory_opt = - Some(FailedPayableDaoFactoryMock::new().make_result(failed_payable_dao)) - } - Some(failed_payable_dao_factory) => { - self.failed_payable_dao_factory_opt = - Some(failed_payable_dao_factory.make_result(failed_payable_dao)) - } - } - - self - } + // pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { + // // TODO: GH-605: Bert Merge Cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 + // match self.sent_payable_dao_factory_opt { + // None => { + // self.sent_payable_dao_factory_opt = + // Some(SentPayableDaoFactoryMock::new().make_result(sent_payable_dao)) + // } + // Some(sent_payable_dao_factory) => { + // self.sent_payable_dao_factory_opt = + // Some(sent_payable_dao_factory.make_result(sent_payable_dao)) + // } + // } + // + // self + // } + // + // pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { + // // TODO: GH-605: Bert Merge cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 + // + // match self.failed_payable_dao_factory_opt { + // None => { + // self.failed_payable_dao_factory_opt = + // Some(FailedPayableDaoFactoryMock::new().make_result(failed_payable_dao)) + // } + // Some(failed_payable_dao_factory) => { + // self.failed_payable_dao_factory_opt = + // Some(failed_payable_dao_factory.make_result(failed_payable_dao)) + // } + // } + // + // self + // } //TODO this method seems to be never used? pub fn banned_dao(mut self, banned_dao: BannedDaoMock) -> Self { @@ -370,7 +408,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()) @@ -382,18 +422,18 @@ 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()) + .make_result(FailedPayableDaoMock::new()) + .make_result(FailedPayableDaoMock::new()), ); - let sent_payable_dao_factory = self - .sent_payable_dao_factory_opt - .unwrap_or(SentPayableDaoFactoryMock::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())); @@ -405,7 +445,6 @@ impl AccountantBuilder { DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), sent_payable_dao_factory: Box::new(sent_payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_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), @@ -432,7 +471,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(()); @@ -468,7 +508,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(()); @@ -568,7 +609,7 @@ pub struct PayableDaoMock { retrieve_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>>>, @@ -580,37 +621,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() @@ -690,10 +730,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 } @@ -741,12 +778,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) } @@ -920,213 +958,168 @@ 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: &BTreeSet) -> 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: &BTreeSet) -> 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.clone()); + 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) -> BTreeSet { + 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: &BTreeSet) -> Result<(), SentPayableDaoError> { + self.replace_records_params .lock() .unwrap() - .push(ids.to_vec()); - self.delete_fingerprints_results.borrow_mut().remove(0) + .push(new_txs.clone()); + 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: &BTreeSet) -> Result<(), SentPayableDaoError> { + self.delete_records_params + .lock() + .unwrap() + .push(hashes.clone()); + self.delete_records_results.borrow_mut().remove(0) } } -impl PendingPayableDaoMock { +impl SentPayableDaoMock { pub fn new() -> Self { - PendingPayableDaoMock::default() + SentPayableDaoMock::default() } - pub fn fingerprints_rowids_params(mut self, params: &Arc>>>) -> Self { - self.fingerprints_rowids_params = params.clone(); - self - } - - pub fn fingerprints_rowids_result(self, result: TransactionHashes) -> Self { - self.fingerprints_rowids_results.borrow_mut().push(result); - self - } - - pub fn insert_fingerprints_params( - mut self, - params: &Arc, SystemTime)>>>, - ) -> Self { - self.insert_new_fingerprints_params = params.clone(); + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + self.get_tx_identifiers_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 get_tx_identifiers_result(self, result: TxIdentifiers) -> Self { + self.get_tx_identifiers_results.borrow_mut().push(result); self } - pub fn delete_fingerprints_params(mut self, params: &Arc>>>) -> Self { - self.delete_fingerprints_params = params.clone(); + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + self.insert_new_records_params = params.clone(); self } - pub fn delete_fingerprints_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.delete_fingerprints_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 return_all_errorless_fingerprints_params( + pub fn retrieve_txs_params( mut self, - params: &Arc>>, + params: &Arc>>>, ) -> Self { - self.return_all_errorless_fingerprints_params = params.clone(); + self.retrieve_txs_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 retrieve_txs_result(self, result: BTreeSet) -> Self { + self.retrieve_txs_results.borrow_mut().push(result); self } - pub fn mark_failures_params(mut self, params: &Arc>>>) -> Self { - self.mark_failures_params = params.clone(); + pub fn confirm_tx_params(mut self, params: &Arc>>>) -> Self { + self.confirm_tx_params = params.clone(); self } - pub fn mark_failures_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.mark_failures_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 increment_scan_attempts_params(mut self, params: &Arc>>>) -> Self { - self.increment_scan_attempts_params = params.clone(); + pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { + self.replace_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 replace_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.replace_records_results.borrow_mut().push(result); self } -} -pub struct PendingPayableDaoFactoryMock { - make_params: Arc>>, - make_results: RefCell>>, -} - -impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { - 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." - ) - }; - self.make_params.lock().unwrap().push(()); - self.make_results.borrow_mut().remove(0) + pub fn update_statuses_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.update_statuses_params = params.clone(); + self } -} -impl PendingPayableDaoFactoryMock { - pub fn new() -> Self { - Self { - make_params: Arc::new(Mutex::new(vec![])), - make_results: RefCell::new(vec![]), - } + pub fn update_statuses_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.update_statuses_results.borrow_mut().push(result); + self } - pub fn make_params(mut self, params: &Arc>>) -> Self { - self.make_params = params.clone(); + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + self.delete_records_params = params.clone(); self } - pub fn make_result(self, result: PendingPayableDaoMock) -> Self { - self.make_results.borrow_mut().push(Box::new(result)); + pub fn delete_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.delete_records_results.borrow_mut().push(result); self } } @@ -1169,12 +1162,12 @@ impl FailedPayableDao for FailedPayableDaoMock { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { self.update_statuses_params .lock() .unwrap() - .push(status_updates); + .push(status_updates.clone()); self.update_statuses_results.borrow_mut().remove(0) } @@ -1259,9 +1252,6 @@ pub struct FailedPayableDaoFactoryMock { impl FailedPayableDaoFactory for FailedPayableDaoFactoryMock { fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!("FailedPayableDao Missing.") - }; self.make_params.lock().unwrap().push(()); self.make_results.borrow_mut().remove(0) } @@ -1286,140 +1276,6 @@ impl FailedPayableDaoFactoryMock { } } -#[derive(Default)] -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>>, - update_tx_blocks_params: Arc>>>, - update_tx_blocks_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: &BTreeSet) -> TxIdentifiers { - self.get_tx_identifiers_params - .lock() - .unwrap() - .push(hashes.clone()); - self.get_tx_identifiers_results.borrow_mut().remove(0) - } - - fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError> { - self.insert_new_records_params - .lock() - .unwrap() - .push(txs.clone()); - self.insert_new_records_results.borrow_mut().remove(0) - } - - fn retrieve_txs(&self, condition: Option) -> BTreeSet { - self.retrieve_txs_params.lock().unwrap().push(condition); - self.retrieve_txs_results.borrow_mut().remove(0) - } - - fn confirm_tx( - &self, - _hash_map: &HashMap, - ) -> Result<(), SentPayableDaoError> { - todo!() - } - - fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError> { - self.replace_records_params - .lock() - .unwrap() - .push(new_txs.clone()); - self.replace_records_results.borrow_mut().remove(0) - } - - fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError> { - self.delete_records_params - .lock() - .unwrap() - .push(hashes.clone()); - self.delete_records_results.borrow_mut().remove(0) - } -} - -impl SentPayableDaoMock { - 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<(), SentPayableDaoError>) -> 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: BTreeSet) -> Self { - self.retrieve_txs_results.borrow_mut().push(result); - self - } - - pub fn update_tx_blocks_params( - mut self, - params: &Arc>>>, - ) -> Self { - self.update_tx_blocks_params = params.clone(); - self - } - - pub fn update_tx_blocks_result(self, result: Result<(), SentPayableDaoError>) -> Self { - self.update_tx_blocks_results.borrow_mut().push(result); - self - } - - pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { - self.replace_records_params = params.clone(); - self - } - - pub fn replace_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { - self.replace_records_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<(), SentPayableDaoError>) -> Self { - self.delete_records_results.borrow_mut().push(result); - self - } -} - pub struct SentPayableDaoFactoryMock { make_params: Arc>>, make_results: RefCell>>, @@ -1427,9 +1283,6 @@ pub struct SentPayableDaoFactoryMock { impl SentPayableDaoFactory for SentPayableDaoFactoryMock { fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!("SentPayableDao Missing.") - }; self.make_params.lock().unwrap().push(()); self.make_results.borrow_mut().remove(0) } @@ -1456,20 +1309,26 @@ impl SentPayableDaoFactoryMock { 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()), } } @@ -1478,24 +1337,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 } } @@ -1563,17 +1444,6 @@ pub fn make_custom_payment_thresholds() -> PaymentThresholds { } } -pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { - PendingPayableFingerprint { - rowid: 33, - timestamp: from_unix_timestamp(222_222_222), - hash: make_tx_hash(456), - attempt: 1, - amount: 12345, - process_error: None, - } -} - pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, @@ -1655,10 +1525,10 @@ where { let conn = Connection::open_in_memory().unwrap(); let execute = |sql: &str| conn.execute(sql, []).unwrap(); - execute("create table whatever (exclamations text)"); - execute("insert into whatever (exclamations) values ('Gosh')"); + execute("create table whatever (exclamation text)"); + execute("insert into whatever (exclamation) values ('Gosh')"); - conn.query_row("select exclamations from whatever", [], tested_fn) + conn.query_row("select exclamation from whatever", [], tested_fn) .unwrap(); } diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 0af412f401..79cfa03f87 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -473,7 +473,6 @@ 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 failed_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let sent_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let receivable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); @@ -486,7 +485,6 @@ impl ActorFactory for ActorFactoryReal { config, DaoFactories { payable_dao_factory, - pending_payable_dao_factory, sent_payable_dao_factory, failed_payable_dao_factory, receivable_dao_factory, @@ -1169,7 +1167,7 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - scan_intervals_opt: Some(ScanIntervals::default()), + scan_intervals_opt: Some(ScanIntervals::compute_default(TEST_DEFAULT_CHAIN)), automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, diff --git a/node/src/blockchain/blockchain_agent/agent_web3.rs b/node/src/blockchain/blockchain_agent/agent_web3.rs index bbf5d377fe..66df08d574 100644 --- a/node/src/blockchain/blockchain_agent/agent_web3.rs +++ b/node/src/blockchain/blockchain_agent/agent_web3.rs @@ -7,11 +7,9 @@ use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::P use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; -use itertools::{Either, Itertools}; +use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use masq_lib::utils::ExpectValue; -use thousands::Separable; #[derive(Debug, Clone)] pub struct BlockchainAgentWeb3 { @@ -322,7 +320,7 @@ mod tests { "\n", ); TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: The computed gas price {} wei is above the ceil value of {} wei set by the Node.\n\ + "WARN: {test_name}: The computed gas price {} wei is above the ceil value of {} wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ {}", expected_calculated_surplus_value_wei.separate_with_commas(), @@ -345,18 +343,16 @@ mod tests { (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 template_1 = RetryTxTemplateBuilder::new() - .receiver_address(account_1.wallet.address()) - .amount_in_wei(account_1.balance_wei) + .payable_account(&account_1) .prev_gas_price_wei(rpc_gas_price_wei - 1) .build(); let template_2 = RetryTxTemplateBuilder::new() - .receiver_address(account_2.wallet.address()) - .amount_in_wei(account_2.balance_wei) + .payable_account(&account_2) .prev_gas_price_wei(rpc_gas_price_wei - 2) .build(); let retry_tx_templates = vec![template_1, template_2]; let expected_log_msg = format!( - "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei set by the Node.\n\ + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ 0x00000000000000000000000077616c6c65743132 with gas price 50,000,000,001\n\ 0x00000000000000000000000077616c6c65743334 with gas price 50,000,000,001" @@ -392,18 +388,16 @@ mod tests { let rpc_gas_price_wei = border_gas_price_wei - 1; let check_value_wei = increase_gas_price_by_margin(border_gas_price_wei); let template_1 = RetryTxTemplateBuilder::new() - .receiver_address(account_1.wallet.address()) - .amount_in_wei(account_1.balance_wei) + .payable_account(&account_1) .prev_gas_price_wei(border_gas_price_wei) .build(); let template_2 = RetryTxTemplateBuilder::new() - .receiver_address(account_2.wallet.address()) - .amount_in_wei(account_2.balance_wei) + .payable_account(&account_2) .prev_gas_price_wei(border_gas_price_wei) .build(); let retry_tx_templates = vec![template_1, template_2]; let expected_log_msg = format!( - "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei set by the Node.\n\ + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ 0x00000000000000000000000077616c6c65743132 with gas price 50,000,000,001\n\ 0x00000000000000000000000077616c6c65743334 with gas price 50,000,000,001" @@ -429,18 +423,16 @@ mod tests { let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); let template_1 = RetryTxTemplateBuilder::new() - .receiver_address(account_1.wallet.address()) - .amount_in_wei(account_1.balance_wei) + .payable_account(&account_1) .prev_gas_price_wei(fetched_gas_price_wei - 2) .build(); let template_2 = RetryTxTemplateBuilder::new() - .receiver_address(account_2.wallet.address()) - .amount_in_wei(account_2.balance_wei) + .payable_account(&account_2) .prev_gas_price_wei(fetched_gas_price_wei - 3) .build(); let retry_tx_templates = vec![template_1, template_2]; let expected_log_msg = format!( - "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei set by the Node.\n\ + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ 0x00000000000000000000000077616c6c65743132 with gas price 64,999,999,998\n\ 0x00000000000000000000000077616c6c65743334 with gas price 64,999,999,998" @@ -463,18 +455,16 @@ mod tests { let account_1 = make_payable_account(12); let account_2 = make_payable_account(34); let template_1 = RetryTxTemplateBuilder::new() - .receiver_address(account_1.wallet.address()) - .amount_in_wei(account_1.balance_wei) + .payable_account(&account_1) .prev_gas_price_wei(ceiling_gas_price_wei - 1) .build(); let template_2 = RetryTxTemplateBuilder::new() - .receiver_address(account_2.wallet.address()) - .amount_in_wei(account_2.balance_wei) + .payable_account(&account_2) .prev_gas_price_wei(ceiling_gas_price_wei - 2) .build(); let retry_tx_templates = vec![template_1, template_2]; let expected_log_msg = format!( - "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei set by the Node.\n\ + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ 0x00000000000000000000000077616c6c65743132 with gas price 64,999,999,998\n\ 0x00000000000000000000000077616c6c65743334 with gas price 64,999,999,997" @@ -500,18 +490,16 @@ mod tests { // The values can never go above the ceiling, therefore, we can assume only values even or // smaller than that in the previous attempts let template_1 = RetryTxTemplateBuilder::new() - .receiver_address(account_1.wallet.address()) - .amount_in_wei(account_1.balance_wei) + .payable_account(&account_1) .prev_gas_price_wei(ceiling_gas_price_wei) .build(); let template_2 = RetryTxTemplateBuilder::new() - .receiver_address(account_2.wallet.address()) - .amount_in_wei(account_2.balance_wei) + .payable_account(&account_2) .prev_gas_price_wei(ceiling_gas_price_wei) .build(); let retry_tx_templates = vec![template_1, template_2]; let expected_log_msg = format!( - "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei set by the Node.\n\ + "The computed gas price(s) in wei is above the ceil value of 50,000,000,000 wei computed by this Node.\n\ Transaction(s) to following receivers are affected:\n\ 0x00000000000000000000000077616c6c65743132 with gas price 650,000,000,000\n\ 0x00000000000000000000000077616c6c65743334 with gas price 650,000,000,000" @@ -537,7 +525,7 @@ mod tests { 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_result = match tx_templates.clone() { + let expected_result = match &tx_templates { Either::Left(new_tx_templates) => Either::Left(PricedNewTxTemplates( new_tx_templates .iter() diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 3eb71f0acf..119acaee91 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,13 +1,24 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::{PayableScanType, ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder}; -use crate::accountant::{ReportTransactionReceipts, RequestTransactionReceipts}; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; +use crate::accountant::scanners::payable_scanner::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, +}; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; +use crate::accountant::scanners::payable_scanner::utils::initial_templates_msg_stats; +use crate::accountant::{ + ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, +}; +use crate::accountant::{RequestTransactionReceipts, TxReceiptResult, TxReceiptsMessage}; use crate::actor_system_factory::SubsFactory; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainInterfaceError, LocalPayableError, }; -use crate::blockchain::blockchain_interface::data_structures::{BatchResults}; +use crate::blockchain::blockchain_interface::data_structures::{ + BatchResults, StatusReadFromReceiptCheck, +}; use crate::blockchain::blockchain_interface::BlockchainInterface; use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal}; @@ -15,12 +26,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; @@ -29,6 +39,7 @@ use actix::{Addr, Recipient}; use futures::Future; use itertools::{Either, Itertools}; use masq_lib::blockchains::chains::Chain; +use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; @@ -36,16 +47,7 @@ use std::path::Path; use std::string::ToString; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -use ethabi::Hash; use web3::types::H256; -use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; -use masq_lib::messages::ScanType; -use crate::accountant::scanners::payable_scanner::msgs::{PricedTemplatesMessage, InitialTemplatesMessage}; -use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; -use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; -use crate::accountant::scanners::payable_scanner::utils::initial_templates_msg_stats; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; pub const DEFAULT_BLOCKCHAIN_SERVICE_URL: &str = "https://0.0.0.0"; @@ -59,12 +61,12 @@ pub struct BlockchainBridge { received_payments_subs_opt: Option>, scan_error_subs_opt: Option>, crashable: bool, - pending_payable_confirmation: TransactionConfirmationTools, + pending_payable_confirmation: TxConfirmationTools, } -struct TransactionConfirmationTools { - new_pp_fingerprints_sub_opt: Option>, - report_transaction_receipts_sub_opt: Option>, +struct TxConfirmationTools { + register_new_pending_payables_sub_opt: Option>, + report_tx_receipts_sub_opt: Option>, } #[derive(PartialEq, Eq)] @@ -88,11 +90,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); @@ -125,7 +126,7 @@ impl Handler for BlockchainBridge { ) -> >::Result { self.handle_scan_future( Self::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg, ) } @@ -137,17 +138,25 @@ 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, ) } } +pub trait MsgInterpretableAsDetailedScanType { + fn detailed_scan_type(&self) -> DetailedScanType; +} + impl Handler for BlockchainBridge { type Result = (); fn handle(&mut self, msg: InitialTemplatesMessage, _ctx: &mut Self::Context) { - self.handle_scan_future(Self::handle_initial_templates_msg, ScanType::Payables, msg); + self.handle_scan_future( + Self::handle_initial_templates_msg, + msg.detailed_scan_type(), + msg, + ); } } @@ -157,28 +166,21 @@ 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, + msg.detailed_scan_type(), 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 { @@ -204,9 +206,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, }, } } @@ -287,7 +289,7 @@ impl BlockchainBridge { fn payment_procedure_result_from_error(e: LocalPayableError) -> Result { match e { - LocalPayableError::Sending(failed_txs) => Ok(BatchResults { + LocalPayableError::Sending { failed_txs, .. } => Ok(BatchResults { sent_txs: vec![], failed_txs, }), @@ -300,43 +302,38 @@ impl BlockchainBridge { msg: OutboundPaymentsInstructions, ) -> Box> { let skeleton_opt = msg.response_skeleton_opt; - let sent_payable_subs = self + let sent_payable_subs_success = self .sent_payable_subs_opt .as_ref() .expect("Accountant is unbound") .clone(); - - let payable_scan_type = if msg.priced_templates.is_left() { - PayableScanType::New - } else { - PayableScanType::Retry - }; - - let payable_scan_type_for_err = payable_scan_type.clone(); - - let send_message_if_failure = move |msg: SentPayables| { - sent_payable_subs.try_send(msg).expect("Accountant is dead"); - }; - let send_message_if_successful = send_message_if_failure.clone(); + let sent_payable_subs_err = sent_payable_subs_success.clone(); + let payable_scan_type = msg.scan_type(); Box::new( self.process_payments(msg.agent, msg.priced_templates) .map_err(move |e: LocalPayableError| { - send_message_if_failure(SentPayables { - payment_procedure_result: Self::payment_procedure_result_from_error( - e.clone(), - ), - payable_scan_type: payable_scan_type_for_err, - response_skeleton_opt: skeleton_opt, - }); + sent_payable_subs_err + .try_send(SentPayables { + payment_procedure_result: Self::payment_procedure_result_from_error( + e.clone(), + ), + payable_scan_type, + response_skeleton_opt: skeleton_opt, + }) + .expect("Accountant is dead"); + format!("ReportAccountsPayable: {}", e) }) .and_then(move |batch_results| { - send_message_if_successful(SentPayables { - payment_procedure_result: Ok(batch_results), - payable_scan_type, - response_skeleton_opt: skeleton_opt, - }); + sent_payable_subs_success + .try_send(SentPayables { + payment_procedure_result: Ok(batch_results), + payable_scan_type, + response_skeleton_opt: skeleton_opt, + }) + .expect("Accountant is dead"); + Ok(()) }), ) @@ -421,21 +418,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!( @@ -452,30 +449,21 @@ impl BlockchainBridge { let logger = self.logger.clone(); let accountant_recipient = self .pending_payable_confirmation - .report_transaction_receipts_sub_opt + .report_tx_receipts_sub_opt .clone() .expect("Accountant is unbound"); - - let transaction_hashes = msg - .pending_payable_fingerprints - .iter() - .map(|finger_print| finger_print.hash) - .collect::>(); Box::new( self.blockchain_interface - .process_transaction_receipts(transaction_hashes) + .process_transaction_receipts(msg.tx_hashes) .map_err(move |e| e.to_string()) - .and_then(move |transaction_receipts_results| { - Self::log_status_of_tx_receipts(&logger, &transaction_receipts_results); - - let pairs = transaction_receipts_results - .into_iter() - .zip(msg.pending_payable_fingerprints.into_iter()) - .collect_vec(); - + .and_then(move |tx_receipt_results| { + Self::log_status_of_tx_receipts( + &logger, + tx_receipt_results.values().collect_vec().as_slice(), + ); accountant_recipient - .try_send(ReportTransactionReceipts { - fingerprints_with_receipts: pairs, + .try_send(TxReceiptsMessage { + results: tx_receipt_results, response_skeleton_opt: msg.response_skeleton_opt, }) .expect("Accountant is dead"); @@ -485,7 +473,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, @@ -519,13 +507,6 @@ impl BlockchainBridge { .submit_payables_in_batch(logger, agent, priced_templates) } - fn new_fingerprints_recipient(&self) -> Recipient { - self.pending_payable_confirmation - .new_pp_fingerprints_sub_opt - .clone() - .expect("Accountant unbound") - } - 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+).*") @@ -572,35 +553,54 @@ impl SubsFactory for BlockchainBridgeSub #[cfg(test)] mod tests { use super::*; + use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; + use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::Submission; + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; use crate::accountant::db_access_objects::payable_dao::PayableAccount; + use crate::accountant::db_access_objects::sent_payable_dao::TxStatus::Pending; + use crate::accountant::db_access_objects::test_utils::{ + assert_on_failed_txs, assert_on_sent_txs, + }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplate; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::make_priced_new_tx_templates; + use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainAgentBuildError; use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError::TransactionID; - use crate::blockchain::blockchain_interface::data_structures::errors::{ - BlockchainAgentBuildError, LocalPayableError, - }; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, RetrievedBlockchainTransactions, + BlockchainTransaction, RetrievedBlockchainTransactions, TxBlock, }; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalErrorKind, RemoteError, + }; + use crate::blockchain::errors::validation_status::ValidationStatus; + use crate::blockchain::errors::validation_status::ValidationStatus::Waiting; use crate::blockchain::test_utils::{ make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; use crate::db_config::persistent_configuration::PersistentConfigError; 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::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::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; @@ -614,22 +614,6 @@ mod tests { use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160}; - use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; - use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, ValidationStatus}; - use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::Submission; - use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::RetryRequired; - use crate::accountant::db_access_objects::failed_payable_dao::ValidationStatus::Waiting; - use crate::accountant::db_access_objects::sent_payable_dao::Tx; - use crate::accountant::db_access_objects::sent_payable_dao::TxStatus::Pending; - use crate::accountant::db_access_objects::test_utils::{assert_on_failed_txs, assert_on_sent_txs}; - use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; - use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplate; - use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::make_priced_new_tx_templates; - use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt}; - use crate::blockchain::errors::rpc_errors::AppRpcError::Local; - use crate::blockchain::errors::rpc_errors::LocalError::Transport; - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; impl Handler> for BlockchainBridge { type Result = (); @@ -643,6 +627,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"); @@ -927,9 +922,7 @@ mod tests { }) .unwrap(); - let time_before = SystemTime::now(); system.run(); - let time_after = SystemTime::now(); let accountant_recording = accountant_recording_arc.lock().unwrap(); // TODO: GH-701: This card is related to the commented out code in this test // let pending_payable_fingerprint_seeds_msg = @@ -939,15 +932,15 @@ mod tests { assert!(batch_results.failed_txs.is_empty()); assert_on_sent_txs( batch_results.sent_txs, - vec![Tx { + vec![SentTx { hash: H256::from_str( "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c", ) .unwrap(), receiver_address: account.wallet.address(), - amount: account.balance_wei, + amount_minor: account.balance_wei, timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: 111_222_333, + gas_price_minor: 111_222_333, nonce: 32, status: Pending(Waiting), }], @@ -985,7 +978,7 @@ mod tests { let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); - let wallet_account = make_wallet("blah"); + let account_wallet = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); let persistent_configuration_mock = PersistentConfigurationMock::default(); let subject = BlockchainBridge::new( @@ -998,7 +991,7 @@ mod tests { let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); let account = PayableAccount { - wallet: wallet_account, + wallet: account_wallet.clone(), balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, @@ -1025,10 +1018,10 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let pending_payable_fingerprint_seeds_msg = - accountant_recording.get_record::(0); - let sent_payables_msg = accountant_recording.get_record::(1); - let scan_error_msg = accountant_recording.get_record::(2); + // let pending_payable_fingerprint_seeds_msg = + // accountant_recording.get_record::(0); + let sent_payables_msg = accountant_recording.get_record::(0); + let scan_error_msg = accountant_recording.get_record::(1); let batch_results = sent_payables_msg.clone().payment_procedure_result.unwrap(); let failed_tx = FailedTx { hash: H256::from_str( @@ -1036,25 +1029,26 @@ mod tests { ) .unwrap(), receiver_address: account.wallet.address(), - amount: account.balance_wei, + amount_minor: account.balance_wei, timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: 111222333, + gas_price_minor: 111222333, nonce: 32, - reason: Submission(Local(Transport("Error(IncompleteMessage)".to_string()))), + reason: Submission(AppRpcErrorKind::Local(LocalErrorKind::Transport)), status: RetryRequired, }; assert_on_failed_txs(batch_results.failed_txs, vec![failed_tx]); - assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap(), - amount: account.balance_wei - }] - ); - assert_eq!(scan_error_msg.scan_type, ScanType::Payables); + // TODO: GH-701: This card is related to the commented out code in this test + // assert_eq!( + // pending_payable_fingerprint_seeds_msg.hashes_and_balances, + // vec![HashAndAmount { + // hash: H256::from_str( + // "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" + // ) + // .unwrap(), + // amount: account.balance_wei + // }] + // ); + assert_eq!(scan_error_msg.scan_type, DetailedScanType::NewPayables); assert_eq!( scan_error_msg.response_skeleton_opt, Some(ResponseSkeleton { @@ -1064,12 +1058,12 @@ mod tests { ); assert!(scan_error_msg .msg - .contains("ReportAccountsPayable: Sending error. Signed and hashed transactions:")); + .contains("ReportAccountsPayable: Sending error: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions:"), "This string didn't contain the expected: {}", scan_error_msg.msg); assert!(scan_error_msg.msg.contains( "FailedTx { hash: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c," )); - assert!(scan_error_msg.msg.contains("reason: Submission(Local(Transport(\"Error(IncompleteMessage)\"))), status: RetryRequired }")); - assert_eq!(accountant_recording.len(), 3); + assert!(scan_error_msg.msg.contains("FailedTx { hash: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c, receiver_address: 0x00000000000000000000000000000000626c6168, amount_minor: 111420204, timestamp:"), "This string didn't contain the expected: {}", scan_error_msg.msg); + assert_eq!(accountant_recording.len(), 2); } #[test] @@ -1110,7 +1104,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.priced_templates) @@ -1122,27 +1116,27 @@ mod tests { assert_on_sent_txs( batch_results.sent_txs, vec![ - Tx { + SentTx { hash: H256::from_str( "c0756e8da662cee896ed979456c77931668b7f8456b9f978fc3305671f8f82ad", ) .unwrap(), receiver_address: account_1.wallet.address(), - amount: account_1.balance_wei, + amount_minor: account_1.balance_wei, timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: 777_777_777, + gas_price_minor: 777_777_777, nonce: 1, status: Pending(ValidationStatus::Waiting), }, - Tx { + SentTx { hash: H256::from_str( "9ba19f88ce43297d700b1f57ed8bc6274d01a5c366b78dd05167f9874c867ba0", ) .unwrap(), receiver_address: account_2.wallet.address(), - amount: account_2.balance_wei, + amount_minor: account_2.balance_wei, timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: 999_999_999, + gas_price_minor: 999_999_999, nonce: 2, status: Pending(ValidationStatus::Waiting), }, @@ -1150,7 +1144,7 @@ mod tests { ); assert!(batch_results.failed_txs.is_empty()); let recording = accountant_recording.lock().unwrap(); - assert_eq!(recording.len(), 1); + assert_eq!(recording.len(), 0); } #[test] @@ -1183,7 +1177,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.priced_templates) @@ -1205,21 +1199,13 @@ mod tests { #[test] fn blockchain_bridge_processes_requests_for_a_complete_and_null_transaction_receipt() { let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); - let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); - let hash_1 = pending_payable_fingerprint_1.hash; - let hash_2 = make_tx_hash(78989); - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 4565, - process_error: None, - }; + let accountant = + accountant.system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)); + let tx_hash_1 = make_tx_hash(123); + let tx_hash_2 = make_tx_hash(456); let first_response = ReceiptResponseBuilder::default() .status(U64::from(1)) - .transaction_hash(hash_1) + .transaction_hash(tx_hash_1) .build(); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) @@ -1240,9 +1226,9 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1256,26 +1242,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: btreemap![ + 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, @@ -1325,7 +1305,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() } @@ -1337,8 +1317,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); @@ -1353,62 +1332,26 @@ mod tests { .begin_batch() .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .raw_response(tx_receipt_response) - .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .err_response( 429, "The requests per second (RPS) of your requests are higher than your plan allows." .to_string(), 7, ) + .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .end_batch() .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!( - ReportTransactionReceipts, - ScanError - )) + .system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)) .start(); - let report_transaction_receipt_recipient: Recipient = + let report_transaction_receipt_recipient: Recipient = accountant_addr.clone().recipient(); let scan_error_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(111334); - let hash_2 = make_tx_hash(100000); - let hash_3 = make_tx_hash(0x1348d); - let hash_4 = make_tx_hash(11111); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.hash = hash_1; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_3 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_3, - attempt: 3, - amount: 4565, - process_error: None, - }; - let fingerprint_4 = PendingPayableFingerprint { - rowid: 450, - timestamp: from_unix_timestamp(230_000_000), - hash: hash_4, - attempt: 1, - amount: 7879, - process_error: None, - }; - let transaction_receipt = TxReceipt { - transaction_hash: Default::default(), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number, - }), - }; + let tx_hash_1 = make_tx_hash(1334); + let tx_hash_2 = make_tx_hash(1000); + let tx_hash_3 = make_tx_hash(1212); + let tx_hash_4 = make_tx_hash(1111); let blockchain_interface = make_blockchain_interface_web3(port); let system = System::new("test_transaction_receipts"); let mut subject = BlockchainBridge::new( @@ -1418,14 +1361,14 @@ mod tests { ); subject .pending_payable_confirmation - .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); + .report_tx_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - fingerprint_1.clone(), - fingerprint_2.clone(), - fingerprint_3.clone(), - fingerprint_4.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::SentPayable(tx_hash_2), + TxHashByTable::SentPayable(tx_hash_3), + TxHashByTable::SentPayable(tx_hash_4), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1439,15 +1382,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: btreemap![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, @@ -1461,32 +1407,17 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_if_submit_batch_fails() { + fn handle_request_transaction_receipts_failing_submit_the_batch() { init_test_logging(); let (accountant, _, accountant_recording) = make_recorder(); let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); - let report_transaction_recipient: Recipient = + let report_transaction_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(0x1b2e6); - let fingerprint_1 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_1, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: make_tx_hash(222444), - attempt: 3, - amount: 4565, - process_error: None, - }; + let tx_hash_1 = make_tx_hash(10101); + let tx_hash_2 = make_tx_hash(10102); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); let blockchain_interface = make_blockchain_interface_web3(port); @@ -1497,17 +1428,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_fingerprints: vec![fingerprint_1, fingerprint_2], + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + ], response_skeleton_opt: None, }; let system = System::new("test"); let _ = subject.handle_scan_future( BlockchainBridge::handle_request_transaction_receipts, - ScanType::PendingPayables, + DetailedScanType::PendingPayables, msg, ); @@ -1516,7 +1450,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() @@ -1885,7 +1819,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 @@ -1945,7 +1879,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 @@ -2084,19 +2018,22 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); + subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, - ScanType::Receivables, + 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); } @@ -2142,7 +2079,7 @@ mod tests { subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg.clone(), ); @@ -2152,7 +2089,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() @@ -2292,21 +2229,3 @@ mod tests { assert_eq!(increase_gas_price_by_margin(9_000_000_000), 11_700_000_000); } } - -#[cfg(test)] -pub mod exportable_test_parts { - use super::*; - use crate::test_utils::recorder::make_blockchain_bridge_subs_from_recorder; - use crate::test_utils::unshared_test_utils::SubsFactoryTestAddrLeaker; - - impl SubsFactory - for SubsFactoryTestAddrLeaker - { - fn make(&self, addr: &Addr) -> BlockchainBridgeSubs { - self.send_leaker_msg_and_return_meaningless_subs( - addr, - make_blockchain_bridge_subs_from_recorder, - ) - } - } -} diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index c93c07b538..7a4d6ddfb6 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -6,104 +6,12 @@ use crate::blockchain::blockchain_interface::data_structures::errors::Blockchain use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use ethereum_types::{H256, U256, U64}; use futures::Future; -use serde_derive::{Deserialize, Serialize}; use serde_json::Value; -use std::fmt::Display; -use std::str::FromStr; use web3::contract::{Contract, Options}; use web3::transports::{Batch, Http}; -use web3::types::{Address, BlockNumber, Filter, Log, TransactionReceipt}; +use web3::types::{Address, BlockNumber, Filter, Log}; use web3::{Error, Web3}; -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TransactionReceiptResult { - RpcResponse(TxReceipt), - LocalError(String), -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TxStatus { - Failed, - Pending, - Succeeded(TransactionBlock), -} - -impl FromStr for TxStatus { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "Pending" => Ok(TxStatus::Pending), - "Failed" => Ok(TxStatus::Failed), // TODO: GH-631: This should be removed - s if s.starts_with("Succeeded") => { - // The format is "Succeeded(block_number, block_hash)" - let parts: Vec<&str> = s[10..s.len() - 1].split(',').collect(); - if parts.len() != 2 { - return Err("Invalid Succeeded format".to_string()); - } - let block_number: u64 = parts[0] - .parse() - .map_err(|_| "Invalid block number".to_string())?; - let block_hash = - H256::from_str(&parts[1][2..]).map_err(|_| "Invalid block hash".to_string())?; - Ok(TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number: U64::from(block_number), - })) - } - _ => Err(format!("Unknown status: {}", s)), - } - } -} - -impl Display for TxStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TxStatus::Failed => write!(f, "Failed"), - TxStatus::Pending => write!(f, "Pending"), - TxStatus::Succeeded(block) => { - write!( - f, - "Succeeded({},{:?})", - block.block_number, block.block_hash - ) - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct TxReceipt { - pub transaction_hash: H256, - pub status: TxStatus, -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct TransactionBlock { - pub block_hash: H256, - pub block_number: U64, -} - -impl From for TxReceipt { - fn from(receipt: TransactionReceipt) -> Self { - let status = match (receipt.status, receipt.block_hash, receipt.block_number) { - (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { - TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }) - } - (Some(status), _, _) if status == U64::from(0) => TxStatus::Failed, - _ => TxStatus::Pending, - }; - - TxReceipt { - transaction_hash: receipt.transaction_hash, - status, - } - } -} - pub struct LowBlockchainIntWeb3 { web3: Web3, web3_batch: Web3>, @@ -222,7 +130,7 @@ mod tests { use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_LITERAL; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::{BlockchainInterfaceError, BlockchainInterface}; - use crate::blockchain::test_utils::make_blockchain_interface_web3; + use crate::blockchain::test_utils::{make_block_hash, make_blockchain_interface_web3, make_tx_hash, TransactionReceiptBuilder}; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use ethereum_types::{H256, U64}; @@ -230,8 +138,8 @@ mod tests { use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::utils::find_free_port; use std::str::FromStr; - use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, TransactionReceipt, U256}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, U256}; + use crate::blockchain::blockchain_interface::data_structures::StatusReadFromReceiptCheck; #[test] fn get_transaction_fee_balance_works() { @@ -601,17 +509,17 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_successful_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - Some(U64::from(10)), - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - match tx_receipt.status { - TxStatus::Succeeded(ref block) => { - assert_eq!(block.block_hash, H256::from_low_u64_be(0x1234)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .block_hash(make_block_hash(0x1234)) + .block_number(10.into()) + .build() + .into(); + + match tx_status { + StatusReadFromReceiptCheck::Succeeded(ref block) => { + assert_eq!(block.block_hash, make_block_hash(0x1234)); assert_eq!(block.block_number, U64::from(10)); } _ => panic!("Expected status to be Succeeded"), @@ -620,139 +528,43 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_failed_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(0)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(0)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Failed); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Reverted); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status() { - let tx_receipt: TxReceipt = - create_tx_receipt(None, None, None, H256::from_low_u64_be(0x5678)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status_and_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - None, - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); - } - - #[test] - fn tx_status_display_works() { - // Test Failed - assert_eq!(TxStatus::Failed.to_string(), "Failed"); - - // Test Pending - assert_eq!(TxStatus::Pending.to_string(), "Pending"); - - // Test Succeeded - let block_number = U64::from(12345); - let block_hash = H256::from_low_u64_be(0xabcdef); - let succeeded = TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }); - assert_eq!( - succeeded.to_string(), - format!("Succeeded({},0x{:x})", block_number, block_hash) - ); - } - - #[test] - fn tx_status_from_str_works() { - // Test Pending - assert_eq!(TxStatus::from_str("Pending"), Ok(TxStatus::Pending)); - - // Test Failed - assert_eq!(TxStatus::from_str("Failed"), Ok(TxStatus::Failed)); - - // Test Succeeded with valid input - let block_number = 123456789; - let block_hash = H256::from_low_u64_be(0xabcdef); - let input = format!("Succeeded({},0x{:x})", block_number, block_hash); - assert_eq!( - TxStatus::from_str(&input), - Ok(TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number: U64::from(block_number), - })) - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - // Test Succeeded with invalid format - assert_eq!( - TxStatus::from_str("Succeeded(123)"), - Err("Invalid Succeeded format".to_string()) - ); - - // Test Succeeded with invalid block number - assert_eq!( - TxStatus::from_str( - "Succeeded(abc,0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef)" - ), - Err("Invalid block number".to_string()) - ); - - // Test Succeeded with invalid block hash - assert_eq!( - TxStatus::from_str("Succeeded(123,0xinvalidhash)"), - Err("Invalid block hash".to_string()) - ); - - // Test unknown status - assert_eq!( - TxStatus::from_str("InProgress"), - Err("Unknown status: InProgress".to_string()) - ); - } - - fn create_tx_receipt( - status: Option, - block_hash: Option, - block_number: Option, - transaction_hash: H256, - ) -> TxReceipt { - let receipt = TransactionReceipt { - status, - root: None, - block_hash, - block_number, - cumulative_gas_used: Default::default(), - gas_used: None, - contract_address: None, - transaction_hash, - transaction_index: Default::default(), - logs: vec![], - logs_bloom: Default::default(), - }; - receipt.into() + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 5658870758..9249c6ee0b 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,8 +4,9 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; +use std::collections::{BTreeMap}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, LocalPayableError}; -use crate::blockchain::blockchain_interface::data_structures::{BatchResults, BlockchainTransaction}; +use crate::blockchain::blockchain_interface::data_structures::{BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainInterface}; @@ -16,20 +17,22 @@ use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; use std::convert::{From, TryInto}; use std::fmt::Debug; -use actix::Recipient; use ethereum_types::U64; use itertools::Either; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::db_access_objects::sent_payable_dao::Tx; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; +use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplates; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{LowBlockchainIntWeb3, TransactionReceiptResult, TxReceipt, TxStatus}; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::LowBlockchainIntWeb3; use crate::blockchain::blockchain_interface::blockchain_interface_web3::utils::{create_blockchain_agent_web3, send_payables_within_batch, BlockchainAgentFutureResult}; - +use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; // 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 @@ -186,7 +189,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) @@ -199,7 +202,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { .and_then(move |masq_token_balance| { let blockchain_agent_future_result = BlockchainAgentFutureResult { - gas_price_wei, + gas_price_minor, transaction_fee_balance, masq_token_balance, }; @@ -217,38 +220,44 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>> - { + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = BTreeMap, + 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::>()) }), ) } @@ -288,14 +297,14 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub struct HashAndAmount { pub hash: H256, - pub amount: u128, + pub amount_minor: u128, } -impl From<&Tx> for HashAndAmount { - fn from(tx: &Tx) -> Self { +impl From<&SentTx> for HashAndAmount { + fn from(tx: &SentTx) -> Self { HashAndAmount { hash: tx.hash, - amount: tx.amount, + amount_minor: tx.amount_minor, } } } @@ -443,22 +452,39 @@ 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::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; - use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; + use crate::blockchain::blockchain_interface::data_structures::{ + BlockchainTransaction, TxBlock, + }; use crate::blockchain::blockchain_interface::{ BlockchainAgentBuildError, BlockchainInterfaceError, BlockchainInterface, RetrievedBlockchainTransactions, }; - use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; + use crate::blockchain::test_utils::{ + all_chains, make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, + }; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; @@ -479,9 +505,6 @@ mod tests { use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplate; use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::RetryTxTemplateBuilder; - use crate::accountant::test_utils::make_payable_account; - use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; #[test] fn constants_are_correct() { @@ -1039,27 +1062,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(); @@ -1101,48 +1116,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] @@ -1150,13 +1162,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) @@ -1339,4 +1350,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 c25460ac6a..a4c771fb1c 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -1,28 +1,23 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedTx, FailureReason, FailureStatus, ValidationStatus, -}; -use crate::accountant::db_access_objects::sent_payable_dao::{Tx, TxStatus}; +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::to_unix_timestamp; use crate::accountant::scanners::payable_scanner::tx_templates::signable::{ SignableTxTemplate, SignableTxTemplates, }; -use crate::accountant::wei_to_gwei; use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ - BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, + BlockchainInterfaceWeb3, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError; use crate::blockchain::blockchain_interface::data_structures::BatchResults; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; -use actix::Recipient; use ethabi::Address; use futures::Future; -use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::WALLET_ADDRESS_LENGTH; use masq_lib::logger::Logger; @@ -38,22 +33,23 @@ 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, } -fn return_sending_error(sent_txs: &Vec, error: &Web3Error) -> LocalPayableError { - LocalPayableError::Sending( - sent_txs +fn return_sending_error(sent_txs: &[SentTx], error: &Web3Error) -> LocalPayableError { + LocalPayableError::Sending { + error: format!("{}", error), + failed_txs: sent_txs .iter() .map(|sent_tx| FailedTx::from((sent_tx, error))) .collect(), - ) + } } pub fn return_batch_results( - txs: Vec, + txs: Vec, responses: Vec>, ) -> BatchResults { txs.into_iter().zip(responses).fold( @@ -70,18 +66,20 @@ pub fn return_batch_results( ) } +fn calculate_payments_column_width(signable_tx_templates: &SignableTxTemplates) -> usize { + let label_length = "[payment wei]".len(); + let largest_amount_length = signable_tx_templates + .largest_amount() + .separate_with_commas() + .len(); + + label_length.max(largest_amount_length) +} + pub fn transmission_log(chain: Chain, signable_tx_templates: &SignableTxTemplates) -> String { let chain_name = chain.rec().literal_identifier; let (first_nonce, last_nonce) = signable_tx_templates.nonce_range(); - let payment_column_width = { - let label_length = "[payment wei]".len(); - let largest_amount_length = signable_tx_templates - .largest_amount() - .separate_with_commas() - .len(); - - label_length.max(largest_amount_length) - }; + let payment_column_width = calculate_payments_column_width(signable_tx_templates); let introduction = once(format!( "\n\ @@ -117,11 +115,11 @@ pub fn transmission_log(chain: Chain, signable_tx_templates: &SignableTxTemplate introduction.chain(body).collect() } -pub fn sign_transaction_data(amount: u128, receiver_address: Address) -> [u8; 68] { +pub fn sign_transaction_data(amount_minor: u128, receiver_address: Address) -> [u8; 68] { let mut data = [0u8; 4 + 32 + 32]; data[0..4].copy_from_slice(&TRANSFER_METHOD_ID); data[16..36].copy_from_slice(&receiver_address.0[..]); - U256::from(amount).to_big_endian(&mut data[36..68]); + U256::from(amount_minor).to_big_endian(&mut data[36..68]); data } @@ -193,7 +191,7 @@ pub fn sign_and_append_payment( signable_tx_template: &SignableTxTemplate, consuming_wallet: &Wallet, logger: &Logger, -) -> Tx { +) -> SentTx { let &SignableTxTemplate { receiver_address, amount_in_wei, @@ -208,20 +206,20 @@ pub fn sign_and_append_payment( let hash = signed_tx.transaction_hash; debug!( logger, - "Appending transaction with hash {:?}, amount: {} wei, to {:?}, nonce: {}, gas price: {} gwei", + "Appending transaction with hash {:?}, amount: {} wei, to {:?}, nonce: {}, gas price: {} wei", hash, amount_in_wei.separate_with_commas(), receiver_address, nonce, - wei_to_gwei::(gas_price_wei).separate_with_commas() + gas_price_wei.separate_with_commas() ); - Tx { + SentTx { hash, receiver_address, - amount: amount_in_wei, + amount_minor: amount_in_wei, timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei, + gas_price_minor: gas_price_wei, nonce, status: TxStatus::Pending(ValidationStatus::Waiting), } @@ -238,7 +236,7 @@ pub fn sign_and_append_multiple_payments( web3_batch: &Web3>, signable_tx_templates: &SignableTxTemplates, consuming_wallet: Wallet, -) -> Vec { +) -> Vec { signable_tx_templates .iter() .map(|signable_tx_template| { @@ -307,7 +305,7 @@ pub fn create_blockchain_agent_web3( masq_token_balance_in_minor_units, ); Box::new(BlockchainAgentWeb3::new( - blockchain_agent_future_result.gas_price_wei.as_u128(), + blockchain_agent_future_result.gas_price_minor.as_u128(), gas_limit_const_part, wallet, cons_wallet_balances, @@ -318,8 +316,9 @@ pub fn create_blockchain_agent_web3( #[cfg(test)] mod tests { use super::*; + use crate::accountant::db_access_objects::failed_payable_dao::{FailureReason, FailureStatus}; use crate::accountant::db_access_objects::test_utils::{ - assert_on_failed_txs, assert_on_sent_txs, FailedTxBuilder, + assert_on_failed_txs, assert_on_sent_txs, FailedTxBuilder, TxBuilder, }; use crate::accountant::gwei_to_wei; use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ @@ -333,23 +332,18 @@ mod tests { BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError::Sending; - use crate::blockchain::errors::rpc_errors::AppRpcError; - use crate::blockchain::errors::rpc_errors::LocalError::Transport; - use crate::blockchain::errors::rpc_errors::RemoteError::Web3RpcError; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::test_utils::{ - make_address, make_blockchain_interface_web3, make_tx_hash, transport_error_code, - transport_error_message, + make_address, transport_error_code, transport_error_message, }; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; use crate::test_utils::make_wallet; - use crate::test_utils::recorder::make_recorder; use crate::test_utils::unshared_test_utils::decode_hex; - use actix::{Actor, System}; + use actix::System; use ethabi::Address; use ethereum_types::H256; - use jsonrpc_core::ErrorCode::ServerError; - use jsonrpc_core::{Error, ErrorCode}; + use itertools::Either; use masq_lib::constants::{DEFAULT_CHAIN, DEFAULT_GAS_PRICE}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; @@ -398,18 +392,15 @@ mod tests { ); let mut batch_result = web3_batch.eth().transport().submit_batch().wait().unwrap(); - let expected_tx = Tx { - hash: H256::from_str( - "94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2", - ) - .unwrap(), - receiver_address: signable_tx_template.receiver_address, - amount: signable_tx_template.amount_in_wei, - timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: signable_tx_template.gas_price_wei, - nonce: signable_tx_template.nonce, - status: TxStatus::Pending(ValidationStatus::Waiting), - }; + let hash = + H256::from_str("94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2") + .unwrap(); + let expected_tx = TxBuilder::default() + .hash(hash) + .template(signable_tx_template) + .timestamp(to_unix_timestamp(SystemTime::now())) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); assert_on_sent_txs(vec![result], vec![expected_tx]); assert_eq!( batch_result.pop().unwrap().unwrap(), @@ -423,14 +414,14 @@ mod tests { amount: 1,000,000,000 wei, \ to 0x0000000000000000000000000077616c6c657431, \ nonce: 1, \ - gas price: 1 gwei" + gas price: 1,000,000,000 wei" )); } #[test] - fn send_and_append_multiple_payments_works() { + fn sign_and_append_multiple_payments_works() { 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, @@ -456,13 +447,35 @@ mod tests { result .iter() .zip(signable_tx_templates.iter()) - .for_each(|(sent_tx, template)| { - assert_eq!(sent_tx.receiver_address, template.receiver_address); - assert_eq!(sent_tx.amount, template.amount_in_wei); - assert_eq!(sent_tx.gas_price_wei, template.gas_price_wei); - assert_eq!(sent_tx.nonce, template.nonce); - assert_eq!(sent_tx.status, TxStatus::Pending(ValidationStatus::Waiting)) - }) + .enumerate() + .for_each(|(index, (sent_tx, template))| { + assert_eq!( + sent_tx.receiver_address, template.receiver_address, + "Transaction {} receiver_address mismatch", + index + ); + assert_eq!( + sent_tx.amount_minor, template.amount_in_wei, + "Transaction {} amount mismatch", + index + ); + assert_eq!( + sent_tx.gas_price_minor, template.gas_price_wei, + "Transaction {} gas_price_wei mismatch", + index + ); + assert_eq!( + sent_tx.nonce, template.nonce, + "Transaction {} nonce mismatch", + index + ); + assert_eq!( + sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting), + "Transaction {} status mismatch", + index + ) + }); } #[test] @@ -588,6 +601,7 @@ mod tests { expected_result: Result, port: u16, ) { + // TODO: GH-701: Add assertions for the new_fingerprints_message here, since it existed earlier init_test_logging(); let (_event_loop_handle, transport) = Http::with_max_parallel( &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), @@ -628,9 +642,14 @@ mod tests { assert_on_sent_txs(resulted_batch.sent_txs, expected_batch.sent_txs); } Err(resulted_err) => match resulted_err { - LocalPayableError::Sending(resulted_failed_txs) => { - if let Err(LocalPayableError::Sending(expected_failed_txs)) = expected_result { - assert_on_failed_txs(resulted_failed_txs, expected_failed_txs); + LocalPayableError::Sending { error, failed_txs } => { + if let Err(LocalPayableError::Sending { + error: expected_error, + failed_txs: expected_failed_txs, + }) = expected_result + { + assert_on_failed_txs(failed_txs, expected_failed_txs); + assert_eq!(error, expected_error) } else { panic!( "Expected different error but received {}", @@ -639,7 +658,7 @@ mod tests { } } other_err => { - panic!("Only LocalPayableError::Sending is returned by send_payables_within_batch nut received: {} ", other_err) + panic!("Only LocalPayableError::Sending is returned by send_payables_within_batch but received something else: {} ", other_err) } }, } @@ -679,26 +698,18 @@ mod tests { let batch_results = { let signed_tx_1 = sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_1, &consuming_wallet); - let sent_tx_1 = Tx { - hash: signed_tx_1.transaction_hash, - receiver_address: template_1.receiver_address, - amount: template_1.amount_in_wei, - timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: template_1.gas_price_wei, - nonce: template_1.nonce, - status: TxStatus::Pending(ValidationStatus::Waiting), - }; + let sent_tx_1 = TxBuilder::default() + .hash(signed_tx_1.transaction_hash) + .template(template_1) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); let signed_tx_2 = sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_2, &consuming_wallet); - let sent_tx_2 = Tx { - hash: signed_tx_2.transaction_hash, - receiver_address: template_2.receiver_address, - amount: template_2.amount_in_wei, - timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: template_2.gas_price_wei, - nonce: template_2.nonce, - status: TxStatus::Pending(ValidationStatus::Waiting), - }; + let sent_tx_2 = TxBuilder::default() + .hash(signed_tx_2.transaction_hash) + .template(template_2) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); BatchResults { sent_txs: vec![sent_tx_1, sent_tx_2], @@ -741,7 +752,7 @@ mod tests { let os_specific_code = transport_error_code(); let os_specific_msg = transport_error_message(); let err_msg = format!( - "Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", + "Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", os_specific_code, os_specific_msg ); let failed_txs = signable_tx_templates @@ -756,14 +767,17 @@ mod tests { .timestamp(to_unix_timestamp(SystemTime::now()) - 5) .gas_price_wei(template.gas_price_wei) .nonce(template.nonce) - .reason(FailureReason::Submission(AppRpcError::Local(Transport( - err_msg.clone(), - )))) + .reason(FailureReason::Submission(AppRpcErrorKind::Local( + LocalErrorKind::Transport, + ))) .status(FailureStatus::RetryRequired) .build() }) .collect(); - let expected_result = Err(Sending(failed_txs)); + let expected_result = Err(Sending { + error: err_msg, + failed_txs, + }); test_send_payables_within_batch( "send_payables_within_batch_fails_on_submit_batch_call", @@ -825,7 +839,9 @@ mod tests { .timestamp(to_unix_timestamp(SystemTime::now()) - 5) .gas_price_wei(template.gas_price_wei) .nonce(template.nonce) - .reason(FailureReason::Submission(AppRpcError::Remote(Web3RpcError { code: 429, message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string() }))) + .reason(FailureReason::Submission(AppRpcErrorKind::Remote( + RemoteErrorKind::Web3RpcError(429), + ))) .status(FailureStatus::RetryRequired) .build() }) @@ -880,30 +896,23 @@ mod tests { let batch_results = { let signed_tx_1 = sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_1, &consuming_wallet); - let sent_tx = Tx { - hash: signed_tx_1.transaction_hash, - receiver_address: template_1.receiver_address, - amount: template_1.amount_in_wei, - timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: template_1.gas_price_wei, - nonce: template_1.nonce, - status: TxStatus::Pending(ValidationStatus::Waiting), - }; + let sent_tx = TxBuilder::default() + .hash(signed_tx_1.transaction_hash) + .template(template_1) + .timestamp(to_unix_timestamp(SystemTime::now())) + .status(TxStatus::Pending(ValidationStatus::Waiting)) + .build(); let signed_tx_2 = sign_transaction(DEFAULT_CHAIN, &web3_batch, &template_2, &consuming_wallet); - let failed_tx = FailedTx { - hash: signed_tx_2.transaction_hash, - receiver_address: template_2.receiver_address, - amount: template_2.amount_in_wei, - timestamp: to_unix_timestamp(SystemTime::now()), - gas_price_wei: template_2.gas_price_wei, - nonce: template_2.nonce, - reason: FailureReason::Submission(AppRpcError::Remote(Web3RpcError { - code: 429, - message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), - })), - status: FailureStatus::RetryRequired, - }; + let failed_tx = FailedTxBuilder::default() + .hash(signed_tx_2.transaction_hash) + .template(template_2) + .timestamp(to_unix_timestamp(SystemTime::now())) + .reason(FailureReason::Submission(AppRpcErrorKind::Remote( + RemoteErrorKind::Web3RpcError(429), + ))) + .status(FailureStatus::RetryRequired) + .build(); BatchResults { sent_txs: vec![sent_tx], diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index 0e903fe0ef..03899343ea 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -1,12 +1,12 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; -use crate::accountant::{comma_joined_stringifiable, join_with_separator}; +use crate::accountant::join_with_separator; use itertools::Either; 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"; @@ -38,9 +38,10 @@ pub enum LocalPayableError { MissingConsumingWallet, GasPriceQueryFailed(BlockchainInterfaceError), TransactionID(BlockchainInterfaceError), - UnusableWallet(String), - Signing(String), - Sending(Vec), + Sending { + error: String, + failed_txs: Vec, + }, UninitializedInterface, } @@ -56,16 +57,11 @@ impl Display for LocalPayableError { Self::TransactionID(blockchain_err) => { write!(f, "Transaction id fetching failed: {}", blockchain_err) } - Self::UnusableWallet(msg) => write!( + Self::Sending { error, failed_txs } => write!( f, - "Unusable wallet for signing payable transactions: \"{}\"", - msg - ), - Self::Signing(msg) => write!(f, "Signing phase: \"{}\"", msg), - Self::Sending(failed_txs) => write!( - f, - "Sending error. Signed and hashed transactions:\n{}", - join_with_separator(failed_txs, |failed_tx| format!("{:?}", failed_tx), "\n") + "Sending error: \"{}\". Signed and hashed transactions: \"{}\"", + error, + join_with_separator(failed_txs, |failed_tx| format!("{:?}", failed_tx), ",") ), Self::UninitializedInterface => { write!(f, "{}", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED) @@ -114,13 +110,13 @@ impl Display for BlockchainAgentBuildError { #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::test_utils::make_failed_tx; use crate::blockchain::blockchain_interface::data_structures::errors::{ LocalPayableError, BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED, }; use crate::blockchain::blockchain_interface::{ BlockchainAgentBuildError, BlockchainInterfaceError, }; - use crate::blockchain::test_utils::make_tx_hash; use crate::test_utils::make_wallet; use masq_lib::utils::{slice_of_strs_to_vec_of_strings, to_string}; @@ -172,13 +168,10 @@ mod tests { "Gas halves shut, no drop left".to_string(), )), LocalPayableError::TransactionID(BlockchainInterfaceError::InvalidResponse), - LocalPayableError::UnusableWallet( - "This is a LEATHER wallet, not LEDGER wallet, stupid.".to_string(), - ), - LocalPayableError::Signing( - "You cannot sign with just three crosses here, clever boy".to_string(), - ), - LocalPayableError::Sending(vec![]), + LocalPayableError::Sending { + error: "Terrible error!!".to_string(), + failed_txs: vec![make_failed_tx(456)], + }, LocalPayableError::UninitializedInterface, ]; @@ -195,12 +188,10 @@ mod tests { "Missing consuming wallet to pay payable from", "Unsuccessful gas price query: \"Blockchain error: Query failed: Gas halves shut, no drop left\"", "Transaction id fetching failed: Blockchain error: Invalid response", - "Unusable wallet for signing payable transactions: \"This is a LEATHER wallet, not \ - LEDGER wallet, stupid.\"", - "Signing phase: \"You cannot sign with just three crosses here, clever boy\"", - "Sending phase: \"Sending to cosmos belongs elsewhere\". Signed and hashed \ - transactions: 0x000000000000000000000000000000000000000000000000000000000000006f, \ - 0x00000000000000000000000000000000000000000000000000000000000000de", + "Sending error: \"Terrible error!!\". Signed and hashed transactions: \"FailedTx { hash: 0x00000000000000\ + 000000000000000000000000000000000000000000000001c8, receiver_address: 0x00000000000\ + 00000002556000000002556000000, amount_minor: 43237380096, timestamp: 29942784, gas_\ + price_minor: 94818816, nonce: 456, reason: PendingTooLong, status: RetryRequired }\"", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED ]) ) diff --git a/node/src/blockchain/blockchain_interface/data_structures/mod.rs b/node/src/blockchain/blockchain_interface/data_structures/mod.rs index 3f392b9727..f79f12345b 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -3,11 +3,15 @@ pub mod errors; use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; -use crate::accountant::db_access_objects::sent_payable_dao::Tx; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; 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 std::fmt::{Display, Formatter}; +use web3::types::{TransactionReceipt, H256}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct BlockchainTransaction { @@ -32,8 +36,95 @@ pub struct RetrievedBlockchainTransactions { pub transactions: Vec, } -#[derive(Default, Debug, PartialEq, Clone)] +#[derive(Default, Debug, PartialEq, Eq, Clone)] pub struct BatchResults { - pub sent_txs: Vec, + pub sent_txs: Vec, pub failed_txs: Vec, } + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RetrievedTxStatus { + pub tx_hash: TxHashByTable, + pub status: StatusReadFromReceiptCheck, +} + +impl RetrievedTxStatus { + pub fn new(tx_hash: TxHashByTable, status: StatusReadFromReceiptCheck) -> Self { + Self { tx_hash, status } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum StatusReadFromReceiptCheck { + Reverted, + Succeeded(TxBlock), + Pending, +} + +impl Display for StatusReadFromReceiptCheck { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StatusReadFromReceiptCheck::Reverted => { + write!(f, "Reverted") + } + StatusReadFromReceiptCheck::Succeeded(block) => { + write!( + f, + "Succeeded({},{:?})", + block.block_number, block.block_hash + ) + } + StatusReadFromReceiptCheck::Pending => write!(f, "Pending"), + } + } +} + +impl From for StatusReadFromReceiptCheck { + fn from(receipt: TransactionReceipt) -> Self { + match (receipt.status, receipt.block_hash, receipt.block_number) { + (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { + StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }) + } + (Some(status), _, _) if status == U64::from(0) => StatusReadFromReceiptCheck::Reverted, + _ => StatusReadFromReceiptCheck::Pending, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Ord, PartialOrd, Serialize, Deserialize)] +pub struct TxBlock { + pub block_hash: H256, + pub block_number: U64, +} + +#[cfg(test)] +mod tests { + use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, + }; + use ethereum_types::{H256, U64}; + + #[test] + fn tx_status_display_works() { + // Test Failed + assert_eq!(StatusReadFromReceiptCheck::Reverted.to_string(), "Reverted"); + + // Test Pending + assert_eq!(StatusReadFromReceiptCheck::Pending.to_string(), "Pending"); + + // Test Succeeded + let block_number = U64::from(12345); + let block_hash = H256::from_low_u64_be(0xabcdef); + let succeeded = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }); + assert_eq!( + succeeded.to_string(), + format!("Succeeded({},0x{:x})", block_number, block_hash) + ); + } +} diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index 5d2c018c87..3db1bbeab9 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -4,22 +4,26 @@ pub mod blockchain_interface_web3; pub mod data_structures; pub mod lower_level_interface; -use actix::Recipient; -use ethereum_types::H256; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainInterfaceError, LocalPayableError}; -use crate::blockchain::blockchain_interface::data_structures::{BatchResults, RetrievedBlockchainTransactions}; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange}; +use crate::blockchain::blockchain_interface::data_structures::errors::{ + BlockchainAgentBuildError, BlockchainInterfaceError, LocalPayableError, +}; +use crate::blockchain::blockchain_interface::data_structures::{ + BatchResults, RetrievedBlockchainTransactions, +}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::sub_lib::wallet::Wallet; use futures::Future; use itertools::Either; use masq_lib::blockchains::chains::Chain; -use web3::types::Address; use masq_lib::logger::Logger; -use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; -use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; +use std::collections::BTreeMap; +use web3::types::Address; pub trait BlockchainInterface { fn contract_address(&self) -> Address; @@ -42,8 +46,13 @@ pub trait BlockchainInterface { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>>; + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = BTreeMap, + Error = BlockchainInterfaceError, + >, + >; fn submit_payables_in_batch( &self, diff --git a/node/src/blockchain/errors/internal_errors.rs b/node/src/blockchain/errors/internal_errors.rs index c70c2e65f0..5375194808 100644 --- a/node/src/blockchain/errors/internal_errors.rs +++ b/node/src/blockchain/errors/internal_errors.rs @@ -7,7 +7,7 @@ pub enum InternalError { PendingTooLongNotReplaced, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum InternalErrorKind { PendingTooLongNotReplaced, } diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index 3eacd21284..b6d1af1117 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -14,7 +14,7 @@ pub enum BlockchainError { Internal(InternalError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum BlockchainErrorKind { AppRpc(AppRpcErrorKind), Internal(InternalErrorKind), @@ -23,19 +23,19 @@ pub enum BlockchainErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::internal_errors::InternalErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; use crate::blockchain::errors::BlockchainErrorKind; #[test] fn blockchain_error_serialization_deserialization() { vec![ ( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), - "{\"AppRpc\":\"Decoder\"}", + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + r#"{"AppRpc":{"Local":"Decoder"}}"#, ), ( BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), - "{\"Internal\":\"PendingTooLongNotReplaced\"}", + r#"{"Internal":"PendingTooLongNotReplaced"}"#, ), ] .into_iter() diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index 9bd1c3b01f..41d9d38632 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -14,7 +14,7 @@ pub enum AppRpcError { pub enum LocalError { Decoder(String), Internal, - Io(String), + IO(String), Signing(String), Transport(String), } @@ -33,7 +33,7 @@ impl From for AppRpcError { // Local Errors Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), Web3Error::Internal => AppRpcError::Local(LocalError::Internal), - Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), + Web3Error::Io(error) => AppRpcError::Local(LocalError::IO(error.to_string())), Web3Error::Signing(error) => { // This variant cannot be tested due to import limitations. AppRpcError::Local(LocalError::Signing(error.to_string())) @@ -53,16 +53,23 @@ impl From for AppRpcError { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum AppRpcErrorKind { - // Local + Local(LocalErrorKind), + Remote(RemoteErrorKind), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +pub enum LocalErrorKind { Decoder, Internal, Io, Signing, Transport, +} - // Remote +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +pub enum RemoteErrorKind { InvalidResponse, Unreachable, Web3RpcError(i64), // Keep only the stable error code @@ -72,16 +79,18 @@ impl From<&AppRpcError> for AppRpcErrorKind { fn from(err: &AppRpcError) -> Self { match err { AppRpcError::Local(local) => match local { - LocalError::Decoder(_) => Self::Decoder, - LocalError::Internal => Self::Internal, - LocalError::Io(_) => Self::Io, - LocalError::Signing(_) => Self::Signing, - LocalError::Transport(_) => Self::Transport, + LocalError::Decoder(_) => Self::Local(LocalErrorKind::Decoder), + LocalError::Internal => Self::Local(LocalErrorKind::Internal), + LocalError::IO(_) => Self::Local(LocalErrorKind::Io), + LocalError::Signing(_) => Self::Local(LocalErrorKind::Signing), + LocalError::Transport(_) => Self::Local(LocalErrorKind::Transport), }, AppRpcError::Remote(remote) => match remote { - RemoteError::InvalidResponse(_) => Self::InvalidResponse, - RemoteError::Unreachable => Self::Unreachable, - RemoteError::Web3RpcError { code, .. } => Self::Web3RpcError(*code), + RemoteError::InvalidResponse(_) => Self::Remote(RemoteErrorKind::InvalidResponse), + RemoteError::Unreachable => Self::Remote(RemoteErrorKind::Unreachable), + RemoteError::Web3RpcError { code, .. } => { + Self::Remote(RemoteErrorKind::Web3RpcError(*code)) + } }, } } @@ -90,7 +99,7 @@ impl From<&AppRpcError> for AppRpcErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::rpc_errors::{ - AppRpcError, AppRpcErrorKind, LocalError, RemoteError, + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, RemoteErrorKind, }; use web3::error::Error as Web3Error; @@ -110,7 +119,7 @@ mod tests { std::io::ErrorKind::Other, "IO error" ))), - AppRpcError::Local(LocalError::Io("IO error".to_string())) + AppRpcError::Local(LocalError::IO("IO error".to_string())) ); assert_eq!( AppRpcError::from(Web3Error::Transport("Transport error".to_string())), @@ -145,60 +154,58 @@ mod tests { AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Decoder( "Decoder error".to_string() ))), - AppRpcErrorKind::Decoder + AppRpcErrorKind::Local(LocalErrorKind::Decoder) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Internal)), - AppRpcErrorKind::Internal + AppRpcErrorKind::Local(LocalErrorKind::Internal) ); assert_eq!( - AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Io("IO error".to_string()))), - AppRpcErrorKind::Io + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::IO("IO error".to_string()))), + AppRpcErrorKind::Local(LocalErrorKind::Io) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Signing( "Signing error".to_string() ))), - AppRpcErrorKind::Signing + AppRpcErrorKind::Local(LocalErrorKind::Signing) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Transport( "Transport error".to_string() ))), - AppRpcErrorKind::Transport + AppRpcErrorKind::Local(LocalErrorKind::Transport) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::InvalidResponse( "Invalid response".to_string() ))), - AppRpcErrorKind::InvalidResponse + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Unreachable)), - AppRpcErrorKind::Unreachable + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Web3RpcError { code: 55, message: "Booga".to_string() })), - AppRpcErrorKind::Web3RpcError(55) + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(55)) ); } #[test] fn app_rpc_error_kind_serialization_deserialization() { let errors = vec![ - // Local Errors - AppRpcErrorKind::Decoder, - AppRpcErrorKind::Internal, - AppRpcErrorKind::Io, - AppRpcErrorKind::Signing, - AppRpcErrorKind::Transport, - // Remote Errors - AppRpcErrorKind::InvalidResponse, - AppRpcErrorKind::Unreachable, - AppRpcErrorKind::Web3RpcError(42), + AppRpcErrorKind::Local(LocalErrorKind::Decoder), + AppRpcErrorKind::Local(LocalErrorKind::Internal), + AppRpcErrorKind::Local(LocalErrorKind::Io), + AppRpcErrorKind::Local(LocalErrorKind::Signing), + AppRpcErrorKind::Local(LocalErrorKind::Transport), + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse), + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable), + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(42)), ]; errors.into_iter().for_each(|error| { @@ -206,8 +213,7 @@ mod tests { let deserialized: AppRpcErrorKind = serde_json::from_str(&serialized).unwrap(); assert_eq!( error, deserialized, - "Failed serde attempt for {:?} that should look \ - like {:?}", + "Failed serde attempt for {:?} that should look like {:?}", deserialized, error ); }); diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index aaa1c7ee80..a3e8ada27e 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -1,11 +1,16 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; use crate::blockchain::errors::BlockchainErrorKind; +use serde::de::{SeqAccess, Visitor}; +use serde::ser::SerializeSeq; +use serde::{ + Deserialize as ManualDeserialize, Deserializer, Serialize as ManualSerialize, Serializer, +}; use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; +use std::collections::BTreeMap; +use std::fmt::Formatter; +use std::hash::Hash; use std::time::SystemTime; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -14,40 +19,101 @@ pub enum ValidationStatus { Reattempting(PreviousAttempts), } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +impl PartialOrd for ValidationStatus { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for ValidationStatus { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (ValidationStatus::Reattempting(..), ValidationStatus::Waiting) => Ordering::Less, + (ValidationStatus::Waiting, ValidationStatus::Reattempting(..)) => Ordering::Greater, + (ValidationStatus::Waiting, ValidationStatus::Waiting) => Ordering::Equal, + (ValidationStatus::Reattempting(prev1), ValidationStatus::Reattempting(prev2)) => { + prev1.cmp(prev2) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PreviousAttempts { - #[serde(flatten)] - inner: HashMap, + inner: BTreeMap, } -impl Hash for PreviousAttempts { - fn hash(&self, state: &mut H) { - for (key, value) in &self.inner { - key.hash(state); - value.hash(state); +// 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 PartialOrd for PreviousAttempts { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(&other)) +impl<'de> ManualDeserialize<'de> for PreviousAttempts { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(PreviousAttemptsVisitor) } } -impl Ord for PreviousAttempts { - fn cmp(&self, other: &Self) -> Ordering { - let self_first_seen = self.inner.iter().map(|(_, stats)| &stats.first_seen).max(); - let other_first_seen = other.inner.iter().map(|(_, stats)| &stats.first_seen).max(); +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, + } - self_first_seen.cmp(&other_first_seen) + let mut error_stats_map: BTreeMap = btreemap!(); + 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)), + inner: btreemap!(error => ErrorStats::now(clock)), } } @@ -64,7 +130,7 @@ impl PreviousAttempts { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct ErrorStats { #[serde(rename = "firstSeen")] pub first_seen: SystemTime, @@ -100,10 +166,14 @@ impl ValidationFailureClock for ValidationFailureClockReal { #[cfg(test)] mod tests { use super::*; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::blockchain::errors::internal_errors::InternalErrorKind; - use crate::blockchain::test_utils::ValidationFailureClockMock; - use std::collections::hash_map::DefaultHasher; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; + use crate::test_utils::serde_serializer_mock::{SerdeSerializerMock, SerializeSeqMock}; + use serde::ser::Error as SerdeError; + use std::collections::BTreeSet; use std::time::Duration; + use std::time::UNIX_EPOCH; #[test] fn previous_attempts_and_validation_failure_clock_work_together_fine() { @@ -111,7 +181,7 @@ mod tests { // new() let timestamp_a = SystemTime::now(); let subject = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); // add_attempt() @@ -122,22 +192,24 @@ mod tests { ); let timestamp_c = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Io), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &validation_failure_clock, ); let timestamp_d = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Io), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &validation_failure_clock, ); let decoder_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Decoder, + ))) .unwrap(); assert!( timestamp_a <= decoder_error_stats.first_seen @@ -165,7 +237,9 @@ mod tests { assert_eq!(internal_error_stats.attempts, 1); let io_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Io)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Io, + ))) .unwrap(); assert!( timestamp_c <= io_error_stats.first_seen && io_error_stats.first_seen <= timestamp_d, @@ -175,48 +249,53 @@ mod tests { io_error_stats.first_seen ); assert_eq!(io_error_stats.attempts, 2); - let other_error_stats = subject - .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Signing)); + let other_error_stats = + subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Signing, + ))); assert_eq!(other_error_stats, None); } - #[test] - fn previous_attempts_hash_works_correctly() { - let now = SystemTime::now(); - let clock = ValidationFailureClockMock::default() - .now_result(now) - .now_result(now) - .now_result(now + Duration::from_secs(2)); - let attempts1 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), - &clock, - ); - let attempts2 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), - &clock, - ); - let attempts3 = - PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Io), &clock); - let hash1 = { - let mut hasher = DefaultHasher::new(); - attempts1.hash(&mut hasher); - hasher.finish() - }; - let hash2 = { - let mut hasher = DefaultHasher::new(); - attempts2.hash(&mut hasher); - hasher.finish() - }; - let hash3 = { - let mut hasher = DefaultHasher::new(); - attempts3.hash(&mut hasher); - hasher.finish() - }; - - assert_eq!(hash1, hash2); - assert_ne!(hash1, hash3); - } + // #[test] + // fn previous_attempts_hash_works_correctly() { + // let now = SystemTime::now(); + // let clock = ValidationFailureClockMock::default() + // .now_result(now) + // .now_result(now) + // .now_result(now + Duration::from_secs(2)); + // let attempts1 = PreviousAttempts::new( + // BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + // &clock, + // ); + // let attempts2 = PreviousAttempts::new( + // BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + // &clock, + // ); + // let attempts3 = PreviousAttempts::new( + // BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), + // &clock, + // ); + // let hash1 = { + // let mut hasher = DefaultHasher::new(); + // attempts1.hash(&mut hasher); + // hasher.finish() + // }; + // let hash2 = { + // let mut hasher = DefaultHasher::new(); + // attempts2.hash(&mut hasher); + // hasher.finish() + // }; + // let hash3 = { + // let mut hasher = DefaultHasher::new(); + // attempts3.hash(&mut hasher); + // hasher.finish() + // }; + // + // assert_eq!(hash1, hash2); + // assert_ne!(hash1, hash3); + // } #[test] fn previous_attempts_ordering_works_correctly_with_mock() { @@ -227,20 +306,161 @@ mod tests { .now_result(now + Duration::from_secs(2)) .now_result(now + Duration::from_secs(3)); let mut attempts1 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &clock, ); - let mut attempts2 = - PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Io), &clock); attempts1 = attempts1.add_attempt( BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), &clock, ); + let mut attempts2 = PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), + &clock, + ); attempts2 = attempts2.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Signing), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Signing)), &clock, ); assert_eq!(attempts2.partial_cmp(&attempts1), Some(Ordering::Greater)); } + + #[test] + fn previous_attempts_custom_serialize_seq_happy_path() { + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + 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, + btreemap!(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" + ); + } + + #[test] + fn validation_status_ordering_works_correctly() { + let now = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(now) + .now_result(now + Duration::from_secs(1)); + + let waiting = ValidationStatus::Waiting; + let reattempting_early = ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &clock, + )); + let reattempting_late = ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), + &clock, + )); + let waiting_identical = waiting.clone(); + let reattempting_early_identical = reattempting_early.clone(); + + let mut set = BTreeSet::new(); + vec![ + reattempting_early.clone(), + waiting.clone(), + reattempting_late.clone(), + waiting_identical.clone(), + reattempting_early_identical.clone(), + ] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); + + let expected_order = vec![ + reattempting_early.clone(), + reattempting_late, + waiting.clone(), + ]; + assert_eq!(set.into_iter().collect::>(), expected_order); + assert_eq!(waiting.cmp(&waiting_identical), Ordering::Equal); + assert_eq!( + reattempting_early.cmp(&reattempting_early_identical), + Ordering::Equal + ); + } } diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 7f9c6ea86c..238703d981 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -5,7 +5,6 @@ use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; -use crate::blockchain::errors::validation_status::ValidationFailureClock; use bip39::{Language, Mnemonic, Seed}; use ethabi::Hash; use ethereum_types::{BigEndianHash, H160, H256, U64}; @@ -14,10 +13,8 @@ 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::{Address, Index, Log, SignedTransaction, TransactionReceipt, H2048, U256}; @@ -188,7 +185,7 @@ pub fn make_default_signed_transaction() -> SignedTransaction { } } -pub fn make_hash(base: u32) -> Hash { +fn make_hash(base: u32) -> H256 { H256::from_uint(&U256::from(base)) } @@ -201,12 +198,15 @@ pub fn make_block_hash(base: u32) -> H256 { } pub fn make_address(base: u32) -> Address { - let value = U256::from(base); + let base = base % 0xfff; + let value = U256::from(base * 3); + let shifted = value << 72; + let value = U256::from(value) << 24; + let value = value | shifted; let mut full_bytes = [0u8; 32]; value.to_big_endian(&mut full_bytes); let mut bytes = [0u8; 20]; - bytes.copy_from_slice(&full_bytes[12..32]); - + bytes.copy_from_slice(&full_bytes[12..]); H160(bytes) } @@ -239,20 +239,51 @@ pub fn transport_error_message() -> String { } } -#[derive(Default)] -pub struct ValidationFailureClockMock { - now_results: RefCell>, +pub struct TransactionReceiptBuilder { + status_opt: Option, + block_hash_opt: Option, + block_number_opt: Option, + transaction_hash: H256, } -impl ValidationFailureClock for ValidationFailureClockMock { - fn now(&self) -> SystemTime { - self.now_results.borrow_mut().remove(0) +impl TransactionReceiptBuilder { + pub fn new(transaction_hash: H256) -> Self { + Self { + status_opt: None, + block_hash_opt: None, + block_number_opt: None, + transaction_hash, + } } -} -impl ValidationFailureClockMock { - pub fn now_result(self, result: SystemTime) -> Self { - self.now_results.borrow_mut().push(result); + 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(), + } + } } diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index 71a0751b00..aa39431831 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -1233,6 +1233,7 @@ mod tests { vec![SocketAddr::new(IpAddr::from_str("1.2.3.4").unwrap(), 1111)]; let mut unprivileged_config = BootstrapperConfig::new(); //values from unprivileged config + let chain = unprivileged_config.blockchain_bridge_config.chain; let gas_price = 123; let blockchain_url_opt = Some("some.service@earth.abc".to_string()); let clandestine_port_opt = Some(44444); @@ -1252,7 +1253,7 @@ mod tests { unprivileged_config.earning_wallet = earning_wallet.clone(); unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); - unprivileged_config.scan_intervals_opt = Some(ScanIntervals::default()); + unprivileged_config.scan_intervals_opt = Some(ScanIntervals::compute_default(chain)); unprivileged_config.automatic_scans_enabled = true; unprivileged_config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; @@ -1276,7 +1277,7 @@ mod tests { assert_eq!(privileged_config.db_password_opt, db_password_opt); assert_eq!( privileged_config.scan_intervals_opt, - Some(ScanIntervals::default()) + Some(ScanIntervals::compute_default(chain)) ); assert_eq!(privileged_config.automatic_scans_enabled, true); assert_eq!( diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 3d0a79b6b0..b03251842a 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -21,7 +21,6 @@ use crate::node_configurator::{ data_directory_from_context, determine_user_specific_data, DirsWrapper, DirsWrapperReal, }; use crate::sub_lib::accountant::PaymentThresholds as PaymentThresholdsFromAccountant; -use crate::sub_lib::accountant::DEFAULT_SCAN_INTERVALS; use crate::sub_lib::neighborhood::NodeDescriptor; use crate::sub_lib::neighborhood::{NeighborhoodMode as NeighborhoodModeEnum, DEFAULT_RATE_PACK}; use crate::sub_lib::utils::make_new_multi_config; @@ -1083,12 +1082,16 @@ impl ValueRetriever for ScanIntervals { fn computed_default( &self, - _bootstrapper_config: &BootstrapperConfig, + bootstrapper_config: &BootstrapperConfig, pc: &dyn PersistentConfiguration, _db_password_opt: &Option, ) -> Option<(String, UiSetupResponseValueStatus)> { let pc_value = pc.scan_intervals().expectv("scan-intervals"); - payment_thresholds_rate_pack_and_scan_intervals(pc_value, *DEFAULT_SCAN_INTERVALS) + let chain = bootstrapper_config.blockchain_bridge_config.chain; + payment_thresholds_rate_pack_and_scan_intervals( + pc_value, + crate::sub_lib::accountant::ScanIntervals::compute_default(chain), + ) } fn is_required(&self, _params: &SetupCluster) -> bool { @@ -1208,7 +1211,9 @@ mod tests { use crate::daemon::dns_inspector::dns_inspector::DnsInspector; use crate::daemon::dns_inspector::DnsInspectionError; use crate::daemon::setup_reporter; - use crate::database::db_initializer::{DbInitializer, DbInitializerReal, DATABASE_FILE}; + use crate::database::db_initializer::{ + DbInitializer, DbInitializerReal, InitializationMode, DATABASE_FILE, + }; use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::db_config::config_dao::{ConfigDao, ConfigDaoReal}; use crate::db_config::persistent_configuration::{ @@ -1229,6 +1234,7 @@ mod tests { use crate::test_utils::unshared_test_utils::{ make_persistent_config_real_with_config_dao_null, make_pre_populated_mocked_directory_wrapper, make_simplified_multi_config, + TEST_SCAN_INTERVALS, }; use crate::test_utils::{assert_string_contains, rate_pack}; use core::option::Option; @@ -1335,15 +1341,19 @@ mod tests { "setup_reporter", "get_modified_setup_database_populated_only_requireds_set", ); + let chain = DEFAULT_CHAIN; + let mut init_config = DbInitializationConfig::test_default(); + if let InitializationMode::CreationAndMigration { external_data } = &mut init_config.mode { + external_data.chain = chain + } else { + panic!("unexpected initialization mode"); + } let data_dir = home_dir.join("data_dir"); - let chain_specific_data_dir = data_dir.join(DEFAULT_CHAIN.rec().literal_identifier); + let chain_specific_data_dir = data_dir.join(chain.rec().literal_identifier); std::fs::create_dir_all(&chain_specific_data_dir).unwrap(); let db_initializer = DbInitializerReal::default(); let conn = db_initializer - .initialize( - &chain_specific_data_dir, - DbInitializationConfig::test_default(), - ) + .initialize(&chain_specific_data_dir, init_config) .unwrap(); let mut config = PersistentConfigurationReal::from(conn); config.change_password(None, "password").unwrap(); @@ -1448,7 +1458,7 @@ mod tests { ), ( "scan-intervals", - &DEFAULT_SCAN_INTERVALS.to_string(), + &accountant::ScanIntervals::compute_default(chain).to_string(), Default, ), ("scans", "on", Default), @@ -3358,6 +3368,7 @@ mod tests { fn rate_pack_computed_default_when_persistent_config_like_default() { assert_computed_default_when_persistent_config_like_default( &RatePack {}, + None, DEFAULT_RATE_PACK.to_string(), ) } @@ -3437,15 +3448,19 @@ mod tests { #[test] fn scan_intervals_computed_default_when_persistent_config_like_default() { + let chain = DEFAULT_CHAIN; + let mut bootstrapper_config = BootstrapperConfig::new(); + bootstrapper_config.blockchain_bridge_config.chain = chain; assert_computed_default_when_persistent_config_like_default( &ScanIntervals {}, - *DEFAULT_SCAN_INTERVALS, + Some(bootstrapper_config), + accountant::ScanIntervals::compute_default(chain), ) } #[test] fn scan_intervals_computed_default_persistent_config_unequal_to_default() { - let mut scan_intervals = *DEFAULT_SCAN_INTERVALS; + let mut scan_intervals = *TEST_SCAN_INTERVALS; scan_intervals.payable_scan_interval = scan_intervals .payable_scan_interval .add(Duration::from_secs(15)); @@ -3469,6 +3484,7 @@ mod tests { fn payment_thresholds_computed_default_when_persistent_config_like_default() { assert_computed_default_when_persistent_config_like_default( &PaymentThresholds {}, + None, DEFAULT_PAYMENT_THRESHOLDS.to_string(), ) } @@ -3491,12 +3507,13 @@ mod tests { fn assert_computed_default_when_persistent_config_like_default( subject: &dyn ValueRetriever, + bootstrapper_config_opt: Option, default: T, ) where T: Display + PartialEq, { - let mut bootstrapper_config = BootstrapperConfig::new(); - //the rate_pack within the mode setting does not determine the result, so I just set a nonsense + let mut bootstrapper_config = bootstrapper_config_opt.unwrap_or(BootstrapperConfig::new()); + //the rate_pack within the mode setting does not affect the result, so I set nonsense bootstrapper_config.neighborhood_config.mode = NeighborhoodModeEnum::OriginateOnly(vec![], rate_pack(0)); let persistent_config = diff --git a/node/src/database/config_dumper.rs b/node/src/database/config_dumper.rs index 17e24899eb..a1a435818b 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 6747867669..6eb69b4a6d 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -4,7 +4,8 @@ use crate::database::rusqlite_wrappers::{ConnectionWrapper, ConnectionWrapperRea use crate::database::db_migrations::db_migrator::{DbMigrator, DbMigratorReal}; use crate::db_config::secure_config_layer::EXAMPLE_ENCRYPTED; use crate::neighborhood::DEFAULT_MIN_HOPS; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use crate::sub_lib::utils::db_connection_launch_panic; use masq_lib::blockchains::chains::Chain; @@ -137,7 +138,6 @@ impl DbInitializerReal { Self::create_payable_table(conn); Self::create_sent_payable_table(conn); Self::create_failed_payable_table(conn); - Self::create_pending_payable_table(conn); Self::create_receivable_table(conn); Self::create_banned_table(conn); } @@ -253,7 +253,7 @@ impl DbInitializerReal { Self::set_config_value( conn, "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(external_params.chain).to_string()), false, "scan intervals", ); @@ -311,27 +311,6 @@ impl DbInitializerReal { .expect("Can't create transaction hash index in failed payments"); } - pub fn create_pending_payable_table(conn: &Connection) { - conn.execute( - "create table if not exists pending_payable ( - rowid integer primary key, - transaction_hash text not null, - amount_high_b integer not null, - amount_low_b integer not null, - payable_timestamp integer not null, - attempt integer not null, - process_error text null - )", - [], - ) - .expect("Can't create pending_payable table"); - conn.execute( - "CREATE UNIQUE INDEX pending_payable_hash_idx ON pending_payable (transaction_hash)", - [], - ) - .expect("Can't create transaction hash index in pending payments"); - } - pub fn create_payable_table(conn: &Connection) { conn.execute( "create table if not exists payable ( @@ -736,50 +715,6 @@ mod tests { assert_no_index_exists_for_table(conn.as_ref(), "config") } - #[test] - fn db_initialize_creates_pending_payable_table() { - let home_dir = ensure_node_home_directory_does_not_exist( - "db_initializer", - "db_initialize_creates_pending_payable_table", - ); - let subject = DbInitializerReal::default(); - - let conn = subject - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - - let mut stmt = conn - .prepare( - "SELECT rowid, - transaction_hash, - amount_high_b, - amount_low_b, - payable_timestamp, - attempt, - process_error - FROM pending_payable", - ) - .unwrap(); - let result = stmt.execute([]).unwrap(); - assert_eq!(result, 1); - let expected_key_words: &[&[&str]] = &[ - &["rowid", "integer", "primary", "key"], - &["transaction_hash", "text", "not", "null"], - &["amount_high_b", "integer", "not", "null"], - &["amount_low_b", "integer", "not", "null"], - &["payable_timestamp", "integer", "not", "null"], - &["attempt", "integer", "not", "null"], - &["process_error", "text", "null"], - ]; - assert_create_table_stm_contains_all_parts(&*conn, "pending_payable", expected_key_words); - let expected_key_words: &[&[&str]] = &[&["transaction_hash"]]; - assert_index_stm_is_coupled_with_right_parameter( - conn.as_ref(), - "pending_payable_hash_idx", - expected_key_words, - ) - } - #[test] fn db_initialize_creates_sent_payable_table() { let home_dir = ensure_node_home_directory_does_not_exist( @@ -1117,7 +1052,7 @@ mod tests { verify( &mut config_vec, "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(TEST_DEFAULT_CHAIN).to_string()), false, ); verify( diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs index 5e4e18368c..b3f2a157a3 100644 --- a/node/src/database/db_migrations/migrations/migration_10_to_11.rs +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -36,9 +36,12 @@ impl DatabaseMigration for Migrate_10_to_11 { status text not null )"; + let sql_statement_for_pending_payable = "drop table pending_payable"; + declaration_utils.execute_upon_transaction(&[ &sql_statement_for_sent_payable, &sql_statement_for_failed_payable, + &sql_statement_for_pending_payable, ]) } @@ -55,10 +58,7 @@ mod tests { use crate::database::test_utils::{ SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, }; - use crate::test_utils::database_utils::{ - assert_create_table_stm_contains_all_parts, assert_table_exists, - bring_db_0_back_to_life_and_return_connection, make_external_data, - }; + use crate::test_utils::database_utils::{assert_create_table_stm_contains_all_parts, assert_table_does_not_exist, assert_table_exists, bring_db_0_back_to_life_and_return_connection, make_external_data}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use std::fs::create_dir_all; @@ -103,6 +103,7 @@ mod tests { "failed_payable", SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, ); + assert_table_does_not_exist(connection.as_ref(), "pending_payable"); TestLogHandler::new().assert_logs_contain_in_order(vec![ "DbMigrator: Database successfully migrated from version 10 to 11", ]); diff --git a/node/src/database/db_migrations/migrations/migration_4_to_5.rs b/node/src/database/db_migrations/migrations/migration_4_to_5.rs index 4b5bbb50a5..204ab49a56 100644 --- a/node/src/database/db_migrations/migrations/migration_4_to_5.rs +++ b/node/src/database/db_migrations/migrations/migration_4_to_5.rs @@ -150,7 +150,7 @@ mod tests { conn: &dyn ConnectionWrapper, transaction_hash_opt: Option, wallet: &Wallet, - amount: i64, + amount_minor: i64, timestamp: SystemTime, ) { let hash_str = transaction_hash_opt @@ -159,7 +159,7 @@ mod tests { let mut stm = conn.prepare("insert into payable (wallet_address, balance, last_paid_timestamp, pending_payment_transaction) values (?,?,?,?)").unwrap(); let params: &[&dyn ToSql] = &[ &wallet, - &amount, + &amount_minor, &to_unix_timestamp(timestamp), if !hash_str.is_empty() { &hash_str diff --git a/node/src/database/db_migrations/migrations/migration_5_to_6.rs b/node/src/database/db_migrations/migrations/migration_5_to_6.rs index a5f902cb96..b32e3b2d0b 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/rusqlite_wrappers.rs b/node/src/database/rusqlite_wrappers.rs index ec867482f7..2177a250b9 100644 --- a/node/src/database/rusqlite_wrappers.rs +++ b/node/src/database/rusqlite_wrappers.rs @@ -5,15 +5,15 @@ use crate::masq_lib::utils::ExpectValue; use rusqlite::{Connection, Error, Statement, ToSql, Transaction}; use std::fmt::Debug; -// We were challenged multiple times to device mocks for testing stubborn, hard to tame, data +// We were challenged multiple times to devise mocks for testing stubborn, hard to tame, data // structures from the 'rusqlite' library. After all, we've adopted two of them, the Connection, // that came first, and the Transaction to come much later. Of these, only the former complies // with the standard policy we follow for mock designs. // // The delay until the second one became a thing, even though we would've been glad having it -// on hand much earlier, was caused by vacuum of ideas on how we could create a mock of these +// on hand much earlier, was caused by a vacuum of ideas on how we could create a mock of these // parameters and have it accepted by the compiler. Passing a lot of time, we came up with a hybrid, -// at least. That said, it has costed us a considerably high price of giving up on simplicity. +// at least. That said, it has cost us a considerably high price of giving up on simplicity. // // The firmest blocker of the design has always rooted in a relationship of serialized lifetimes, // affecting each other, that has been so hard to maintain right. Yet the choices made @@ -74,12 +74,12 @@ impl ConnectionWrapperReal { } } -// Whole point of this outer wrapper, that is common to both the real and mock transactions, is to +// The whole point of this outer wrapper that is common to both the real and mock transactions is to // make a chance to deconstruct all components of a transaction in place. It plays a crucial role -// during the final commit. Note that an usual mock based on the direct use of a trait object +// during the final commit. Note that a usual mock based on the direct use of a trait object // cannot be consumed by any of its methods because of the Rust rules for trait objects. They say // clearly that we can access it via '&self', '&mut self' but not 'self'. However, to have a thing -// consume itself we need to be provided with the full ownership. +// consume itself, we need to be provided with the full ownership. // // Leaving remains of an already committed transaction around would expose us to a risk. Let's // imagine somebody trying to make use of it the second time, while the inner element providing diff --git a/node/src/database/test_utils/transaction_wrapper_mock.rs b/node/src/database/test_utils/transaction_wrapper_mock.rs index d0577c72f6..5b9a717e9e 100644 --- a/node/src/database/test_utils/transaction_wrapper_mock.rs +++ b/node/src/database/test_utils/transaction_wrapper_mock.rs @@ -137,7 +137,7 @@ impl TransactionInnerWrapper for TransactionInnerWrapperMock { // is to be formed. // With that said, we're relieved to have at least one working solution now. Speaking of the 'prepare' -// method, an error would be hardly needed because the production code simply unwraps the results by +// method, an error would hardly be needed because the production code simply unwraps the results by // using 'expect'. That is a function excluded from the requirement of writing tests for. // The 'Statement' produced by this method must be better understood. The 'prepare' method has @@ -199,12 +199,12 @@ impl SetupForProdCodeAndAlteredStmts { // necessary base. If the continuity is broken the later statement might not work. If // we record some changes on the transaction, other changes tried to be done from // a different connection might meet a different state of the database and thwart the - // efforts. (This behaviour probably depends on the global setup of the db). + // efforts. (This behavior probably depends on the global setup of the db). // // // Also imagine a 'Statement' that wouldn't cause an error whereupon any potential // rollback of this txn should best drag off both the prod code and altered statements - // all together, disappearing. If we did not use this txn some of the changes would stay. + // all together, disappearing. If we did not use this txn some changes would stay. { self.txn_bearing_prod_code_stmts_opt .as_ref() diff --git a/node/src/db_config/config_dao_null.rs b/node/src/db_config/config_dao_null.rs index f1fc58cd49..8cd87c075e 100644 --- a/node/src/db_config/config_dao_null.rs +++ b/node/src/db_config/config_dao_null.rs @@ -4,13 +4,13 @@ use crate::database::db_initializer::DbInitializerReal; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoError, ConfigDaoRecord}; use crate::neighborhood::DEFAULT_MIN_HOPS; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::{CURRENT_SCHEMA_VERSION, DEFAULT_GAS_PRICE}; use std::collections::HashMap; - /* This class exists because the Daemon uses the same configuration code that the Node uses, and @@ -139,7 +139,10 @@ impl Default for ConfigDaoNull { ); data.insert( "scan_intervals".to_string(), - (Some(DEFAULT_SCAN_INTERVALS.to_string()), false), + ( + Some(accountant::ScanIntervals::compute_default(Chain::default()).to_string()), + false, + ), ); data.insert("max_block_count".to_string(), (None, false)); Self { data } @@ -208,7 +211,7 @@ mod tests { subject.get("scan_intervals").unwrap(), ConfigDaoRecord::new( "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(Chain::default()).to_string()), false ) ); diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 801aa44562..a66e74c5f1 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -330,7 +330,7 @@ fn get_public_ip(multi_config: &MultiConfig) -> Result match IpAddr::from_str(&ip_str) { Ok(ip_addr) => Ok(ip_addr), - Err(_) => todo!("Drive in a better error message"), //Err(ConfiguratorError::required("ip", &format! ("blockety blip: '{}'", ip_str), + Err(_) => todo!("Drive in a better error message. The multiconfig wouldn't allow a bad format, though."), //Err(ConfiguratorError::required("ip", &format! ("blockety blip: '{}'", ip_str), }, None => Ok(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), // sentinel: means "Try Automap" } @@ -494,7 +494,7 @@ fn configure_accountant_config( |pc: &mut dyn PersistentConfiguration, curves| pc.set_payment_thresholds(curves), )?; - check_payment_thresholds(&payment_thresholds)?; + validate_payment_thresholds(&payment_thresholds)?; let scan_intervals = process_combined_params( "scan-intervals", @@ -505,6 +505,8 @@ fn configure_accountant_config( |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), )?; + validate_scan_intervals(&scan_intervals)?; + let automatic_scans_enabled = value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == "on"; @@ -515,12 +517,13 @@ fn configure_accountant_config( Ok(()) } -fn check_payment_thresholds( +fn validate_payment_thresholds( payment_thresholds: &PaymentThresholds, ) -> Result<(), ConfiguratorError> { if payment_thresholds.debt_threshold_gwei <= payment_thresholds.permanent_debt_allowed_gwei { let msg = format!( - "Value of DebtThresholdGwei ({}) must be bigger than PermanentDebtAllowedGwei ({})", + "Value of DebtThresholdGwei ({}) must be bigger than PermanentDebtAllowedGwei ({}) \ + as the smallest value", payment_thresholds.debt_threshold_gwei, payment_thresholds.permanent_debt_allowed_gwei ); return Err(ConfiguratorError::required("payment-thresholds", &msg)); @@ -534,6 +537,21 @@ fn check_payment_thresholds( Ok(()) } +fn validate_scan_intervals(scan_intervals: &ScanIntervals) -> Result<(), ConfiguratorError> { + if scan_intervals.payable_scan_interval < scan_intervals.pending_payable_scan_interval { + Err(ConfiguratorError::required( + "scan-intervals", + &format!( + "The PendingPayableScanInterval value ({} s) must not exceed the PayableScanInterval \ + value ({} s) and should ideally be approximately half of it", + scan_intervals.pending_payable_scan_interval.as_secs(), + scan_intervals.payable_scan_interval.as_secs()), + )) + } else { + Ok(()) + } +} + fn configure_rate_pack( multi_config: &MultiConfig, persist_config: &mut dyn PersistentConfiguration, @@ -2099,8 +2117,8 @@ mod tests { } #[test] - fn configure_accountant_config_discovers_invalid_payment_thresholds_params_combination_given_from_users_input( - ) { + fn configure_accountant_config_discovers_invalid_payment_thresholds_combination_in_users_input() + { let multi_config = make_simplified_multi_config([ "--payment-thresholds", "19999|10000|1000|20000|1000|20000", @@ -2116,7 +2134,8 @@ mod tests { &mut persistent_config, ); - let expected_msg = "Value of DebtThresholdGwei (19999) must be bigger than PermanentDebtAllowedGwei (20000)"; + let expected_msg = "Value of DebtThresholdGwei (19999) must be bigger than \ + PermanentDebtAllowedGwei (20000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2127,14 +2146,15 @@ mod tests { } #[test] - fn check_payment_thresholds_works_for_equal_debt_parameters() { + fn validate_payment_thresholds_works_for_equal_debt_parameters() { let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; payment_thresholds.permanent_debt_allowed_gwei = 10000; payment_thresholds.debt_threshold_gwei = 10000; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); - let expected_msg = "Value of DebtThresholdGwei (10000) must be bigger than PermanentDebtAllowedGwei (10000)"; + let expected_msg = "Value of DebtThresholdGwei (10000) must be bigger than \ + PermanentDebtAllowedGwei (10000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2145,14 +2165,15 @@ mod tests { } #[test] - fn check_payment_thresholds_works_for_too_small_debt_threshold() { + fn validate_payment_thresholds_works_for_too_small_debt_threshold() { let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; payment_thresholds.permanent_debt_allowed_gwei = 10000; payment_thresholds.debt_threshold_gwei = 9999; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); - let expected_msg = "Value of DebtThresholdGwei (9999) must be bigger than PermanentDebtAllowedGwei (10000)"; + let expected_msg = "Value of DebtThresholdGwei (9999) must be bigger than \ + PermanentDebtAllowedGwei (10000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2163,7 +2184,8 @@ mod tests { } #[test] - fn check_payment_thresholds_does_not_permit_threshold_interval_longer_than_1_000_000_000_s() { + fn validate_payment_thresholds_does_not_permit_threshold_interval_longer_than_1_000_000_000_s() + { //this goes to the furthest extreme where the delta of debt limits is just 1 gwei, which, //if divided by the slope interval equal or longer 10^9 and rounded, gives 0 let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; @@ -2171,7 +2193,7 @@ mod tests { payment_thresholds.debt_threshold_gwei = 101; payment_thresholds.threshold_interval_sec = 1_000_000_001; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); let expected_msg = "Value of ThresholdIntervalSec must not exceed 1,000,000,000 s"; assert_eq!( @@ -2186,6 +2208,28 @@ mod tests { assert_eq!(last_value_possible, -1) } + #[test] + fn configure_accountant_config_discovers_invalid_scan_intervals_combination_in_users_input() { + let multi_config = make_simplified_multi_config(["--scan-intervals", "600|601|600"]); + let mut bootstrapper_config = BootstrapperConfig::new(); + let mut persistent_config = + configure_default_persistent_config(ACCOUNTANT_CONFIG_PARAMS | MAPPING_PROTOCOL) + .set_scan_intervals_result(Ok(())); + + let result = configure_accountant_config( + &multi_config, + &mut bootstrapper_config, + &mut persistent_config, + ); + + let expected_msg = "The PendingPayableScanInterval value (601 s) must not exceed \ + the PayableScanInterval value (600 s) and should ideally be approximately half of it"; + assert_eq!( + result, + Err(ConfiguratorError::required("scan-intervals", expected_msg)) + ) + } + #[test] fn unprivileged_parse_args_with_invalid_consuming_wallet_private_key_reacts_correctly() { running_test(); diff --git a/node/src/stream_handler_pool.rs b/node/src/stream_handler_pool.rs index 470f0c44f6..5772e9cf8c 100644 --- a/node/src/stream_handler_pool.rs +++ b/node/src/stream_handler_pool.rs @@ -1760,7 +1760,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); @@ -1927,7 +1927,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index e00a0132cf..039b1fe4f7 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -2,16 +2,14 @@ use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; use crate::accountant::db_access_objects::failed_payable_dao::FailedPayableDaoFactory; use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoFactory; use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoFactory; use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; 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}; @@ -19,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; @@ -39,11 +38,6 @@ lazy_static! { threshold_interval_sec: 21600, unban_below_gwei: 500_000_000, }; - pub static ref DEFAULT_SCAN_INTERVALS: ScanIntervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(600), - pending_payable_scan_interval: Duration::from_secs(60), - receivable_scan_interval: Duration::from_secs(600) - }; } //please, alphabetical order @@ -74,7 +68,6 @@ impl PaymentThresholds { pub struct DaoFactories { pub payable_dao_factory: Box, pub sent_payable_dao_factory: Box, - pub pending_payable_dao_factory: Box, // TODO: This should go away pub failed_payable_dao_factory: Box, pub receivable_dao_factory: Box, pub banned_dao_factory: Box, @@ -88,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), + } } } @@ -104,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, @@ -195,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 { @@ -234,12 +254,6 @@ mod tests { threshold_interval_sec: 21600, unban_below_gwei: 500_000_000, }; - let scan_intervals_expected = ScanIntervals { - payable_scan_interval: Duration::from_secs(600), - pending_payable_scan_interval: Duration::from_secs(60), - receivable_scan_interval: Duration::from_secs(600), - }; - assert_eq!(*DEFAULT_SCAN_INTERVALS, scan_intervals_expected); assert_eq!(*DEFAULT_PAYMENT_THRESHOLDS, payment_thresholds_expected); assert_eq!(*DEFAULT_EARNING_WALLET, default_earning_wallet_expected); assert_eq!( @@ -292,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 8ce62e467c..25834d5f65 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -3,9 +3,14 @@ use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; -use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; +use crate::accountant::{ + PayableScanType, RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder, +}; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use crate::blockchain::blockchain_bridge::{ + MsgInterpretableAsDetailedScanType, RetrieveTransactions, +}; +use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::peer_actors::BindMessage; use actix::Message; use actix::Recipient; @@ -48,6 +53,15 @@ pub struct OutboundPaymentsInstructions { pub response_skeleton_opt: Option, } +impl MsgInterpretableAsDetailedScanType for OutboundPaymentsInstructions { + fn detailed_scan_type(&self) -> DetailedScanType { + match self.priced_templates { + Either::Left(_) => DetailedScanType::NewPayables, + Either::Right(_) => DetailedScanType::RetryPayables, + } + } +} + impl OutboundPaymentsInstructions { pub fn new( priced_templates: Either, @@ -60,6 +74,13 @@ impl OutboundPaymentsInstructions { response_skeleton_opt, } } + + pub fn scan_type(&self) -> PayableScanType { + match &self.priced_templates { + Either::Left(_new_templates) => PayableScanType::New, + Either::Right(_retry_templates) => PayableScanType::Retry, + } + } } impl SkeletonOptHolder for OutboundPaymentsInstructions { @@ -85,12 +106,21 @@ impl ConsumingWalletBalances { #[cfg(test)] mod tests { + use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::make_priced_new_tx_templates; + use crate::accountant::test_utils::make_payable_account; use crate::actor_system_factory::SubsFactory; - use crate::blockchain::blockchain_bridge::{BlockchainBridge, BlockchainBridgeSubsFactoryReal}; + use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; + use crate::blockchain::blockchain_bridge::{ + BlockchainBridge, BlockchainBridgeSubsFactoryReal, MsgInterpretableAsDetailedScanType, + }; use crate::blockchain::test_utils::make_blockchain_interface_web3; + use crate::sub_lib::accountant::DetailedScanType; + use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{make_blockchain_bridge_subs_from_recorder, Recorder}; use actix::{Actor, System}; + use itertools::Either; use masq_lib::utils::find_free_port; use std::sync::{Arc, Mutex}; @@ -122,4 +152,24 @@ mod tests { system.run(); assert_eq!(subs, BlockchainBridge::make_subs_from(&addr)) } + + #[test] + fn detailed_scan_type_is_implemented_for_outbound_payments_instructions() { + let msg_a = OutboundPaymentsInstructions { + priced_templates: Either::Left(make_priced_new_tx_templates(vec![( + make_payable_account(123), + 123, + )])), + agent: Box::new(BlockchainAgentMock::default()), + response_skeleton_opt: None, + }; + let msg_b = OutboundPaymentsInstructions { + priced_templates: Either::Right(PricedRetryTxTemplates(vec![])), + agent: Box::new(BlockchainAgentMock::default()), + response_skeleton_opt: None, + }; + + assert_eq!(msg_a.detailed_scan_type(), DetailedScanType::NewPayables); + assert_eq!(msg_b.detailed_scan_type(), DetailedScanType::RetryPayables) + } } diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index 53a3e84886..bd26eb627f 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -307,6 +307,7 @@ mod tests { use super::*; use crate::sub_lib::combined_parameters::CombinedParamsDataTypes::U128; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; + use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; use std::panic::catch_unwind; #[test] @@ -455,7 +456,7 @@ mod tests { let panic_3 = catch_unwind(|| { let _: &[(&str, CombinedParamsDataTypes)] = - (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))).into(); + (&CombinedParams::ScanIntervals(Initialized(*TEST_SCAN_INTERVALS))).into(); }) .unwrap_err(); let panic_3_msg = panic_3.downcast_ref::().unwrap(); @@ -464,7 +465,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - ScanIntervals::default() + *TEST_SCAN_INTERVALS ) ); } @@ -502,7 +503,7 @@ mod tests { ); let panic_3 = catch_unwind(|| { - (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))) + (&CombinedParams::ScanIntervals(Initialized(*TEST_SCAN_INTERVALS))) .initialize_objects(HashMap::new()); }) .unwrap_err(); @@ -512,7 +513,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - ScanIntervals::default() + *TEST_SCAN_INTERVALS ) ); } diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index fb8ba3a83c..a2b6d9ee1a 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -109,9 +109,10 @@ pub fn assert_table_exists(conn: &dyn ConnectionWrapper, table_name: &str) { } pub fn assert_table_does_not_exist(conn: &dyn ConnectionWrapper, table_name: &str) { - let error_stm = conn - .prepare(&format!("select * from {}", table_name)) - .unwrap_err(); + let error_stm = match conn.prepare(&format!("select * from {}", table_name)) { + Ok(_) => panic!("Table {} should not exist, but it does", table_name), + Err(e) => e, + }; let error_msg = match error_stm { Error::SqliteFailure(_, Some(msg)) => msg, x => panic!("we expected SqliteFailure but we got: {:?}", x), diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index b36199b75d..601ee7bd13 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -14,6 +14,7 @@ pub mod persistent_configuration_mock; pub mod recorder; pub mod recorder_counter_msgs; pub mod recorder_stop_conditions; +pub mod serde_serializer_mock; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; pub mod tokio_wrapper_mocks; @@ -547,6 +548,7 @@ pub mod unshared_test_utils { use crossbeam_channel::{unbounded, Receiver, Sender}; use itertools::Either; use lazy_static::lazy_static; + use masq_lib::blockchains::chains::Chain; use masq_lib::constants::HTTP_PORT; use masq_lib::messages::{ToMessageBody, UiCrashRequest}; use masq_lib::multi_config::MultiConfig; @@ -569,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, @@ -629,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; @@ -673,16 +695,16 @@ pub mod unshared_test_utils { ) -> PersistentConfigurationMock { persistent_config_mock .payment_thresholds_result(Ok(PaymentThresholds::default())) - .scan_intervals_result(Ok(ScanIntervals::default())) + .scan_intervals_result(Ok(*TEST_SCAN_INTERVALS)) } pub fn make_persistent_config_real_with_config_dao_null() -> PersistentConfigurationReal { PersistentConfigurationReal::new(Box::new(ConfigDaoNull::default())) } - pub fn make_bc_with_defaults() -> BootstrapperConfig { + pub fn make_bc_with_defaults(chain: Chain) -> BootstrapperConfig { let mut config = BootstrapperConfig::new(); - config.scan_intervals_opt = Some(ScanIntervals::default()); + config.scan_intervals_opt = Some(ScanIntervals::compute_default(chain)); config.automatic_scans_enabled = true; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; config.payment_thresholds_opt = Some(PaymentThresholds::default()); @@ -933,8 +955,7 @@ pub mod unshared_test_utils { ) -> Box { if self.panic_on_schedule_attempt { panic!( - "Message scheduling request for {:?} and interval {}ms, thought not \ - expected", + "Message scheduling request for {:?} and interval {}ms, thought not expected", msg, interval.as_millis() ); diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index db5a9d38e1..f52b1a0c83 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -8,8 +8,8 @@ use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::accountant::{ReportTransactionReceipts, ScanForPendingPayables, ScanForRetryPayables}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::accountant::{ScanForPendingPayables, ScanForRetryPayables, TxReceiptsMessage}; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::daemon::crash_notification::CrashNotification; use crate::daemon::DaemonBindMessage; @@ -153,7 +153,7 @@ recorder_message_handler_t_m_p!(NodeFromUiMessage); recorder_message_handler_t_m_p!(NodeToUiMessage); recorder_message_handler_t_m_p!(NoLookupIncipientCoresPackage); recorder_message_handler_t_p!(OutboundPaymentsInstructions); -recorder_message_handler_t_m_p!(PendingPayableFingerprintSeeds); +recorder_message_handler_t_m_p!(RegisterNewPendingPayables); recorder_message_handler_t_m_p!(PoolBindMessage); recorder_message_handler_t_m_p!(InitialTemplatesMessage); recorder_message_handler_t_m_p!(ReceivedPayments); @@ -162,7 +162,7 @@ recorder_message_handler_t_m_p!(RemoveStreamMsg); recorder_message_handler_t_m_p!(ReportExitServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportRoutingServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportServicesConsumedMessage); -recorder_message_handler_t_m_p!(ReportTransactionReceipts); +recorder_message_handler_t_m_p!(TxReceiptsMessage); recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); @@ -529,8 +529,8 @@ pub fn make_accountant_subs_from_recorder(addr: &Addr) -> AccountantSu report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, PricedTemplatesMessage), 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), diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 9a3214eeae..f10e0e4a62 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -184,9 +184,9 @@ macro_rules! match_lazily_every_type_id{ mod tests { use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; + use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; - use masq_lib::messages::ScanType; use std::any::TypeId; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::vec; @@ -249,16 +249,16 @@ mod tests { let mut cond_set = StopConditions::AllGreedily(vec![MsgIdentification::ByPredicate { predicate: Box::new(|msg| { let scan_err_msg: &ScanError = msg.downcast_ref().unwrap(); - scan_err_msg.scan_type == ScanType::PendingPayables + scan_err_msg.scan_type == DetailedScanType::PendingPayables }), }]); let wrong_msg = ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: None, msg: "booga".to_string(), }; let good_msg = ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: None, msg: "blah".to_string(), }; 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 0000000000..7130cd0c0b --- /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") + } +}