diff --git a/USER-INTERFACE-INTERFACE.md b/USER-INTERFACE-INTERFACE.md index 3a5cacffa..1112ad7a5 100644 --- a/USER-INTERFACE-INTERFACE.md +++ b/USER-INTERFACE-INTERFACE.md @@ -769,7 +769,10 @@ searching over; this is the upper limit for the debt's age, or how long it has b "wallet": , "ageS": , "balanceGwei": , - "pendingPayableHashOpt": + "txProcessingInfoOpt": { + "pendingTxHashOpt": , + "failures": + } }, [...] ], @@ -826,9 +829,25 @@ payment was later also confirmed on the blockchain. `balanceGwei` is a number of gwei we owe to this particular Node. -`pendingPayableHashOpt` is present only sporadically. When it is, it denotes that we've recently sent a payment to the -blockchain, but our confirmation detector has not yet determined that the payment has been confirmed. The value is -either null or stores a transaction hash of the pending transaction. +`txProcessingInfoOpt` is present only sporadically. When it is, it denotes that we've recently sent a tx to +the blockchain, but the payment hasn't been confirmed yet. The value is either null, which means that no tx has been +recently issued for the respective account, or stores an object representing an already begun payment operation; +this object has two fields. + +The first, `pendingTxHashOpt`, if present, contains a transaction hash of an alive, pending transaction that is still +waiting for resolution. + +The second, `failures`, is a positive number of times we've tried to send this same tx to the blockchain in this latest +payment, but it has failed repeatedly. The tx always bears the same nonce and is bound for the same wallet. +There are three possible observable states: + +1. `pendingTxHashOpt` is not null and `failures` is 0. This means that we've yet made only a single attempt to send +a tx to the blockchain, and the tx is pending. +2. `pendingTxHashOpt` is not null and `failures` is greater than 0. This means that we've made more than one attempt. +There is an alive pending tx, plus we've experienced X failures before that. +3. `pendingTxHashOpt` is null and `failures` is greater than 0. This means that the payment mechanism is in a mid-state +preparing a submission of another payment attempt, but the blockchain hasn't been updated about this new tx yet. +At the same time, there have already been preceding failed tx attempts. `receivable` is the field devoted to receivable records if any exist. diff --git a/masq/src/commands/financials_command/mod.rs b/masq/src/commands/financials_command/mod.rs index c01b6ca43..f4788d44b 100644 --- a/masq/src/commands/financials_command/mod.rs +++ b/masq/src/commands/financials_command/mod.rs @@ -324,8 +324,8 @@ mod tests { use crate::test_utils::mocks::CommandContextMock; use atty::Stream; use masq_lib::messages::{ - ToMessageBody, TopRecordsOrdering, UiFinancialStatistics, UiFinancialsResponse, - UiPayableAccount, UiReceivableAccount, + ToMessageBody, TopRecordsOrdering, TxProcessingInfo, UiFinancialStatistics, + UiFinancialsResponse, UiPayableAccount, UiReceivableAccount, }; use masq_lib::ui_gateway::MessageBody; use masq_lib::utils::slice_of_strs_to_vec_of_strings; @@ -1028,15 +1028,27 @@ mod tests { wallet: "0xA884A2F1A5Ec6C2e499644666a5E6af97B966888".to_string(), age_s: 5645405400, balance_gwei: 68843325667, - pending_payable_hash_opt: None, + tx_processing_info_opt: None, }, UiPayableAccount { wallet: "0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440".to_string(), age_s: 150000, - balance_gwei: 8, - pending_payable_hash_opt: Some( - "0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e" - .to_string(), + balance_gwei: 999888777, + tx_processing_info_opt: Some(TxProcessingInfo{pending_tx_hash_opt: Some("0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e".to_string()),failures: 0} + ), + }, + UiPayableAccount { + wallet: "0x563cCaC5596b7ac986ff8F7ca056789a122c3230".to_string(), + age_s: 12055, + balance_gwei: 33444555, + tx_processing_info_opt: Some(TxProcessingInfo{pending_tx_hash_opt: None,failures: 3} + ), + }, + UiPayableAccount { + wallet: "0xeF456a11A5Ec6C2e499655787a5E6af97c961123".to_string(), + age_s: 161514, + balance_gwei: 1111, + tx_processing_info_opt: Some(TxProcessingInfo{pending_tx_hash_opt: Some("0xb45a663d56121112f4d45c1c3a245be32eAA6afd20b759b762f1dba945ec2f41".to_string()),failures: 1} ), }, ]), @@ -1059,9 +1071,9 @@ mod tests { wallet: "0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440".to_string(), age_s: 150000, balance_gwei: 8, - pending_payable_hash_opt: Some( + tx_processing_info_opt: Some(TxProcessingInfo{pending_tx_hash_opt: Some( "0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e" - .to_string(), + .to_string()),failures: 0} ), }]), receivable_opt: None, @@ -1117,9 +1129,11 @@ mod tests { \n\ Payable\n\ \n\ - # Wallet Age [s] Balance [MASQ] Pending tx \n\ - 1 0xA884A2F1A5Ec6C2e499644666a5E6af97B966888 5,645,405,400 68.84 None \n\ - 2 0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440 150,000 < 0.01 0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e\n\ + # Wallet Age [s] Balance [MASQ] Pending tx \n\ + 1 0xA884A2F1A5Ec6C2e499644666a5E6af97B966888 5,645,405,400 68.84 None \n\ + 2 0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440 150,000 0.99 0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e \n\ + 3 0x563cCaC5596b7ac986ff8F7ca056789a122c3230 12,055 0.03 Processing... 3 failed attempts \n\ + 4 0xeF456a11A5Ec6C2e499655787a5E6af97c961123 161,514 < 0.01 0xb45a663d56121112f4d45c1c3a245be32eAA6afd20b759b762f1dba945ec2f41 (1 failed attempt)\n\ \n\ \n\ \n\ @@ -1253,9 +1267,11 @@ mod tests { \n\ Payable\n\ \n\ - # Wallet Age [s] Balance [gwei] Pending tx \n\ - 1 0xA884A2F1A5Ec6C2e499644666a5E6af97B966888 5,645,405,400 68,843,325,667 None \n\ - 2 0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440 150,000 8 0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e\n\ + # Wallet Age [s] Balance [gwei] Pending tx \n\ + 1 0xA884A2F1A5Ec6C2e499644666a5E6af97B966888 5,645,405,400 68,843,325,667 None \n\ + 2 0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440 150,000 999,888,777 0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e \n\ + 3 0x563cCaC5596b7ac986ff8F7ca056789a122c3230 12,055 33,444,555 Processing... 3 failed attempts \n\ + 4 0xeF456a11A5Ec6C2e499655787a5E6af97c961123 161,514 1,111 0xb45a663d56121112f4d45c1c3a245be32eAA6afd20b759b762f1dba945ec2f41 (1 failed attempt)\n\ \n\ \n\ \n\ @@ -1345,25 +1361,26 @@ mod tests { #[test] fn custom_query_balance_range_can_be_shorthanded() { let transact_params_arc = Arc::new(Mutex::new(vec![])); - let expected_response = UiFinancialsResponse { - stats_opt: None, - query_results_opt: Some(QueryResults { - payable_opt: Some(vec![UiPayableAccount { + let expected_response = + UiFinancialsResponse { + stats_opt: None, + query_results_opt: Some(QueryResults { + payable_opt: Some(vec![UiPayableAccount { wallet: "0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440".to_string(), age_s: 150000, balance_gwei: 1200000000000, - pending_payable_hash_opt: Some( + tx_processing_info_opt: Some(TxProcessingInfo{pending_tx_hash_opt: Some( "0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e" - .to_string(), + .to_string()),failures: 4} ), }]), - receivable_opt: Some(vec![UiReceivableAccount { - wallet: "0x8bA50675e590b545D2128905b89039256Eaa24F6".to_string(), - age_s: 45700, - balance_gwei: 5050330000, - }]), - }), - }; + receivable_opt: Some(vec![UiReceivableAccount { + wallet: "0x8bA50675e590b545D2128905b89039256Eaa24F6".to_string(), + age_s: 45700, + balance_gwei: 5050330000, + }]), + }), + }; let args = slice_of_strs_to_vec_of_strings(&[ "financials", "--payable", @@ -1411,8 +1428,8 @@ mod tests { "\n\ Specific payable query: 0-350000 sec 5-UNLIMITED MASQ\n\ \n\ - # Wallet Age [s] Balance [MASQ] Pending tx \n\ - 1 0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440 150,000 1,200.00 0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e\n\ + # Wallet Age [s] Balance [MASQ] Pending tx \n\ + 1 0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440 150,000 1,200.00 0x0290db1d56121112f4d45c1c3f36348644f6afd20b759b762f1dba9c4949066e (4 failed attempts)\n\ \n\ \n\ \n\ @@ -1603,16 +1620,16 @@ mod tests { wallet: "0xA884A2F1A5Ec6C2e499644666a5E6af97B966888".to_string(), age_s: 5405400, balance_gwei: 644000000, - pending_payable_hash_opt: Some( + tx_processing_info_opt: Some(TxProcessingInfo{pending_tx_hash_opt: Some( "0x3648c8b8c7e067ac30b80b6936159326d564dd13b7ae465b26647154ada2c638" - .to_string(), + .to_string()), failures: 0} ), }, UiPayableAccount { wallet: "0xEA674fdac714fd979de3EdF0F56AA9716B198ec8".to_string(), age_s: 28120444, balance_gwei: 97524120, - pending_payable_hash_opt: None, + tx_processing_info_opt: None, }, ]), receivable_opt: Some(vec![ @@ -1689,22 +1706,22 @@ mod tests { wallet: "0x6e250504DdfFDb986C4F0bb8Df162503B4118b05".to_string(), age_s: 4445, balance_gwei: 3862654858938090, - pending_payable_hash_opt: Some( + tx_processing_info_opt: Some( TxProcessingInfo{pending_tx_hash_opt: Some( "0x5fe272ed1e941cc05fbd624ec4b1546cd03c25d53e24ba2c18b11feb83cd4581" - .to_string(), + .to_string()),failures: 0} ), }, UiPayableAccount { wallet: "0xA884A2F1A5Ec6C2e499644666a5E6af97B966888".to_string(), age_s: 70000, balance_gwei: 708090, - pending_payable_hash_opt: None, + tx_processing_info_opt: Some(TxProcessingInfo{pending_tx_hash_opt: None, failures: 3}), }, UiPayableAccount { wallet: "0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440".to_string(), age_s: 6089909, balance_gwei: 66658, - pending_payable_hash_opt: None, + tx_processing_info_opt: None, }, ]), receivable_opt: None, @@ -1754,7 +1771,7 @@ mod tests { \n\ # Wallet Age [s] Balance [MASQ] Pending tx \n\ 1 0x6e250504DdfFDb986C4F0bb8Df162503B4118b05 4,445 3,862,654.85 0x5fe272ed1e941cc05fbd624ec4b1546cd03c25d53e24ba2c18b11feb83cd4581\n\ - 2 0xA884A2F1A5Ec6C2e499644666a5E6af97B966888 70,000 < 0.01 None \n\ + 2 0xA884A2F1A5Ec6C2e499644666a5E6af97B966888 70,000 < 0.01 Processing... 3 failed attempts \n\ 3 0x6DbcCaC5596b7ac986ff8F7ca06F212aEB444440 6,089,909 < 0.01 None \n" ); assert_eq!(stderr_arc.lock().unwrap().get_string(), String::new()); diff --git a/masq/src/commands/financials_command/pretty_print_utils.rs b/masq/src/commands/financials_command/pretty_print_utils.rs index 2adbfbad4..1dbea4991 100644 --- a/masq/src/commands/financials_command/pretty_print_utils.rs +++ b/masq/src/commands/financials_command/pretty_print_utils.rs @@ -12,7 +12,7 @@ pub(in crate::commands::financials_command) mod restricted { use masq_lib::messages::{UiPayableAccount, UiReceivableAccount}; use masq_lib::short_writeln; use masq_lib::utils::to_string; - use std::fmt::{Debug, Display}; + use std::fmt::{Debug, Display, Formatter}; use std::io::Write; use thousands::Separable; @@ -27,10 +27,27 @@ pub(in crate::commands::financials_command) mod restricted { self.wallet.to_string(), self.age_s.separate_with_commas(), process_gwei_into_requested_format(self.balance_gwei, is_gwei), - if let Some(hash) = &self.pending_payable_hash_opt { - hash.to_string() - } else { - "None".to_string() + match &self.tx_processing_info_opt { + Some(current_tx_info) => match ¤t_tx_info.pending_tx_hash_opt { + Some(hash) => { + if current_tx_info.failures == 0 { + hash.clone() + } else { + format!( + "{} ({})", + hash, + AttemptsConjugator::new(current_tx_info.failures) + ) + } + } + None => { + format!( + "Processing... {}", + AttemptsConjugator::new(current_tx_info.failures) + ) + } + }, + None => "None".to_string(), }, ] } @@ -47,6 +64,26 @@ pub(in crate::commands::financials_command) mod restricted { } } + pub(super) struct AttemptsConjugator { + failures: usize, + } + + impl AttemptsConjugator { + pub fn new(failures: usize) -> Self { + Self { failures } + } + } + + impl Display for AttemptsConjugator { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.failures == 1 { + write!(f, "1 failed attempt") + } else { + write!(f, "{} failed attempts", self.failures) + } + } + } + pub fn financial_status_totals_title(stdout: &mut dyn Write, is_gwei: bool) { short_writeln!( stdout, @@ -250,7 +287,7 @@ pub(in crate::commands::financials_command) mod restricted { #[cfg(test)] mod tests { use crate::commands::financials_command::pretty_print_utils::restricted::{ - figure_out_max_widths, StringValuesFormattableAccount, + figure_out_max_widths, AttemptsConjugator, StringValuesFormattableAccount, }; #[derive(Clone)] @@ -311,4 +348,20 @@ mod tests { //the second number is always 42 as the length of wallet address assert_eq!(result, vec![3, 42, 5, 10]) } + + #[test] + fn failures_conjugator_works_for_singular_failure() { + let subject = AttemptsConjugator::new(1); + assert_eq!(subject.to_string(), "1 failed attempt") + } + + #[test] + fn failures_conjugator_works_for_plural_failure() { + let failures = vec![2, 5, 10]; + + failures.iter().for_each(|failure| { + let subject = AttemptsConjugator::new(*failure); + assert_eq!(subject.to_string(), format!("{} failed attempts", failure)) + }) + } } diff --git a/masq_lib/src/messages.rs b/masq_lib/src/messages.rs index 29ee0e629..521d7b997 100644 --- a/masq_lib/src/messages.rs +++ b/masq_lib/src/messages.rs @@ -679,8 +679,15 @@ pub struct UiPayableAccount { pub age_s: u64, #[serde(rename = "balanceGwei")] pub balance_gwei: u64, - #[serde(rename = "pendingPayableHashOpt")] - pub pending_payable_hash_opt: Option, + #[serde(rename = "txProcessingInfoOpt")] + pub tx_processing_info_opt: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct TxProcessingInfo { + #[serde(rename = "pendingTxHashOpt")] + pub pending_tx_hash_opt: Option, + pub failures: usize, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index f9a723f54..bbdbbef99 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -5,7 +5,8 @@ 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, RowId, TopStmConfig, VigilantRusqliteFlatten, + CustomQuery, DaoFactoryReal, PayableAccountWithTxInfo, RangeStmConfig, RowId, TopStmConfig, + VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; use crate::accountant::db_big_integer::big_int_db_processor::{ @@ -13,13 +14,12 @@ use crate::accountant::db_big_integer::big_int_db_processor::{ ParamByUse, SQLParamsBuilder, TableNameDAO, WeiChange, WeiChangeDirection, }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, join_with_commas, sign_conversion, PendingPayableId}; +use crate::accountant::{checked_conversion, join_with_commas, sign_conversion}; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; use ethabi::Address; -#[cfg(test)] -use ethereum_types::{BigEndianHash, H256, U256}; use itertools::Either; +use masq_lib::messages::TxProcessingInfo; use masq_lib::utils::ExpectValue; #[cfg(test)] use rusqlite::OptionalExtension; @@ -39,7 +39,6 @@ pub struct PayableAccount { pub wallet: Wallet, pub balance_wei: u128, pub last_paid_timestamp: SystemTime, - pub pending_payable_opt: Option, } impl From<&FailedTx> for PayableAccount { @@ -48,7 +47,6 @@ impl From<&FailedTx> for PayableAccount { wallet: Wallet::from(failed_tx.receiver_address), balance_wei: failed_tx.amount_minor, last_paid_timestamp: from_unix_timestamp(failed_tx.timestamp), - pending_payable_opt: None, } } } @@ -63,7 +61,7 @@ impl Display for PayableRetrieveCondition { match self { PayableRetrieveCondition::ByAddresses(addresses) => write!( f, - "AND wallet_address IN ({})", + "WHERE wallet_address IN ({})", join_with_commas(addresses, |hash| format!("'{:?}'", hash)) ), } @@ -78,11 +76,6 @@ pub trait PayableDao: Debug + Send { amount_minor: u128, ) -> Result<(), PayableDaoError>; - fn mark_pending_payables_rowids( - &self, - mark_instructions: &[MarkPendingPayableID], - ) -> Result<(), PayableDaoError>; - fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError>; fn retrieve_payables( @@ -90,7 +83,8 @@ pub trait PayableDao: Debug + Send { condition_opt: Option, ) -> Vec; - fn custom_query(&self, custom_query: CustomQuery) -> Option>; + fn custom_query(&self, custom_query: CustomQuery) + -> Option>; fn total(&self) -> u128; @@ -153,30 +147,6 @@ impl PayableDao for PayableDaoReal { Ok(()) } - fn mark_pending_payables_rowids( - &self, - _mark_instructions: &[MarkPendingPayableID], - ) -> Result<(), PayableDaoError> { - 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: &[SentTx]) -> Result<(), PayableDaoError> { confirmed_payables.iter().try_for_each(|confirmed_payable| { let main_sql = "update payable set \ @@ -207,8 +177,7 @@ impl PayableDao for PayableDaoReal { condition_opt: Option, ) -> Vec { let raw_sql = "\ - select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp from \ - payable where pending_payable_rowid is null" + select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp from payable" .to_string(); let sql = match condition_opt { None => raw_sql, @@ -233,7 +202,6 @@ impl PayableDao for PayableDaoReal { high_b, low_b, )), last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), - pending_payable_opt: None, }) } _ => panic!("Database is corrupt: PAYABLE table columns and/or types"), @@ -244,7 +212,10 @@ impl PayableDao for PayableDaoReal { .collect() } - fn custom_query(&self, custom_query: CustomQuery) -> Option> { + fn custom_query( + &self, + custom_query: CustomQuery, + ) -> Option> { let variant_top = TopStmConfig{ limit_clause: "limit :limit_count", gwei_min_resolution_clause: "where (balance_high_b > 0) or ((balance_high_b = 0) and (balance_low_b >= 1000000000))", @@ -263,7 +234,7 @@ impl PayableDao for PayableDaoReal { Self::stm_assembler_of_payable_cq, variant_top, variant_range, - Self::create_payable_account, + Self::create_payable_account_with_tx_info, ) } @@ -305,29 +276,18 @@ impl PayableDao for PayableDaoReal { let high_bytes_result = row.get(0); let low_bytes_result = row.get(1); let last_paid_timestamp_result = row.get(2); - let pending_payable_rowid_result: Result, Error> = row.get(3); match ( high_bytes_result, low_bytes_result, last_paid_timestamp_result, - pending_payable_rowid_result, ) { - (Ok(high_bytes), Ok(low_bytes), Ok(last_paid_timestamp), Ok(rowid)) => { - Ok(PayableAccount { - wallet: wallet.clone(), - balance_wei: checked_conversion::(BigIntDivider::reconstitute( - high_bytes, low_bytes, - )), - last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), - pending_payable_opt: match rowid { - Some(rowid) => Some(PendingPayableId::new( - u64::try_from(rowid).unwrap(), - H256::from_uint(&U256::from(0)), //garbage - )), - None => None, - }, - }) - } + (Ok(high_bytes), Ok(low_bytes), Ok(last_paid_timestamp)) => Ok(PayableAccount { + wallet: wallet.clone(), + balance_wei: checked_conversion::(BigIntDivider::reconstitute( + high_bytes, low_bytes, + )), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), + }), e => panic!( "Database is corrupt: PAYABLE table columns and/or types: {:?}", e @@ -347,27 +307,40 @@ impl PayableDaoReal { } } - fn create_payable_account(row: &Row) -> rusqlite::Result { + fn create_payable_account_with_tx_info( + row: &Row, + ) -> rusqlite::Result { let wallet_result: Result = row.get(0); 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 tx_hash_opt_result = row.get(4); + let previous_failures = row.get(5); match ( wallet_result, balance_high_bytes_result, balance_low_bytes_result, last_paid_timestamp_result, + tx_hash_opt_result, + previous_failures, ) { - (Ok(wallet), Ok(high_bytes), Ok(low_bytes), Ok(last_paid_timestamp)) => { - Ok(PayableAccount { + ( + Ok(wallet), + Ok(high_bytes), + Ok(low_bytes), + Ok(last_paid_timestamp), + Ok(tx_hash_opt), + Ok(previous_failures), + ) => Ok(PayableAccountWithTxInfo { + account: 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, - }) - } + }, + tx_opt: Self::maybe_construct_tx_info(tx_hash_opt, previous_failures), + }), e => panic!( "Database is corrupt: PAYABLE table columns and/or types: {:?}", e @@ -375,20 +348,63 @@ impl PayableDaoReal { } } + fn maybe_construct_tx_info( + pending_tx_hash_opt: Option, + previous_failures: usize, + ) -> Option { + if pending_tx_hash_opt.is_some() || previous_failures > 0 { + Some(TxProcessingInfo { + pending_tx_hash_opt, + failures: previous_failures, + }) + } else { + None + } + } + fn stm_assembler_of_payable_cq(feeder: AssemblerFeeder) -> String { format!( - "select - wallet_address, - balance_high_b, - balance_low_b, - last_paid_timestamp - from - payable - {} {} - order by - {}, - {} - {}", + "SELECT + p.wallet_address, + p.balance_high_b, + p.balance_low_b, + p.last_paid_timestamp, + CASE WHEN s.status LIKE '%Pending%' + THEN s.tx_hash + ELSE NULL + END AS pending_tx_hash, + /* + The following case stm counts up the failing attempts in the tx processing while it + is still ongoing. + */ + CASE WHEN EXISTS ( + SELECT 1 + FROM failed_payable + WHERE status NOT LIKE '%Concluded%' + AND receiver_address = p.wallet_address + ) + THEN ( + WITH nonces_of_failures_by_wallet AS + ( + SELECT nonce + FROM failed_payable + WHERE receiver_address = p.wallet_address + ) + SELECT COUNT(*) + FROM nonces_of_failures_by_wallet + WHERE nonce = (SELECT MAX(nonce) FROM nonces_of_failures_by_wallet) + ) + ELSE 0 + END AS recent_failures_count + FROM + payable p + LEFT JOIN + sent_payable s on p.wallet_address = s.receiver_address + {} {} + ORDER BY + {}, + {} + {}", feeder.main_where_clause, feeder.where_clause_extension, feeder.order_by_first_param, @@ -404,162 +420,17 @@ impl TableNameDAO for PayableDaoReal { } } -// 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::failed_payable_dao::{ + FailedPayableDao, FailedPayableDaoReal, FailureStatus, + }; 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::sent_payable_dao::{ + SentPayableDao, SentPayableDaoReal, SentTx, TxStatus, + }; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; use crate::accountant::db_access_objects::utils::{ current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, TxHash, }; @@ -568,6 +439,7 @@ mod tests { assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, trick_rusqlite_with_read_only_conn, }; + use crate::blockchain::errors::validation_status::ValidationStatus; use crate::blockchain::test_utils::make_tx_hash; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, @@ -576,6 +448,7 @@ mod tests { use crate::test_utils::make_wallet; use itertools::Itertools; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; + use masq_lib::messages::TxProcessingInfo; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::ToSql; use rusqlite::{Connection, OpenFlags}; @@ -723,203 +596,6 @@ mod tests { ) } - #[test] - 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 - // 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.")] - fn mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified( - ) { - // 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() { - // 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() { - // 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")] - fn mark_pending_payables_rowids_is_strict_about_empty_input() { - // 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 { account_1: TxWalletAndTimestamp, account_2: TxWalletAndTimestamp, @@ -971,8 +647,6 @@ mod tests { &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, ); let mut sent_tx = make_sent_tx((idx as u32 + 1) * 1234); sent_tx.hash = test_inputs.hash; @@ -1055,18 +729,15 @@ mod tests { 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: wallet_1.clone(), balance_wei: initial_amount_1, last_paid_timestamp: expected_last_paid_timestamp_1, - pending_payable_opt: None, }; let expected_status_before_2 = PayableAccount { wallet: wallet_2.clone(), balance_wei: initial_amount_2, last_paid_timestamp: expected_last_paid_timestamp_2, - pending_payable_opt: None, }; let expected_resulting_status_1 = PayableAccount { wallet: wallet_1.clone(), @@ -1074,7 +745,6 @@ mod tests { last_paid_timestamp: from_unix_timestamp( setup_holder.account_1.pending_payable.timestamp, ), - pending_payable_opt: None, }; let expected_resulting_status_2 = PayableAccount { wallet: wallet_2.clone(), @@ -1082,7 +752,6 @@ mod tests { 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)); @@ -1199,10 +868,10 @@ mod tests { } #[test] - fn retrieve_payables_should_return_payables_with_no_pending_transaction() { + fn retrieve_payables_should_return_every_payable() { let home_dir = ensure_node_home_directory_exists( "payable_dao", - "retrieve_payables_should_return_payables_with_no_pending_transaction", + "retrieve_payables_should_return_every_payable", ); let subject = PayableDaoReal::new( DbInitializerReal::default() @@ -1213,36 +882,32 @@ mod tests { flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE); let conn = Connection::open_with_flags(&home_dir.join(DATABASE_FILE), flags).unwrap(); let conn = ConnectionWrapperReal::new(conn); - let insert = |wallet: &str, pending_payable_rowid: Option| { - insert_payable_record_fn( - &conn, - wallet, - 1234567890123456, - 111_111_111, - pending_payable_rowid, - ); + let insert = |wallet: &str, num: i128| { + insert_payable_record_fn(&conn, wallet, num * 111_111_111, num as i64 * 222_222_222); }; - insert("0x0000000000000000000000000000000000666f6f", Some(15)); - insert(&make_wallet("foobar").to_string(), None); - insert("0x0000000000000000000000000000000000626172", Some(16)); - insert(&make_wallet("barfoo").to_string(), None); + insert("0x0000000000000000000000000000000000666f6f", 1); + insert(&make_wallet("foobar").to_string(), 2); + insert("0x0000000000000000000000000000000000626172", 3); let result = subject.retrieve_payables(None); assert_eq!( result, vec![ + PayableAccount { + wallet: Wallet::new("0x0000000000000000000000000000000000666f6f"), + balance_wei: 111_111_111, + last_paid_timestamp: from_unix_timestamp(222_222_222), + }, PayableAccount { wallet: make_wallet("foobar"), - balance_wei: 1234567890123456 as u128, - last_paid_timestamp: from_unix_timestamp(111_111_111), - pending_payable_opt: None + balance_wei: 222_222_222, + last_paid_timestamp: from_unix_timestamp(444_444_444), }, PayableAccount { - wallet: make_wallet("barfoo"), - balance_wei: 1234567890123456 as u128, - last_paid_timestamp: from_unix_timestamp(111_111_111), - pending_payable_opt: None + wallet: Wallet::new("0x0000000000000000000000000000000000626172"), + balance_wei: 333_333_333, + last_paid_timestamp: from_unix_timestamp(666_666_666), }, ] ); @@ -1263,21 +928,15 @@ mod tests { flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE); let conn = Connection::open_with_flags(&home_dir.join(DATABASE_FILE), flags).unwrap(); let conn = ConnectionWrapperReal::new(conn); - let insert = |wallet: &str, pending_payable_rowid: Option| { - insert_payable_record_fn( - &conn, - wallet, - 1234567890123456, - 111_111_111, - pending_payable_rowid, - ); + let insert = |wallet: &str| { + insert_payable_record_fn(&conn, wallet, 1234567890123456, 111_111_111); }; let wallet1 = make_wallet("foobar"); let wallet2 = make_wallet("barfoo"); - insert("0x0000000000000000000000000000000000666f6f", Some(15)); - insert(&wallet1.to_string(), None); - insert("0x0000000000000000000000000000000000626172", None); - insert(&wallet2.to_string(), None); + insert("0x0000000000000000000000000000000000666f6f"); + insert(&wallet1.to_string()); + insert("0x0000000000000000000000000000000000626172"); + insert(&wallet2.to_string()); let set = BTreeSet::from([wallet1.address(), wallet2.address()]); let result = subject.retrieve_payables(Some(ByAddresses(set))); @@ -1289,13 +948,11 @@ mod tests { wallet: wallet2, balance_wei: 1234567890123456 as u128, last_paid_timestamp: from_unix_timestamp(111_111_111), - pending_payable_opt: None }, PayableAccount { wallet: wallet1, balance_wei: 1234567890123456 as u128, last_paid_timestamp: from_unix_timestamp(111_111_111), - pending_payable_opt: None }, ] ); @@ -1303,7 +960,10 @@ mod tests { #[test] fn custom_query_handles_empty_table_in_top_records_mode() { - let main_test_setup = |_conn: &dyn ConnectionWrapper, _insert: InsertPayableHelperFn| {}; + let main_test_setup = + |payable_dao_real: PayableDaoReal, _: SentPayableDaoReal, _: FailedPayableDaoReal| { + payable_dao_real + }; let subject = custom_query_test_body_for_payable( "custom_query_handles_empty_table_in_top_records_mode", main_test_setup, @@ -1317,26 +977,16 @@ mod tests { assert_eq!(result, None) } - type InsertPayableHelperFn<'b> = - &'b dyn for<'a> Fn(&'a dyn ConnectionWrapper, &'a str, i128, i64, Option); - fn insert_payable_record_fn( conn: &dyn ConnectionWrapper, wallet: &str, balance: i128, timestamp: i64, - pending_payable_rowid: Option, ) { let (high_bytes, low_bytes) = BigIntDivider::deconstruct(balance); - let params: &[&dyn ToSql] = &[ - &wallet, - &high_bytes, - &low_bytes, - ×tamp, - &pending_payable_rowid, - ]; + let params: &[&dyn ToSql] = &[&wallet, &high_bytes, &low_bytes, ×tamp]; conn - .prepare("insert into payable (wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid) values (?, ?, ?, ?, ?)") + .prepare("insert into payable (wallet_address, balance_high_b, balance_low_b, last_paid_timestamp) values (?, ?, ?, ?)") .unwrap() .execute(params) .unwrap(); @@ -1344,51 +994,57 @@ mod tests { fn accounts_for_tests_of_top_records( now: i64, - ) -> Box { - Box::new(move |conn, insert: InsertPayableHelperFn| { - insert( - conn, - "0x1111111111111111111111111111111111111111", - 1_000_000_002, - now - 86_401, - None, - ); - insert( - conn, - "0x2222222222222222222222222222222222222222", - 7_562_000_300_000, - now - 86_001, - None, - ); - insert( - conn, - "0x3333333333333333333333333333333333333333", - 999_999_999, //balance smaller than 1 gwei - now - 86_000, - None, - ); - insert( - conn, - "0x4444444444444444444444444444444444444444", - 10_000_000_100, - now - 86_300, - None, - ); - insert( - conn, - "0x5555555555555555555555555555555555555555", - 10_000_000_100, - now - 86_401, - Some(1), - ); - }) + ) -> Box PayableDaoReal> + { + Box::new( + move |payable_dao_real: PayableDaoReal, + _: SentPayableDaoReal, + _: FailedPayableDaoReal| { + let insert_payable = |unix_time: i64, wallet_addr: &str, amount_minor: u128| { + payable_dao_real + .more_money_payable( + from_unix_timestamp(unix_time), + &Wallet::new(wallet_addr), + amount_minor, + ) + .unwrap() + }; + insert_payable( + now - 86_401, + "0x1111111111111111111111111111111111111111", + 1_000_000_002, + ); + insert_payable( + now - 86_001, + "0x2222222222222222222222222222222222222222", + 7_562_000_300_000, + ); + insert_payable( + now - 86_000, + "0x3333333333333333333333333333333333333333", + 999_999_999, //balance smaller than 1 gwei + ); + insert_payable( + now - 86_300, + "0x4444444444444444444444444444444444444444", + 10_000_000_100, + ); + insert_payable( + now - 86_401, + "0x5555555555555555555555555555555555555555", + 10_000_000_100, + ); + + payable_dao_real + }, + ) } #[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 the debt age but not the balance which allows to check double - // ordering, primarily by balance and then age. + // Two accounts differ only in the debt age but not the balance, which allows checking + // 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( @@ -1406,23 +1062,29 @@ mod tests { assert_eq!( result, vec![ - PayableAccount { - wallet: Wallet::new("0x2222222222222222222222222222222222222222"), - balance_wei: 7_562_000_300_000, - last_paid_timestamp: from_unix_timestamp(now - 86_001), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x2222222222222222222222222222222222222222"), + balance_wei: 7_562_000_300_000, + last_paid_timestamp: from_unix_timestamp(now - 86_001), + }, + tx_opt: None }, - PayableAccount { - wallet: Wallet::new("0x5555555555555555555555555555555555555555"), - balance_wei: 10_000_000_100, - last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x5555555555555555555555555555555555555555"), + balance_wei: 10_000_000_100, + last_paid_timestamp: from_unix_timestamp(now - 86_401), + }, + tx_opt: None }, - PayableAccount { - wallet: Wallet::new("0x4444444444444444444444444444444444444444"), - balance_wei: 10_000_000_100, - last_paid_timestamp: from_unix_timestamp(now - 86_300), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x4444444444444444444444444444444444444444"), + balance_wei: 10_000_000_100, + last_paid_timestamp: from_unix_timestamp(now - 86_300), + }, + tx_opt: None }, ] ); @@ -1431,8 +1093,8 @@ 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 the debt age but not the balance which allows to check double - // ordering, primarily by balance and then age. + // Two accounts differ only in the debt age but not the balance, which allows checking + // 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( @@ -1450,31 +1112,313 @@ mod tests { assert_eq!( result, vec![ - PayableAccount { - wallet: Wallet::new("0x5555555555555555555555555555555555555555"), - balance_wei: 10_000_000_100, - last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x5555555555555555555555555555555555555555"), + balance_wei: 10_000_000_100, + last_paid_timestamp: from_unix_timestamp(now - 86_401), + }, + tx_opt: None }, - PayableAccount { - wallet: Wallet::new("0x1111111111111111111111111111111111111111"), - balance_wei: 1_000_000_002, - last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x1111111111111111111111111111111111111111"), + balance_wei: 1_000_000_002, + last_paid_timestamp: from_unix_timestamp(now - 86_401), + }, + tx_opt: None }, - PayableAccount { - wallet: Wallet::new("0x4444444444444444444444444444444444444444"), - balance_wei: 10_000_000_100, - last_paid_timestamp: from_unix_timestamp(now - 86_300), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x4444444444444444444444444444444444444444"), + balance_wei: 10_000_000_100, + last_paid_timestamp: from_unix_timestamp(now - 86_300), + }, + tx_opt: None + } + ] + ); + } + + #[test] + fn custom_query_top_records_mode_can_report_tx_info() { + let now = current_unix_timestamp(); + let wallet_addr = "0x1111111111111111111111111111111111111111"; + let mut sent_tx_1 = make_sent_tx(789); + sent_tx_1.receiver_address = Wallet::new(wallet_addr).address(); + let sent_tx_hash_1 = sent_tx_1.hash; + let wallet_addr = "0x3333333333333333333333333333333333333333"; + let mut sent_tx_2 = make_sent_tx(345); + sent_tx_2.receiver_address = Wallet::new(wallet_addr).address(); + let sent_tx_hash_2 = sent_tx_2.hash; + let mut failed_tx_1 = make_failed_tx(123); + let wallet_addr = "0x2222222222222222222222222222222222222222"; + failed_tx_1.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_1.status = FailureStatus::Concluded; + failed_tx_1.nonce = 99; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_2.nonce = 99; + failed_tx_2.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + // Will be ignored as it is not of the highest nonce for this wallet + let mut failed_tx_3 = make_failed_tx(678); + failed_tx_3.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_3.nonce = 98; + failed_tx_3.status = FailureStatus::Concluded; + let mut failed_tx_4 = make_failed_tx(567); + let wallet_addr = "0x3333333333333333333333333333333333333333"; + failed_tx_4.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_4.status = FailureStatus::RetryRequired; + failed_tx_4.nonce = 100; + let main_test_setup = Box::new( + move |payable_dao_real: PayableDaoReal, + sent_payable_dao_real: SentPayableDaoReal, + failed_payable_dao_real: FailedPayableDaoReal| { + let insert_payable = |unix_time: i64, wallet_addr: &str, amount_minor: u128| { + payable_dao_real + .more_money_payable( + from_unix_timestamp(unix_time), + &Wallet::new(wallet_addr), + amount_minor, + ) + .unwrap() + }; + insert_payable( + now - 80_000, + "0x1111111111111111111111111111111111111111", + 222_000_000_000, + ); + insert_payable( + now - 80_000, + "0x2222222222222222222222222222222222222222", + 333_000_000_000, + ); + insert_payable( + now - 80_000, + "0x3333333333333333333333333333333333333333", + 111_000_000_000, + ); + insert_payable( + now - 80_000, + "0x4444444444444444444444444444444444444444", + 1_000_000_000, + ); + failed_payable_dao_real + .insert_new_records(&btreeset![ + failed_tx_1, + failed_tx_2, + failed_tx_3, + failed_tx_4 + ]) + .unwrap(); + sent_payable_dao_real + .insert_new_records(&btreeset![sent_tx_1, sent_tx_2]) + .unwrap(); + payable_dao_real + }, + ); + let subject = custom_query_test_body_for_payable( + "custom_query_top_records_mode_can_report_tx_info", + main_test_setup, + ); + + let result = subject + .custom_query(CustomQuery::TopRecords { + count: 3, + ordered_by: Balance, + }) + .unwrap(); + + assert_eq!( + result, + vec![ + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x2222222222222222222222222222222222222222"), + balance_wei: 333_000_000_000, + last_paid_timestamp: from_unix_timestamp(now - 80_000), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: None, + failures: 2 + }) + }, + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x1111111111111111111111111111111111111111"), + balance_wei: 222_000_000_000, + last_paid_timestamp: from_unix_timestamp(now - 80_000), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: Some(format!("{:?}", sent_tx_hash_1)), + failures: 0 + }) + }, + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x3333333333333333333333333333333333333333"), + balance_wei: 111_000_000_000, + last_paid_timestamp: from_unix_timestamp(now - 80_000), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: Some(format!("{:?}", sent_tx_hash_2)), + failures: 1 + }) + } + ] + ); + } + + #[test] + fn custom_query_range_mode_can_report_tx_info() { + let now = current_unix_timestamp(); + let wallet_addr = "0x2222222222222222222222222222222222222222"; + let mut sent_tx_1 = make_sent_tx(789); + sent_tx_1.receiver_address = Wallet::new(wallet_addr).address(); + let sent_tx_hash_1 = sent_tx_1.hash; + let wallet_addr = "0x4444444444444444444444444444444444444444"; + let mut sent_tx_2 = make_sent_tx(345); + sent_tx_2.receiver_address = Wallet::new(wallet_addr).address(); + let sent_tx_hash_2 = sent_tx_2.hash; + let mut failed_tx_1 = make_failed_tx(123); + let wallet_addr = "0x3333333333333333333333333333333333333333"; + failed_tx_1.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_1.nonce = 99; + let wallet_addr = "0x2222222222222222222222222222222222222222"; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_2.nonce = 100; + let mut failed_tx_3 = make_failed_tx(567); + failed_tx_3.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_3.nonce = 100; + let mut failed_tx_4 = make_failed_tx(222); + let wallet_addr = "0x5555555555555555555555555555555555555555"; + failed_tx_4.receiver_address = Wallet::new(wallet_addr).address(); + failed_tx_4.nonce = 98; + failed_tx_4.status = FailureStatus::Concluded; + let main_test_setup = Box::new( + move |payable_dao_real: PayableDaoReal, + sent_payable_dao_real: SentPayableDaoReal, + failed_payable_dao_real: FailedPayableDaoReal| { + let insert_payable = |unix_time: i64, wallet_addr: &str, amount_minor: u128| { + payable_dao_real + .more_money_payable( + from_unix_timestamp(unix_time), + &Wallet::new(wallet_addr), + amount_minor, + ) + .unwrap() + }; + insert_payable( + now - 80_000, + "0x1111111111111111111111111111111111111111", + 5_000_000_000, + ); + insert_payable( + now - 80_000, + "0x2222222222222222222222222222222222222222", + 4_000_000_000, + ); + insert_payable( + now - 80_000, + "0x3333333333333333333333333333333333333333", + 3_000_000_000, + ); + insert_payable( + now - 80_000, + "0x4444444444444444444444444444444444444444", + 2_000_000_000, + ); + insert_payable( + now - 80_000, + "0x5555555555555555555555555555555555555555", + 1_000_000_000, + ); + failed_payable_dao_real + .insert_new_records(&btreeset![ + failed_tx_1, + failed_tx_2, + failed_tx_3, + failed_tx_4 + ]) + .unwrap(); + sent_payable_dao_real + .insert_new_records(&btreeset![sent_tx_1, sent_tx_2]) + .unwrap(); + payable_dao_real + }, + ); + let subject = custom_query_test_body_for_payable( + "custom_query_range_mode_can_report_tx_info", + main_test_setup, + ); + + let result = subject + .custom_query(CustomQuery::RangeQuery { + max_age_s: 80_100, + min_age_s: 79_900, + max_amount_gwei: 4, + min_amount_gwei: 1, + timestamp: from_unix_timestamp(now), + }) + .unwrap(); + + assert_eq!( + result, + vec![ + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x2222222222222222222222222222222222222222"), + balance_wei: 4_000_000_000, + last_paid_timestamp: from_unix_timestamp(now - 80_000), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: Some(format!("{:?}", sent_tx_hash_1)), + failures: 2 + }) }, + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x3333333333333333333333333333333333333333"), + balance_wei: 3_000_000_000, + last_paid_timestamp: from_unix_timestamp(now - 80_000), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: None, + failures: 1 + }) + }, + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x4444444444444444444444444444444444444444"), + balance_wei: 2_000_000_000, + last_paid_timestamp: from_unix_timestamp(now - 80_000), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: Some(format!("{:?}", sent_tx_hash_2)), + failures: 0 + }) + }, + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x5555555555555555555555555555555555555555"), + balance_wei: 1_000_000_000, + last_paid_timestamp: from_unix_timestamp(now - 80_000), + }, + // No TxProcessingInfo despite existing FailedTx records (The record has the failure + // status = 'Concluded') + tx_opt: None + } ] ); } #[test] fn custom_query_handles_empty_table_in_range_mode() { - let main_test_setup = |_conn: &dyn ConnectionWrapper, _insert: InsertPayableHelperFn| {}; + let main_test_setup = + |payable_dao: PayableDaoReal, _: SentPayableDaoReal, _: FailedPayableDaoReal| { + payable_dao + }; let subject = custom_query_test_body_for_payable( "custom_query_handles_empty_table_in_range_mode", main_test_setup, @@ -1496,56 +1440,66 @@ mod tests { // Two accounts differ only in the debt age but not the balance which allows to check double // ordering, primarily by balance and then age. let now = current_unix_timestamp(); - let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { - insert( - conn, - "0x1111111111111111111111111111111111111111", - gwei_to_wei::<_, u64>(499_999_999), //too small + let main_setup = |payable_dao: PayableDaoReal, + sent_payable_dao: SentPayableDaoReal, + _: FailedPayableDaoReal| { + let insert_payable_record = |time: i64, wallet_addr: &str, amount: u64| { + payable_dao + .more_money_payable( + from_unix_timestamp(time), + &Wallet::new(wallet_addr), + gwei_to_wei::<_, u64>(amount), //too small + ) + .unwrap(); + }; + insert_payable_record( now - 70_000, - None, + "0x1111111111111111111111111111111111111111", + 499_999_999, //too small ); - insert( - conn, - "0x2222222222222222222222222222222222222222", - gwei_to_wei::<_, u64>(1_800_456_000), + insert_payable_record( now - 55_120, - Some(1), + "0x2222222222222222222222222222222222222222", + 1_800_456_000, ); - insert( - conn, - "0x3333333333333333333333333333333333333333", - gwei_to_wei::<_, u64>(600_123_456), + insert_payable_record( now - 200_001, //too old - None, + "0x3333333333333333333333333333333333333333", + 600_123_456, ); - insert( - conn, - "0x4444444444444444444444444444444444444444", - gwei_to_wei::<_, u64>(1_033_456_000_u64), + insert_payable_record( now - 19_999, //too young - None, + "0x4444444444444444444444444444444444444444", + 1_033_456_000, ); - insert( - conn, - "0x5555555555555555555555555555555555555555", - gwei_to_wei::<_, u64>(35_000_000_001), //too big + insert_payable_record( now - 30_786, - None, + "0x5555555555555555555555555555555555555555", + 35_000_000_001, //too big ); - insert( - conn, - "0x6666666666666666666666666666666666666666", - gwei_to_wei::<_, u64>(1_800_456_000u64), + insert_payable_record( now - 100_401, - None, + "0x6666666666666666666666666666666666666666", + 1_800_456_000, ); - insert( - conn, - "0x7777777777777777777777777777777777777777", - gwei_to_wei::<_, u64>(2_500_647_000u64), + insert_payable_record( now - 80_333, - None, + "0x7777777777777777777777777777777777777777", + 2_500_647_000, ); + sent_payable_dao + .insert_new_records(&btreeset![SentTx { + hash: make_tx_hash(0xABC), + receiver_address: Wallet::new("0x6666666666666666666666666666666666666666") + .address(), + amount_minor: 0, + timestamp: 0, + gas_price_minor: 0, + nonce: 0, + status: TxStatus::Pending(ValidationStatus::Waiting), + }]) + .unwrap(); + payable_dao }; let subject = custom_query_test_body_for_payable("custom_query_in_range_mode", main_setup); @@ -1562,24 +1516,33 @@ mod tests { assert_eq!( result, vec![ - PayableAccount { - wallet: Wallet::new("0x7777777777777777777777777777777777777777"), - balance_wei: gwei_to_wei(2_500_647_000_u32), - last_paid_timestamp: from_unix_timestamp(now - 80_333), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x7777777777777777777777777777777777777777"), + balance_wei: gwei_to_wei(2_500_647_000_u32), + last_paid_timestamp: from_unix_timestamp(now - 80_333), + }, + tx_opt: None }, - PayableAccount { - wallet: Wallet::new("0x6666666666666666666666666666666666666666"), - balance_wei: gwei_to_wei(1_800_456_000_u32), - last_paid_timestamp: from_unix_timestamp(now - 100_401), - pending_payable_opt: None + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x6666666666666666666666666666666666666666"), + balance_wei: gwei_to_wei(1_800_456_000_u32), + last_paid_timestamp: from_unix_timestamp(now - 100_401), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: Some(format!("{:?}", make_tx_hash(0xABC))), + failures: 0 + }) + }, + PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x2222222222222222222222222222222222222222"), + balance_wei: gwei_to_wei(1_800_456_000_u32), + last_paid_timestamp: from_unix_timestamp(now - 55_120), + }, + tx_opt: None }, - PayableAccount { - 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: None - } ] ); } @@ -1587,24 +1550,26 @@ mod tests { #[test] fn range_query_does_not_display_values_from_below_1_gwei() { let now = current_unix_timestamp(); - let timestamp_1 = now - 11_001; - let timestamp_2 = now - 5000; - let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { - insert( - conn, - "0x1111111111111111111111111111111111111111", - 400_005_601, - timestamp_1, - None, - ); - insert( - conn, - "0x2222222222222222222222222222222222222222", - 30_000_300_000, - timestamp_2, - None, - ); - }; + let timestamp_1 = from_unix_timestamp(now - 11_001); + let timestamp_2 = from_unix_timestamp(now - 5000); + let main_setup = + |payable_dao: PayableDaoReal, _: SentPayableDaoReal, _: FailedPayableDaoReal| { + payable_dao + .more_money_payable( + timestamp_1, + &Wallet::new("0x1111111111111111111111111111111111111111"), + 400_005_601, + ) + .unwrap(); + payable_dao + .more_money_payable( + timestamp_2, + &Wallet::new("0x2222222222222222222222222222222222222222"), + 30_000_300_000, + ) + .unwrap(); + payable_dao + }; let subject = custom_query_test_body_for_payable( "range_query_does_not_display_values_from_below_1_gwei", main_setup, @@ -1622,12 +1587,14 @@ mod tests { assert_eq!( result, - vec![PayableAccount { - wallet: Wallet::new("0x2222222222222222222222222222222222222222"), - balance_wei: 30_000_300_000, - last_paid_timestamp: from_unix_timestamp(timestamp_2), - pending_payable_opt: None - },] + vec![PayableAccountWithTxInfo { + account: PayableAccount { + wallet: Wallet::new("0x2222222222222222222222222222222222222222"), + balance_wei: 30_000_300_000, + last_paid_timestamp: timestamp_2, + }, + tx_opt: None + }] ) } @@ -1643,28 +1610,24 @@ mod tests { "0x1111111111111111111111111111111111111111", 999_999_999, timestamp - 1000, - None, ); insert_payable_record_fn( &*conn, "0x2222222222222222222222222222222222222222", 1_000_123_123, timestamp - 2000, - None, ); insert_payable_record_fn( &*conn, "0x3333333333333333333333333333333333333333", 1_000_000_000, timestamp - 3000, - None, ); insert_payable_record_fn( &*conn, "0x4444444444444444444444444444444444444444", 1_000_000_001, timestamp - 4000, - Some(3), ); let subject = PayableDaoReal::new(conn); @@ -1688,14 +1651,12 @@ mod tests { "0x1111111111111111111111111111111111111111", 123_456, 111_111_111, - None, ); insert_payable_record_fn( &*conn, "0x2222222222222222222222222222222222222222", -999_999, 222_222_222, - None, ); let subject = PayableDaoReal::new(conn); @@ -1722,7 +1683,7 @@ mod tests { )] fn create_payable_account_panics_on_database_error() { assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types( - PayableDaoReal::create_payable_account, + PayableDaoReal::create_payable_account_with_tx_info, ); } @@ -1737,14 +1698,72 @@ mod tests { fn custom_query_test_body_for_payable(test_name: &str, main_setup_fn: F) -> PayableDaoReal where - F: Fn(&dyn ConnectionWrapper, InsertPayableHelperFn), + F: FnOnce(PayableDaoReal, SentPayableDaoReal, FailedPayableDaoReal) -> PayableDaoReal, { let home_dir = ensure_node_home_directory_exists("payable_dao", test_name); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - main_setup_fn(conn.as_ref(), &insert_payable_record_fn); - PayableDaoReal::new(conn) + let conn = || { + DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap() + }; + let failed_payable_dao = FailedPayableDaoReal::new(conn()); + let sent_payable_dao = SentPayableDaoReal::new(conn()); + let payable_dao = PayableDaoReal::new(conn()); + main_setup_fn(payable_dao, sent_payable_dao, failed_payable_dao) + } + + #[test] + fn maybe_construct_tx_info_with_tx_hash_present_and_no_errors() { + let tx_hash = make_tx_hash(123); + + let result = PayableDaoReal::maybe_construct_tx_info(Some(format!("{:?}", tx_hash)), 0); + + assert_eq!( + result, + Some(TxProcessingInfo { + pending_tx_hash_opt: Some(format!("{:?}", tx_hash)), + failures: 0 + }) + ); + } + + #[test] + fn maybe_construct_tx_info_with_tx_hash_present_and_also_errors() { + let tx_hash = make_tx_hash(123); + let errors = 3; + + let result = + PayableDaoReal::maybe_construct_tx_info(Some(format!("{:?}", tx_hash)), errors); + + assert_eq!( + result, + Some(TxProcessingInfo { + pending_tx_hash_opt: Some(format!("{:?}", make_tx_hash(123))), + failures: errors + }) + ); + } + + #[test] + fn maybe_construct_tx_info_with_only_errors_present() { + let errors = 1; + + let result = PayableDaoReal::maybe_construct_tx_info(None, errors); + + assert_eq!( + result, + Some(TxProcessingInfo { + pending_tx_hash_opt: None, + failures: errors + }) + ); + } + + #[test] + fn maybe_construct_tx_info_returns_none() { + let result = PayableDaoReal::maybe_construct_tx_info(None, 0); + + assert_eq!(result, None); } #[test] @@ -1754,7 +1773,7 @@ mod tests { assert_eq!( PayableRetrieveCondition::ByAddresses(BTreeSet::from([address_1, address_2])) .to_string(), - "AND wallet_address IN ('0x0000000000000000000000000000006669727374', '0x00000000000000000000000000007365636f6e64')" + "WHERE wallet_address IN ('0x0000000000000000000000000000006669727374', '0x00000000000000000000000000007365636f6e64')" ); } } diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 98c14ac3e..faa9d8656 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -14,7 +14,8 @@ use crate::sub_lib::accountant::PaymentThresholds; use ethereum_types::H256; use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::messages::{ - RangeQuery, TopRecordsConfig, TopRecordsOrdering, UiPayableAccount, UiReceivableAccount, + RangeQuery, TopRecordsConfig, TopRecordsOrdering, TxProcessingInfo, UiPayableAccount, + UiReceivableAccount, }; use rusqlite::{Row, Statement, ToSql}; use std::collections::HashMap; @@ -303,27 +304,34 @@ impl From<&RangeQuery> for CustomQuery { } } -pub fn remap_payable_accounts(accounts: Vec) -> Vec { +#[derive(Debug, PartialEq, Eq)] +pub struct PayableAccountWithTxInfo { + pub account: PayableAccount, + pub tx_opt: Option, +} + +pub fn remap_payable_accounts(accounts: Vec) -> Vec { accounts .into_iter() - .map(|account| UiPayableAccount { - wallet: account.wallet.to_string(), - age_s: to_age(account.last_paid_timestamp), - balance_gwei: { - let gwei = (account.balance_wei / (WEIS_IN_GWEI as u128)) as u64; - if gwei > 0 { - gwei - } else { - panic!( - "Broken code: PayableAccount with less than 1 gwei passed through db query \ - constraints; wallet: {}, balance: {}", - account.wallet, account.balance_wei - ) - } - }, - pending_payable_hash_opt: account - .pending_payable_opt - .map(|full_id| full_id.hash.to_string()), + .map(|account_with_tx_info| { + let account = account_with_tx_info.account; + UiPayableAccount { + wallet: account.wallet.to_string(), + age_s: to_age(account.last_paid_timestamp), + balance_gwei: { + let gwei = (account.balance_wei / (WEIS_IN_GWEI as u128)) as u64; + if gwei > 0 { + gwei + } else { + panic!( + "Broken code: PayableAccount with less than 1 gwei passed through \ + db query constraints; wallet: {}, balance: {}", + account.wallet, account.balance_wei + ) + } + }, + tx_processing_info_opt: account_with_tx_info.tx_opt, + } }) .collect() } @@ -334,14 +342,23 @@ pub fn remap_receivable_accounts(accounts: Vec) -> Vec(payment_thresholds.maturity_threshold_sec) + 1, ), - pending_payable_opt: None, }, // above minimum balance, to the right of minimum time (not in buffer zone, below the curve) PayableAccount { @@ -3973,7 +3970,6 @@ mod tests { now - checked_conversion::(payment_thresholds.threshold_interval_sec) + 1, ), - pending_payable_opt: None, }, ]; let payable_dao = PayableDaoMock::new() @@ -4043,7 +4039,6 @@ mod tests { + 10, ), ), - pending_payable_opt: None, }, // Slightly above the curve (balance intersection), to the right of minimum time PayableAccount { @@ -4054,7 +4049,6 @@ mod tests { DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 10, ), ), - pending_payable_opt: None, }, ]; let payable_dao = @@ -6253,11 +6247,13 @@ mod tests { //that part is in the responsibility of the database manager, answering the specific SQL query let payable_custom_query_params_arc = Arc::new(Mutex::new(vec![])); let receivable_custom_query_params_arc = Arc::new(Mutex::new(vec![])); - let payable_accounts_retrieved = vec![PayableAccount { - wallet: make_wallet("abcd123"), - balance_wei: 58_568_686_005, - last_paid_timestamp: SystemTime::now().sub(Duration::from_secs(5000)), - pending_payable_opt: None, + let payable_accounts_retrieved = vec![PayableAccountWithTxInfo { + account: PayableAccount { + wallet: make_wallet("abcd123"), + balance_wei: 58_568_686_005, + last_paid_timestamp: SystemTime::now().sub(Duration::from_secs(5000)), + }, + tx_opt: None, }]; let payable_dao = PayableDaoMock::new() .custom_query_params(&payable_custom_query_params_arc) @@ -6303,7 +6299,7 @@ mod tests { wallet: make_wallet("abcd123").to_string(), age_s: extracted_payable_ages[0], balance_gwei: 58, - pending_payable_hash_opt: None + tx_processing_info_opt: None, },]), receivable_opt: Some(vec![UiReceivableAccount { wallet: make_wallet("efe4848").to_string(), @@ -6402,11 +6398,16 @@ mod tests { fn compute_financials_processes_request_with_range_queries_only() { let payable_custom_query_params_arc = Arc::new(Mutex::new(vec![])); let receivable_custom_query_params_arc = Arc::new(Mutex::new(vec![])); - let payable_accounts_retrieved = vec![PayableAccount { - wallet: make_wallet("abcd123"), - balance_wei: 5_686_860_056, - last_paid_timestamp: SystemTime::now().sub(Duration::from_secs(7580)), - pending_payable_opt: None, + let payable_accounts_retrieved = vec![PayableAccountWithTxInfo { + account: PayableAccount { + wallet: make_wallet("abcd123"), + balance_wei: 5_686_860_056, + last_paid_timestamp: SystemTime::now().sub(Duration::from_secs(7580)), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: None, + failures: 2, + }), }]; let payable_dao = PayableDaoMock::new() .custom_query_params(&payable_custom_query_params_arc) @@ -6469,7 +6470,10 @@ mod tests { wallet: make_wallet("abcd123").to_string(), age_s: extracted_payable_ages[0], balance_gwei: 5, - pending_payable_hash_opt: None + tx_processing_info_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: None, + failures: 2 + }), },]), receivable_opt: Some(vec![ UiReceivableAccount { @@ -6661,11 +6665,16 @@ mod tests { )] fn compute_financials_blows_up_on_screwed_sql_query_for_payables_returning_balance_smaller_than_one_gwei( ) { - let payable_accounts_retrieved = vec![PayableAccount { - wallet: make_wallet("abcd123"), - balance_wei: 8_686_005, - last_paid_timestamp: SystemTime::now().sub(Duration::from_secs(5000)), - pending_payable_opt: None, + let payable_accounts_retrieved = vec![PayableAccountWithTxInfo { + account: PayableAccount { + wallet: make_wallet("abcd123"), + balance_wei: 8_686_005, + last_paid_timestamp: SystemTime::now().sub(Duration::from_secs(5000)), + }, + tx_opt: Some(TxProcessingInfo { + pending_tx_hash_opt: None, + failures: 3, + }), }]; let payable_dao = PayableDaoMock::new().custom_query_result(Some(payable_accounts_retrieved)); diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index b82d2c46e..34dc69189 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -710,7 +710,6 @@ mod tests { wallet: make_wallet("hi"), balance_wei: balance, last_paid_timestamp, - pending_payable_opt: None, }; let custom_payment_thresholds = PaymentThresholds { maturity_threshold_sec: 1111, @@ -775,7 +774,6 @@ mod tests { 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) @@ -804,7 +802,6 @@ mod tests { 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) @@ -838,7 +835,6 @@ mod tests { 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) 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 aceb532b0..fba4a347e 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 @@ -79,7 +79,6 @@ mod tests { wallet: wallet.clone(), balance_wei, last_paid_timestamp: SystemTime::now(), - pending_payable_opt: None, }; let new_tx_template = NewTxTemplate::from(&payable_account); @@ -174,13 +173,11 @@ mod tests { wallet: wallet1.clone(), balance_wei: 1000, last_paid_timestamp: SystemTime::now(), - pending_payable_opt: None, }, PayableAccount { wallet: wallet2.clone(), balance_wei: 2000, last_paid_timestamp: SystemTime::now(), - pending_payable_opt: None, }, ]; diff --git a/node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs b/node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs index ca8dfa870..af657a633 100644 --- a/node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/tx_templates/mod.rs @@ -37,7 +37,6 @@ mod tests { wallet: wallet.clone(), balance_wei, last_paid_timestamp: SystemTime::now(), - pending_payable_opt: None, }; let base_tx_template = BaseTxTemplate::from(&payable_account); diff --git a/node/src/accountant/scanners/payable_scanner/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index 3ace3b8b6..f518bd134 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -285,27 +285,25 @@ mod tests { wallet: make_wallet("wallet0"), balance_wei: same_amount_significance, last_paid_timestamp: from_unix_timestamp(now_t - 5000), - pending_payable_opt: None, }, - //this debt is more significant because beside being high in amount it's also older, so should be prioritized and picked + // This debt is more significant because besides being high in amount, it's also older, + // so should be prioritized and picked PayableAccount { wallet: make_wallet("wallet1"), balance_wei: same_amount_significance, last_paid_timestamp: from_unix_timestamp(now_t - 10000), - pending_payable_opt: None, }, - //similarly these two wallets have debts equally old but the second has a bigger balance and should be chosen + // Similarly, these two wallets have debts equally old, but the second has a bigger + // balance and should be chosen PayableAccount { wallet: make_wallet("wallet3"), balance_wei: 100, last_paid_timestamp: same_age_significance, - pending_payable_opt: None, }, PayableAccount { wallet: make_wallet("wallet2"), balance_wei: 330, last_paid_timestamp: same_age_significance, - pending_payable_opt: None, }, ]; @@ -367,7 +365,6 @@ mod tests { + payment_thresholds.threshold_interval_sec, ), ), - pending_payable_opt: None, }, 10_000_000_001_152_000_u128, ), @@ -380,7 +377,6 @@ mod tests { payment_thresholds.maturity_threshold_sec + 55, ), ), - pending_payable_opt: None, }, 999_978_993_055_555_580, ), diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 8d6fb49ed..dce47d2ec 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -8,8 +8,7 @@ use crate::accountant::db_access_objects::failed_payable_dao::{ FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::payable_dao::{ - MarkPendingPayableID, PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, - PayableRetrieveCondition, + PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, PayableRetrieveCondition, }; use crate::accountant::db_access_objects::receivable_dao::{ @@ -19,7 +18,8 @@ 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, + from_unix_timestamp, to_unix_timestamp, CustomQuery, PayableAccountWithTxInfo, TxHash, + TxIdentifiers, }; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; use crate::accountant::scanners::payable_scanner::msgs::PricedTemplatesMessage; @@ -90,7 +90,6 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( wallet, balance_wei: balance, last_paid_timestamp: timestamp_opt.unwrap_or(SystemTime::now()), - pending_payable_opt: None, } } @@ -612,7 +611,7 @@ pub struct PayableDaoMock { transactions_confirmed_params: Arc>>>, transactions_confirmed_results: RefCell>>, custom_query_params: Arc>>>, - custom_query_result: RefCell>>>, + custom_query_result: RefCell>>>, total_results: RefCell>, } @@ -631,25 +630,6 @@ impl PayableDao for PayableDaoMock { self.more_money_payable_results.borrow_mut().remove(0) } - fn mark_pending_payables_rowids( - &self, - _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() @@ -669,7 +649,10 @@ impl PayableDao for PayableDaoMock { self.retrieve_payables_results.borrow_mut().remove(0) } - fn custom_query(&self, custom_query: CustomQuery) -> Option> { + fn custom_query( + &self, + custom_query: CustomQuery, + ) -> Option> { self.custom_query_params.lock().unwrap().push(custom_query); self.custom_query_result.borrow_mut().remove(0) } @@ -747,7 +730,7 @@ impl PayableDaoMock { self } - pub fn custom_query_result(self, result: Option>) -> Self { + pub fn custom_query_result(self, result: Option>) -> Self { self.custom_query_result.borrow_mut().push(result); self } @@ -1458,7 +1441,6 @@ pub fn make_qualified_and_unqualified_payables( last_paid_timestamp: from_unix_timestamp( to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 + 1, ), - pending_payable_opt: None, }]; let qualified_payable_accounts = vec![ PayableAccount { @@ -1469,7 +1451,6 @@ pub fn make_qualified_and_unqualified_payables( last_paid_timestamp: from_unix_timestamp( to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 1, ), - pending_payable_opt: None, }, PayableAccount { wallet: make_wallet("wallet3"), @@ -1479,7 +1460,6 @@ pub fn make_qualified_and_unqualified_payables( last_paid_timestamp: from_unix_timestamp( to_unix_timestamp(now) - payment_thresholds.maturity_threshold_sec as i64 - 100, ), - pending_payable_opt: None, }, ]; diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 183f58659..9eed6535f 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -733,7 +733,6 @@ mod tests { last_paid_timestamp: SystemTime::now() .checked_sub(Duration::from_secs(1000)) .unwrap(), - pending_payable_opt: None, }, PayableAccount { wallet: wallet_2.clone(), @@ -741,7 +740,6 @@ mod tests { last_paid_timestamp: SystemTime::now() .checked_sub(Duration::from_secs(500)) .unwrap(), - pending_payable_opt: None, }, ]; let mut subject = BlockchainBridge::new( @@ -901,7 +899,6 @@ mod tests { wallet: wallet_account, balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), - pending_payable_opt: None, }; let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default() @@ -994,7 +991,6 @@ mod tests { wallet: account_wallet.clone(), balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), - pending_payable_opt: None, }; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let agent = BlockchainAgentMock::default() diff --git a/node/tests/financials_test.rs b/node/tests/financials_test.rs index 7aff319d2..d285092a2 100644 --- a/node/tests/financials_test.rs +++ b/node/tests/financials_test.rs @@ -100,7 +100,7 @@ fn financials_command_retrieves_payable_and_receivable_records_integration() { let receivable = query_results.receivable_opt.unwrap(); assert_eq!(payable[0].wallet, wallet_payable.to_string()); assert_eq!(payable[0].balance_gwei, 45678357_u64); - assert_eq!(payable[0].pending_payable_hash_opt, None); + assert_eq!(payable[0].tx_processing_info_opt, None); let act_period = after.duration_since(before).unwrap().as_secs(); let age_payable = payable[0].age_s; assert!(age_payable >= 678 && age_payable <= (age_payable + act_period));