From 24e0a8aa1b996cddecf55507c65da14d59e02e3e Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:57:58 +0200 Subject: [PATCH 01/48] GH-598-json-hotfix (#699) * GH-598-json-hotfix: corrected * GH-598-json-hotfix: little fix * GH-598-json-hotfix: rearrangement * GH-598-json-hotfix: addressing the review * GH-598-json-hotfix: finished addressing review --------- Co-authored-by: Bert --- .../db_access_objects/failed_payable_dao.rs | 33 +- .../db_access_objects/sent_payable_dao.rs | 18 +- node/src/blockchain/errors/mod.rs | 8 +- node/src/blockchain/errors/rpc_errors.rs | 80 ++-- .../blockchain/errors/validation_status.rs | 197 +++++++++- node/src/test_utils/mod.rs | 1 + node/src/test_utils/serde_serializer_mock.rs | 348 ++++++++++++++++++ 7 files changed, 613 insertions(+), 72 deletions(-) create mode 100644 node/src/test_utils/serde_serializer_mock.rs diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 296bfe8d2..3202807b3 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -382,7 +382,7 @@ mod tests { make_read_only_db_connection, FailedTxBuilder, }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ PreviousAttempts, ValidationFailureClockReal, }; @@ -591,8 +591,8 @@ mod tests { fn failure_reason_from_str_works() { // Submission error assert_eq!( - FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder"}}}"#).unwrap(), - FailureReason::Submission(AppRpcErrorKind::Decoder) + FailureReason::from_str(r#"{"Submission":{"Local":"Decoder"}}"#).unwrap(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Decoder)) ); // Reverted @@ -640,8 +640,8 @@ mod tests { ); assert_eq!( - FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"ServerUnreachable":{"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}}}}"#).unwrap(), - FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), &validation_failure_clock))) + FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":[{"error":{"AppRpc":{"Remote":"Unreachable"}},"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}]}}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &validation_failure_clock))) ); assert_eq!( @@ -652,9 +652,8 @@ mod tests { // Invalid Variant assert_eq!( FailureStatus::from_str("\"UnknownStatus\"").unwrap_err(), - "unknown variant `UnknownStatus`, \ - expected one of `RetryRequired`, `RecheckRequired`, `Concluded` \ - at line 1 column 15 in '\"UnknownStatus\"'" + "unknown variant `UnknownStatus`, expected one of `RetryRequired`, `RecheckRequired`, \ + `Concluded` at line 1 column 15 in '\"UnknownStatus\"'" ); // Invalid Input @@ -724,7 +723,9 @@ mod tests { .reason(PendingTooLong) .status(RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -775,13 +776,19 @@ mod tests { subject .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) .unwrap(); + let timestamp = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp) + .now_result(timestamp); let hashmap = HashMap::from([ (tx1.hash, Concluded), ( tx2.hash, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), - &ValidationFailureClockReal::default(), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &clock, ))), ), (tx3.hash, Concluded), @@ -797,8 +804,8 @@ mod tests { assert_eq!( updated_txs[1].status, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), - &ValidationFailureClockReal::default() + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &clock ))) ); assert_eq!(tx3.status, RetryRequired); diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs index a79e8ffbd..09e293edf 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -442,7 +442,7 @@ mod tests { use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; @@ -458,11 +458,15 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ) .add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -694,7 +698,9 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -1189,8 +1195,8 @@ mod tests { ); assert_eq!( - TxStatus::from_str(r#"{"Pending":{"Reattempting":{"InvalidResponse":{"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}}}}"#).unwrap(), - TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::InvalidResponse), &validation_failure_clock))) + TxStatus::from_str(r#"{"Pending":{"Reattempting":[{"error":{"AppRpc":{"Remote":"InvalidResponse"}},"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}]}}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &validation_failure_clock))) ); assert_eq!( diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index ab18ae9df..5cd1a6f3c 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -23,19 +23,19 @@ pub enum BlockchainErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::internal_errors::InternalErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; use crate::blockchain::errors::BlockchainErrorKind; #[test] fn blockchain_error_serialization_deserialization() { vec![ ( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), - "{\"AppRpc\":\"Decoder\"}", + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + r#"{"AppRpc":{"Local":"Decoder"}}"#, ), ( BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), - "{\"Internal\":\"PendingTooLongNotReplaced\"}", + r#"{"Internal":"PendingTooLongNotReplaced"}"#, ), ] .into_iter() diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index 4d8482e17..e717fbf25 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -14,7 +14,7 @@ pub enum AppRpcError { pub enum LocalError { Decoder(String), Internal, - Io(String), + IO(String), Signing(String), Transport(String), } @@ -33,7 +33,7 @@ impl From for AppRpcError { // Local Errors Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), Web3Error::Internal => AppRpcError::Local(LocalError::Internal), - Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), + Web3Error::Io(error) => AppRpcError::Local(LocalError::IO(error.to_string())), Web3Error::Signing(error) => { // This variant cannot be tested due to import limitations. AppRpcError::Local(LocalError::Signing(error.to_string())) @@ -53,18 +53,25 @@ impl From for AppRpcError { } } -#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AppRpcErrorKind { - // Local + Local(LocalErrorKind), + Remote(RemoteErrorKind), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum LocalErrorKind { Decoder, Internal, IO, Signing, Transport, +} - // Remote +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RemoteErrorKind { InvalidResponse, - ServerUnreachable, + Unreachable, Web3RpcError(i64), // Keep only the stable error code } @@ -72,16 +79,18 @@ impl From<&AppRpcError> for AppRpcErrorKind { fn from(err: &AppRpcError) -> Self { match err { AppRpcError::Local(local) => match local { - LocalError::Decoder(_) => Self::Decoder, - LocalError::Internal => Self::Internal, - LocalError::Io(_) => Self::IO, - LocalError::Signing(_) => Self::Signing, - LocalError::Transport(_) => Self::Transport, + LocalError::Decoder(_) => Self::Local(LocalErrorKind::Decoder), + LocalError::Internal => Self::Local(LocalErrorKind::Internal), + LocalError::IO(_) => Self::Local(LocalErrorKind::IO), + LocalError::Signing(_) => Self::Local(LocalErrorKind::Signing), + LocalError::Transport(_) => Self::Local(LocalErrorKind::Transport), }, AppRpcError::Remote(remote) => match remote { - RemoteError::InvalidResponse(_) => Self::InvalidResponse, - RemoteError::Unreachable => Self::ServerUnreachable, - RemoteError::Web3RpcError { code, .. } => Self::Web3RpcError(*code), + RemoteError::InvalidResponse(_) => Self::Remote(RemoteErrorKind::InvalidResponse), + RemoteError::Unreachable => Self::Remote(RemoteErrorKind::Unreachable), + RemoteError::Web3RpcError { code, .. } => { + Self::Remote(RemoteErrorKind::Web3RpcError(*code)) + } }, } } @@ -90,7 +99,7 @@ impl From<&AppRpcError> for AppRpcErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::rpc_errors::{ - AppRpcError, AppRpcErrorKind, LocalError, RemoteError, + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, RemoteErrorKind, }; use web3::error::Error as Web3Error; @@ -110,7 +119,7 @@ mod tests { std::io::ErrorKind::Other, "IO error" ))), - AppRpcError::Local(LocalError::Io("IO error".to_string())) + AppRpcError::Local(LocalError::IO("IO error".to_string())) ); assert_eq!( AppRpcError::from(Web3Error::Transport("Transport error".to_string())), @@ -145,60 +154,58 @@ mod tests { AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Decoder( "Decoder error".to_string() ))), - AppRpcErrorKind::Decoder + AppRpcErrorKind::Local(LocalErrorKind::Decoder) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Internal)), - AppRpcErrorKind::Internal + AppRpcErrorKind::Local(LocalErrorKind::Internal) ); assert_eq!( - AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Io("IO error".to_string()))), - AppRpcErrorKind::IO + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::IO("IO error".to_string()))), + AppRpcErrorKind::Local(LocalErrorKind::IO) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Signing( "Signing error".to_string() ))), - AppRpcErrorKind::Signing + AppRpcErrorKind::Local(LocalErrorKind::Signing) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Transport( "Transport error".to_string() ))), - AppRpcErrorKind::Transport + AppRpcErrorKind::Local(LocalErrorKind::Transport) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::InvalidResponse( "Invalid response".to_string() ))), - AppRpcErrorKind::InvalidResponse + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Unreachable)), - AppRpcErrorKind::ServerUnreachable + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Web3RpcError { code: 55, message: "Booga".to_string() })), - AppRpcErrorKind::Web3RpcError(55) + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(55)) ); } #[test] fn app_rpc_error_kind_serialization_deserialization() { let errors = vec![ - // Local Errors - AppRpcErrorKind::Decoder, - AppRpcErrorKind::Internal, - AppRpcErrorKind::IO, - AppRpcErrorKind::Signing, - AppRpcErrorKind::Transport, - // Remote Errors - AppRpcErrorKind::InvalidResponse, - AppRpcErrorKind::ServerUnreachable, - AppRpcErrorKind::Web3RpcError(42), + AppRpcErrorKind::Local(LocalErrorKind::Decoder), + AppRpcErrorKind::Local(LocalErrorKind::Internal), + AppRpcErrorKind::Local(LocalErrorKind::IO), + AppRpcErrorKind::Local(LocalErrorKind::Signing), + AppRpcErrorKind::Local(LocalErrorKind::Transport), + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse), + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable), + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(42)), ]; errors.into_iter().for_each(|error| { @@ -206,8 +213,7 @@ mod tests { let deserialized: AppRpcErrorKind = serde_json::from_str(&serialized).unwrap(); assert_eq!( error, deserialized, - "Failed serde attempt for {:?} that should look \ - like {:?}", + "Failed serde attempt for {:?} that should look like {:?}", deserialized, error ); }); diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 72ca28346..34cb2c5e3 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -1,9 +1,14 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; use crate::blockchain::errors::BlockchainErrorKind; +use serde::de::{SeqAccess, Visitor}; +use serde::ser::SerializeSeq; +use serde::{ + Deserialize as ManualDeserialize, Deserializer, Serialize as ManualSerialize, Serializer, +}; use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt::Formatter; use std::time::SystemTime; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -12,12 +17,74 @@ pub enum ValidationStatus { Reattempting(PreviousAttempts), } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PreviousAttempts { - #[serde(flatten)] inner: HashMap, } +// had to implement it manually in an array JSON layout, as the original, default HashMap +// serialization threw errors because the values of keys were represented by nested enums that +// serde doesn't translate into a complex JSON value (unlike the plain string required for a key) +impl ManualSerialize for PreviousAttempts { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct Entry<'a> { + #[serde(rename = "error")] + error_kind: &'a BlockchainErrorKind, + #[serde(flatten)] + stats: &'a ErrorStats, + } + + let mut seq = serializer.serialize_seq(Some(self.inner.len()))?; + for (error_kind, stats) in self.inner.iter() { + seq.serialize_element(&Entry { error_kind, stats })?; + } + seq.end() + } +} + +impl<'de> ManualDeserialize<'de> for PreviousAttempts { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(PreviousAttemptsVisitor) + } +} + +struct PreviousAttemptsVisitor; + +impl<'de> Visitor<'de> for PreviousAttemptsVisitor { + type Value = PreviousAttempts; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("PreviousAttempts") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + #[derive(Deserialize)] + struct EntryOwned { + #[serde(rename = "error")] + error_kind: BlockchainErrorKind, + #[serde(flatten)] + stats: ErrorStats, + } + + let mut error_stats_map: HashMap = hashmap!(); + while let Some(entry) = seq.next_element::()? { + error_stats_map.insert(entry.error_kind, entry.stats); + } + Ok(PreviousAttempts { + inner: error_stats_map, + }) + } +} + impl PreviousAttempts { pub fn new(error: BlockchainErrorKind, clock: &dyn ValidationFailureClock) -> Self { Self { @@ -75,6 +142,11 @@ impl ValidationFailureClock for ValidationFailureClockReal { mod tests { use super::*; use crate::blockchain::errors::internal_errors::InternalErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; + use crate::blockchain::test_utils::ValidationFailureClockMock; + use crate::test_utils::serde_serializer_mock::{SerdeSerializerMock, SerializeSeqMock}; + use serde::ser::Error as SerdeError; + use std::time::{Duration, UNIX_EPOCH}; #[test] fn previous_attempts_and_validation_failure_clock_work_together_fine() { @@ -82,7 +154,7 @@ mod tests { // new() let timestamp_a = SystemTime::now(); let subject = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); // add_attempt() @@ -93,22 +165,24 @@ mod tests { ); let timestamp_c = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), &validation_failure_clock, ); let timestamp_d = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), &validation_failure_clock, ); let decoder_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Decoder, + ))) .unwrap(); assert!( timestamp_a <= decoder_error_stats.first_seen @@ -136,7 +210,9 @@ mod tests { assert_eq!(internal_error_stats.attempts, 1); let io_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::IO, + ))) .unwrap(); assert!( timestamp_c <= io_error_stats.first_seen && io_error_stats.first_seen <= timestamp_d, @@ -146,9 +222,106 @@ mod tests { io_error_stats.first_seen ); assert_eq!(io_error_stats.attempts, 2); - let other_error_stats = subject - .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Signing)); + let other_error_stats = + subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Signing, + ))); assert_eq!(other_error_stats, None); } + + #[test] + fn previous_attempts_custom_serialize_seq_happy_path() { + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = serde_json::to_string(&PreviousAttempts::new(err, &clock)).unwrap(); + + assert_eq!( + result, + r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":{"secs_since_epoch":1234567890,"nanos_since_epoch":0},"attempts":1}]"# + ); + } + + #[test] + fn previous_attempts_custom_serialize_seq_initialization_err() { + let mock = SerdeSerializerMock::default() + .serialize_seq_result(Err(serde_json::Error::custom("lethally acid bobbles"))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "lethally acid bobbles"); + } + + #[test] + fn previous_attempts_custom_serialize_seq_element_err() { + let mock = SerdeSerializerMock::default() + .serialize_seq_result(Ok(SerializeSeqMock::default().serialize_element_result( + Err(serde_json::Error::custom("jelly gummies gone off")), + ))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "jelly gummies gone off"); + } + + #[test] + fn previous_attempts_custom_serialize_end_err() { + let mock = + SerdeSerializerMock::default().serialize_seq_result(Ok(SerializeSeqMock::default() + .serialize_element_result(Ok(())) + .end_result(Err(serde_json::Error::custom("funny belly ache"))))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "funny belly ache"); + } + + #[test] + fn previous_attempts_custom_deserialize_happy_path() { + let str = r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":{"secs_since_epoch":1234567890,"nanos_since_epoch":0},"attempts":1}]"#; + + let result = serde_json::from_str::(str); + + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + assert_eq!( + result.unwrap().inner, + hashmap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) + ); + } + + #[test] + fn previous_attempts_custom_deserialize_sad_path() { + let str = + r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":"Yesterday","attempts":1}]"#; + + let result = serde_json::from_str::(str); + + assert_eq!( + result.unwrap_err().to_string(), + "invalid type: string \"Yesterday\", expected struct SystemTime at line 1 column 79" + ); + } } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index b36199b75..588eb87e6 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -14,6 +14,7 @@ pub mod persistent_configuration_mock; pub mod recorder; pub mod recorder_counter_msgs; pub mod recorder_stop_conditions; +pub mod serde_serializer_mock; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; pub mod tokio_wrapper_mocks; diff --git a/node/src/test_utils/serde_serializer_mock.rs b/node/src/test_utils/serde_serializer_mock.rs new file mode 100644 index 000000000..7130cd0c0 --- /dev/null +++ b/node/src/test_utils/serde_serializer_mock.rs @@ -0,0 +1,348 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use serde::ser::{ + SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, + SerializeTupleStruct, SerializeTupleVariant, +}; +use serde::{Serialize, Serializer}; +use serde_json::Error; +use std::cell::RefCell; + +#[derive(Default)] +pub struct SerdeSerializerMock { + serialize_seq_results: RefCell>>, +} + +impl Serializer for SerdeSerializerMock { + type Ok = (); + type Error = Error; + type SerializeSeq = SerializeSeqMock; + type SerializeTuple = SerializeTupleMock; + type SerializeTupleStruct = SerializeTupleStructMock; + type SerializeTupleVariant = SerializeTupleVariantMock; + type SerializeMap = SerializeMapMock; + type SerializeStruct = SerializeStructMock; + type SerializeStructVariant = SerializeStructVariantMock; + + fn serialize_bool(self, _v: bool) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i8(self, _v: i8) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i16(self, _v: i16) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i32(self, _v: i32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i64(self, _v: i64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u8(self, _v: u8) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u16(self, _v: u16) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u32(self, _v: u32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u64(self, _v: u64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_f32(self, _v: f32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_f64(self, _v: f64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_char(self, _v: char) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_str(self, _v: &str) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_none(self) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_some(self, _value: &T) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_unit(self) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_seq(self, _len: Option) -> Result { + self.serialize_seq_results.borrow_mut().remove(0) + } + + fn serialize_tuple(self, _len: usize) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_map(self, _len: Option) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } +} + +impl SerdeSerializerMock { + pub fn serialize_seq_result(self, serializer: Result) -> Self { + self.serialize_seq_results.borrow_mut().push(serializer); + self + } +} + +#[derive(Default)] +pub struct SerializeSeqMock { + serialize_element_results: RefCell>>, + end_results: RefCell>>, +} + +impl SerializeSeq for SerializeSeqMock { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + self.serialize_element_results.borrow_mut().remove(0) + } + + fn end(self) -> Result { + self.end_results.borrow_mut().remove(0) + } +} + +impl SerializeSeqMock { + pub fn serialize_element_result(self, result: Result<(), Error>) -> Self { + self.serialize_element_results.borrow_mut().push(result); + self + } + + pub fn end_result(self, result: Result<(), Error>) -> Self { + self.end_results.borrow_mut().push(result); + self + } +} + +pub struct SerializeTupleMock {} + +impl SerializeTuple for SerializeTupleMock { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeTupleStructMock {} + +impl SerializeTupleStruct for SerializeTupleStructMock { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeTupleVariantMock {} + +impl SerializeTupleVariant for SerializeTupleVariantMock { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeMapMock {} + +impl SerializeMap for SerializeMapMock { + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, _key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_value(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeStructMock {} + +impl SerializeStruct for SerializeStructMock { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeStructVariantMock {} + +impl SerializeStructVariant for SerializeStructVariantMock { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} From ca6cb36a6ee16bd772ff1be0c0920db92354a51f Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:15:27 +0200 Subject: [PATCH 02/48] GH-642: Redesigning PendingPayableScanner (#677) * GH-642: interim commit * GH-642: interim commit * GH-642: interim commit * GH-642: big initial messy reconstruction continuing... * GH-642: big initial messy reconstruction... just realized I may've forgotten to update with the last changes from the other card * GH-642: tests compiling...failing lots of them * GH-642: fn confirm_transactions has been reimplemented * GH-642: fn handle_failed_transactions has been reimplemented * GH-642: fixed mainly internal, but smaller functions in the pending payable scanner; various From and Display implementations and these sorts * GH-642: progressed quite greatelly; fixed many tests; took action against the mark pending payable rowid fn * GH-642: another bunch fixed...down to 24 * GH-642: another bunch fixed...down to 10 * GH-642: the base of this card is done * GH-642: lots of fixes in names * GH-642: filling cache with failed txs to recheck at startup * GH-642: rpc failers during receipt checks can be handled now * GH-642: interim commit * GH-642: first I need to finish the impl of the db system of tx statuses...opened in its own PR and then will start from here on * GH-642: pending payable scanner machinery has been given the true skeleton * GH-642: before creating a new whole folder for scanner utils * GH-642: interim commit * GH-642: preparing tests before writing the guts of the core fns * GH-642: integration of the caches...100% at start_scan, 90% finish_scan * GH-642: another big portion of work in interpreting the receipts * GH-642: finishing tests for the receipt interpretation but I should rearrange the code a bit - maybe to add a separative class * GH-642: mod structure changed, new file for TxReceiptInterpreter * GH-642: fixed two unreliable tests * GH-642: interim commit * GH-642: worked away on the implementation of handling failed txs * GH-642: more todos!() gone * GH-642: processing failures is done; next tx confiramtions * GH-642: tx reclaim implemented * GH-642: finished the brain functions in PPS * GH-642: ValidationStatus extension - huge chunk of work; still some failing tests remain * GH-642: interim commit (some of the Validation error stuff will have to be fixed) * GH-683: savepoint * GH-683: interim commit * GH-683: mostly done * GH-683: renamed error * GH-683: additional fix to renaming * GH-683: finished * GH-642: finished * GH-683: fixed for a review * GH-683: fixed screwed string replacement * GH-683: finished fixing it * GH-683: another fix...BlockchainError * GH-642: added unreachable! * GH-642: before bigger issue addressing * GH-642: got rid of the BlockchainFailure::Unrecognized layer * GH-642: savepoint * GH-642: hashmap for receipt status result deployed * GH-642: finally solid... as much as under this card, tests fixed * GH-598-json-hotfix: interim commit * GH-642: dragging the failing tests down to bare minimum * GH-642: interim commit * GH-642: before fixing the todo!() left over * GH-642: finished * GH-642: cosmetics * GH-642: grrr - cosmetics - forgot e * GH-642: review 2 addressed * GH-642: added the cache clean-up on getting a scan error --------- Co-authored-by: Bert --- masq_lib/src/utils.rs | 122 +- .../db_access_objects/failed_payable_dao.rs | 104 +- node/src/accountant/db_access_objects/mod.rs | 2 +- .../db_access_objects/payable_dao.rs | 1014 +++---- .../db_access_objects/pending_payable_dao.rs | 1407 +++++----- .../db_access_objects/receivable_dao.rs | 6 +- ...able_and_failed_payable_data_conversion.rs | 137 + .../db_access_objects/sent_payable_dao.rs | 489 +++- .../db_access_objects/test_utils.rs | 17 +- .../src/accountant/db_access_objects/utils.rs | 4 + .../db_big_integer/big_int_db_processor.rs | 1 + node/src/accountant/mod.rs | 1172 +++++---- node/src/accountant/scanners/mod.rs | 1162 ++++---- .../scanners/pending_payable_scanner/mod.rs | 2339 +++++++++++++---- .../pending_payable_scanner/test_utils.rs | 23 + .../tx_receipt_interpreter.rs | 706 +++++ .../scanners/pending_payable_scanner/utils.rs | 1299 +++++++-- .../scanners/receivable_scanner/mod.rs | 2 +- .../accountant/scanners/scan_schedulers.rs | 36 +- .../src/accountant/scanners/scanners_utils.rs | 173 +- node/src/accountant/scanners/test_utils.rs | 94 +- node/src/accountant/test_utils.rs | 513 ++-- node/src/actor_system_factory.rs | 6 +- node/src/blockchain/blockchain_bridge.rs | 394 ++- .../lower_level_interface_web3.rs | 262 +- .../blockchain_interface_web3/mod.rs | 219 +- .../blockchain_interface_web3/utils.rs | 330 +-- .../data_structures/errors.rs | 30 +- .../data_structures/mod.rs | 99 +- .../blockchain/blockchain_interface/mod.rs | 36 +- node/src/blockchain/errors/internal_errors.rs | 2 +- node/src/blockchain/errors/mod.rs | 2 +- node/src/blockchain/errors/rpc_errors.rs | 6 +- node/src/blockchain/test_utils.rs | 49 + .../migrations/migration_4_to_5.rs | 4 +- node/src/database/rusqlite_wrappers.rs | 12 +- .../test_utils/transaction_wrapper_mock.rs | 6 +- node/src/stream_handler_pool.rs | 4 +- node/src/sub_lib/accountant.rs | 15 +- node/src/test_utils/mod.rs | 15 +- node/src/test_utils/recorder.rs | 12 +- 41 files changed, 8176 insertions(+), 4149 deletions(-) create mode 100644 node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs create mode 100644 node/src/accountant/scanners/pending_payable_scanner/test_utils.rs create mode 100644 node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs diff --git a/masq_lib/src/utils.rs b/masq_lib/src/utils.rs index 8d563ef37..9355d0624 100644 --- a/masq_lib/src/utils.rs +++ b/masq_lib/src/utils.rs @@ -463,6 +463,25 @@ macro_rules! test_only_use { } } +#[macro_export(local_inner_macros)] +macro_rules! btreemap { + () => { + ::std::collections::BTreeMap::new() + }; + ($($key:expr => $val:expr,)+) => { + btreemap!($($key => $val),+) + }; + ($($key:expr => $value:expr),+) => { + { + let mut _btm = ::std::collections::BTreeMap::new(); + $( + let _ = _btm.insert($key, $value); + )* + _btm + } + }; +} + #[macro_export(local_inner_macros)] macro_rules! hashmap { () => { @@ -482,10 +501,30 @@ macro_rules! hashmap { }; } +#[macro_export(local_inner_macros)] +macro_rules! hashset { + () => { + ::std::collections::HashSet::new() + }; + ($($val:expr,)+) => { + hashset!($($val),+) + }; + ($($value:expr),+) => { + { + let mut _hs = ::std::collections::HashSet::new(); + $( + let _ = _hs.insert($value); + )* + _hs + } + }; +} + #[cfg(test)] mod tests { use super::*; - use std::collections::HashMap; + use itertools::Itertools; + use std::collections::{BTreeMap, HashMap, HashSet}; use std::env::current_dir; use std::fmt::Write; use std::fs::{create_dir_all, File, OpenOptions}; @@ -814,7 +853,8 @@ mod tests { let hashmap_with_one_element = hashmap!(1 => 2); let hashmap_with_multiple_elements = hashmap!(1 => 2, 10 => 20, 12 => 42); let hashmap_with_trailing_comma = hashmap!(1 => 2, 10 => 20,); - let hashmap_of_string = hashmap!("key" => "val"); + let hashmap_of_string = hashmap!("key_1" => "val_a", "key_2" => "val_b"); + let hashmap_with_duplicate = hashmap!(1 => 2, 1 => 2); let expected_empty_hashmap: HashMap = HashMap::new(); let mut expected_hashmap_with_one_element = HashMap::new(); @@ -827,7 +867,10 @@ mod tests { expected_hashmap_with_trailing_comma.insert(1, 2); expected_hashmap_with_trailing_comma.insert(10, 20); let mut expected_hashmap_of_string = HashMap::new(); - expected_hashmap_of_string.insert("key", "val"); + expected_hashmap_of_string.insert("key_1", "val_a"); + expected_hashmap_of_string.insert("key_2", "val_b"); + let mut expected_hashmap_with_duplicate = HashMap::new(); + expected_hashmap_with_duplicate.insert(1, 2); assert_eq!(empty_hashmap, expected_empty_hashmap); assert_eq!(hashmap_with_one_element, expected_hashmap_with_one_element); assert_eq!( @@ -839,5 +882,78 @@ mod tests { expected_hashmap_with_trailing_comma ); assert_eq!(hashmap_of_string, expected_hashmap_of_string); + assert_eq!(hashmap_with_duplicate, expected_hashmap_with_duplicate); + } + + #[test] + fn btreemap_macro_works() { + let empty_btm: BTreeMap = btreemap!(); + let btm_with_one_element = btreemap!("ABC" => "234"); + let btm_with_multiple_elements = btreemap!("Bobble" => 2, "Hurrah" => 20, "Boom" => 42); + let btm_with_trailing_comma = btreemap!(12 => 1, 22 =>2,); + let btm_with_duplicate = btreemap!("A"=>123, "A"=>222); + + let expected_empty_btm: BTreeMap = BTreeMap::new(); + let mut expected_btm_with_one_element = BTreeMap::new(); + expected_btm_with_one_element.insert("ABC", "234"); + let mut expected_btm_with_multiple_elements = BTreeMap::new(); + expected_btm_with_multiple_elements.insert("Bobble", 2); + expected_btm_with_multiple_elements.insert("Hurrah", 20); + expected_btm_with_multiple_elements.insert("Boom", 42); + let mut expected_btm_with_trailing_comma = BTreeMap::new(); + expected_btm_with_trailing_comma.insert(12, 1); + expected_btm_with_trailing_comma.insert(22, 2); + let mut expected_btm_with_duplicate = BTreeMap::new(); + expected_btm_with_duplicate.insert("A", 222); + assert_eq!(empty_btm, expected_empty_btm); + assert_eq!(btm_with_one_element, expected_btm_with_one_element); + assert_eq!( + btm_with_multiple_elements, + expected_btm_with_multiple_elements + ); + assert_eq!( + btm_with_multiple_elements.into_iter().collect_vec(), + vec![("Bobble", 2), ("Boom", 42), ("Hurrah", 20)] + ); + assert_eq!(btm_with_trailing_comma, expected_btm_with_trailing_comma); + assert_eq!(btm_with_duplicate, expected_btm_with_duplicate); + } + + #[test] + fn hashset_macro_works() { + let empty_hashset: HashSet = hashset!(); + let hashset_with_one_element = hashset!(2); + let hashset_with_multiple_elements = hashset!(2, 20, 42); + let hashset_with_trailing_comma = hashset!(2, 20,); + let hashset_of_string = hashset!("val_a", "val_b"); + let hashset_with_duplicate = hashset!(2, 2); + + let expected_empty_hashset: HashSet = HashSet::new(); + let mut expected_hashset_with_one_element = HashSet::new(); + expected_hashset_with_one_element.insert(2); + let mut expected_hashset_with_multiple_elements = HashSet::new(); + expected_hashset_with_multiple_elements.insert(2); + expected_hashset_with_multiple_elements.insert(20); + expected_hashset_with_multiple_elements.insert(42); + let mut expected_hashset_with_trailing_comma = HashSet::new(); + expected_hashset_with_trailing_comma.insert(2); + expected_hashset_with_trailing_comma.insert(20); + let mut expected_hashset_of_string = HashSet::new(); + expected_hashset_of_string.insert("val_a"); + expected_hashset_of_string.insert("val_b"); + let mut expected_hashset_with_duplicate = HashSet::new(); + expected_hashset_with_duplicate.insert(2); + assert_eq!(empty_hashset, expected_empty_hashset); + assert_eq!(hashset_with_one_element, expected_hashset_with_one_element); + assert_eq!( + hashset_with_multiple_elements, + expected_hashset_with_multiple_elements + ); + assert_eq!( + hashset_with_trailing_comma, + expected_hashset_with_trailing_comma + ); + assert_eq!(hashset_of_string, expected_hashset_of_string); + assert_eq!(hashset_with_duplicate, expected_hashset_with_duplicate); } } diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 3202807b3..7a6e509bc 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -1,11 +1,11 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::utils::{ - DaoFactoryReal, TxHash, TxIdentifiers, VigilantRusqliteFlatten, + DaoFactoryReal, TxHash, TxIdentifiers, TxRecordWithHash, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; -use crate::blockchain::errors::validation_status::PreviousAttempts; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; use itertools::Itertools; use masq_lib::utils::ExpectValue; @@ -73,46 +73,61 @@ impl FromStr for FailureStatus { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum ValidationStatus { - Waiting, - Reattempting(PreviousAttempts), -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct FailedTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub reason: FailureReason, pub status: FailureStatus, } +impl TxRecordWithHash for FailedTx { + fn hash(&self) -> TxHash { + self.hash + } +} + +#[derive(Debug, PartialEq, Eq)] pub enum FailureRetrieveCondition { + ByTxHash(Vec), ByStatus(FailureStatus), + EveryRecheckRequiredRecord, } impl Display for FailureRetrieveCondition { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { + FailureRetrieveCondition::ByTxHash(hashes) => { + write!( + f, + "WHERE tx_hash IN ({})", + comma_joined_stringifiable(hashes, |hash| format!("'{:?}'", hash)) + ) + } FailureRetrieveCondition::ByStatus(status) => { write!(f, "WHERE status = '{}'", status) } + FailureRetrieveCondition::EveryRecheckRequiredRecord => { + write!(f, "WHERE status LIKE 'RecheckRequired%'") + } } } } pub trait FailedPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; + //TODO potentially atomically fn insert_new_records(&self, txs: &[FailedTx]) -> Result<(), FailedPayableDaoError>; fn retrieve_txs(&self, condition: Option) -> Vec; fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &HashSet) -> Result<(), FailedPayableDaoError>; } @@ -187,11 +202,11 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { status ) VALUES {}", comma_joined_stringifiable(txs, |tx| { - let amount_checked = checked_conversion::(tx.amount); - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let amount_checked = checked_conversion::(tx.amount_minor); + let gas_price_minor_checked = checked_conversion::(tx.gas_price_minor); let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); let (gas_price_wei_high_b, gas_price_wei_low_b) = - BigIntDivider::deconstruct(gas_price_wei_checked); + BigIntDivider::deconstruct(gas_price_minor_checked); format!( "('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}', '{}')", tx.hash, @@ -255,11 +270,11 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { Address::from_str(&receiver_address_str[2..]).expect("Failed to parse Address"); let amount_high_b = row.get(2).expectv("amount_high_b"); let amount_low_b = row.get(3).expectv("amount_low_b"); - let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let amount_minor = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; let timestamp = row.get(4).expectv("timestamp"); let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); - let gas_price_wei = + let gas_price_minor = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); let reason_str: String = row.get(8).expectv("reason"); @@ -272,9 +287,9 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { Ok(FailedTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, reason, status, @@ -287,7 +302,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { if status_updates.is_empty() { return Err(FailedPayableDaoError::EmptyInput); @@ -376,15 +391,16 @@ mod tests { }; use crate::accountant::db_access_objects::failed_payable_dao::{ FailedPayableDao, FailedPayableDaoError, FailedPayableDaoReal, FailureReason, - FailureRetrieveCondition, FailureStatus, ValidationStatus, + FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::test_utils::{ make_read_only_db_connection, FailedTxBuilder, }; - use crate::accountant::db_access_objects::utils::current_unix_timestamp; + use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxRecordWithHash}; + use crate::accountant::test_utils::make_failed_tx; use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ - PreviousAttempts, ValidationFailureClockReal, + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, }; use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::{make_tx_hash, ValidationFailureClockMock}; @@ -470,12 +486,12 @@ mod tests { [FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 0, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ nonce: 0, reason: PendingTooLong, status: RetryRequired }, \ FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 0, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 0, gas_price_minor: 0, \ nonce: 0, reason: PendingTooLong, status: RecheckRequired(Waiting) }]" .to_string() )) @@ -587,6 +603,29 @@ mod tests { assert_eq!(result.get(&another_present_hash), Some(&2u64)); } + #[test] + fn display_for_failure_retrieve_condition_works() { + let tx_hash_1 = make_tx_hash(123); + let tx_hash_2 = make_tx_hash(456); + assert_eq!(FailureRetrieveCondition::ByTxHash(vec![tx_hash_1, tx_hash_2]).to_string(), + "WHERE tx_hash IN ('0x000000000000000000000000000000000000000000000000000000000000007b', \ + '0x00000000000000000000000000000000000000000000000000000000000001c8')" + ); + assert_eq!( + FailureRetrieveCondition::ByStatus(RetryRequired).to_string(), + "WHERE status = '\"RetryRequired\"'" + ); + assert_eq!( + FailureRetrieveCondition::ByStatus(RecheckRequired(ValidationStatus::Waiting)) + .to_string(), + "WHERE status = '{\"RecheckRequired\":\"Waiting\"}'" + ); + assert_eq!( + FailureRetrieveCondition::EveryRecheckRequiredRecord.to_string(), + "WHERE status LIKE 'RecheckRequired%'" + ); + } + #[test] fn failure_reason_from_str_works() { // Submission error @@ -794,7 +833,7 @@ mod tests { (tx3.hash, Concluded), ]); - let result = subject.update_statuses(hashmap); + let result = subject.update_statuses(&hashmap); let updated_txs = subject.retrieve_txs(None); assert_eq!(result, Ok(())); @@ -815,6 +854,7 @@ mod tests { updated_txs[3].status, RecheckRequired(ValidationStatus::Waiting) ); + assert_eq!(updated_txs.len(), 4); } #[test] @@ -828,7 +868,7 @@ mod tests { .unwrap(); let subject = FailedPayableDaoReal::new(wrapped_conn); - let result = subject.update_statuses(HashMap::new()); + let result = subject.update_statuses(&HashMap::new()); assert_eq!(result, Err(FailedPayableDaoError::EmptyInput)); } @@ -842,7 +882,7 @@ mod tests { let wrapped_conn = make_read_only_db_connection(home_dir); let subject = FailedPayableDaoReal::new(Box::new(wrapped_conn)); - let result = subject.update_statuses(HashMap::from([(make_tx_hash(1), Concluded)])); + let result = subject.update_statuses(&HashMap::from([(make_tx_hash(1), Concluded)])); assert_eq!( result, @@ -955,4 +995,14 @@ mod tests { )) ) } + + #[test] + fn tx_record_with_hash_is_implemented_for_failed_tx() { + let failed_tx = make_failed_tx(1234); + let hash = failed_tx.hash; + + let hash_from_trait = failed_tx.hash(); + + assert_eq!(hash_from_trait, hash); + } } diff --git a/node/src/accountant/db_access_objects/mod.rs b/node/src/accountant/db_access_objects/mod.rs index ae165909a..0141e8796 100644 --- a/node/src/accountant/db_access_objects/mod.rs +++ b/node/src/accountant/db_access_objects/mod.rs @@ -3,8 +3,8 @@ pub mod banned_dao; pub mod failed_payable_dao; pub mod payable_dao; -pub mod pending_payable_dao; pub mod receivable_dao; +pub mod sent_payable_and_failed_payable_data_conversion; pub mod sent_payable_dao; mod test_utils; pub mod utils; diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index c7d438a41..0226c0c68 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -1,24 +1,24 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::{ - PendingPayableRowid, WalletAddress, -}; -use crate::accountant::db_big_integer::big_int_db_processor::{BigIntDbProcessor, BigIntDbProcessorReal, BigIntSqlConfig, DisplayableRusqliteParamPair, ParamByUse, SQLParamsBuilder, TableNameDAO, WeiChange, WeiChangeDirection}; -use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_access_objects::utils; use crate::accountant::db_access_objects::utils::{ sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, CustomQuery, DaoFactoryReal, - RangeStmConfig, TopStmConfig, VigilantRusqliteFlatten, + RangeStmConfig, RowId, TopStmConfig, TxHash, VigilantRusqliteFlatten, }; -use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::{ - compose_case_expression, execute_command, serialize_wallets, +use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; +use crate::accountant::db_big_integer::big_int_db_processor::{ + BigIntDbProcessor, BigIntDbProcessorReal, BigIntSqlConfig, DisplayableRusqliteParamPair, + ParamByUse, SQLParamsBuilder, TableNameDAO, WeiChange, WeiChangeDirection, }; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, sign_conversion, PendingPayableId}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; +use ethabi::Address; #[cfg(test)] use ethereum_types::{BigEndianHash, U256}; +use itertools::Either; use masq_lib::utils::ExpectValue; #[cfg(test)] use rusqlite::OptionalExtension; @@ -26,7 +26,6 @@ use rusqlite::{Error, Row}; use std::fmt::Debug; use std::str::FromStr; use std::time::SystemTime; -use itertools::Either; use web3::types::H256; #[derive(Debug, PartialEq, Eq)] @@ -48,18 +47,15 @@ pub trait PayableDao: Debug + Send { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError>; fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], + mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError>; - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], - ) -> Result<(), PayableDaoError>; + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError>; fn non_pending_payables(&self) -> Vec; @@ -81,6 +77,11 @@ impl PayableDaoFactory for DaoFactoryReal { } } +pub struct MarkPendingPayableID { + pub receiver_wallet: Address, + pub rowid: RowId, +} + #[derive(Debug)] pub struct PayableDaoReal { conn: Box, @@ -92,7 +93,7 @@ impl PayableDao for PayableDaoReal { &self, timestamp: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError> { let main_sql = "insert into payable (wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid) \ values (:wallet, :balance_high_b, :balance_low_b, :last_paid_timestamp, null) on conflict (wallet_address) do update set \ @@ -105,7 +106,7 @@ impl PayableDao for PayableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( @@ -123,46 +124,42 @@ impl PayableDao for PayableDaoReal { fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], + _mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError> { - if wallets_and_rowids.is_empty() { - panic!("broken code: empty input is not permit to enter this method") - } - - let case_expr = compose_case_expression(wallets_and_rowids); - let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); - //the Wallet type is secure against SQL injections - let sql = format!( - "update payable set \ - pending_payable_rowid = {} \ - where - pending_payable_rowid is null and wallet_address in ({}) - returning - pending_payable_rowid", - case_expr, wallets, - ); - execute_command(&*self.conn, wallets_and_rowids, &sql) + todo!("Will be an object of removal in GH-662") + // if wallets_and_rowids.is_empty() { + // panic!("broken code: empty input is not permit to enter this method") + // } + // + // let case_expr = compose_case_expression(wallets_and_rowids); + // let wallets = serialize_wallets(wallets_and_rowids, Some('\'')); + // //the Wallet type is secure against SQL injections + // let sql = format!( + // "update payable set \ + // pending_payable_rowid = {} \ + // where + // pending_payable_rowid is null and wallet_address in ({}) + // returning + // pending_payable_rowid", + // case_expr, wallets, + // ); + // execute_command(&*self.conn, wallets_and_rowids, &sql) } - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], - ) -> Result<(), PayableDaoError> { - confirmed_payables.iter().try_for_each(|pending_payable_fingerprint| { - + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError> { + confirmed_payables.iter().try_for_each(|confirmed_payable| { let main_sql = "update payable set \ balance_high_b = balance_high_b + :balance_high_b, balance_low_b = balance_low_b + :balance_low_b, \ - last_paid_timestamp = :last_paid, pending_payable_rowid = null where pending_payable_rowid = :rowid"; + last_paid_timestamp = :last_paid, pending_payable_rowid = null where wallet_address = :wallet"; let update_clause_with_compensated_overflow = "update payable set \ balance_high_b = :balance_high_b, balance_low_b = :balance_low_b, last_paid_timestamp = :last_paid, \ - pending_payable_rowid = null where pending_payable_rowid = :rowid"; + pending_payable_rowid = null where wallet_address = :wallet"; - let i64_rowid = checked_conversion::(pending_payable_fingerprint.rowid); - let last_paid = to_unix_timestamp(pending_payable_fingerprint.timestamp); + let wallet = format!("{:?}", confirmed_payable.receiver_address); let params = SQLParamsBuilder::default() - .key( PendingPayableRowid(&i64_rowid)) - .wei_change(WeiChange::new( "balance", pending_payable_fingerprint.amount, WeiChangeDirection::Subtraction)) - .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &last_paid))]) + .key( WalletAddress(&wallet)) + .wei_change(WeiChange::new("balance", confirmed_payable.amount_minor, WeiChangeDirection::Subtraction)) + .other_params(vec![ParamByUse::BeforeAndAfterOverflow(DisplayableRusqliteParamPair::new(":last_paid", &confirmed_payable.timestamp))]) .build(); self.big_int_db_processor.execute(Either::Left(self.conn.as_ref()), BigIntSqlConfig::new( @@ -389,175 +386,182 @@ impl TableNameDAO for PayableDaoReal { } } -mod mark_pending_payable_associated_functions { - use crate::accountant::comma_joined_stringifiable; - use crate::accountant::db_access_objects::payable_dao::PayableDaoError; - use crate::accountant::db_access_objects::utils::{ - update_rows_and_return_valid_count, VigilantRusqliteFlatten, - }; - use crate::database::rusqlite_wrappers::ConnectionWrapper; - use crate::sub_lib::wallet::Wallet; - use itertools::Itertools; - use rusqlite::Row; - use std::fmt::Display; - - pub fn execute_command( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - sql: &str, - ) -> Result<(), PayableDaoError> { - let mut stm = conn.prepare(sql).expect("Internal Error"); - let validator = validate_row_updated; - let rows_affected_res = update_rows_and_return_valid_count(&mut stm, validator); - - match rows_affected_res { - Ok(rows_affected) => match rows_affected { - num if num == wallets_and_rowids.len() => Ok(()), - num => mismatched_row_count_panic(conn, wallets_and_rowids, num), - }, - Err(errs) => { - let err_msg = format!( - "Multi-row update to mark pending payable hit these errors: {:?}", - errs - ); - Err(PayableDaoError::RusqliteError(err_msg)) - } - } - } - - pub fn compose_case_expression(wallets_and_rowids: &[(&Wallet, u64)]) -> String { - //the Wallet type is secure against SQL injections - fn when_clause((wallet, rowid): &(&Wallet, u64)) -> String { - format!("when wallet_address = '{wallet}' then {rowid}") - } - - format!( - "case {} end", - wallets_and_rowids.iter().map(when_clause).join("\n") - ) - } - - pub fn serialize_wallets( - wallets_and_rowids: &[(&Wallet, u64)], - quotes_opt: Option, - ) -> String { - wallets_and_rowids - .iter() - .map(|(wallet, _)| match quotes_opt { - Some(char) => format!("{}{}{}", char, wallet, char), - None => wallet.to_string(), - }) - .join(", ") - } - - fn validate_row_updated(row: &Row) -> Result { - row.get::>(0).map(|opt| opt.is_some()) - } - - fn mismatched_row_count_panic( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - actual_count: usize, - ) -> ! { - let serialized_wallets = serialize_wallets(wallets_and_rowids, None); - let expected_count = wallets_and_rowids.len(); - let extension = explanatory_extension(conn, wallets_and_rowids); - panic!( - "Marking pending payable rowid for wallets {serialized_wallets} affected \ - {actual_count} rows but expected {expected_count}. {extension}" - ) - } - - pub(super) fn explanatory_extension( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> String { - let resulting_pairs_collection = - query_resulting_pairs_of_wallets_and_rowids(conn, wallets_and_rowids); - let resulting_pairs_summary = if resulting_pairs_collection.is_empty() { - "".to_string() - } else { - pairs_in_pretty_string(&resulting_pairs_collection, |rowid_opt: &Option| { - match rowid_opt { - Some(rowid) => Box::new(*rowid), - None => Box::new("N/A"), - } - }) - }; - let wallets_and_non_optional_rowids = - pairs_in_pretty_string(wallets_and_rowids, |rowid: &u64| Box::new(*rowid)); - format!( - "\ - The demanded data according to {} looks different from the resulting state {}!. Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ - points to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ - probably had not managed to complete successfully before another payment was requested: preventive measures failed.\n", - wallets_and_non_optional_rowids, resulting_pairs_summary) - } - - fn query_resulting_pairs_of_wallets_and_rowids( - conn: &dyn ConnectionWrapper, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> Vec<(Wallet, Option)> { - let select_dealt_accounts = - format!( - "select wallet_address, pending_payable_rowid from payable where wallet_address in ({})", - serialize_wallets(wallets_and_rowids, Some('\'')) - ); - let row_processor = |row: &Row| { - Ok(( - row.get::(0) - .expect("database corrupt: wallet addresses found in bad format"), - row.get::>(1) - .expect("database_corrupt: rowid found in bad format"), - )) - }; - conn.prepare(&select_dealt_accounts) - .expect("select failed") - .query_map([], row_processor) - .expect("no args yet binding failed") - .vigilant_flatten() - .collect() - } - - fn pairs_in_pretty_string( - pairs: &[(W, R)], - rowid_pretty_writer: fn(&R) -> Box, - ) -> String { - comma_joined_stringifiable(pairs, |(wallet, rowid)| { - format!( - "( Wallet: {}, Rowid: {} )", - wallet, - rowid_pretty_writer(rowid) - ) - }) - } -} +// TODO Will be an object of removal in GH-662 +// mod mark_pending_payable_associated_functions { +// use crate::accountant::comma_joined_stringifiable; +// use crate::accountant::db_access_objects::payable_dao::{MarkPendingPayableID, PayableDaoError}; +// use crate::accountant::db_access_objects::utils::{ +// update_rows_and_return_valid_count, VigilantRusqliteFlatten, +// }; +// use crate::database::rusqlite_wrappers::ConnectionWrapper; +// use crate::sub_lib::wallet::Wallet; +// use itertools::Itertools; +// use rusqlite::Row; +// use std::fmt::Display; +// +// pub fn execute_command( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// sql: &str, +// ) -> Result<(), PayableDaoError> { +// let mut stm = conn.prepare(sql).expect("Internal Error"); +// let validator = validate_row_updated; +// let rows_affected_res = update_rows_and_return_valid_count(&mut stm, validator); +// +// match rows_affected_res { +// Ok(rows_affected) => match rows_affected { +// num if num == wallets_and_rowids.len() => Ok(()), +// num => mismatched_row_count_panic(conn, wallets_and_rowids, num), +// }, +// Err(errs) => { +// let err_msg = format!( +// "Multi-row update to mark pending payable hit these errors: {:?}", +// errs +// ); +// Err(PayableDaoError::RusqliteError(err_msg)) +// } +// } +// } +// +// pub fn compose_case_expression(wallets_and_rowids: &[(&Wallet, u64)]) -> String { +// //the Wallet type is secure against SQL injections +// fn when_clause((wallet, rowid): &(&Wallet, u64)) -> String { +// format!("when wallet_address = '{wallet}' then {rowid}") +// } +// +// format!( +// "case {} end", +// wallets_and_rowids.iter().map(when_clause).join("\n") +// ) +// } +// +// pub fn serialize_wallets( +// wallets_and_rowids: &[MarkPendingPayableID], +// quotes_opt: Option, +// ) -> String { +// wallets_and_rowids +// .iter() +// .map(|(wallet, _)| match quotes_opt { +// Some(char) => format!("{}{}{}", char, wallet, char), +// None => wallet.to_string(), +// }) +// .join(", ") +// } +// +// fn validate_row_updated(row: &Row) -> Result { +// row.get::>(0).map(|opt| opt.is_some()) +// } +// +// fn mismatched_row_count_panic( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// actual_count: usize, +// ) -> ! { +// let serialized_wallets = serialize_wallets(wallets_and_rowids, None); +// let expected_count = wallets_and_rowids.len(); +// let extension = explanatory_extension(conn, wallets_and_rowids); +// panic!( +// "Marking pending payable rowid for wallets {serialized_wallets} affected \ +// {actual_count} rows but expected {expected_count}. {extension}" +// ) +// } +// +// pub(super) fn explanatory_extension( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// ) -> String { +// let resulting_pairs_collection = +// query_resulting_pairs_of_wallets_and_rowids(conn, wallets_and_rowids); +// let resulting_pairs_summary = if resulting_pairs_collection.is_empty() { +// "".to_string() +// } else { +// pairs_in_pretty_string(&resulting_pairs_collection, |rowid_opt: &Option| { +// match rowid_opt { +// Some(rowid) => Box::new(*rowid), +// None => Box::new("N/A"), +// } +// }) +// }; +// let wallets_and_non_optional_rowids = +// pairs_in_pretty_string(wallets_and_rowids, |rowid: &u64| Box::new(*rowid)); +// format!( +// "\ +// The demanded data according to {} looks different from the resulting state {}!. Operation failed.\n\ +// Notes:\n\ +// a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ +// b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ +// points to figure out if you were put in danger of double payment,\n\ +// c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ +// The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ +// probably had not managed to complete successfully before another payment was requested: preventive measures failed.\n", +// wallets_and_non_optional_rowids, resulting_pairs_summary) +// } +// +// fn query_resulting_pairs_of_wallets_and_rowids( +// conn: &dyn ConnectionWrapper, +// wallets_and_rowids: &[(&Wallet, u64)], +// ) -> Vec<(Wallet, Option)> { +// let select_dealt_accounts = +// format!( +// "select wallet_address, pending_payable_rowid from payable where wallet_address in ({})", +// serialize_wallets(wallets_and_rowids, Some('\'')) +// ); +// let row_processor = |row: &Row| { +// Ok(( +// row.get::(0) +// .expect("database corrupt: wallet addresses found in bad format"), +// row.get::>(1) +// .expect("database_corrupt: rowid found in bad format"), +// )) +// }; +// conn.prepare(&select_dealt_accounts) +// .expect("select failed") +// .query_map([], row_processor) +// .expect("no args yet binding failed") +// .vigilant_flatten() +// .collect() +// } +// +// fn pairs_in_pretty_string( +// pairs: &[(W, R)], +// rowid_pretty_writer: fn(&R) -> Box, +// ) -> String { +// comma_joined_stringifiable(pairs, |(wallet, rowid)| { +// format!( +// "( Wallet: {}, Rowid: {} )", +// wallet, +// rowid_pretty_writer(rowid) +// ) +// }) +// } +// } #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::utils::{from_unix_timestamp, current_unix_timestamp, to_unix_timestamp}; + use crate::accountant::db_access_objects::sent_payable_dao::SentTx; + use crate::accountant::db_access_objects::utils::{ + current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, + }; use crate::accountant::gwei_to_wei; - use crate::accountant::db_access_objects::payable_dao::mark_pending_payable_associated_functions::explanatory_extension; - use crate::accountant::test_utils::{assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_pending_payable_fingerprint, trick_rusqlite_with_read_only_conn}; + use crate::accountant::test_utils::{ + assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_sent_tx, + trick_rusqlite_with_read_only_conn, + }; use crate::blockchain::test_utils::make_tx_hash; - use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; + use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::test_utils::make_wallet; + use itertools::Itertools; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::ToSql; use rusqlite::{Connection, OpenFlags}; - use rusqlite::{ToSql}; use std::path::Path; use std::str::FromStr; - use crate::database::test_utils::ConnectionWrapperMock; + use std::time::Duration; #[test] fn more_money_payable_works_for_new_address() { @@ -704,260 +708,271 @@ mod tests { fn mark_pending_payables_marks_pending_transactions_for_new_addresses() { //the extra unchanged record checks the safety of right count of changed rows; //experienced serious troubles in the past - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_marks_pending_transactions_for_new_addresses", - ); - let wallet_0 = make_wallet("wallet"); - let wallet_1 = make_wallet("booga"); - let pending_payable_rowid_1 = 656; - let wallet_2 = make_wallet("bagaboo"); - let pending_payable_rowid_2 = 657; - let boxed_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ - last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; - let mut stm = boxed_conn.prepare(insert).unwrap(); - let params = [ - [&wallet_0 as &dyn ToSql, &12345, &1, &45678], - [&wallet_1, &0, &i64::MAX, &150_000_000], - [&wallet_2, &3, &0, &151_000_000], - ] - .into_iter() - .flatten() - .collect::>(); - stm.execute(params.as_slice()).unwrap(); - } - let subject = PayableDaoReal::new(boxed_conn); - - subject - .mark_pending_payables_rowids(&[ - (&wallet_1, pending_payable_rowid_1), - (&wallet_2, pending_payable_rowid_2), - ]) - .unwrap(); - - let account_statuses = [&wallet_0, &wallet_1, &wallet_2] - .iter() - .map(|wallet| subject.account_status(wallet).unwrap()) - .collect::>(); - assert_eq!( - account_statuses, - vec![ - PayableAccount { - wallet: wallet_0, - balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), - last_paid_timestamp: from_unix_timestamp(45678), - pending_payable_opt: None, - }, - PayableAccount { - wallet: wallet_1, - balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), - last_paid_timestamp: from_unix_timestamp(150_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_1, - make_tx_hash(0) - )), - }, - //notice the hashes are garbage generated by a test method not knowing doing better - PayableAccount { - wallet: wallet_2, - balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), - last_paid_timestamp: from_unix_timestamp(151_000_000), - pending_payable_opt: Some(PendingPayableId::new( - pending_payable_rowid_2, - make_tx_hash(0) - )) - } - ] - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_marks_pending_transactions_for_new_addresses", + // ); + // let wallet_0 = make_wallet("wallet"); + // let wallet_1 = make_wallet("booga"); + // let pending_payable_rowid_1 = 656; + // let wallet_2 = make_wallet("bagaboo"); + // let pending_payable_rowid_2 = 657; + // let boxed_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // { + // let insert = "insert into payable (wallet_address, balance_high_b, balance_low_b, \ + // last_paid_timestamp) values (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)"; + // let mut stm = boxed_conn.prepare(insert).unwrap(); + // let params = [ + // [&wallet_0 as &dyn ToSql, &12345, &1, &45678], + // [&wallet_1, &0, &i64::MAX, &150_000_000], + // [&wallet_2, &3, &0, &151_000_000], + // ] + // .into_iter() + // .flatten() + // .collect::>(); + // stm.execute(params.as_slice()).unwrap(); + // } + // let subject = PayableDaoReal::new(boxed_conn); + // + // subject + // .mark_pending_payables_rowids(&[ + // (&wallet_1, pending_payable_rowid_1), + // (&wallet_2, pending_payable_rowid_2), + // ]) + // .unwrap(); + // + // let account_statuses = [&wallet_0, &wallet_1, &wallet_2] + // .iter() + // .map(|wallet| subject.account_status(wallet).unwrap()) + // .collect::>(); + // assert_eq!( + // account_statuses, + // vec![ + // PayableAccount { + // wallet: wallet_0, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(12345, 1)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(45678), + // pending_payable_opt: None, + // }, + // PayableAccount { + // wallet: wallet_1, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(0, i64::MAX)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(150_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_1, + // make_tx_hash(0) + // )), + // }, + // //notice the hashes are garbage generated by a test method not knowing doing better + // PayableAccount { + // wallet: wallet_2, + // balance_wei: u128::try_from(BigIntDivider::reconstitute(3, 0)).unwrap(), + // last_paid_timestamp: from_unix_timestamp(151_000_000), + // pending_payable_opt: Some(PendingPayableId::new( + // pending_payable_rowid_2, + // make_tx_hash(0) + // )) + // } + // ] + // ) } #[test] - #[should_panic(expected = "\ - Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ - 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ - The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ - ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ - points to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ - probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] + // #[should_panic(expected = "\ + // Marking pending payable rowid for wallets 0x000000000000000000000000000000626f6f6761, \ + // 0x0000000000000000000000000000007961686f6f affected 0 rows but expected 2. \ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 ), \ + // ( Wallet: 0x0000000000000000000000000000007961686f6f, Rowid: 789 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 456 )!. Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be suspected but see other\n\ + // points to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for this account\n\ + // probably had not managed to complete successfully before another payment was requested: preventive measures failed.")] fn mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified( ) { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let first_wallet = make_wallet("booga"); - let first_rowid = 456; - insert_payable_record_fn( - &*conn, - &first_wallet.to_string(), - 123456, - 789789, - Some(first_rowid), - ); - let subject = PayableDaoReal::new(conn); - - let _ = subject.mark_pending_payables_rowids(&[ - (&first_wallet, first_rowid as u64), - (&make_wallet("yahoo"), 789), - ]); + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_returned_different_row_count_than_expected_with_one_account_missing_and_one_unmodified", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let first_wallet = make_wallet("booga"); + // let first_rowid = 456; + // insert_payable_record_fn( + // &*conn, + // &first_wallet.to_string(), + // 123456, + // 789789, + // Some(first_rowid), + // ); + // let subject = PayableDaoReal::new(conn); + // + // let _ = subject.mark_pending_payables_rowids(&[ + // (&first_wallet, first_rowid as u64), + // (&make_wallet("yahoo"), 789), + // ]); } #[test] fn explanatory_extension_shows_resulting_account_with_unpopulated_rowid() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", - ); - let wallet_1 = make_wallet("hooga"); - let rowid_1 = 550; - let wallet_2 = make_wallet("booga"); - let rowid_2 = 555; - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let record_seeds = [ - (&wallet_1.to_string(), 12345, 1_000_000_000, None), - (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), - ]; - record_seeds - .into_iter() - .for_each(|(wallet, balance, timestamp, rowid_opt)| { - insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) - }); - - let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); - - assert_eq!(result, "\ - The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ - ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ - the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ - ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ - Operation failed.\n\ - Notes:\n\ - a) if row ids have stayed non-populated it points out that writing failed but without the double \ - payment threat,\n\ - b) if some accounts on the resulting side are missing, other kind of serious issues should be \ - suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ - c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ - The operation which is supposed to clear out the ids of the payments previously requested for \ - this account\nprobably had not managed to complete successfully before another payment was \ - requested: preventive measures failed.\n".to_string()) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "explanatory_extension_shows_resulting_account_with_unpopulated_rowid", + // ); + // let wallet_1 = make_wallet("hooga"); + // let rowid_1 = 550; + // let wallet_2 = make_wallet("booga"); + // let rowid_2 = 555; + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let record_seeds = [ + // (&wallet_1.to_string(), 12345, 1_000_000_000, None), + // (&wallet_2.to_string(), 23456, 1_000_000_111, Some(540)), + // ]; + // record_seeds + // .into_iter() + // .for_each(|(wallet, balance, timestamp, rowid_opt)| { + // insert_payable_record_fn(&*conn, wallet, balance, timestamp, rowid_opt) + // }); + // + // let result = explanatory_extension(&*conn, &[(&wallet_1, rowid_1), (&wallet_2, rowid_2)]); + // + // assert_eq!(result, "\ + // The demanded data according to ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: 550 ), \ + // ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 555 ) looks different from \ + // the resulting state ( Wallet: 0x000000000000000000000000000000626f6f6761, Rowid: 540 ), \ + // ( Wallet: 0x000000000000000000000000000000686f6f6761, Rowid: N/A )!. \ + // Operation failed.\n\ + // Notes:\n\ + // a) if row ids have stayed non-populated it points out that writing failed but without the double \ + // payment threat,\n\ + // b) if some accounts on the resulting side are missing, other kind of serious issues should be \ + // suspected but see other\npoints to figure out if you were put in danger of double payment,\n\ + // c) seeing ids different from those demanded might be a sign of some payments having been doubled.\n\ + // The operation which is supposed to clear out the ids of the payments previously requested for \ + // this account\nprobably had not managed to complete successfully before another payment was \ + // requested: preventive measures failed.\n".to_string()) } #[test] fn mark_pending_payables_rowids_handles_general_sql_error() { - let home_dir = ensure_node_home_directory_exists( - "payable_dao", - "mark_pending_payables_rowids_handles_general_sql_error", - ); - let wallet = make_wallet("booga"); - let rowid = 656; - let conn = payable_read_only_conn(&home_dir); - let conn_wrapped = ConnectionWrapperReal::new(conn); - let subject = PayableDaoReal::new(Box::new(conn_wrapped)); - - let result = subject.mark_pending_payables_rowids(&[(&wallet, rowid)]); - - assert_eq!( - result, - Err(PayableDaoError::RusqliteError( - "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ - Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ - database\"))]" - .to_string() - )) - ) + // TODO Will be an object of removal in GH-662 + // let home_dir = ensure_node_home_directory_exists( + // "payable_dao", + // "mark_pending_payables_rowids_handles_general_sql_error", + // ); + // let wallet = make_wallet("booga"); + // let rowid = 656; + // let single_mark_instruction = MarkPendingPayableID::new(wallet.address(), rowid); + // let conn = payable_read_only_conn(&home_dir); + // let conn_wrapped = ConnectionWrapperReal::new(conn); + // let subject = PayableDaoReal::new(Box::new(conn_wrapped)); + // + // let result = subject.mark_pending_payables_rowids(&[single_mark_instruction]); + // + // assert_eq!( + // result, + // Err(PayableDaoError::RusqliteError( + // "Multi-row update to mark pending payable hit these errors: [SqliteFailure(\ + // Error { code: ReadOnly, extended_code: 8 }, Some(\"attempt to write a readonly \ + // database\"))]" + // .to_string() + // )) + // ) } #[test] - #[should_panic(expected = "broken code: empty input is not permit to enter this method")] + //#[should_panic(expected = "broken code: empty input is not permit to enter this method")] fn mark_pending_payables_rowids_is_strict_about_empty_input() { - let wrapped_conn = ConnectionWrapperMock::default(); - let subject = PayableDaoReal::new(Box::new(wrapped_conn)); - - let _ = subject.mark_pending_payables_rowids(&[]); + // TODO Will be an object of removal in GH-662 + // let wrapped_conn = ConnectionWrapperMock::default(); + // let subject = PayableDaoReal::new(Box::new(wrapped_conn)); + // + // let _ = subject.mark_pending_payables_rowids(&[]); } struct TestSetupValuesHolder { - fingerprint_1: PendingPayableFingerprint, - fingerprint_2: PendingPayableFingerprint, - wallet_1: Wallet, - wallet_2: Wallet, - previous_timestamp_1: SystemTime, - previous_timestamp_2: SystemTime, + account_1: TxWalletAndTimestamp, + account_2: TxWalletAndTimestamp, + } + + struct TxWalletAndTimestamp { + pending_payable: SentTx, + previous_timestamp: SystemTime, } - fn make_fingerprint_pair_and_insert_initial_payable_records( + struct TestInputs { + hash: TxHash, + previous_timestamp: SystemTime, + new_payable_timestamp: SystemTime, + receiver_wallet: Address, + initial_amount_wei: u128, + balance_change: u128, + } + + fn insert_initial_payable_records_and_return_sent_txs( conn: &dyn ConnectionWrapper, - initial_amount_1: u128, - initial_amount_2: u128, - balance_change_1: u128, - balance_change_2: u128, + (initial_amount_1, balance_change_1): (u128, u128), + (initial_amount_2, balance_change_2): (u128, u128), ) -> TestSetupValuesHolder { - let hash_1 = make_tx_hash(12345); - let rowid_1 = 789; - let previous_timestamp_1_s = 190_000_000; - let new_payable_timestamp_1 = from_unix_timestamp(199_000_000); - let wallet_1 = make_wallet("bobble"); - let hash_2 = make_tx_hash(54321); - let rowid_2 = 792; - let previous_timestamp_2_s = 187_100_000; - let new_payable_timestamp_2 = from_unix_timestamp(191_333_000); - let wallet_2 = make_wallet("booble bobble"); - { + let now = SystemTime::now(); + let (account_1, account_2) = [ + TestInputs { + hash: make_tx_hash(12345), + previous_timestamp: now.checked_sub(Duration::from_secs(45_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("bobbles").address(), + initial_amount_wei: initial_amount_1, + balance_change: balance_change_1, + }, + TestInputs { + hash: make_tx_hash(54321), + previous_timestamp: now.checked_sub(Duration::from_secs(22_000)).unwrap(), + new_payable_timestamp: now.checked_sub(Duration::from_secs(2)).unwrap(), + receiver_wallet: make_wallet("yet more bobbles").address(), + initial_amount_wei: initial_amount_2, + balance_change: balance_change_2, + }, + ] + .into_iter() + .enumerate() + .map(|(idx, test_inputs)| { insert_payable_record_fn( conn, - &wallet_1.to_string(), - i128::try_from(initial_amount_1).unwrap(), - previous_timestamp_1_s, - Some(rowid_1 as i64), + &format!("{:?}", test_inputs.receiver_wallet), + i128::try_from(test_inputs.initial_amount_wei).unwrap(), + to_unix_timestamp(test_inputs.previous_timestamp), + // TODO argument will be eliminated in GH-662 + None, ); - insert_payable_record_fn( - conn, - &wallet_2.to_string(), - i128::try_from(initial_amount_2).unwrap(), - previous_timestamp_2_s, - Some(rowid_2 as i64), - ) - } - let fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: new_payable_timestamp_1, - hash: hash_1, - attempt: 1, - amount: balance_change_1, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: new_payable_timestamp_2, - hash: hash_2, - attempt: 1, - amount: balance_change_2, - process_error: None, - }; - let previous_timestamp_1 = from_unix_timestamp(previous_timestamp_1_s); - let previous_timestamp_2 = from_unix_timestamp(previous_timestamp_2_s); + let mut sent_tx = make_sent_tx((idx as u64 + 1) * 1234); + sent_tx.hash = test_inputs.hash; + sent_tx.amount_minor = test_inputs.balance_change; + sent_tx.receiver_address = test_inputs.receiver_wallet; + sent_tx.timestamp = to_unix_timestamp(test_inputs.new_payable_timestamp); + sent_tx.amount_minor = test_inputs.balance_change; + + TxWalletAndTimestamp { + pending_payable: sent_tx, + previous_timestamp: test_inputs.previous_timestamp, + } + }) + .collect_tuple() + .unwrap(); + TestSetupValuesHolder { - fingerprint_1, - fingerprint_2, - wallet_1, - wallet_2, - previous_timestamp_1, - previous_timestamp_2, + account_1, + account_2, } } @@ -968,7 +983,7 @@ mod tests { //initial (1, 9999) let initial_changing_end_resulting_values = (initial, 11111, initial as u128 - 11111); //change (-1, abs(i64::MIN) - 11111) - transaction_confirmed_works( + test_transaction_confirmed_works( "transaction_confirmed_works_without_overflow", initial_changing_end_resulting_values, ) @@ -981,77 +996,80 @@ mod tests { //initial (0, 10000) //change (-1, abs(i64::MIN) - 111) //10000 + (abs(i64::MIN) - 111) > i64::MAX -> overflow - transaction_confirmed_works( + test_transaction_confirmed_works( "transaction_confirmed_works_hitting_overflow", initial_changing_end_resulting_values, ) } - fn transaction_confirmed_works( + fn test_transaction_confirmed_works( test_name: &str, (initial_amount_1, balance_change_1, expected_balance_after_1): (u128, u128, u128), ) { let home_dir = ensure_node_home_directory_exists("payable_dao", test_name); - //a hardcoded set that just makes a complement to the crucial, supplied one; this points to the ability of - //handling multiple transactions together + // A hardcoded set that just makes a complement to the crucial, supplied first one; this + // shows the ability to handle multiple transactions together let initial_amount_2 = 5_678_901; let balance_change_2 = 678_902; let expected_balance_after_2 = 4_999_999; let boxed_conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let setup_holder = make_fingerprint_pair_and_insert_initial_payable_records( + let setup_holder = insert_initial_payable_records_and_return_sent_txs( boxed_conn.as_ref(), - initial_amount_1, - initial_amount_2, - balance_change_1, - balance_change_2, + (initial_amount_1, balance_change_1), + (initial_amount_2, balance_change_2), ); let subject = PayableDaoReal::new(boxed_conn); - let status_1_before_opt = subject.account_status(&setup_holder.wallet_1); - let status_2_before_opt = subject.account_status(&setup_holder.wallet_2); + let wallet_1 = Wallet::from(setup_holder.account_1.pending_payable.receiver_address); + let wallet_2 = Wallet::from(setup_holder.account_2.pending_payable.receiver_address); + let status_1_before_opt = subject.account_status(&wallet_1); + let status_2_before_opt = subject.account_status(&wallet_2); let result = subject.transactions_confirmed(&[ - setup_holder.fingerprint_1.clone(), - setup_holder.fingerprint_2.clone(), + setup_holder.account_1.pending_payable.clone(), + setup_holder.account_2.pending_payable.clone(), ]); assert_eq!(result, Ok(())); + let expected_last_paid_timestamp_1 = + from_unix_timestamp(to_unix_timestamp(setup_holder.account_1.previous_timestamp)); + let expected_last_paid_timestamp_2 = + from_unix_timestamp(to_unix_timestamp(setup_holder.account_2.previous_timestamp)); + // TODO yes these pending_payable_opt values are unsensible now but it will eventually be all cleaned up with GH-662 let expected_status_before_1 = PayableAccount { - wallet: setup_holder.wallet_1.clone(), + wallet: wallet_1.clone(), balance_wei: initial_amount_1, - last_paid_timestamp: setup_holder.previous_timestamp_1, - pending_payable_opt: Some(PendingPayableId::new( - setup_holder.fingerprint_1.rowid, - H256::from_uint(&U256::from(0)), - )), //hash is just garbage + last_paid_timestamp: expected_last_paid_timestamp_1, + pending_payable_opt: None, }; let expected_status_before_2 = PayableAccount { - wallet: setup_holder.wallet_2.clone(), + wallet: wallet_2.clone(), balance_wei: initial_amount_2, - last_paid_timestamp: setup_holder.previous_timestamp_2, - pending_payable_opt: Some(PendingPayableId::new( - setup_holder.fingerprint_2.rowid, - H256::from_uint(&U256::from(0)), - )), //hash is just garbage + last_paid_timestamp: expected_last_paid_timestamp_2, + pending_payable_opt: None, }; let expected_resulting_status_1 = PayableAccount { - wallet: setup_holder.wallet_1.clone(), + wallet: wallet_1.clone(), balance_wei: expected_balance_after_1, - last_paid_timestamp: setup_holder.fingerprint_1.timestamp, + last_paid_timestamp: from_unix_timestamp( + setup_holder.account_1.pending_payable.timestamp, + ), pending_payable_opt: None, }; let expected_resulting_status_2 = PayableAccount { - wallet: setup_holder.wallet_2.clone(), + wallet: wallet_2.clone(), balance_wei: expected_balance_after_2, - last_paid_timestamp: setup_holder.fingerprint_2.timestamp, + last_paid_timestamp: from_unix_timestamp( + setup_holder.account_2.pending_payable.timestamp, + ), pending_payable_opt: None, }; assert_eq!(status_1_before_opt, Some(expected_status_before_1)); assert_eq!(status_2_before_opt, Some(expected_status_before_2)); - let resulting_account_1_opt = subject.account_status(&setup_holder.wallet_1); + let resulting_account_1_opt = subject.account_status(&wallet_1); assert_eq!(resulting_account_1_opt, Some(expected_resulting_status_1)); - let resulting_account_2_opt = subject.account_status(&setup_holder.wallet_2); + let resulting_account_2_opt = subject.account_status(&wallet_2); assert_eq!(resulting_account_2_opt, Some(expected_resulting_status_2)) } @@ -1063,22 +1081,20 @@ mod tests { ); let conn = payable_read_only_conn(&home_dir); let conn_wrapped = Box::new(ConnectionWrapperReal::new(conn)); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - let hash = make_tx_hash(12345); - let rowid = 789; - pending_payable_fingerprint.hash = hash; - pending_payable_fingerprint.rowid = rowid; + let mut confirmed_transaction = make_sent_tx(5); + confirmed_transaction.amount_minor = 12345; + let wallet_address = confirmed_transaction.receiver_address; let subject = PayableDaoReal::new(conn_wrapped); - let result = subject.transactions_confirmed(&[pending_payable_fingerprint]); + let result = subject.transactions_confirmed(&[confirmed_transaction]); assert_eq!( result, - Err(PayableDaoError::RusqliteError( + Err(PayableDaoError::RusqliteError(format!( "Error from invalid update command for payable table and change of -12345 wei to \ - 'pending_payable_rowid = 789' with error 'attempt to write a readonly database'" - .to_string() - )) + 'wallet_address = {:?}' with error 'attempt to write a readonly database'", + wallet_address + ))) ) } @@ -1086,26 +1102,21 @@ mod tests { #[should_panic( expected = "Overflow detected with 340282366920938463463374607431768211455: cannot be converted from u128 to i128" )] - fn transaction_confirmed_works_for_overflow_from_amount_stored_in_pending_payable_fingerprint() - { + fn transaction_confirmed_works_for_overflow_from_sent_tx_record() { let home_dir = ensure_node_home_directory_exists( "payable_dao", - "transaction_confirmed_works_for_overflow_from_amount_stored_in_pending_payable_fingerprint", + "transaction_confirmed_works_for_overflow_from_sent_tx_record", ); let subject = PayableDaoReal::new( DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(), ); - let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - let hash = make_tx_hash(12345); - let rowid = 789; - pending_payable_fingerprint.hash = hash; - pending_payable_fingerprint.rowid = rowid; - pending_payable_fingerprint.amount = u128::MAX; + let mut sent_tx = make_sent_tx(456); + sent_tx.amount_minor = u128::MAX; //The overflow occurs before we start modifying the payable account so we can have the database empty - let _ = subject.transactions_confirmed(&[pending_payable_fingerprint]); + let _ = subject.transactions_confirmed(&[sent_tx]); } #[test] @@ -1117,38 +1128,37 @@ mod tests { let conn = DbInitializerReal::default() .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); - let setup_holder = make_fingerprint_pair_and_insert_initial_payable_records( + let setup_holder = insert_initial_payable_records_and_return_sent_txs( conn.as_ref(), - 1_111_111, - 2_222_222, - 111_111, - 222_222, + (1_111_111, 111_111), + (2_222_222, 222_222), ); + let wallet_1 = Wallet::from(setup_holder.account_1.pending_payable.receiver_address); + let wallet_2 = Wallet::from(setup_holder.account_2.pending_payable.receiver_address); conn.prepare("delete from payable where wallet_address = ?") .unwrap() - .execute(&[&setup_holder.wallet_2]) + .execute(&[&wallet_2.to_string()]) .unwrap(); let subject = PayableDaoReal::new(conn); - let expected_account = PayableAccount { - wallet: setup_holder.wallet_1.clone(), - balance_wei: 1_111_111 - setup_holder.fingerprint_1.amount, - last_paid_timestamp: setup_holder.fingerprint_1.timestamp, - pending_payable_opt: None, - }; - let result = subject - .transactions_confirmed(&[setup_holder.fingerprint_1, setup_holder.fingerprint_2]); + let result = subject.transactions_confirmed(&[ + setup_holder.account_1.pending_payable, + setup_holder.account_2.pending_payable, + ]); + let expected_err_msg = format!( + "Expected 1 row to be changed for the unique key \ + {} but got this count: 0", + wallet_2 + ); assert_eq!( result, - Err(PayableDaoError::RusqliteError( - "Expected 1 row to be changed for the unique key 792 but got this count: 0" - .to_string() - )) + Err(PayableDaoError::RusqliteError(expected_err_msg)) ); - let account_1_opt = subject.account_status(&setup_holder.wallet_1); - assert_eq!(account_1_opt, Some(expected_account)); - let account_2_opt = subject.account_status(&setup_holder.wallet_2); + let expected_resulting_balance_1 = 1_111_111 - 111_111; + let account_1 = subject.account_status(&wallet_1).unwrap(); + assert_eq!(account_1.balance_wei, expected_resulting_balance_1); + let account_2_opt = subject.account_status(&wallet_2); assert_eq!(account_2_opt, None); } diff --git a/node/src/accountant/db_access_objects/pending_payable_dao.rs b/node/src/accountant/db_access_objects/pending_payable_dao.rs index e555fcc9a..414c364d8 100644 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ b/node/src/accountant/db_access_objects/pending_payable_dao.rs @@ -5,7 +5,6 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; @@ -36,7 +35,7 @@ pub struct TransactionHashes { pub trait PendingPayableDao { // Note that the order of the returned results is not guaranteed fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes; - fn return_all_errorless_fingerprints(&self) -> Vec; + // fn return_all_errorless_fingerprints(&self) -> Vec; fn insert_new_fingerprints( &self, hashes_and_amounts: &[HashAndAmount], @@ -87,42 +86,42 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { } } - fn return_all_errorless_fingerprints(&self) -> Vec { - let mut stm = self - .conn - .prepare( - "select rowid, transaction_hash, amount_high_b, amount_low_b, \ - payable_timestamp, attempt from pending_payable where process_error is null", - ) - .expect("Internal error"); - stm.query_map([], |row| { - let rowid: u64 = Self::get_with_expect(row, 0); - let transaction_hash: String = Self::get_with_expect(row, 1); - let amount_high_bytes: i64 = Self::get_with_expect(row, 2); - let amount_low_bytes: i64 = Self::get_with_expect(row, 3); - let timestamp: i64 = Self::get_with_expect(row, 4); - let attempt: u16 = Self::get_with_expect(row, 5); - Ok(PendingPayableFingerprint { - rowid, - timestamp: from_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { - panic!( - "Invalid hash format (\"{}\": {:?}) - database corrupt", - transaction_hash, e - ) - }), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_bytes, - amount_low_bytes, - )), - process_error: None, - }) - }) - .expect("rusqlite failure") - .vigilant_flatten() - .collect() - } + // fn return_all_errorless_fingerprints(&self) -> Vec { + // let mut stm = self + // .conn + // .prepare( + // "select rowid, transaction_hash, amount_high_b, amount_low_b, \ + // payable_timestamp, attempt from pending_payable where process_error is null", + // ) + // .expect("Internal error"); + // stm.query_map([], |row| { + // let rowid: u64 = Self::get_with_expect(row, 0); + // let transaction_hash: String = Self::get_with_expect(row, 1); + // let amount_high_bytes: i64 = Self::get_with_expect(row, 2); + // let amount_low_bytes: i64 = Self::get_with_expect(row, 3); + // let timestamp: i64 = Self::get_with_expect(row, 4); + // let attempt: u16 = Self::get_with_expect(row, 5); + // Ok(SentTx { + // rowid, + // timestamp: from_unix_timestamp(timestamp), + // hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { + // panic!( + // "Invalid hash format (\"{}\": {:?}) - database corrupt", + // transaction_hash, e + // ) + // }), + // attempt, + // amount_minor: checked_conversion::(BigIntDivider::reconstitute( + // amount_high_bytes, + // amount_low_bytes, + // )), + // process_error: None, + // }) + // }) + // .expect("rusqlite failure") + // .vigilant_flatten() + // .collect() + // } fn insert_new_fingerprints( &self, @@ -179,7 +178,7 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { { Ok(x) if x == ids.len() => Ok(()), Ok(num) => panic!( - "deleting fingerprint, expected {} rows to be changed, but the actual number is {}", + "deleting sent tx record, expected {} rows to be changed, but the actual number is {}", ids.len(), num ), @@ -225,21 +224,6 @@ impl PendingPayableDao for PendingPayableDaoReal<'_> { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PendingPayable { - pub recipient_wallet: Wallet, - pub hash: H256, -} - -impl PendingPayable { - pub fn new(recipient_wallet: Wallet, hash: H256) -> Self { - Self { - recipient_wallet, - hash, - } - } -} - #[derive(Debug)] pub struct PendingPayableDaoReal<'a> { conn: Box, @@ -272,12 +256,11 @@ impl PendingPayableDaoFactory for DaoFactoryReal { #[cfg(test)] mod tests { use crate::accountant::checked_conversion; - use crate::accountant::db_access_objects::pending_payable_dao::{ + use crate::accountant::db_access_objects::sent_payable_dao::{ PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, }; use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::test_utils::make_tx_hash; use crate::database::db_initializer::{ @@ -291,660 +274,660 @@ mod tests { use std::time::SystemTime; use web3::types::H256; - #[test] - fn insert_new_fingerprints_happy_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "insert_new_fingerprints_happy_path", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(4546); - let amount_1 = 55556; - let hash_2 = make_tx_hash(6789); - let amount_2 = 44445; - let batch_wide_timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - let _ = subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - - let records = subject.return_all_errorless_fingerprints(); - assert_eq!( - records, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_1.hash, - attempt: 1, - amount: hash_and_amount_1.amount, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_and_amount_2.hash, - attempt: 1, - amount: hash_and_amount_2.amount, - process_error: None - } - ] - ) - } - - #[test] - fn insert_new_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "insert_new_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let hash = make_tx_hash(45466); - let amount = 55556; - let timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { hash, amount }; - - let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); - - assert_eq!( - result, - Err(PendingPayableDaoError::InsertionFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic(expected = "expected 1 changed rows but got 0")] - fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { - let setup_conn = Connection::open_in_memory().unwrap(); - // injecting a by-plan failing statement into the mocked connection in order to provoke - // a reaction that would've been untestable directly on the table the act is closely coupled with - let statement = { - setup_conn - .execute("create table example (id integer)", []) - .unwrap(); - setup_conn.prepare("select id from example").unwrap() - }; - let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); - let hash_1 = make_tx_hash(4546); - let amount_1 = 55556; - let batch_wide_timestamp = from_unix_timestamp(200_000_000); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - let hash_and_amount = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - - let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); - } - - #[test] - fn fingerprints_rowids_when_records_reachable() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_records_reachable", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_unix_timestamp(195_000_000); - // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, - // then complain about its excessive size if supplied in unquoted strings - let hash_1 = - H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") - .unwrap(); - let hash_2 = - H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") - .unwrap(); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 4567, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 6789, - }; - let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; - { - subject - .insert_new_fingerprints(&fingerprints_init_input, timestamp) - .unwrap(); - } - - let result = subject.fingerprints_rowids(&[hash_1, hash_2]); - - let first_expected_pair = &(1, hash_1); - assert!( - result.rowid_results.contains(first_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - first_expected_pair, - result.rowid_results - ); - let second_expected_pair = &(2, hash_2); - assert!( - result.rowid_results.contains(second_expected_pair), - "Returned rowid pairs should have contained {:?} but all it did is {:?}", - second_expected_pair, - result.rowid_results - ); - assert_eq!(result.rowid_results.len(), 2); - } - - #[test] - fn fingerprints_rowids_when_nonexistent_records() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "fingerprints_rowids_when_nonexistent_records", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let hash_1 = make_tx_hash(11119); - let hash_2 = make_tx_hash(22229); - let hash_3 = make_tx_hash(33339); - let hash_4 = make_tx_hash(44449); - // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for - // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of - // just an ambiguous 1 - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_2, - amount: 8901234, - }], - SystemTime::now(), - ) - .unwrap(); - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: hash_3, - amount: 1234567, - }], - SystemTime::now(), - ) - .unwrap(); - subject.delete_fingerprints(&[1]).unwrap(); - - let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); - - assert_eq!(result.rowid_results, vec![(2, hash_3),]); - assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); - } - - #[test] - fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let batch_wide_timestamp = from_unix_timestamp(195_000_000); - let hash_1 = make_tx_hash(11119); - let amount_1 = 787; - let hash_2 = make_tx_hash(10000); - let amount_2 = 333; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2], - batch_wide_timestamp, - ) - .unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp: batch_wide_timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp: batch_wide_timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: None - } - ] - ) - } - - #[test] - fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(wrapped_conn); - let timestamp = from_unix_timestamp(198_000_000); - let hash = make_tx_hash(10000); - let amount = 333; - let hash_and_amount_1 = HashAndAmount { - hash: make_tx_hash(11119), - amount: 2000, - }; - let hash_and_amount_2 = HashAndAmount { hash, amount }; - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - subject.mark_failures(&[1]).unwrap(); - } - - let result = subject.return_all_errorless_fingerprints(); - - assert_eq!( - result, - vec![PendingPayableFingerprint { - rowid: 2, - timestamp, - hash, - attempt: 1, - amount, - process_error: None - }] - ) - } - - #[test] - #[should_panic( - expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" - )] - fn return_all_errorless_fingerprints_panics_on_malformed_hash() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "return_all_errorless_fingerprints_panics_on_malformed_hash", - ); - let wrapped_conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - { - wrapped_conn - .prepare("insert into pending_payable \ - (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ - values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") - .unwrap() - .execute([]) - .unwrap(); - } - let subject = PendingPayableDaoReal::new(wrapped_conn); - - let _ = subject.return_all_errorless_fingerprints(); - } - - #[test] - fn delete_fingerprints_happy_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_happy_path", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[ - HashAndAmount { - hash: make_tx_hash(1234), - amount: 1111, - }, - HashAndAmount { - hash: make_tx_hash(2345), - amount: 5555, - }, - HashAndAmount { - hash: make_tx_hash(3456), - amount: 2222, - }, - ], - SystemTime::now(), - ) - .unwrap(); - } - - let result = subject.delete_fingerprints(&[2, 3]); - - assert_eq!(result, Ok(())); - let records_in_the_db = subject.return_all_errorless_fingerprints(); - let record_left_in = &records_in_the_db[0]; - assert_eq!(record_left_in.hash, make_tx_hash(1234)); - assert_eq!(record_left_in.rowid, 1); - assert_eq!(records_in_the_db.len(), 1); - } - - #[test] - fn delete_fingerprints_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let rowid = 45; - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.delete_fingerprints(&[rowid]); - - assert_eq!( - result, - Err(PendingPayableDaoError::RecordDeletion( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "deleting fingerprint, expected 2 rows to be changed, but the actual number is 1" - )] - fn delete_fingerprints_changed_different_number_of_rows_than_expected() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "delete_fingerprints_changed_different_number_of_rows_than_expected", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let rowid_1 = 1; - let rowid_2 = 2; - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[HashAndAmount { - hash: make_tx_hash(666666), - amount: 5555, - }], - SystemTime::now(), - ) - .unwrap(); - } - - let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); - } - - #[test] - fn increment_scan_attempts_works() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(345); - let hash_2 = make_tx_hash(456); - let hash_3 = make_tx_hash(567); - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: 1122, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: 2233, - }; - let hash_and_amount_3 = HashAndAmount { - hash: hash_3, - amount: 3344, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints( - &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], - timestamp, - ) - .unwrap(); - } - - let result = subject.increment_scan_attempts(&[2, 3]); - - assert_eq!(result, Ok(())); - let mut all_records = subject.return_all_errorless_fingerprints(); - assert_eq!(all_records.len(), 3); - let record_1 = all_records.remove(0); - assert_eq!(record_1.hash, hash_1); - assert_eq!(record_1.attempt, 1); - let record_2 = all_records.remove(0); - assert_eq!(record_2.hash, hash_2); - assert_eq!(record_2.attempt, 2); - let record_3 = all_records.remove(0); - assert_eq!(record_3.hash, hash_3); - assert_eq!(record_3.attempt, 2); - } - - #[test] - fn increment_scan_attempts_works_sad_path() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_works_sad_path", - ); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.increment_scan_attempts(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::UpdateFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" - )] - fn increment_scan_attempts_panics_on_unexpected_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "increment_scan_attempts_panics_on_unexpected_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.increment_scan_attempts(&[1, 2]); - } - - #[test] - fn mark_failures_works() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_works"); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let hash_1 = make_tx_hash(555); - let amount_1 = 1234; - let hash_2 = make_tx_hash(666); - let amount_2 = 2345; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - let timestamp = from_unix_timestamp(190_000_000); - let subject = PendingPayableDaoReal::new(conn); - { - subject - .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - .unwrap(); - } - - let result = subject.mark_failures(&[2]); - - assert_eq!(result, Ok(())); - let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); - let mut assert_stm = assert_conn - .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") - .unwrap(); - let found_fingerprints = assert_stm - .query_map([], |row| { - let rowid: u64 = row.get(0).unwrap(); - let transaction_hash: String = row.get(1).unwrap(); - let amount_high_b: i64 = row.get(2).unwrap(); - let amount_low_b: i64 = row.get(3).unwrap(); - let timestamp: i64 = row.get(4).unwrap(); - let attempt: u16 = row.get(5).unwrap(); - let process_error: Option = row.get(6).unwrap(); - Ok(PendingPayableFingerprint { - rowid, - timestamp: from_unix_timestamp(timestamp), - hash: H256::from_str(&transaction_hash[2..]).unwrap(), - attempt, - amount: checked_conversion::(BigIntDivider::reconstitute( - amount_high_b, - amount_low_b, - )), - process_error, - }) - }) - .unwrap() - .flatten() - .collect::>(); - assert_eq!( - *found_fingerprints, - vec![ - PendingPayableFingerprint { - rowid: 1, - timestamp, - hash: hash_1, - attempt: 1, - amount: amount_1, - process_error: None - }, - PendingPayableFingerprint { - rowid: 2, - timestamp, - hash: hash_2, - attempt: 1, - amount: amount_2, - process_error: Some("ERROR".to_string()) - } - ] - ) - } - - #[test] - fn mark_failures_sad_path() { - let home_dir = - ensure_node_home_directory_exists("pending_payable_dao", "mark_failures_sad_path"); - { - DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - } - let conn_read_only = Connection::open_with_flags( - home_dir.join(DATABASE_FILE), - OpenFlags::SQLITE_OPEN_READ_ONLY, - ) - .unwrap(); - let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - - let result = subject.mark_failures(&[1]); - - assert_eq!( - result, - Err(PendingPayableDaoError::ErrorMarkFailed( - "attempt to write a readonly database".to_string() - )) - ) - } - - #[test] - #[should_panic( - expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" - )] - fn mark_failures_panics_on_wrong_row_change_count() { - let home_dir = ensure_node_home_directory_exists( - "pending_payable_dao", - "mark_failures_panics_on_wrong_row_change_count", - ); - let conn = DbInitializerReal::default() - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - let subject = PendingPayableDaoReal::new(conn); - - let _ = subject.mark_failures(&[10, 20]); - } + // #[test] + // fn insert_new_fingerprints_happy_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "insert_new_fingerprints_happy_path", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(4546); + // let amount_1 = 55556; + // let hash_2 = make_tx_hash(6789); + // let amount_2 = 44445; + // let batch_wide_timestamp = from_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // + // let _ = subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2], + // batch_wide_timestamp, + // ) + // .unwrap(); + // + // let records = subject.return_all_errorless_fingerprints(); + // assert_eq!( + // records, + // vec![ + // SentTx { + // rowid: 1, + // timestamp: batch_wide_timestamp, + // hash: hash_and_amount_1.hash, + // attempt: 1, + // amount_minor: hash_and_amount_1.amount, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp: batch_wide_timestamp, + // hash: hash_and_amount_2.hash, + // attempt: 1, + // amount_minor: hash_and_amount_2.amount, + // process_error: None + // } + // ] + // ) + // } + // + // #[test] + // fn insert_new_fingerprints_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "insert_new_fingerprints_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let hash = make_tx_hash(45466); + // let amount = 55556; + // let timestamp = from_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // let hash_and_amount = HashAndAmount { hash, amount }; + // + // let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::InsertionFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic(expected = "expected 1 changed rows but got 0")] + // fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { + // let setup_conn = Connection::open_in_memory().unwrap(); + // // injecting a by-plan failing statement into the mocked connection in order to provoke + // // a reaction that would've been untestable directly on the table the act is closely coupled with + // let statement = { + // setup_conn + // .execute("create table example (id integer)", []) + // .unwrap(); + // setup_conn.prepare("select id from example").unwrap() + // }; + // let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); + // let hash_1 = make_tx_hash(4546); + // let amount_1 = 55556; + // let batch_wide_timestamp = from_unix_timestamp(200_000_000); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // let hash_and_amount = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // + // let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); + // } + // + // #[test] + // fn fingerprints_rowids_when_records_reachable() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "fingerprints_rowids_when_records_reachable", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let timestamp = from_unix_timestamp(195_000_000); + // // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, + // // then complain about its excessive size if supplied in unquoted strings + // let hash_1 = + // H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") + // .unwrap(); + // let hash_2 = + // H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") + // .unwrap(); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: 4567, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: 6789, + // }; + // let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; + // { + // subject + // .insert_new_fingerprints(&fingerprints_init_input, timestamp) + // .unwrap(); + // } + // + // let result = subject.fingerprints_rowids(&[hash_1, hash_2]); + // + // let first_expected_pair = &(1, hash_1); + // assert!( + // result.rowid_results.contains(first_expected_pair), + // "Returned rowid pairs should have contained {:?} but all it did is {:?}", + // first_expected_pair, + // result.rowid_results + // ); + // let second_expected_pair = &(2, hash_2); + // assert!( + // result.rowid_results.contains(second_expected_pair), + // "Returned rowid pairs should have contained {:?} but all it did is {:?}", + // second_expected_pair, + // result.rowid_results + // ); + // assert_eq!(result.rowid_results.len(), 2); + // } + // + // #[test] + // fn fingerprints_rowids_when_nonexistent_records() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "fingerprints_rowids_when_nonexistent_records", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let hash_1 = make_tx_hash(11119); + // let hash_2 = make_tx_hash(22229); + // let hash_3 = make_tx_hash(33339); + // let hash_4 = make_tx_hash(44449); + // // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for + // // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of + // // just an ambiguous 1 + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: hash_2, + // amount_minor: 8901234, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: hash_3, + // amount_minor: 1234567, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // subject.delete_fingerprints(&[1]).unwrap(); + // + // let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); + // + // assert_eq!(result.rowid_results, vec![(2, hash_3),]); + // assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); + // } + // + // #[test] + // fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let batch_wide_timestamp = from_unix_timestamp(195_000_000); + // let hash_1 = make_tx_hash(11119); + // let amount_1 = 787; + // let hash_2 = make_tx_hash(10000); + // let amount_2 = 333; + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // + // { + // subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2], + // batch_wide_timestamp, + // ) + // .unwrap(); + // } + // + // let result = subject.return_all_errorless_fingerprints(); + // + // assert_eq!( + // result, + // vec![ + // SentTx { + // rowid: 1, + // timestamp: batch_wide_timestamp, + // hash: hash_1, + // attempt: 1, + // amount_minor: amount_1, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp: batch_wide_timestamp, + // hash: hash_2, + // attempt: 1, + // amount_minor: amount_2, + // process_error: None + // } + // ] + // ) + // } + // + // #[test] + // fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // let timestamp = from_unix_timestamp(198_000_000); + // let hash = make_tx_hash(10000); + // let amount = 333; + // let hash_and_amount_1 = HashAndAmount { + // hash: make_tx_hash(11119), + // amount_minor: 2000, + // }; + // let hash_and_amount_2 = HashAndAmount { hash, amount }; + // { + // subject + // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) + // .unwrap(); + // subject.mark_failures(&[1]).unwrap(); + // } + // + // let result = subject.return_all_errorless_fingerprints(); + // + // assert_eq!( + // result, + // vec![SentTx { + // rowid: 2, + // timestamp, + // hash, + // attempt: 1, + // amount, + // process_error: None + // }] + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" + // )] + // fn return_all_errorless_fingerprints_panics_on_malformed_hash() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "return_all_errorless_fingerprints_panics_on_malformed_hash", + // ); + // let wrapped_conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // { + // wrapped_conn + // .prepare("insert into pending_payable \ + // (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ + // values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") + // .unwrap() + // .execute([]) + // .unwrap(); + // } + // let subject = PendingPayableDaoReal::new(wrapped_conn); + // + // let _ = subject.return_all_errorless_fingerprints(); + // } + // + // #[test] + // fn delete_fingerprints_happy_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_happy_path", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[ + // HashAndAmount { + // hash: make_tx_hash(1234), + // amount_minor: 1111, + // }, + // HashAndAmount { + // hash: make_tx_hash(2345), + // amount_minor: 5555, + // }, + // HashAndAmount { + // hash: make_tx_hash(3456), + // amount_minor: 2222, + // }, + // ], + // SystemTime::now(), + // ) + // .unwrap(); + // } + // + // let result = subject.delete_fingerprints(&[2, 3]); + // + // assert_eq!(result, Ok(())); + // let records_in_the_db = subject.return_all_errorless_fingerprints(); + // let record_left_in = &records_in_the_db[0]; + // assert_eq!(record_left_in.hash, make_tx_hash(1234)); + // assert_eq!(record_left_in.rowid, 1); + // assert_eq!(records_in_the_db.len(), 1); + // } + // + // #[test] + // fn delete_fingerprints_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let rowid = 45; + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.delete_fingerprints(&[rowid]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::RecordDeletion( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "deleting sent tx record, expected 2 rows to be changed, but the actual number is 1" + // )] + // fn delete_fingerprints_changed_different_number_of_rows_than_expected() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "delete_fingerprints_changed_different_number_of_rows_than_expected", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let rowid_1 = 1; + // let rowid_2 = 2; + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[HashAndAmount { + // hash: make_tx_hash(666666), + // amount_minor: 5555, + // }], + // SystemTime::now(), + // ) + // .unwrap(); + // } + // + // let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); + // } + // + // #[test] + // fn increment_scan_attempts_works() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_works", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(345); + // let hash_2 = make_tx_hash(456); + // let hash_3 = make_tx_hash(567); + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: 1122, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: 2233, + // }; + // let hash_and_amount_3 = HashAndAmount { + // hash: hash_3, + // amount_minor: 3344, + // }; + // let timestamp = from_unix_timestamp(190_000_000); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints( + // &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], + // timestamp, + // ) + // .unwrap(); + // } + // + // let result = subject.increment_scan_attempts(&[2, 3]); + // + // assert_eq!(result, Ok(())); + // let mut all_records = subject.return_all_errorless_fingerprints(); + // assert_eq!(all_records.len(), 3); + // let record_1 = all_records.remove(0); + // assert_eq!(record_1.hash, hash_1); + // assert_eq!(record_1.attempt, 1); + // let record_2 = all_records.remove(0); + // assert_eq!(record_2.hash, hash_2); + // assert_eq!(record_2.attempt, 2); + // let record_3 = all_records.remove(0); + // assert_eq!(record_3.hash, hash_3); + // assert_eq!(record_3.attempt, 2); + // } + // + // #[test] + // fn increment_scan_attempts_works_sad_path() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_works_sad_path", + // ); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.increment_scan_attempts(&[1]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::UpdateFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" + // )] + // fn increment_scan_attempts_panics_on_unexpected_row_change_count() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "increment_scan_attempts_panics_on_unexpected_row_change_count", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // + // let _ = subject.increment_scan_attempts(&[1, 2]); + // } + // + // #[test] + // fn mark_failures_works() { + // let home_dir = + // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_works"); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let hash_1 = make_tx_hash(555); + // let amount_1 = 1234; + // let hash_2 = make_tx_hash(666); + // let amount_2 = 2345; + // let hash_and_amount_1 = HashAndAmount { + // hash: hash_1, + // amount_minor: amount_1, + // }; + // let hash_and_amount_2 = HashAndAmount { + // hash: hash_2, + // amount_minor: amount_2, + // }; + // let timestamp = from_unix_timestamp(190_000_000); + // let subject = PendingPayableDaoReal::new(conn); + // { + // subject + // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) + // .unwrap(); + // } + // + // let result = subject.mark_failures(&[2]); + // + // assert_eq!(result, Ok(())); + // let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); + // let mut assert_stm = assert_conn + // .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") + // .unwrap(); + // let found_fingerprints = assert_stm + // .query_map([], |row| { + // let rowid: u64 = row.get(0).unwrap(); + // let transaction_hash: String = row.get(1).unwrap(); + // let amount_high_b: i64 = row.get(2).unwrap(); + // let amount_low_b: i64 = row.get(3).unwrap(); + // let timestamp: i64 = row.get(4).unwrap(); + // let attempt: u16 = row.get(5).unwrap(); + // let process_error: Option = row.get(6).unwrap(); + // Ok(SentTx { + // rowid, + // timestamp: from_unix_timestamp(timestamp), + // hash: H256::from_str(&transaction_hash[2..]).unwrap(), + // attempt, + // amount_minor: checked_conversion::(BigIntDivider::reconstitute( + // amount_high_b, + // amount_low_b, + // )), + // process_error, + // }) + // }) + // .unwrap() + // .flatten() + // .collect::>(); + // assert_eq!( + // *found_fingerprints, + // vec![ + // SentTx { + // rowid: 1, + // timestamp, + // hash: hash_1, + // attempt: 1, + // amount_minor: amount_1, + // process_error: None + // }, + // SentTx { + // rowid: 2, + // timestamp, + // hash: hash_2, + // attempt: 1, + // amount_minor: amount_2, + // process_error: Some("ERROR".to_string()) + // } + // ] + // ) + // } + // + // #[test] + // fn mark_failures_sad_path() { + // let home_dir = + // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_sad_path"); + // { + // DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // } + // let conn_read_only = Connection::open_with_flags( + // home_dir.join(DATABASE_FILE), + // OpenFlags::SQLITE_OPEN_READ_ONLY, + // ) + // .unwrap(); + // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); + // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); + // + // let result = subject.mark_failures(&[1]); + // + // assert_eq!( + // result, + // Err(PendingPayableDaoError::ErrorMarkFailed( + // "attempt to write a readonly database".to_string() + // )) + // ) + // } + // + // #[test] + // #[should_panic( + // expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" + // )] + // fn mark_failures_panics_on_wrong_row_change_count() { + // let home_dir = ensure_node_home_directory_exists( + // "sent_payable_dao", + // "mark_failures_panics_on_wrong_row_change_count", + // ); + // let conn = DbInitializerReal::default() + // .initialize(&home_dir, DbInitializationConfig::test_default()) + // .unwrap(); + // let subject = PendingPayableDaoReal::new(conn); + // + // let _ = subject.mark_failures(&[10, 20]); + // } } diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index ad8f52462..9d100c633 100644 --- a/node/src/accountant/db_access_objects/receivable_dao.rs +++ b/node/src/accountant/db_access_objects/receivable_dao.rs @@ -55,7 +55,7 @@ pub trait ReceivableDao { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError>; fn more_money_received( @@ -112,7 +112,7 @@ impl ReceivableDao for ReceivableDaoReal { &self, timestamp: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError> { let main_sql = "insert into receivable (wallet_address, balance_high_b, balance_low_b, last_received_timestamp) values \ (:wallet, :balance_high_b, :balance_low_b, :last_received_timestamp) on conflict (wallet_address) do update set \ @@ -125,7 +125,7 @@ impl ReceivableDao for ReceivableDaoReal { .key(WalletAddress(wallet)) .wei_change(WeiChange::new( "balance", - amount, + amount_minor, WeiChangeDirection::Addition, )) .other_params(vec![ParamByUse::BeforeOverflowOnly( diff --git a/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs new file mode 100644 index 000000000..26c7dd5fe --- /dev/null +++ b/node/src/accountant/db_access_objects/sent_payable_and_failed_payable_data_conversion.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; + +impl From<(FailedTx, TxBlock)> for SentTx { + fn from((failed_tx, confirmation_block): (FailedTx, TxBlock)) -> Self { + SentTx { + hash: failed_tx.hash, + receiver_address: failed_tx.receiver_address, + amount_minor: failed_tx.amount_minor, + timestamp: failed_tx.timestamp, + gas_price_minor: failed_tx.gas_price_minor, + nonce: failed_tx.nonce, + status: TxStatus::Confirmed { + block_hash: format!("{:?}", confirmation_block.block_hash), + block_number: confirmation_block.block_number.as_u64(), + detection: Detection::Reclaim, + }, + } + } +} + +impl From<(SentTx, FailureReason)> for FailedTx { + fn from((sent_tx, failure_reason): (SentTx, FailureReason)) -> Self { + FailedTx { + hash: sent_tx.hash, + receiver_address: sent_tx.receiver_address, + amount_minor: sent_tx.amount_minor, + timestamp: sent_tx.timestamp, + gas_price_minor: sent_tx.gas_price_minor, + nonce: sent_tx.nonce, + reason: failure_reason, + status: FailureStatus::RetryRequired, + } + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, SentTx, TxStatus}; + use crate::accountant::db_access_objects::utils::to_unix_timestamp; + use crate::accountant::gwei_to_wei; + use crate::accountant::test_utils::make_transaction_block; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; + use crate::blockchain::errors::validation_status::ValidationStatus; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::make_wallet; + use std::time::{Duration, SystemTime}; + + #[test] + fn sent_tx_record_can_be_converted_from_failed_tx_record() { + let failed_tx = FailedTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + }; + let tx_block = make_transaction_block(789); + + let result = SentTx::from((failed_tx.clone(), tx_block)); + + assert_eq!( + result, + SentTx { + hash: make_tx_hash(456), + receiver_address: make_wallet("abc").address(), + amount_minor: 456789012, + timestamp: 345678974, + gas_price_minor: 123456789, + nonce: 11, + status: TxStatus::Confirmed { + block_hash: + "0x000000000000000000000000000000000000000000000000000000003b9acd15" + .to_string(), + block_number: 491169069, + detection: Detection::Reclaim, + }, + } + ); + } + + #[test] + fn conversion_from_sent_tx_and_failure_reason_to_failed_tx_works() { + let sent_tx = SentTx { + hash: make_tx_hash(789), + receiver_address: make_wallet("receiver").address(), + amount_minor: 123_456_789, + timestamp: to_unix_timestamp( + SystemTime::now() + .checked_sub(Duration::from_secs(10_000)) + .unwrap(), + ), + gas_price_minor: gwei_to_wei(424_u64), + nonce: 456_u64.into(), + status: TxStatus::Pending(ValidationStatus::Waiting), + }; + + let result_1 = FailedTx::from((sent_tx.clone(), FailureReason::Reverted)); + let result_2 = FailedTx::from(( + sent_tx.clone(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + )); + + assert_conversion_into_failed_tx(result_1, sent_tx.clone(), FailureReason::Reverted); + assert_conversion_into_failed_tx( + result_2, + sent_tx, + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + ); + } + + fn assert_conversion_into_failed_tx( + result: FailedTx, + original_sent_tx: SentTx, + expected_failure_reason: FailureReason, + ) { + assert_eq!(result.hash, original_sent_tx.hash); + assert_eq!(result.receiver_address, original_sent_tx.receiver_address); + assert_eq!(result.amount_minor, original_sent_tx.amount_minor); + assert_eq!(result.timestamp, original_sent_tx.timestamp); + assert_eq!(result.gas_price_minor, original_sent_tx.gas_price_minor); + assert_eq!(result.nonce, original_sent_tx.nonce); + assert_eq!(result.status, FailureStatus::RetryRequired); + assert_eq!(result.reason, expected_failure_reason); + } +} diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs index 09e293edf..a82bafdce 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -1,19 +1,21 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use std::collections::{HashMap, HashSet}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use ethereum_types::{H256}; -use web3::types::Address; -use masq_lib::utils::ExpectValue; -use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers}; +use crate::accountant::db_access_objects::utils::{ + DaoFactoryReal, TxHash, TxIdentifiers, TxRecordWithHash, +}; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; +use crate::accountant::{checked_conversion, comma_joined_stringifiable}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; +use ethereum_types::H256; use itertools::Itertools; +use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; -use crate::accountant::db_access_objects::failed_payable_dao::ValidationStatus; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use web3::types::Address; #[derive(Debug, PartialEq, Eq)] pub enum SentPayableDaoError { @@ -25,16 +27,22 @@ pub enum SentPayableDaoError { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Tx { +pub struct SentTx { pub hash: TxHash, pub receiver_address: Address, - pub amount: u128, + pub amount_minor: u128, pub timestamp: i64, - pub gas_price_wei: u128, + pub gas_price_minor: u128, pub nonce: u64, pub status: TxStatus, } +impl TxRecordWithHash for SentTx { + fn hash(&self) -> TxHash { + self.hash + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TxStatus { Pending(ValidationStatus), @@ -69,26 +77,21 @@ pub enum Detection { Reclaim, } -impl From<&TxConfirmation> for TxStatus { - fn from(tx_confirmation: &TxConfirmation) -> Self { +impl From for TxStatus { + fn from(tx_block: TxBlock) -> Self { TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number) - .expect("block number too big"), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).expect("block number too big"), + detection: Detection::Normal, } } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TxConfirmation { - block_info: TransactionBlock, - detection: Detection, -} - +#[derive(Debug, PartialEq, Eq)] pub enum RetrieveCondition { IsPending, ByHash(Vec), + ByNonce(Vec), } impl Display for RetrieveCondition { @@ -104,19 +107,29 @@ impl Display for RetrieveCondition { comma_joined_stringifiable(tx_hashes, |hash| format!("'{:?}'", hash)) ) } + RetrieveCondition::ByNonce(nonces) => { + write!( + f, + "WHERE nonce IN ({})", + comma_joined_stringifiable(nonces, |nonce| nonce.to_string()) + ) + } } } } pub trait SentPayableDao { fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; - fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError>; - fn retrieve_txs(&self, condition: Option) -> Vec; - fn confirm_tx( + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError>; + fn retrieve_txs(&self, condition: Option) -> Vec; + //TODO potentially atomically + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError>; + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError>; + fn update_statuses( &self, - hash_map: &HashMap, + hash_map: &HashMap, ) -> Result<(), SentPayableDaoError>; - fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; } @@ -156,7 +169,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn insert_new_records(&self, txs: &[Tx]) -> Result<(), SentPayableDaoError> { + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { if txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } @@ -190,8 +203,8 @@ impl SentPayableDao for SentPayableDaoReal<'_> { status \ ) VALUES {}", comma_joined_stringifiable(txs, |tx| { - let amount_checked = checked_conversion::(tx.amount); - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let amount_checked = checked_conversion::(tx.amount_minor); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (amount_high_b, amount_low_b) = BigIntDivider::deconstruct(amount_checked); let (gas_price_wei_high_b, gas_price_wei_low_b) = BigIntDivider::deconstruct(gas_price_wei_checked); @@ -226,7 +239,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } - fn retrieve_txs(&self, condition_opt: Option) -> Vec { + fn retrieve_txs(&self, condition_opt: Option) -> Vec { let raw_sql = "SELECT tx_hash, receiver_address, amount_high_b, amount_low_b, \ timestamp, gas_price_wei_high_b, gas_price_wei_low_b, nonce, status FROM sent_payable" .to_string(); @@ -248,22 +261,22 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Address::from_str(&receiver_address_str[2..]).expect("Failed to parse H160"); let amount_high_b = row.get(2).expectv("amount_high_b"); let amount_low_b = row.get(3).expectv("amount_low_b"); - let amount = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; + let amount_minor = BigIntDivider::reconstitute(amount_high_b, amount_low_b) as u128; let timestamp = row.get(4).expectv("timestamp"); let gas_price_wei_high_b = row.get(5).expectv("gas_price_wei_high_b"); let gas_price_wei_low_b = row.get(6).expectv("gas_price_wei_low_b"); - let gas_price_wei = + let gas_price_minor = BigIntDivider::reconstitute(gas_price_wei_high_b, gas_price_wei_low_b) as u128; let nonce = row.get(7).expectv("nonce"); let status_str: String = row.get(8).expectv("status"); let status = TxStatus::from_str(&status_str).expect("Failed to parse TxStatus"); - Ok(Tx { + Ok(SentTx { hash, receiver_address, - amount, + amount_minor, timestamp, - gas_price_wei, + gas_price_minor, nonce, status, }) @@ -273,18 +286,15 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .collect() } - fn confirm_tx( - &self, - hash_map: &HashMap, - ) -> Result<(), SentPayableDaoError> { + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { if hash_map.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - for (hash, tx_confirmation) in hash_map { + for (hash, tx_block) in hash_map { let sql = format!( "UPDATE sent_payable SET status = '{}' WHERE tx_hash = '{:?}'", - TxStatus::from(tx_confirmation), + TxStatus::from(*tx_block), hash ); @@ -308,12 +318,12 @@ impl SentPayableDao for SentPayableDaoReal<'_> { Ok(()) } - fn replace_records(&self, new_txs: &[Tx]) -> Result<(), SentPayableDaoError> { + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { if new_txs.is_empty() { return Err(SentPayableDaoError::EmptyInput); } - let build_case = |value_fn: fn(&Tx) -> String| { + let build_case = |value_fn: fn(&SentTx) -> String| { new_txs .iter() .map(|tx| format!("WHEN nonce = {} THEN {}", tx.nonce, value_fn(tx))) @@ -323,23 +333,23 @@ impl SentPayableDao for SentPayableDaoReal<'_> { let tx_hash_cases = build_case(|tx| format!("'{:?}'", tx.hash)); let receiver_address_cases = build_case(|tx| format!("'{:?}'", tx.receiver_address)); let amount_high_b_cases = build_case(|tx| { - let amount_checked = checked_conversion::(tx.amount); + let amount_checked = checked_conversion::(tx.amount_minor); let (high, _) = BigIntDivider::deconstruct(amount_checked); high.to_string() }); let amount_low_b_cases = build_case(|tx| { - let amount_checked = checked_conversion::(tx.amount); + let amount_checked = checked_conversion::(tx.amount_minor); let (_, low) = BigIntDivider::deconstruct(amount_checked); low.to_string() }); let timestamp_cases = build_case(|tx| tx.timestamp.to_string()); let gas_price_wei_high_b_cases = build_case(|tx| { - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (high, _) = BigIntDivider::deconstruct(gas_price_wei_checked); high.to_string() }); let gas_price_wei_low_b_cases = build_case(|tx| { - let gas_price_wei_checked = checked_conversion::(tx.gas_price_wei); + let gas_price_wei_checked = checked_conversion::(tx.gas_price_minor); let (_, low) = BigIntDivider::deconstruct(gas_price_wei_checked); low.to_string() }); @@ -391,6 +401,47 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } + fn update_statuses( + &self, + status_updates: &HashMap, + ) -> Result<(), SentPayableDaoError> { + if status_updates.is_empty() { + return Err(SentPayableDaoError::EmptyInput); + } + + let case_statements = status_updates + .iter() + .map(|(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status)) + .join(" "); + let tx_hashes = comma_joined_stringifiable(&status_updates.keys().collect_vec(), |hash| { + format!("'{:?}'", hash) + }); + + let sql = format!( + "UPDATE sent_payable \ + SET \ + status = CASE \ + {case_statements} \ + END \ + WHERE tx_hash IN ({tx_hashes})" + ); + + match self.conn.prepare(&sql).expect("Internal error").execute([]) { + Ok(rows_changed) => { + if rows_changed == status_updates.len() { + Ok(()) + } else { + Err(SentPayableDaoError::PartialExecution(format!( + "Only {} of {} records had their status updated.", + rows_changed, + status_updates.len(), + ))) + } + } + Err(e) => Err(SentPayableDaoError::SqlExecutionFailed(e.to_string())), + } + } + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { if hashes.is_empty() { return Err(SentPayableDaoError::EmptyInput); @@ -421,30 +472,54 @@ impl SentPayableDao for SentPayableDaoReal<'_> { } } +pub trait SentPayableDaoFactory { + fn make(&self) -> Box; +} + +impl SentPayableDaoFactory for DaoFactoryReal { + fn make(&self) -> Box { + Box::new(SentPayableDaoReal::new(self.make_connection())) + } +} + #[cfg(test)] mod tests { - use std::collections::{HashMap, HashSet}; - use std::ops::Add; - use std::str::FromStr; - use std::sync::{Arc, Mutex}; - use std::time::{Duration, UNIX_EPOCH}; - use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, TxConfirmation, TxStatus}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ + ByHash, ByNonce, IsPending, + }; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{ + EmptyInput, PartialExecution, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, + TxStatus, + }; + use crate::accountant::db_access_objects::test_utils::{ + make_read_only_db_connection, TxBuilder, + }; + use crate::accountant::db_access_objects::utils::TxRecordWithHash; + use crate::accountant::test_utils::make_sent_tx; + use crate::blockchain::blockchain_interface::data_structures::TxBlock; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{ + make_block_hash, make_tx_hash, ValidationFailureClockMock, + }; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; use crate::database::test_utils::ConnectionWrapperMock; - use ethereum_types::{ H256, U64}; + use ethereum_types::{H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; - use rusqlite::{Connection}; - use crate::accountant::db_access_objects::failed_payable_dao::{ValidationStatus}; - use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending}; - use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; - use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; - use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, RemoteErrorKind}; - use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; - use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; + use rusqlite::Connection; + use std::collections::{HashMap, HashSet}; + use std::ops::{Add, Sub}; + use std::str::FromStr; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] fn insert_new_records_works() { @@ -530,15 +605,15 @@ mod tests { result, Err(SentPayableDaoError::InvalidInput( "Duplicate hashes found in the input. Input Transactions: \ - [Tx { \ + [SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ nonce: 0, status: Pending(Waiting) }, \ - Tx { \ + SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204020, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204020, gas_price_minor: 0, \ nonce: 0, status: Confirmed { block_hash: \ \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ block_number: 7890123, detection: Reclaim } }]" @@ -657,8 +732,8 @@ mod tests { '0x0000000000000000000000000000000000000000000000000000000123456789', \ '0x0000000000000000000000000000000000000000000000000000000987654321'\ )" - .to_string() ); + assert_eq!(ByNonce(vec![45, 47]).to_string(), "WHERE nonce IN (45, 47)") } #[test] @@ -742,6 +817,35 @@ mod tests { assert_eq!(result, vec![tx1, tx3]); } + #[test] + fn tx_can_be_retrieved_by_nonce() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "tx_can_be_retrieved_by_nonce"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + let tx1 = TxBuilder::default() + .hash(make_tx_hash(123)) + .nonce(33) + .build(); + let tx2 = TxBuilder::default() + .hash(make_tx_hash(456)) + .nonce(34) + .build(); + let tx3 = TxBuilder::default() + .hash(make_tx_hash(789)) + .nonce(35) + .build(); + subject + .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .unwrap(); + + let result = subject.retrieve_txs(Some(ByNonce(vec![33, 35]))); + + assert_eq!(result, vec![tx1, tx3]); + } + #[test] fn confirm_tx_works() { let home_dir = ensure_node_home_directory_exists("sent_payable_dao", "confirm_tx_works"); @@ -757,26 +861,20 @@ mod tests { let updated_pre_assert_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); let pre_assert_status_tx1 = updated_pre_assert_txs[0].status.clone(); let pre_assert_status_tx2 = updated_pre_assert_txs[1].status.clone(); - let tx_confirmation_1 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(3), - block_number: U64::from(1), - }, - detection: Detection::Normal, + let confirmed_tx_block_1 = TxBlock { + block_hash: make_block_hash(3), + block_number: U64::from(1), }; - let tx_confirmation_2 = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(4), - block_number: U64::from(2), - }, - detection: Detection::Reclaim, + let confirmed_tx_block_2 = TxBlock { + block_hash: make_block_hash(4), + block_number: U64::from(2), }; let hash_map = HashMap::from([ - (tx1.hash, tx_confirmation_1.clone()), - (tx2.hash, tx_confirmation_2.clone()), + (tx1.hash, confirmed_tx_block_1.clone()), + (tx2.hash, confirmed_tx_block_2.clone()), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); let updated_txs = subject.retrieve_txs(Some(ByHash(vec![tx1.hash, tx2.hash]))); assert_eq!(result, Ok(())); @@ -787,9 +885,9 @@ mod tests { assert_eq!( updated_txs[0].status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_1.block_info.block_hash), - block_number: tx_confirmation_1.block_info.block_number.as_u64(), - detection: tx_confirmation_1.detection + block_hash: format!("{:?}", confirmed_tx_block_1.block_hash), + block_number: confirmed_tx_block_1.block_number.as_u64(), + detection: Detection::Normal } ); assert_eq!( @@ -799,9 +897,9 @@ mod tests { assert_eq!( updated_txs[1].status, TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation_2.block_info.block_hash), - block_number: tx_confirmation_2.block_info.block_number.as_u64(), - detection: tx_confirmation_2.detection + block_hash: format!("{:?}", confirmed_tx_block_2.block_hash), + block_number: confirmed_tx_block_2.block_number.as_u64(), + detection: Detection::Normal } ); } @@ -821,7 +919,7 @@ mod tests { subject.insert_new_records(&vec![tx]).unwrap(); let hash_map = HashMap::new(); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); } @@ -843,27 +941,21 @@ mod tests { let hash_map = HashMap::from([ ( existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::from(1), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::from(1), }, ), ( non_existent_hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(2), - block_number: U64::from(2), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(2), + block_number: U64::from(2), }, ), ]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -885,16 +977,13 @@ mod tests { let hash = make_tx_hash(1); let hash_map = HashMap::from([( hash, - TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(1), - block_number: U64::default(), - }, - detection: Detection::Normal, + TxBlock { + block_hash: make_block_hash(1), + block_number: U64::default(), }, )]); - let result = subject.confirm_tx(&hash_map); + let result = subject.confirm_txs(&hash_map); assert_eq!( result, @@ -1007,6 +1096,146 @@ mod tests { ) } + #[test] + fn update_statuses_works() { + let home_dir = + ensure_node_home_directory_exists("sent_payable_dao", "update_statuses_works"); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let timestamp_a = SystemTime::now().sub(Duration::from_millis(11)); + let timestamp_b = SystemTime::now().sub(Duration::from_millis(1234)); + let subject = SentPayableDaoReal::new(wrapped_conn); + let mut tx1 = make_sent_tx(456); + tx1.status = TxStatus::Pending(ValidationStatus::Waiting); + let mut tx2 = make_sent_tx(789); + tx2.status = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ))); + let mut tx3 = make_sent_tx(123); + tx3.status = TxStatus::Pending(ValidationStatus::Waiting); + subject + .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone()]) + .unwrap(); + let hashmap = HashMap::from([ + ( + tx1.hash, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ))), + ), + ( + tx2.hash, + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + ( + tx3.hash, + TxStatus::Confirmed { + block_hash: + "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + }, + ), + ]); + + let result = subject.update_statuses(&hashmap); + + let updated_txs = subject.retrieve_txs(None); + assert_eq!(result, Ok(())); + assert_eq!( + updated_txs[0].status, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a) + ))) + ); + assert_eq!( + updated_txs[1].status, + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable + )), + &ValidationFailureClockMock::default().now_result(timestamp_b) + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable + )), + &ValidationFailureClockReal::default() + ) + )) + ); + assert_eq!( + updated_txs[2].status, + TxStatus::Confirmed { + block_hash: "0x0000000000000000000000000000000000000000000000000000000000000002" + .to_string(), + block_number: 123, + detection: Detection::Normal, + } + ); + assert_eq!(updated_txs.len(), 3) + } + + #[test] + fn update_statuses_handles_empty_input_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_statuses_handles_empty_input_error", + ); + let wrapped_conn = DbInitializerReal::default() + .initialize(&home_dir, DbInitializationConfig::test_default()) + .unwrap(); + let subject = SentPayableDaoReal::new(wrapped_conn); + + let result = subject.update_statuses(&HashMap::new()); + + assert_eq!(result, Err(SentPayableDaoError::EmptyInput)); + } + + #[test] + fn update_statuses_handles_sql_error() { + let home_dir = ensure_node_home_directory_exists( + "sent_payable_dao", + "update_statuses_handles_sql_error", + ); + let wrapped_conn = make_read_only_db_connection(home_dir); + let subject = SentPayableDaoReal::new(Box::new(wrapped_conn)); + + let result = subject.update_statuses(&HashMap::from([( + make_tx_hash(1), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockReal::default(), + ))), + )])); + + assert_eq!( + result, + Err(SentPayableDaoError::SqlExecutionFailed( + "attempt to write a readonly database".to_string() + )) + ); + } + #[test] fn replace_records_works_as_expected() { let home_dir = ensure_node_home_directory_exists( @@ -1189,6 +1418,7 @@ mod tests { fn tx_status_from_str_works() { let validation_failure_clock = ValidationFailureClockMock::default() .now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); + assert_eq!( TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), TxStatus::Pending(ValidationStatus::Waiting) @@ -1232,22 +1462,29 @@ mod tests { } #[test] - fn tx_status_can_be_converted_from_tx_confirmation() { - let tx_confirmation = TxConfirmation { - block_info: TransactionBlock { - block_hash: make_block_hash(6), - block_number: 456789_u64.into(), - }, - detection: Detection::Normal, + fn tx_status_can_be_made_from_transaction_block() { + let tx_block = TxBlock { + block_hash: make_block_hash(6), + block_number: 456789_u64.into(), }; assert_eq!( - TxStatus::from(&tx_confirmation), + TxStatus::from(tx_block), TxStatus::Confirmed { - block_hash: format!("{:?}", tx_confirmation.block_info.block_hash), - block_number: u64::try_from(tx_confirmation.block_info.block_number).unwrap(), - detection: tx_confirmation.detection, + block_hash: format!("{:?}", tx_block.block_hash), + block_number: u64::try_from(tx_block.block_number).unwrap(), + detection: Detection::Normal, } ) } + + #[test] + fn tx_record_with_hash_is_implemented_for_sent_tx() { + let sent_tx = make_sent_tx(1234); + let hash = sent_tx.hash; + + let hash_from_trait = sent_tx.hash(); + + assert_eq!(hash_from_trait, hash); + } } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index a1a2eeb31..e395aa2de 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -2,10 +2,11 @@ #![cfg(test)] use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedTx, FailureReason, FailureStatus, ValidationStatus, + FailedTx, FailureReason, FailureStatus, }; -use crate::accountant::db_access_objects::sent_payable_dao::{Tx, TxStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; @@ -50,13 +51,13 @@ impl TxBuilder { self } - pub fn build(self) -> Tx { - Tx { + pub fn build(self) -> SentTx { + SentTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: self.amount_opt.unwrap_or_default(), timestamp: self.timestamp_opt.unwrap_or_else(current_unix_timestamp), - gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), status: self .status_opt @@ -111,9 +112,9 @@ impl FailedTxBuilder { FailedTx { hash: self.hash_opt.unwrap_or_default(), receiver_address: self.receiver_address_opt.unwrap_or_default(), - amount: self.amount_opt.unwrap_or_default(), + amount_minor: self.amount_opt.unwrap_or_default(), timestamp: self.timestamp_opt.unwrap_or_default(), - gas_price_wei: self.gas_price_wei_opt.unwrap_or_default(), + gas_price_minor: self.gas_price_wei_opt.unwrap_or_default(), nonce: self.nonce_opt.unwrap_or_default(), reason: self .reason_opt diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 8fbc875c2..21c9cdc83 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -46,6 +46,10 @@ pub fn from_unix_timestamp(unix_timestamp: i64) -> SystemTime { SystemTime::UNIX_EPOCH + interval } +pub trait TxRecordWithHash { + fn hash(&self) -> TxHash; +} + pub struct DaoFactoryReal { pub data_directory: PathBuf, pub init_config: DbInitializationConfig, diff --git a/node/src/accountant/db_big_integer/big_int_db_processor.rs b/node/src/accountant/db_big_integer/big_int_db_processor.rs index 3ef15278d..c362e3740 100644 --- a/node/src/accountant/db_big_integer/big_int_db_processor.rs +++ b/node/src/accountant/db_big_integer/big_int_db_processor.rs @@ -322,6 +322,7 @@ pub trait DisplayableParamValue: ToSql + Display {} impl DisplayableParamValue for i64 {} impl DisplayableParamValue for &str {} +impl DisplayableParamValue for String {} impl DisplayableParamValue for Wallet {} #[derive(Default)] diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 2ca502557..f54a7dbd6 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -14,10 +14,10 @@ use masq_lib::constants::{SCAN_ERROR, WEIS_IN_GWEI}; use std::cell::{Ref, RefCell}; use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; use crate::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoError}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, SentTx}; use crate::accountant::db_access_objects::utils::{ - remap_payable_accounts, remap_receivable_accounts, CustomQuery, DaoFactoryReal, + remap_payable_accounts, remap_receivable_accounts, CustomQuery, DaoFactoryReal, TxHash, }; use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, @@ -25,13 +25,22 @@ use crate::accountant::financials::visibility_restricted_module::{ use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{StartScanError, Scanners}; -use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableScanResult, Retry, TxHashByTable, +}; +use crate::accountant::scanners::scan_schedulers::{ + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, +}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; +use crate::accountant::scanners::{Scanners, StartScanError}; +use crate::blockchain::blockchain_bridge::{ + BlockMarker, RegisterNewPendingPayables, RetrieveTransactions, +}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, ProcessedPayableFallible, + BlockchainTransaction, ProcessedPayableFallible, StatusReadFromReceiptCheck, }; +use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::bootstrapper::BootstrapperConfig; use crate::database::db_initializer::DbInitializationConfig; use crate::sub_lib::accountant::AccountantSubs; @@ -57,17 +66,17 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, - UiScanRequest, + QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; +use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::ui_gateway::MessageTarget::ClientId; use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; +use std::collections::HashMap; #[cfg(test)] use std::default::Default; use std::fmt::Display; @@ -76,10 +85,6 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; -use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours @@ -89,7 +94,7 @@ pub struct Accountant { earning_wallet: Wallet, payable_dao: Box, receivable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, crashable: bool, scanners: Scanners, scan_schedulers: ScanSchedulers, @@ -136,9 +141,11 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } +pub type TxReceiptResult = Result; + #[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, +pub struct TxReceiptsMessage { + pub results: HashMap, pub response_skeleton_opt: Option, } @@ -230,20 +237,20 @@ impl Handler for Accountant { self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) => self .scan_schedulers .pending_payable .schedule(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::Schedule(scan_type) => unreachable!( "Early stopped pending payable scan was suggested to be followed up \ by the scan for {:?}, which is not supported though", scan_type ), - ScanRescheduleAfterEarlyStop::DoNotSchedule => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the pending payable scan did find results" @@ -267,16 +274,16 @@ impl Handler for Accountant { let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); match scheduling_hint { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - ScanRescheduleAfterEarlyStop::Schedule(other_scan_type) => unreachable!( + ScanReschedulingAfterEarlyStop::Schedule(other_scan_type) => unreachable!( "Early stopped new payable scan was suggested to be followed up by the scan \ for {:?}, which is not supported though", other_scan_type ), - ScanRescheduleAfterEarlyStop::DoNotSchedule => { + ScanReschedulingAfterEarlyStop::DoNotSchedule => { trace!( self.logger, "No early rescheduling, as the new payable scan did find results" @@ -308,10 +315,10 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: TxReceiptsMessage, ctx: &mut Self::Context) -> Self::Result { let response_skeleton_opt = msg.response_skeleton_opt; match self.scanners.finish_pending_payable_scan(msg, &self.logger) { PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) => { @@ -321,18 +328,30 @@ impl Handler for Accountant { .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - // Externally triggered scan should never be allowed to spark a procedure that - // would bring over payables with fresh nonces. The job's done. + // Non-automatic scan for pending payables is not permitted to spark a payable + // scan bringing over new payables with fresh nonces. The job's done here. } else { self.scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger) } } - PendingPayableScanResult::PaymentRetryRequired => self - .scan_schedulers - .payable - .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + PendingPayableScanResult::PaymentRetryRequired(retry_either) => match retry_either { + Either::Left(Retry::RetryPayments) => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + Either::Left(Retry::RetryTxStatusCheckOnly) => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + Either::Right(node_to_ui_msg) => self + .ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"), + }, }; } } @@ -469,7 +488,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable_fingerprints: Vec, + pub tx_hashes: Vec, pub response_skeleton_opt: Option, } @@ -479,14 +498,14 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); fn handle( &mut self, - msg: PendingPayableFingerprintSeeds, + msg: RegisterNewPendingPayables, _ctx: &mut Self::Context, ) -> Self::Result { - self.handle_new_pending_payable_fingerprints(msg) + self.register_new_pending_sent_tx(msg) } } @@ -519,13 +538,12 @@ impl Accountant { let earning_wallet = config.earning_wallet.clone(); let financial_statistics = Rc::new(RefCell::new(FinancialStatistics::default())); let payable_dao = dao_factories.payable_dao_factory.make(); - let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); + let sent_payable_dao = dao_factories.sent_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); let scan_schedulers = ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), - config.when_pending_too_long_sec, Rc::clone(&financial_statistics), ); @@ -534,7 +552,7 @@ impl Accountant { earning_wallet, payable_dao, receivable_dao, - pending_payable_dao, + sent_payable_dao, scanners, crashable: config.crash_point == CrashPoint::Message, scan_schedulers, @@ -561,8 +579,8 @@ impl Accountant { report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), - init_pending_payable_fingerprints: recipient!(addr, PendingPayableFingerprintSeeds), - report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), + register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), + report_transaction_status: recipient!(addr, TxReceiptsMessage), report_sent_payments: recipient!(addr, SentPayables), scan_errors: recipient!(addr, ScanError), ui_message_sub: recipient!(addr, NodeFromUiMessage), @@ -596,12 +614,12 @@ impl Accountant { byte_rate, payload_size ), - Err(e) => panic!("Recording services provided for {} but has hit fatal database error: {:?}", wallet, e) + Err(e) => panic!("Was recording services provided for {} but hit a fatal database error: {:?}", wallet, e) }; } else { warning!( self.logger, - "Declining to record a receivable against our wallet {} for service we provided", + "Declining to record a receivable against our wallet {} for services we provided", wallet ); } @@ -924,7 +942,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -944,7 +962,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::NewPayables, @@ -978,6 +996,8 @@ impl Accountant { .expect("BlockchainBridge is dead"); } Err(e) => { + // It is thrown away and there is no rescheduling downstream because every error + // happening here on the start resolves into a panic by the current design let _ = self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::RetryPayables, e, @@ -990,7 +1010,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -1003,14 +1023,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: ScanRescheduleAfterEarlyStop = match result { + let hint: ScanReschedulingAfterEarlyStop = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1036,7 +1056,7 @@ impl Accountant { scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error(&self.logger, scanner.into(), is_externally_triggered); @@ -1119,27 +1139,23 @@ impl Accountant { } } - fn handle_new_pending_payable_fingerprints(&self, msg: PendingPayableFingerprintSeeds) { - fn serialize_hashes(fingerprints_data: &[HashAndAmount]) -> String { - comma_joined_stringifiable(fingerprints_data, |hash_and_amount| { - format!("{:?}", hash_and_amount.hash) - }) + fn register_new_pending_sent_tx(&self, msg: RegisterNewPendingPayables) { + fn serialize_hashes(tx_hashes: &[SentTx]) -> String { + comma_joined_stringifiable(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } - match self - .pending_payable_dao - .insert_new_fingerprints(&msg.hashes_and_balances, msg.batch_wide_timestamp) - { + + match self.sent_payable_dao.insert_new_records(&msg.new_sent_txs) { Ok(_) => debug!( self.logger, - "Saved new pending payable fingerprints for: {}", - serialize_hashes(&msg.hashes_and_balances) + "Registered new pending payables for: {}", + serialize_hashes(&msg.new_sent_txs) ), Err(e) => error!( self.logger, - "Failed to process new pending payable fingerprints due to '{:?}', \ - disabling the automated confirmation for all these transactions: {}", - e, - serialize_hashes(&msg.hashes_and_balances) + "Failed to save new pending payable records for {} due to '{:?}' which is integral \ + to the function of the automated tx confirmation", + serialize_hashes(&msg.new_sent_txs), + e ), } } @@ -1149,33 +1165,31 @@ impl Accountant { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingPayable { + pub recipient_wallet: Wallet, + pub hash: TxHash, +} + +impl PendingPayable { + pub fn new(recipient_wallet: Wallet, hash: TxHash) -> Self { + Self { + recipient_wallet, + hash, + } + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct PendingPayableId { pub rowid: u64, - pub hash: H256, + pub hash: TxHash, } impl PendingPayableId { - pub fn new(rowid: u64, hash: H256) -> Self { + pub fn new(rowid: u64, hash: TxHash) -> Self { Self { rowid, hash } } - - fn rowids(ids: &[Self]) -> Vec { - ids.iter().map(|id| id.rowid).collect() - } - - fn serialize_hashes_to_string(ids: &[Self]) -> String { - comma_joined_stringifiable(ids, |id| format!("{:?}", id.hash)) - } -} - -impl From for PendingPayableId { - fn from(pending_payable_fingerprint: PendingPayableFingerprint) -> Self { - Self { - hash: pending_payable_fingerprint.hash, - rowid: pending_payable_fingerprint.rowid, - } - } } pub fn comma_joined_stringifiable(collection: &[T], stringify: F) -> String @@ -1216,33 +1230,58 @@ pub fn wei_to_gwei, S: Display + Copy + Div + From> for Accountant { type Result = (); @@ -1314,7 +1352,8 @@ mod tests { fn new_calls_factories_properly() { let config = make_bc_with_defaults(); let payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let receivable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let banned_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); @@ -1323,11 +1362,14 @@ mod tests { .make_result(PayableDaoMock::new()) // For Accountant .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()); // For PendingPayable Scanner - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() - .make_params(&pending_payable_dao_factory_params_arc) - .make_result(PendingPayableDaoMock::new()) // For Accountant - .make_result(PendingPayableDaoMock::new()) // For Payable Scanner - .make_result(PendingPayableDaoMock::new()); // For PendingPayable Scanner + let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() + .make_params(&sent_payable_dao_factory_params_arc) + .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner + .make_result(SentPayableDaoMock::new()); // For PendingPayable Scanner + let failed_payable_dao_factory = FailedPayableDaoFactoryMock::new() + .make_params(&failed_payable_dao_factory_params_arc) + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(vec![])); // For PendingPayableScanner; let receivable_dao_factory = ReceivableDaoFactoryMock::new() .make_params(&receivable_dao_factory_params_arc) .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1343,7 +1385,8 @@ mod tests { config, DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), @@ -1355,9 +1398,13 @@ mod tests { vec![(), (), ()] ); assert_eq!( - *pending_payable_dao_factory_params_arc.lock().unwrap(), + *sent_payable_dao_factory_params_arc.lock().unwrap(), vec![(), (), ()] ); + assert_eq!( + *failed_payable_dao_factory_params_arc.lock().unwrap(), + vec![()] + ); assert_eq!( *receivable_dao_factory_params_arc.lock().unwrap(), vec![(), ()] @@ -1375,12 +1422,16 @@ mod tests { .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()), // For PendingPayable Scanner ); - let pending_payable_dao_factory = Box::new( - PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) // For Accountant - .make_result(PendingPayableDaoMock::new()) // For Payable Scanner - .make_result(PendingPayableDaoMock::new()), // For PendingPayable Scanner + let sent_payable_dao_factory = Box::new( + SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner + .make_result(SentPayableDaoMock::new()), // For PendingPayable Scanner ); + let failed_payable_dao_factory = Box::new( + FailedPayableDaoFactoryMock::new() + .make_result(FailedPayableDaoMock::new().retrieve_txs_result(vec![])), + ); // For PendingPayableScanner; let receivable_dao_factory = Box::new( ReceivableDaoFactoryMock::new() .make_result(ReceivableDaoMock::new()) // For Accountant @@ -1395,7 +1446,8 @@ mod tests { bootstrapper_config, DaoFactories { payable_dao_factory, - pending_payable_dao_factory, + sent_payable_dao_factory, + failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, config_dao_factory, @@ -1555,14 +1607,12 @@ mod tests { #[test] fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { let config = bc_from_earning_wallet(make_wallet("earning_wallet")); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(1, make_tx_hash(123))], - no_rowid_results: vec![], - }); + let tx_hash = make_tx_hash(123); + let sent_payable_dao = + SentPayableDaoMock::default().get_tx_identifiers_result(hashmap! (tx_hash => 1)); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); let mut subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); @@ -1576,7 +1626,7 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: make_wallet("blah"), - hash: make_tx_hash(123), + hash: tx_hash, })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1836,20 +1886,15 @@ mod tests { receivable_scan_interval: Duration::from_millis(10_000), pending_payable_scan_interval: Duration::from_secs(100), }); - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let sent_tx = make_sent_tx(555); + let tx_hash = sent_tx.hash; + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![sent_tx]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge @@ -1879,7 +1924,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable_fingerprints: vec![fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1898,12 +1943,22 @@ mod tests { let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transaction_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Ok(())); + let mut subject = AccountantBuilder::default().build(); + let mut sent_tx = make_sent_tx(123); + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let sent_payable_cache = + PendingPayableCacheMock::default().get_record_by_hash_result(Some(sent_tx.clone())); + let pending_payable_scanner = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Real( + pending_payable_scanner, + ))); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let ui_gateway = ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); @@ -1919,26 +1974,27 @@ mod tests { Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); let subject_addr = subject.start(); - let tx_fingerprint = make_pending_payable_fingerprint(); - let report_tx_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(777), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(456), - block_number: 78901234.into(), - }), - }), - tx_fingerprint.clone(), + let tx_block = TxBlock { + block_hash: make_tx_hash(456), + block_number: 78901234.into(), + }; + let tx_receipts_msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Succeeded(tx_block), )], response_skeleton_opt, }; - subject_addr.try_send(report_tx_receipts).unwrap(); + subject_addr.try_send(tx_receipts_msg).unwrap(); system.run(); let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transaction_confirmed_params, vec![vec![tx_fingerprint]]); + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, + }; + assert_eq!(*transaction_confirmed_params, vec![vec![sent_tx]]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2111,20 +2167,28 @@ mod tests { #[test] fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( ) { + // TODO now only GH-605 logic is missing let response_skeleton_opt = Some(ResponseSkeleton { client_id: 4555, context_id: 5566, }); - // TODO when we have more logic in place with the other cards taken in, we'll need to configure these - // accordingly + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]) - .mark_failures_result(Ok(())); + let sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let sent_payable_dao = SentPayableDaoMock::default() + .retrieve_txs_result(vec![sent_tx.clone()]) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); subject.scan_schedulers.automatic_scans_enabled = false; let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -2139,14 +2203,10 @@ mod tests { let system = System::new("test"); let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, - ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(234), - status: TxStatus::Failed - }), - make_pending_payable_fingerprint() - )], + TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + StatusReadFromReceiptCheck::Reverted + ),], response_skeleton_opt }, &subject_addr @@ -2179,6 +2239,11 @@ mod tests { subject_addr.try_send(pending_payable_request).unwrap(); system.run(); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); + assert_eq!(*insert_new_records_params, vec![vec![expected_failed_tx]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash]]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -2717,7 +2782,7 @@ mod tests { let system = System::new(test_name); let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); - let pp_fingerprint = make_pending_payable_fingerprint(); + let tx_hash = make_tx_hash(456); let payable_scanner = ScannerMock::new() .scan_started_at_result(None) .scan_started_at_result(None) @@ -2741,11 +2806,13 @@ mod tests { .scan_started_at_result(None) .start_scan_params(&scan_params.pending_payable_start_scan) .start_scan_result(Ok(RequestTransactionReceipts { - pending_payable_fingerprints: vec![pp_fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, })) .finish_scan_params(&scan_params.pending_payable_finish_scan) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryPayments), + )); let receivable_scanner = ScannerMock::new() .scan_started_at_result(None) .start_scan_params(&scan_params.receivable_start_scan) @@ -2762,13 +2829,9 @@ mod tests { let (peer_actors, addresses) = peer_actors_builder().build_and_provide_addresses(); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); - let expected_report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(789), - status: TxStatus::Failed, - }), - make_pending_payable_fingerprint(), + let expected_tx_receipts_msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok( + StatusReadFromReceiptCheck::Reverted, )], response_skeleton_opt: None, }; @@ -2781,7 +2844,7 @@ mod tests { }; let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, - expected_report_transaction_receipts.clone(), + expected_tx_receipts_msg.clone(), &subject_addr ); let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( @@ -2810,7 +2873,7 @@ mod tests { &scan_params, ¬ify_and_notify_later_params.pending_payables_notify_later, pending_payable_expected_notify_later_interval, - expected_report_transaction_receipts, + expected_tx_receipts_msg, before, after, ); @@ -2828,9 +2891,9 @@ mod tests { ¬ify_and_notify_later_params.receivables_notify_later, receivable_scan_interval, ); - // Given the assertions prove that the pending payable scanner has run multiple times - // before the new payable scanner started or was scheduled, the front position belongs to - // the one first mentioned, no doubts. + // Since the assertions proved that the pending payable scanner had run multiple times + // before the new payable scanner started or was scheduled, the front position definitely + // belonged to the one first mentioned. } #[derive(Default)] @@ -2840,10 +2903,9 @@ mod tests { payable_finish_scan: Arc>>, pending_payable_start_scan: Arc, Logger, String)>>>, - pending_payable_finish_scan: Arc>>, + pending_payable_finish_scan: Arc>>, receivable_start_scan: Arc, Logger, String)>>>, - // receivable_finish_scan ... not needed } #[derive(Default)] @@ -2862,7 +2924,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -2921,7 +2983,7 @@ mod tests { payable_scanner: ScannerMock, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -2975,7 +3037,7 @@ mod tests { config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, receivable_scanner: ScannerMock< @@ -3048,7 +3110,7 @@ mod tests { Mutex>, >, pending_payable_expected_notify_later_interval: Duration, - expected_report_tx_receipts_msg: ReportTransactionReceipts, + expected_tx_receipts_msg: TxReceiptsMessage, act_started_at: SystemTime, act_finished_at: SystemTime, ) { @@ -3061,12 +3123,9 @@ mod tests { assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); let mut pending_payable_finish_scan_params = scan_params.pending_payable_finish_scan.lock().unwrap(); - let (actual_report_tx_receipts_msg, pp_finish_scan_logger) = + let (actual_tx_receipts_msg, pp_finish_scan_logger) = pending_payable_finish_scan_params.remove(0); - assert_eq!( - actual_report_tx_receipts_msg, - expected_report_tx_receipts_msg - ); + assert_eq!(actual_tx_receipts_msg, expected_tx_receipts_msg); assert_using_the_same_logger(&pp_finish_scan_logger, test_name, Some("pp finish scan")); let scan_for_pending_payables_notify_later_params = scan_for_pending_payables_notify_later_params_arc @@ -3287,11 +3346,13 @@ mod tests { #[test] fn initial_pending_payable_scan_if_some_payables_found() { - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let sent_payable_dao = + SentPayableDaoMock::default().retrieve_txs_result(vec![make_sent_tx(789)]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let system = System::new("test"); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -3304,7 +3365,7 @@ mod tests { System::current().stop(); system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, ScanRescheduleAfterEarlyStop::DoNotSchedule); + assert_eq!(hint, ScanReschedulingAfterEarlyStop::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); @@ -3313,11 +3374,12 @@ mod tests { #[test] fn initial_pending_payable_scan_if_no_payables_found() { - let pending_payable_dao = - PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![]); + let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) + .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) .build(); let flag_before = subject.scanners.initial_pending_payable_scan(); @@ -3326,7 +3388,7 @@ mod tests { let flag_after = subject.scanners.initial_pending_payable_scan(); assert_eq!( hint, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); assert_eq!(flag_before, true); assert_eq!(flag_after, false); @@ -3493,6 +3555,7 @@ mod tests { response_skeleton_opt: None, }; let transaction_hash = make_tx_hash(789); + let tx_hash = make_tx_hash(456); let creditor_wallet = make_wallet("blah"); let counter_msg_2 = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( @@ -3500,23 +3563,16 @@ mod tests { )]), response_skeleton_opt: None, }; - let tx_receipt = TxReceipt { - transaction_hash, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(369369), - block_number: 4444444444u64.into(), - }), - }; - let pending_payable_fingerprint = make_pending_payable_fingerprint(); - let counter_msg_3 = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(tx_receipt), - pending_payable_fingerprint.clone(), - )], + let tx_status = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), + }); + let counter_msg_3 = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], response_skeleton_opt: None, }; let request_transaction_receipts_msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![pending_payable_fingerprint], + tx_hashes: vec![TxHashByTable::SentPayable(tx_hash)], response_skeleton_opt: None, }; let qualified_payables_msg = QualifiedPayablesMessage { @@ -3853,7 +3909,7 @@ mod tests { system.run(); assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); @@ -3942,7 +3998,7 @@ mod tests { fn start_scan_error_in_new_payables_and_unexpected_reaction_by_receivable_scan_scheduling() { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() - .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4051,41 +4107,40 @@ mod tests { } #[test] - fn scan_for_pending_payables_finds_still_pending_payables() { + fn scan_for_pending_payables_finds_various_payables() { init_test_logging(); + let test_name = "scan_for_pending_payables_finds_various_payables"; + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 2, - amount: 7999, - process_error: None, + let tx_hash_1 = make_tx_hash(456); + let tx_hash_2 = make_tx_hash(789); + let tx_hash_3 = make_tx_hash(123); + let expected_composed_msg_for_blockchain_bridge = RequestTransactionReceipts { + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + TxHashByTable::FailedPayable(tx_hash_3), + ], + response_skeleton_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![ - payable_fingerprint_1.clone(), - payable_fingerprint_2.clone(), - ]); - let config = bc_from_earning_wallet(make_wallet("mine")); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Ok(expected_composed_msg_for_blockchain_bridge.clone())); + let consuming_wallet = make_wallet("consuming"); let system = System::new("pending payable scan"); let mut subject = AccountantBuilder::default() - .consuming_wallet(make_paying_wallet(b"consuming")) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) + .logger(Logger::new(test_name)) .build(); - + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); @@ -4095,19 +4150,24 @@ mod tests { }) .unwrap(); + let before = SystemTime::now(); system.run(); + let after = SystemTime::now(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(before <= timestamp && timestamp <= after); + assert_eq!(response_skeleton_opt, None); + assert!( + start_scan_params.is_empty(), + "Should be empty but {:?}", + start_scan_params + ); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let received_msg = blockchain_bridge_recording.get_record::(0); - assert_eq!( - received_msg, - &RequestTransactionReceipts { - pending_payable_fingerprints: vec![payable_fingerprint_1, payable_fingerprint_2], - response_skeleton_opt: None, - } - ); + assert_eq!(received_msg, &expected_composed_msg_for_blockchain_bridge); assert_eq!(blockchain_bridge_recording.len(), 1); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); } #[test] @@ -4163,7 +4223,7 @@ mod tests { { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() - .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + .resolve_rescheduling_on_error_result(ScanReschedulingAfterEarlyStop::Schedule( ScanType::Receivables, )); subject.scan_schedulers.reschedule_on_error_resolver = @@ -4267,7 +4327,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", consuming_wallet, )); } @@ -4312,7 +4372,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", earning_wallet, )); } @@ -4404,7 +4464,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", consuming_wallet )); } @@ -4449,7 +4509,7 @@ mod tests { .is_empty()); TestLogHandler::new().exists_log_containing(&format!( - "WARN: Accountant: Declining to record a receivable against our wallet {} for service we provided", + "WARN: Accountant: Declining to record a receivable against our wallet {} for services we provided", earning_wallet, )); } @@ -4718,8 +4778,8 @@ mod tests { #[test] #[should_panic( - expected = "Recording services provided for 0x000000000000000000000000000000626f6f6761 \ - but has hit fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" + expected = "Was recording services provided for 0x000000000000000000000000000000626f6f6761 \ + but hit a fatal database error: RusqliteError(\"we cannot help ourselves; this is baaad\")" )] fn record_service_provided_panics_on_fatal_errors() { init_test_logging(); @@ -4810,27 +4870,19 @@ mod tests { #[test] fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let expected_wallet = make_wallet("paying_you"); let expected_hash = H256::from("transaction_hash".keccak256()); let expected_rowid = 45623; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(expected_rowid, expected_hash)], - no_rowid_results: vec![], - }); - let payable_dao = PayableDaoMock::new() - .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) - .mark_pending_payables_rowids_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result(hashmap! (expected_hash => expected_rowid)); let system = System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) - .payable_daos(vec![ForPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .build(); let pending_payable_interval = Duration::from_millis(55); subject.scan_schedulers.pending_payable.interval = pending_payable_interval; @@ -4851,14 +4903,8 @@ mod tests { System::current().stop(); system.run(); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); - assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); - let mark_pending_payables_rowids_params = - mark_pending_payables_rowids_params_arc.lock().unwrap(); - assert_eq!( - *mark_pending_payables_rowids_params, - vec![vec![(expected_wallet, expected_rowid)]] - ); + let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); + assert_eq!(*get_tx_identifiers_params, vec![hashset!(expected_hash)]); let pending_payable_notify_later_params = pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -4908,7 +4954,7 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "booga".to_string(), - hashes: vec![make_tx_hash(456)], + hashes: hashset![make_tx_hash(456)], }), response_skeleton_opt: None, }; @@ -4945,17 +4991,33 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryPayments), + )); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.retry_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); let system = System::new(test_name); - let (mut msg, _) = - make_report_transaction_receipts_msg(vec![TxStatus::Pending, TxStatus::Failed]); + let (mut msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Pending, + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Reverted, + }, + ]); let response_skeleton_opt = Some(ResponseSkeleton { client_id: 45, context_id: 7, @@ -4981,21 +5043,146 @@ mod tests { } #[test] - fn accountant_confirms_payable_txs_and_schedules_the_new_payable_scanner_timely() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + fn accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed() { + init_test_logging(); + let test_name = + "accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Left(Retry::RetryTxStatusCheckOnly), + )); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let interval = Duration::from_secs(20); + subject.scan_schedulers.pending_payable.interval = interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), + ); + let system = System::new(test_name); + let msg = TxReceiptsMessage { + results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: None, + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + System::current().stop(); + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payable_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + interval + )] + ); + assert_using_the_same_logger(&logger, test_name, None) + } + + #[test] + fn accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected() + { + init_test_logging(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let test_name = + "accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + subject.ui_message_sub_opt = Some(ui_gateway.start().recipient()); + let response_skeleton = ResponseSkeleton { + client_id: 123, + context_id: 333, + }; + let node_to_ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(123), + body: UiScanResponse {}.tmb(333), + }; + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( + Either::Right(node_to_ui_msg.clone()), + )); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + let system = System::new(test_name); + + let msg = TxReceiptsMessage { + results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: Some(response_skeleton), + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let captured_msg = ui_gateway_recording.get_record::(0); + assert_eq!(captured_msg, &node_to_ui_msg); + assert_using_the_same_logger(&logger, test_name, None) + } + + #[test] + fn accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely() { + init_test_logging(); + let test_name = + "accountant_confirms_all_pending_txs_and_schedules_the_new_payable_scanner_timely"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); let system = System::new("new_payable_scanner_timely"); let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(3)) .unwrap(); @@ -5019,23 +5206,36 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(123), - block_number: U64::from(100), - }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(555)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); System::current().stop(); system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); let (_, last_new_payable_timestamp_actual, scan_interval_actual) = compute_interval_params.remove(0); @@ -5060,19 +5260,24 @@ mod tests { #[test] fn accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + init_test_logging(); + let test_name = + "accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_secs(8)) .unwrap(); @@ -5094,25 +5299,34 @@ mod tests { ); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let tx_block_1 = make_transaction_block(4567); + let tx_block_2 = make_transaction_block(1234); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(123), - block_number: U64::from(100), - }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), + let (msg, _) = make_tx_receipts_msg(vec![ + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_1), + }, + SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::FailedPayable(make_tx_hash(456)), + status: StatusReadFromReceiptCheck::Succeeded(tx_block_2), + }, ]); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); - let system = System::new("new_payable_scanner_asap"); + let system = System::new(test_name); System::current().stop(); system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (captured_msg, logger) = finish_scan_params.remove(0); + assert_eq!(captured_msg, msg); + assert_using_the_same_logger(&logger, test_name, None); + assert!( + finish_scan_params.is_empty(), + "Should be empty but {:?}", + finish_scan_params + ); let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); let (_, last_new_payable_timestamp_actual, scan_interval_actual) = compute_interval_params.remove(0); @@ -5129,20 +5343,23 @@ mod tests { new_payable_notify_later ); let new_payable_notify = new_payable_notify_arc.lock().unwrap(); - assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]) + assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]); } #[test] fn scheduler_for_new_payables_operates_with_proper_now_timestamp() { let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let system = System::new("scheduler_for_new_payables_operates_with_proper_now_timestamp"); + let test_name = "scheduler_for_new_payables_operates_with_proper_now_timestamp"; let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_millis(3500)) .unwrap(); @@ -5158,17 +5375,15 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); + let system = System::new(test_name); let subject_addr = subject.start(); - let (msg, _) = make_report_transaction_receipts_msg(vec![ - TxStatus::Succeeded(TransactionBlock { + let (msg, _) = make_tx_receipts_msg(vec![SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable::SentPayable(make_tx_hash(123)), + status: StatusReadFromReceiptCheck::Succeeded(TxBlock { block_hash: make_tx_hash(123), block_number: U64::from(100), }), - TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(234), - block_number: U64::from(200), - }), - ]); + }]); subject_addr.try_send(msg).unwrap(); @@ -5198,126 +5413,147 @@ mod tests { ); } - fn make_report_transaction_receipts_msg( - status_txs: Vec, - ) -> (ReportTransactionReceipts, Vec) { - let (receipt_result_fingerprint_pairs, fingerprints): (Vec<_>, Vec<_>) = status_txs - .into_iter() - .enumerate() - .map(|(idx, status)| { - let transaction_hash = make_tx_hash(idx as u32); - let transaction_receipt_result = TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash, - status, - }); - let fingerprint = PendingPayableFingerprint { - rowid: idx as u64, - timestamp: from_unix_timestamp(1_000_000_000 * idx as i64), - hash: transaction_hash, - attempt: 2, - amount: 1_000_000 * idx as u128 * idx as u128, - process_error: None, - }; - ( - (transaction_receipt_result, fingerprint.clone()), - fingerprint, - ) - }) - .unzip(); + pub struct SeedsToMakeUpPayableWithStatus { + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + } + + fn make_tx_receipts_msg( + seeds: Vec, + ) -> (TxReceiptsMessage, Vec) { + let (tx_receipt_results, tx_record_vec) = seeds.into_iter().enumerate().fold( + (hashmap![], vec![]), + |(mut tx_receipt_results, mut record_by_table_vec), (idx, seed_params)| { + let tx_hash = seed_params.tx_hash; + let status = seed_params.status; + let (key, value, record) = + make_receipt_check_result_and_record(tx_hash, status, idx as u64); + tx_receipt_results.insert(key, value); + record_by_table_vec.push(record); + (tx_receipt_results, record_by_table_vec) + }, + ); - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: receipt_result_fingerprint_pairs, + let msg = TxReceiptsMessage { + results: tx_receipt_results, response_skeleton_opt: None, }; - (msg, fingerprints) + (msg, tx_record_vec) + } + + fn make_receipt_check_result_and_record( + tx_hash: TxHashByTable, + status: StatusReadFromReceiptCheck, + idx: u64, + ) -> (TxHashByTable, TxReceiptResult, TxByTable) { + match tx_hash { + TxHashByTable::SentPayable(hash) => { + let mut sent_tx = make_sent_tx(1 + idx); + sent_tx.hash = hash; + + if let StatusReadFromReceiptCheck::Succeeded(block) = &status { + sent_tx.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + } + } + + let result = Ok(status); + let record_by_table = TxByTable::SentPayable(sent_tx); + (tx_hash, result, record_by_table) + } + TxHashByTable::FailedPayable(hash) => { + let mut failed_tx = make_failed_tx(1 + idx); + failed_tx.hash = hash; + + let result = Ok(status); + let record_by_table = TxByTable::FailedPayable(failed_tx); + (tx_hash, result, record_by_table) + } + } } #[test] - fn accountant_handles_inserting_new_fingerprints() { + fn accountant_handles_registering_new_pending_payables() { init_test_logging(); - let insert_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .insert_fingerprints_params(&insert_fingerprint_params_arc) - .insert_fingerprints_result(Ok(())); + let test_name = "accountant_handles_registering_new_pending_payables"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); let subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForAccountantBody(pending_payable_dao)]) + .sent_payable_daos(vec![ForAccountantBody(sent_payable_dao)]) + .logger(Logger::new(test_name)) .build(); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); - let timestamp = SystemTime::now(); + let mut sent_tx_1 = make_sent_tx(456); let hash_1 = make_tx_hash(0x6c81c); - let amount_1 = 12345; + sent_tx_1.hash = hash_1; + let mut sent_tx_2 = make_sent_tx(789); let hash_2 = make_tx_hash(0x1b207); - let amount_2 = 87654; - let hash_and_amount_1 = HashAndAmount { - hash: hash_1, - amount: amount_1, - }; - let hash_and_amount_2 = HashAndAmount { - hash: hash_2, - amount: amount_2, - }; - let init_params = vec![hash_and_amount_1, hash_and_amount_2]; - let init_fingerprints_msg = PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: init_params.clone(), - }; + sent_tx_2.hash = hash_2; + let new_sent_txs = vec![sent_tx_1.clone(), sent_tx_2.clone()]; + let msg = RegisterNewPendingPayables { new_sent_txs }; let _ = accountant_subs - .init_pending_payable_fingerprints - .try_send(init_fingerprints_msg) + .register_new_pending_payables + .try_send(msg) .unwrap(); - let system = System::new("ordering payment fingerprint test"); + let system = System::new("ordering payment sent tx record test"); System::current().stop(); assert_eq!(system.run(), 0); - let insert_fingerprint_params = insert_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *insert_fingerprint_params, - vec![(vec![hash_and_amount_1, hash_and_amount_2], timestamp)] - ); - TestLogHandler::new().exists_log_containing( - "DEBUG: Accountant: Saved new pending payable fingerprints for: \ - 0x000000000000000000000000000000000000000000000000000000000006c81c, 0x000000000000000000000000000000000000000000000000000000000001b207", - ); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Registered new pending payables for: \ + 0x000000000000000000000000000000000000000000000000000000000006c81c, \ + 0x000000000000000000000000000000000000000000000000000000000001b207", + )); } #[test] - fn payable_fingerprint_insertion_clearly_failed_and_we_log_it_at_least() { - //despite it doesn't end so here this event would be a cause of a later panic + fn sent_payable_insertion_clearly_failed_and_we_log_at_least() { + // Even though it's factually a filed db operation, which is treated by an instant panic + // due to the broken db reliance, this is an exception. We give out some time to complete + // the actual paying and panic soon after when we figure out, from a different place + // that some sent tx records are missing. This should eventually be eliminated by GH-655 init_test_logging(); - let insert_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .insert_fingerprints_params(&insert_fingerprint_params_arc) - .insert_fingerprints_result(Err(PendingPayableDaoError::InsertionFailed( + let test_name = "sent_payable_insertion_clearly_failed_and_we_log_at_least"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Err(SentPayableDaoError::SqlExecutionFailed( "Crashed".to_string(), ))); - let amount = 2345; - let transaction_hash = make_tx_hash(0x1c8); - let hash_and_amount = HashAndAmount { - hash: transaction_hash, - amount, - }; + let tx_hash_1 = make_tx_hash(0x1c8); + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.hash = tx_hash_1; + let tx_hash_2 = make_tx_hash(0x1b2); + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.hash = tx_hash_2; let subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForAccountantBody(pending_payable_dao)]) + .sent_payable_daos(vec![ForAccountantBody(sent_payable_dao)]) + .logger(Logger::new(test_name)) .build(); - let timestamp = SystemTime::now(); - let report_new_fingerprints = PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: vec![hash_and_amount], + let msg = RegisterNewPendingPayables { + new_sent_txs: vec![sent_tx_1.clone(), sent_tx_2.clone()], }; - let _ = subject.handle_new_pending_payable_fingerprints(report_new_fingerprints); + let _ = subject.register_new_pending_sent_tx(msg); - let insert_fingerprint_params = insert_fingerprint_params_arc.lock().unwrap(); - assert_eq!( - *insert_fingerprint_params, - vec![(vec![hash_and_amount], timestamp)] - ); - TestLogHandler::new().exists_log_containing("ERROR: Accountant: Failed to process \ - new pending payable fingerprints due to 'InsertionFailed(\"Crashed\")', disabling the automated \ - confirmation for all these transactions: 0x00000000000000000000000000000000000000000000000000000000000001c8"); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Failed to save new pending payable records for \ + 0x00000000000000000000000000000000000000000000000000000000000001c8, \ + 0x00000000000000000000000000000000000000000000000000000000000001b2 \ + due to 'SqlExecutionFailed(\"Crashed\")' which is integral to the function \ + of the automated tx confirmation" + )); } const EXAMPLE_RESPONSE_SKELETON: ResponseSkeleton = ResponseSkeleton { @@ -5329,7 +5565,7 @@ mod tests { #[test] fn handling_scan_error_for_externally_triggered_payables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_payables", ScanError { scan_type: ScanType::Payables, @@ -5341,19 +5577,21 @@ mod tests { #[test] fn handling_scan_error_for_externally_triggered_pending_payables() { - assert_scan_error_is_handled_properly( + let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); + test_scan_error_is_handled_properly_more_specifically( "handling_scan_error_for_externally_triggered_pending_payables", ScanError { scan_type: ScanType::PendingPayables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, + Some(additional_test_setup_and_assertions), ); } #[test] fn handling_scan_error_for_externally_triggered_receivables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_receivables", ScanError { scan_type: ScanType::Receivables, @@ -5365,7 +5603,7 @@ mod tests { #[test] fn handling_scan_error_for_internally_triggered_payables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_payables", ScanError { scan_type: ScanType::Payables, @@ -5377,19 +5615,21 @@ mod tests { #[test] fn handling_scan_error_for_internally_triggered_pending_payables() { - assert_scan_error_is_handled_properly( + let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); + test_scan_error_is_handled_properly_more_specifically( "handling_scan_error_for_internally_triggered_pending_payables", ScanError { scan_type: ScanType::PendingPayables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + Some(additional_test_setup_and_assertions), ); } #[test] fn handling_scan_error_for_internally_triggered_receivables() { - assert_scan_error_is_handled_properly( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_receivables", ScanError { scan_type: ScanType::Receivables, @@ -5399,6 +5639,34 @@ mod tests { ); } + fn prepare_setup_and_assertion_fns() -> (Box, Box) { + let ensure_empty_cache_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_sent_tx_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_failed_tx_params_arc); + let scanner = PendingPayableScannerBuilder::new() + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + ( + Box::new(|scanners: &mut Scanners| { + scanners.replace_scanner(ScannerReplacement::PendingPayable( + ReplacementType::Real(scanner), + )); + }) as Box, + Box::new(move || { + let ensure_empty_cache_sent_tx_params = + ensure_empty_cache_sent_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_sent_tx_params, vec![()]); + let ensure_empty_cache_failed_tx_params = + ensure_empty_cache_failed_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_failed_tx_params, vec![()]); + }) as Box, + ) + } + #[test] fn financials_request_with_nothing_to_respond_to_is_refused() { let system = System::new("test"); @@ -6144,15 +6412,32 @@ mod tests { let _: u64 = wei_to_gwei(u128::MAX); } - fn assert_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + fn test_scan_error_is_handled_properly(test_name: &str, message: ScanError) { + test_scan_error_is_handled_properly_more_specifically(test_name, message, None) + } + fn test_scan_error_is_handled_properly_more_specifically( + test_name: &str, + message: ScanError, + additional_assertion_opt: Option<(Box, Box)>, + ) { init_test_logging(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); - subject - .scanners - .reset_scan_started(message.scan_type, MarkScanner::Started(SystemTime::now())); + let (adjust_scanner, run_additional_assertion) = match additional_assertion_opt { + Some(two_functions) => two_functions, + None => ( + Box::new(|scanners: &mut Scanners| { + scanners.reset_scan_started( + message.scan_type, + MarkScanner::Started(SystemTime::now()), + ) + }) as Box, + Box::new(|| ()) as Box, + ), + }; + adjust_scanner(&mut subject.scanners); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -6209,6 +6494,7 @@ mod tests { )); } } + run_additional_assertion(); } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index d49cf3efc..71ff0f62c 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -8,16 +8,15 @@ pub mod scanners_utils; pub mod test_utils; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; -use crate::accountant::db_access_objects::pending_payable_dao::{PendingPayable, PendingPayableDao}; use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::{ScanError, ScanForPendingPayables, ScanForRetryPayables}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_sent_tx_record, investigate_debt_extremes, payables_debug_summary, separate_errors, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMissingInDb}; +use crate::accountant::{PendingPayable, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, + TxReceiptsMessage, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; @@ -41,7 +40,8 @@ use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use variant_count::VariantCount; -use web3::types::H256; +use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao}; +use crate::accountant::db_access_objects::utils::{TxHash}; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; @@ -59,7 +59,7 @@ pub struct Scanners { dyn PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, >, >, @@ -77,21 +77,20 @@ impl Scanners { pub fn new( dao_factories: DaoFactories, payment_thresholds: Rc, - when_pending_too_long_sec: u64, financial_statistics: Rc>, ) -> Self { let payable = Box::new(PayableScanner::new( dao_factories.payable_dao_factory.make(), - dao_factories.pending_payable_dao_factory.make(), + dao_factories.sent_payable_dao_factory.make(), Rc::clone(&payment_thresholds), Box::new(PaymentAdjusterReal::new()), )); let pending_payable = Box::new(PendingPayableScanner::new( dao_factories.payable_dao_factory.make(), - dao_factories.pending_payable_dao_factory.make(), + dao_factories.sent_payable_dao_factory.make(), + dao_factories.failed_payable_dao_factory.make(), Rc::clone(&payment_thresholds), - when_pending_too_long_sec, Rc::clone(&financial_statistics), )); @@ -215,6 +214,7 @@ impl Scanners { } (None, None) => (), } + self.pending_payable .start_scan(wallet, timestamp, response_skeleton_opt, logger) } @@ -255,7 +255,7 @@ impl Scanners { pub fn finish_pending_payable_scan( &mut self, - msg: ReportTransactionReceipts, + msg: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { self.pending_payable.finish_scan(msg, logger) @@ -275,6 +275,7 @@ impl Scanners { self.payable.mark_as_ended(logger); } ScanType::PendingPayables => { + self.empty_caches(logger); self.pending_payable.mark_as_ended(logger); } ScanType::Receivables => { @@ -283,6 +284,20 @@ impl Scanners { }; } + fn empty_caches(&mut self, logger: &Logger) { + let pending_payable_scanner = self + .pending_payable + .as_any_mut() + .downcast_mut::() + .expect("mismatched types"); + pending_payable_scanner + .current_sent_payables + .ensure_empty_cache(logger); + pending_payable_scanner + .yet_unproven_failed_payables + .ensure_empty_cache(logger); + } + pub fn try_skipping_payable_adjustment( &self, msg: BlockchainAgentWithContextMessage, @@ -308,7 +323,7 @@ impl Scanners { } // This is a helper function reducing a boilerplate of complex trait resolving where - // the compiler requires to specify which trigger message distinguish the scan to run. + // the compiler requires to specify which trigger message distinguishes the scan to run. // The payable scanner offers two modes through doubled implementations of StartableScanner // which uses the trigger message type as the only distinction between them. fn start_correct_payable_scanner<'a, TriggerMessage>( @@ -428,7 +443,7 @@ impl ScannerCommon { None => { error!( logger, - "Called scan_finished() for {:?} scanner but timestamp was not found", + "Called scan_finished() for {:?} scanner but could not find any timestamp", scan_type ); } @@ -461,7 +476,7 @@ pub struct PayableScanner { pub payable_threshold_gauge: Box, pub common: ScannerCommon, pub payable_dao: Box, - pub pending_payable_dao: Box, + pub sent_payable_dao: Box, pub payment_adjuster: Box, } @@ -536,8 +551,9 @@ impl Scanner for PayableScanner { ); if !sent_payables.is_empty() { - self.mark_pending_payable(&sent_payables, logger); + self.check_on_missing_sent_tx_records(&sent_payables); } + self.handle_sent_payable_errors(err_opt, logger); self.mark_as_ended(logger); @@ -600,14 +616,14 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { impl PayableScanner { pub fn new( payable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, payment_thresholds: Rc, payment_adjuster: Box, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, - pending_payable_dao, + sent_payable_dao, payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), payment_adjuster, } @@ -675,169 +691,200 @@ impl PayableScanner { } } - fn separate_existent_and_nonexistent_fingerprints<'a>( - &'a self, - sent_payables: &[&'a PendingPayable], - ) -> (Vec, Vec) { - let hashes = sent_payables + fn check_for_missing_records( + &self, + just_baked_sent_payables: &[&PendingPayable], + ) -> Vec { + let actual_sent_payables_len = just_baked_sent_payables.len(); + let hashset_with_hashes_to_eliminate_duplicates = just_baked_sent_payables .iter() .map(|pending_payable| pending_payable.hash) - .collect::>(); - let mut sent_payables_hashmap = sent_payables - .iter() - .map(|payable| (payable.hash, &payable.recipient_wallet)) - .collect::>(); - - let transaction_hashes = self.pending_payable_dao.fingerprints_rowids(&hashes); - let mut hashes_from_db = transaction_hashes - .rowid_results - .iter() - .map(|(_rowid, hash)| *hash) - .collect::>(); - for hash in &transaction_hashes.no_rowid_results { - hashes_from_db.insert(*hash); - } - let sent_payables_hashes = hashes.iter().copied().collect::>(); + .collect::>(); - if !Self::is_symmetrical(sent_payables_hashes, hashes_from_db) { + if hashset_with_hashes_to_eliminate_duplicates.len() != actual_sent_payables_len { panic!( - "Inconsistency in two maps, they cannot be matched by hashes. Data set directly \ - sent from BlockchainBridge: {:?}, set derived from the DB: {:?}", - sent_payables, transaction_hashes - ) + "Found duplicates in the recent sent txs: {:?}", + just_baked_sent_payables + ); } - let pending_payables_with_rowid = transaction_hashes - .rowid_results - .into_iter() - .map(|(rowid, hash)| { - let wallet = sent_payables_hashmap - .remove(&hash) - .expect("expect transaction hash, but it disappear"); - PendingPayableMetadata::new(wallet, hash, Some(rowid)) - }) - .collect_vec(); - let pending_payables_without_rowid = transaction_hashes - .no_rowid_results + let transaction_hashes_and_rowids_from_db = self + .sent_payable_dao + .get_tx_identifiers(&hashset_with_hashes_to_eliminate_duplicates); + let hashes_from_db = transaction_hashes_and_rowids_from_db + .keys() + .copied() + .collect::>(); + + let missing_sent_payables_hashes: Vec = hashset_with_hashes_to_eliminate_duplicates + .difference(&hashes_from_db) + .copied() + .collect(); + + let mut sent_payables_hashmap = just_baked_sent_payables + .iter() + .map(|payable| (payable.hash, &payable.recipient_wallet)) + .collect::>(); + missing_sent_payables_hashes .into_iter() .map(|hash| { - let wallet = sent_payables_hashmap + let wallet_address = sent_payables_hashmap .remove(&hash) - .expect("expect transaction hash, but it disappear"); - PendingPayableMetadata::new(wallet, hash, None) + .expectv("wallet") + .address(); + PendingPayableMissingInDb::new(wallet_address, hash) }) - .collect_vec(); - - (pending_payables_with_rowid, pending_payables_without_rowid) + .collect() } - fn is_symmetrical( - sent_payables_hashes: HashSet, - fingerptint_hashes: HashSet, - ) -> bool { - sent_payables_hashes == fingerptint_hashes - } - - fn mark_pending_payable(&self, sent_payments: &[&PendingPayable], logger: &Logger) { - fn missing_fingerprints_msg(nonexistent: &[PendingPayableMetadata]) -> String { + fn check_on_missing_sent_tx_records(&self, sent_payments: &[&PendingPayable]) { + fn missing_record_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { format!( - "Expected pending payable fingerprints for {} were not found; system unreliable", - comma_joined_stringifiable(nonexistent, |pp_triple| format!( - "(tx: {:?}, to wallet: {})", - pp_triple.hash, pp_triple.recipient + "Expected sent-payable records for {} were not found. The system has become unreliable", + comma_joined_stringifiable(nonexistent, |missing_sent_tx_ids| format!( + "(tx: {:?}, to wallet: {:?})", + missing_sent_tx_ids.hash, missing_sent_tx_ids.recipient )) ) } - fn ready_data_for_supply<'a>( - existent: &'a [PendingPayableMetadata], - ) -> Vec<(&'a Wallet, u64)> { - existent - .iter() - .map(|pp_triple| (pp_triple.recipient, pp_triple.rowid_opt.expectv("rowid"))) - .collect() - } - let (existent, nonexistent) = - self.separate_existent_and_nonexistent_fingerprints(sent_payments); - let mark_pp_input_data = ready_data_for_supply(&existent); - if !mark_pp_input_data.is_empty() { - if let Err(e) = self - .payable_dao - .as_ref() - .mark_pending_payables_rowids(&mark_pp_input_data) - { - mark_pending_payable_fatal_error( - sent_payments, - &nonexistent, - e, - missing_fingerprints_msg, - logger, - ) - } - debug!( - logger, - "Payables {} marked as pending in the payable table", - comma_joined_stringifiable(sent_payments, |pending_p| format!( - "{:?}", - pending_p.hash - )) - ) - } - if !nonexistent.is_empty() { - panic!("{}", missing_fingerprints_msg(&nonexistent)) + let missing_sent_tx_records = self.check_for_missing_records(sent_payments); + if !missing_sent_tx_records.is_empty() { + panic!("{}", missing_record_msg(&missing_sent_tx_records)) } } + // TODO this has become dead (GH-662) + #[allow(dead_code)] + fn mark_pending_payable(&self, _sent_payments: &[&PendingPayable], _logger: &Logger) { + todo!("remove me when the time comes") + // fn missing_fingerprints_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { + // format!( + // "Expected pending payable fingerprints for {} were not found; system unreliable", + // comma_joined_stringifiable(nonexistent, |pp_triple| format!( + // "(tx: {:?}, to wallet: {})", + // pp_triple.hash, pp_triple.recipient + // )) + // ) + // } + // fn ready_data_for_supply<'a>( + // existent: &'a [PendingPayableMissingInDb], + // ) -> Vec<(&'a Wallet, u64)> { + // existent + // .iter() + // .map(|pp_triple| (pp_triple.recipient, pp_triple.rowid_opt.expectv("rowid"))) + // .collect() + // } + // + // // TODO eventually should be taken over by GH-655 + // let missing_sent_tx_records = + // self.check_for_missing_records(sent_payments); + // + // if !existent.is_empty() { + // if let Err(e) = self + // .payable_dao + // .as_ref() + // .mark_pending_payables_rowids(&existent) + // { + // mark_pending_payable_fatal_error( + // sent_payments, + // &nonexistent, + // e, + // missing_fingerprints_msg, + // logger, + // ) + // } + // debug!( + // logger, + // "Payables {} marked as pending in the payable table", + // comma_joined_stringifiable(sent_payments, |pending_p| format!( + // "{:?}", + // pending_p.hash + // )) + // ) + // } + // if !missing_sent_tx_records.is_empty() { + // panic!("{}", missing_fingerprints_msg(&missing_sent_tx_records)) + // } + } + fn handle_sent_payable_errors( &self, err_opt: Option, logger: &Logger, ) { - if let Some(err) = err_opt { + fn decide_on_tx_error_handling( + err: &PayableTransactingErrorEnum, + ) -> Option<&HashSet> { match err { LocallyCausedError(PayableTransactionError::Sending { hashes, .. }) - | RemotelyCausedErrors(hashes) => { - self.discard_failed_transactions_with_possible_fingerprints(hashes, logger) - } - non_fatal => - debug!( - logger, - "Ignoring a non-fatal error on our end from before the transactions are hashed: {:?}", - non_fatal - ) + | RemotelyCausedErrors(hashes) => Some(hashes), + _ => None, + } + } + + if let Some(err) = err_opt { + if let Some(hashes) = decide_on_tx_error_handling(&err) { + self.discard_failed_transactions_with_possible_sent_tx_records(hashes, logger) + } else { + debug!( + logger, + "A non-fatal error {:?} will be ignored as it is from before any tx could \ + even be hashed", + err + ) } } } - fn discard_failed_transactions_with_possible_fingerprints( + fn discard_failed_transactions_with_possible_sent_tx_records( &self, - hashes_of_failed: Vec, + hashes_of_failed: &HashSet, logger: &Logger, ) { - fn serialize_hashes(hashes: &[H256]) -> String { + fn serialize_hashes(hashes: &[TxHash]) -> String { comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) } - let existent_and_nonexistent = self - .pending_payable_dao - .fingerprints_rowids(&hashes_of_failed); - let missing_fgp_err_msg_opt = err_msg_for_failure_with_expected_but_missing_fingerprints( - existent_and_nonexistent.no_rowid_results, + + let existent_sent_tx_in_db = self.sent_payable_dao.get_tx_identifiers(&hashes_of_failed); + + let hashes_of_missing_sent_tx = hashes_of_failed + .difference( + &existent_sent_tx_in_db + .keys() + .copied() + .collect::>(), + ) + .copied() + .sorted() + .collect(); + + let missing_fgp_err_msg_opt = err_msg_for_failure_with_expected_but_missing_sent_tx_record( + hashes_of_missing_sent_tx, serialize_hashes, ); - if !existent_and_nonexistent.rowid_results.is_empty() { - let (ids, hashes) = separate_rowids_and_hashes(existent_and_nonexistent.rowid_results); + + if !existent_sent_tx_in_db.is_empty() { + let hashes = existent_sent_tx_in_db + .keys() + .copied() + .sorted() + .collect_vec(); warning!( logger, - "Deleting fingerprints for failed transactions {}", + "Deleting sent payable records for {}", serialize_hashes(&hashes) ); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&ids) { + if let Err(e) = self + .sent_payable_dao + .delete_records(&existent_sent_tx_in_db.keys().copied().collect()) + { if let Some(msg) = missing_fgp_err_msg_opt { error!(logger, "{}", msg) }; panic!( - "Database corrupt: payable fingerprint deletion for transactions {} \ - failed due to {:?}", + "Database corrupt: sent payable record deletion for txs {} failed \ + due to {:?}", serialize_hashes(&hashes), e ) @@ -968,51 +1015,84 @@ impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScann #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; - use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayable, PendingPayableDaoError, TransactionHashes, + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, SentPayableDaoError, SentTx, TxStatus, }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; - use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables}; - use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, + UnpricedQualifiedPayables, + }; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, Retry, + TxHashByTable, + }; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + OperationOutcome, PayableScanResult, + }; + use crate::accountant::scanners::test_utils::{ + assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, + PendingPayableCacheMock, ReplacementType, ScannerReplacement, + }; + use crate::accountant::scanners::{ + ManulTriggerError, PayableScanner, PendingPayableScanner, ReceivableScanner, Scanner, + ScannerCommon, Scanners, StartScanError, StartableScanner, + }; + use crate::accountant::test_utils::{ + make_custom_payment_thresholds, make_failed_tx, make_payable_account, + make_qualified_and_unqualified_payables, make_receivable_account, make_sent_tx, + BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, + FailedPayableDaoMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, + PayableThresholdsGaugeMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, + ReceivableDaoMock, ReceivableScannerBuilder, SentPayableDaoFactoryMock, SentPayableDaoMock, + }; + use crate::accountant::{ + gwei_to_wei, PendingPayable, ReceivedPayments, RequestTransactionReceipts, ScanError, + ScanForRetryPayables, SentPayables, TxReceiptsMessage, + }; + use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ BlockchainTransaction, ProcessedPayableFallible, RpcPayableFailure, + StatusReadFromReceiptCheck, TxBlock, }; - use crate::blockchain::test_utils::make_tx_hash; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, RemoteError, RemoteErrorKind, + }; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::mocks::ConfigDaoMock; use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, - DEFAULT_PAYMENT_THRESHOLDS, + DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::{Message, System}; use ethereum_types::U64; + use itertools::Either; use masq_lib::logger::Logger; + use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::{Regex}; + use masq_lib::ui_gateway::NodeToUiMessage; + use regex::Regex; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; + use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use web3::types::{H256}; use web3::Error; - use masq_lib::messages::ScanType; - use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; - use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; impl Scanners { pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { @@ -1096,18 +1176,19 @@ mod tests { let payable_dao_factory = PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()); - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()); - let receivable_dao = ReceivableDaoMock::new(); - let receivable_dao_factory = ReceivableDaoFactoryMock::new().make_result(receivable_dao); + let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()); + let failed_payable_dao_factory = + FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new()); + let receivable_dao_factory = + ReceivableDaoFactoryMock::new().make_result(ReceivableDaoMock::new()); let banned_dao_factory = BannedDaoFactoryMock::new().make_result(BannedDaoMock::new()); let set_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_mock = ConfigDaoMock::new() .set_params(&set_params_arc) .set_result(Ok(())); let config_dao_factory = ConfigDaoFactoryMock::new().make_result(config_dao_mock); - let when_pending_too_long_sec = 1234; let financial_statistics = FinancialStatistics { total_paid_payable_wei: 1, total_paid_receivable_wei: 2, @@ -1119,13 +1200,13 @@ mod tests { let mut scanners = Scanners::new( DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), }, Rc::clone(&payment_thresholds_rc), - when_pending_too_long_sec, Rc::new(RefCell::new(financial_statistics.clone())), ); @@ -1136,8 +1217,8 @@ mod tests { .unwrap(); let pending_payable_scanner = scanners .pending_payable - .as_any() - .downcast_ref::() + .as_any_mut() + .downcast_mut::() .unwrap(); let receivable_scanner = scanners .receivable @@ -1151,10 +1232,6 @@ mod tests { assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!(scanners.aware_of_unresolved_pending_payable, false); assert_eq!(scanners.initial_pending_payable_scan, true); - assert_eq!( - pending_payable_scanner.when_pending_too_long_sec, - when_pending_too_long_sec - ); assert_eq!( *pending_payable_scanner.financial_statistics.borrow(), financial_statistics @@ -1167,6 +1244,19 @@ mod tests { pending_payable_scanner.common.initiated_at_opt.is_some(), false ); + let dumped_records = pending_payable_scanner + .yet_unproven_failed_payables + .dump_cache(); + assert!( + dumped_records.is_empty(), + "There should be no yet unproven failures but found {:?}.", + dumped_records + ); + assert_eq!( + receivable_scanner.common.payment_thresholds.as_ref(), + &payment_thresholds + ); + assert_eq!(receivable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!( *receivable_scanner.financial_statistics.borrow(), financial_statistics @@ -1472,9 +1562,9 @@ mod tests { fn payable_scanner_handles_sent_payable_message() { init_test_logging(); let test_name = "payable_scanner_handles_sent_payable_message"; - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let mark_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let correct_payable_hash_1 = make_tx_hash(0x6f); let correct_payable_rowid_1 = 125; let correct_payable_wallet_1 = make_wallet("tralala"); @@ -1485,7 +1575,7 @@ mod tests { let failure_payable_wallet_2 = make_wallet("hihihi"); let failure_payable_2 = RpcPayableFailure { rpc_error: Error::InvalidResponse( - "Learn how to write before you send your garbage!".to_string(), + "Ged rid of your illiteracy before you send your garbage!".to_string(), ), recipient_wallet: failure_payable_wallet_2, hash: failure_payable_hash_2, @@ -1495,28 +1585,21 @@ mod tests { let correct_payable_wallet_3 = make_wallet("booga"); let correct_pending_payable_3 = PendingPayable::new(correct_payable_wallet_3.clone(), correct_payable_hash_3); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (correct_payable_rowid_3, correct_payable_hash_3), - (correct_payable_rowid_1, correct_payable_hash_1), - ], - no_rowid_results: vec![], - }) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(failure_payable_rowid_2, failure_payable_hash_2)], - no_rowid_results: vec![], - }) - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result(hashmap!(correct_payable_hash_3 => correct_payable_rowid_3, + correct_payable_hash_1 => correct_payable_rowid_1, + )) + .get_tx_identifiers_result(hashmap!(failure_payable_hash_2 => failure_payable_rowid_2)) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); let payable_dao = PayableDaoMock::new() .mark_pending_payables_rowids_params(&mark_pending_payables_params_arc) .mark_pending_payables_rowids_result(Ok(())) .mark_pending_payables_rowids_result(Ok(())); let mut payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let logger = Logger::new(test_name); let sent_payable = SentPayables { @@ -1547,41 +1630,30 @@ mod tests { assert_eq!(is_scan_running, false); assert_eq!(aware_of_unresolved_pending_payable_before, false); assert_eq!(aware_of_unresolved_pending_payable_after, true); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); + let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); assert_eq!( - *fingerprints_rowids_params, + *get_tx_identifiers_params, vec![ - vec![correct_payable_hash_1, correct_payable_hash_3], - vec![failure_payable_hash_2] + hashset![correct_payable_hash_1, correct_payable_hash_3], + hashset![failure_payable_hash_2] ] ); - let mark_pending_payables_params = mark_pending_payables_params_arc.lock().unwrap(); - assert_eq!( - *mark_pending_payables_params, - vec![vec![ - (correct_payable_wallet_3, correct_payable_rowid_3), - (correct_payable_wallet_1, correct_payable_rowid_1), - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); + let delete_records_params = delete_records_params_arc.lock().unwrap(); assert_eq!( - *delete_fingerprints_params, - vec![vec![failure_payable_rowid_2]] + *delete_records_params, + vec![hashset![failure_payable_hash_2]] ); let log_handler = TestLogHandler::new(); log_handler.assert_logs_contain_in_order(vec![ &format!( - "WARN: {test_name}: Remote transaction failure: 'Got invalid response: Learn how to write before you send your garbage!' \ - for payment to 0x0000000000000000000000000000686968696869 and transaction hash \ - 0x00000000000000000000000000000000000000000000000000000000000000de. Please check your blockchain service URL configuration" + "WARN: {test_name}: Remote sent payable failure 'Got invalid response: Ged rid of \ + your illiteracy before you send your garbage!' \ + for wallet 0x0000000000000000000000000000686968696869 and tx hash \ + 0x00000000000000000000000000000000000000000000000000000000000000de" ), &format!("DEBUG: {test_name}: Got 2 properly sent payables of 3 attempts"), &format!( - "DEBUG: {test_name}: Payables 0x000000000000000000000000000000000000000000000000000000000000006f, \ - 0x000000000000000000000000000000000000000000000000000000000000014d marked as pending in the payable table" - ), - &format!( - "WARN: {test_name}: Deleting fingerprints for failed transactions \ + "WARN: {test_name}: Deleting sent payable records for \ 0x00000000000000000000000000000000000000000000000000000000000000de" ), ]); @@ -1590,44 +1662,86 @@ mod tests { )); } + #[test] + fn no_missing_records() { + let wallet_1 = make_wallet("abc"); + let hash_1 = make_tx_hash(123); + let wallet_2 = make_wallet("def"); + let hash_2 = make_tx_hash(345); + let wallet_3 = make_wallet("ghi"); + let hash_3 = make_tx_hash(546); + let wallet_4 = make_wallet("jkl"); + let hash_4 = make_tx_hash(678); + let pending_payables_owned = vec![ + PendingPayable::new(wallet_1.clone(), hash_1), + PendingPayable::new(wallet_2.clone(), hash_2), + PendingPayable::new(wallet_3.clone(), hash_3), + PendingPayable::new(wallet_4.clone(), hash_4), + ]; + let pending_payables_ref = pending_payables_owned + .iter() + .collect::>(); + let sent_payable_dao = SentPayableDaoMock::new().get_tx_identifiers_result( + hashmap!(hash_4 => 4, hash_1 => 1, hash_3 => 3, hash_2 => 2), + ); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .build(); + + let missing_records = subject.check_for_missing_records(&pending_payables_ref); + + assert!( + missing_records.is_empty(), + "We thought the vec would be empty but contained: {:?}", + missing_records + ); + } + #[test] #[should_panic( - expected = "Expected pending payable fingerprints for (tx: 0x0000000000000000000000000000000000000000000000000000000000000315, \ - to wallet: 0x000000000000000000000000000000626f6f6761), (tx: 0x000000000000000000000000000000000000000000000000000000000000007b, \ - to wallet: 0x00000000000000000000000000000061676f6f62) were not found; system unreliable" + expected = "Found duplicates in the recent sent txs: [PendingPayable { recipient_wallet: \ + Wallet { kind: Address(0x0000000000000000000000000000000000616263) }, hash: \ + 0x000000000000000000000000000000000000000000000000000000000000007b }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x0000000000000000000000000000000000646566) }, \ + hash: 0x00000000000000000000000000000000000000000000000000000000000001c8 }, \ + PendingPayable { recipient_wallet: Wallet { kind: \ + Address(0x0000000000000000000000000000000000676869) }, hash: \ + 0x00000000000000000000000000000000000000000000000000000000000001c8 }, PendingPayable { \ + recipient_wallet: Wallet { kind: Address(0x00000000000000000000000000000000006a6b6c) }, \ + hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }]" )] - fn payable_scanner_panics_when_fingerprints_for_correct_payments_not_found() { - let hash_1 = make_tx_hash(0x315); - let payment_1 = PendingPayable::new(make_wallet("booga"), hash_1); - let hash_2 = make_tx_hash(0x7b); - let payment_2 = PendingPayable::new(make_wallet("agoob"), hash_2); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![], - no_rowid_results: vec![hash_1, hash_2], - }); - let payable_dao = PayableDaoMock::new(); - let mut subject = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + fn just_baked_pending_payables_contain_duplicates() { + let hash_1 = make_tx_hash(123); + let hash_2 = make_tx_hash(456); + let hash_3 = make_tx_hash(789); + let pending_payables = vec![ + PendingPayable::new(make_wallet("abc"), hash_1), + PendingPayable::new(make_wallet("def"), hash_2), + PendingPayable::new(make_wallet("ghi"), hash_2), + PendingPayable::new(make_wallet("jkl"), hash_3), + ]; + let pending_payables_ref = pending_payables.iter().collect::>(); + let sent_payable_dao = SentPayableDaoMock::new() + .get_tx_identifiers_result(hashmap!(hash_1 => 1, hash_2 => 3, hash_3 => 5)); + let subject = PayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) .build(); - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(payment_1), - ProcessedPayableFallible::Correct(payment_2), - ]), - response_skeleton_opt: None, - }; - let _ = subject.finish_scan(sent_payable, &Logger::new("test")); + subject.check_for_missing_records(&pending_payables_ref); } - fn assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name: &str, - pending_payable_dao: PendingPayableDaoMock, - hash_1: H256, - hash_2: H256, - ) { + #[test] + #[should_panic(expected = "Expected sent-payable records for \ + (tx: 0x00000000000000000000000000000000000000000000000000000000000000f8, \ + to wallet: 0x00000000000000000000000000626c6168323232) \ + were not found. The system has become unreliable")] + fn payable_scanner_found_out_nonexistent_sent_tx_records() { + init_test_logging(); + let test_name = "payable_scanner_found_out_nonexistent_sent_tx_records"; + let hash_1 = make_tx_hash(0xff); + let hash_2 = make_tx_hash(0xf8); + let sent_payable_dao = + SentPayableDaoMock::default().get_tx_identifiers_result(hashmap!(hash_1 => 7881)); let payable_1 = PendingPayable::new(make_wallet("blah111"), hash_1); let payable_2 = PendingPayable::new(make_wallet("blah222"), hash_2); let payable_dao = PayableDaoMock::new().mark_pending_payables_rowids_result(Err( @@ -1635,7 +1749,7 @@ mod tests { )); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let sent_payables = SentPayables { payment_procedure_result: Ok(vec![ @@ -1645,99 +1759,36 @@ mod tests { response_skeleton_opt: None, }; - let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { - subject.finish_scan(sent_payables, &Logger::new(test_name)) - })); - - let caught_panic = caught_panic_in_err.unwrap_err(); - let panic_msg = caught_panic.downcast_ref::().unwrap(); - assert_eq!( - panic_msg, - "Unable to create a mark in the payable table for wallets 0x00000000000\ - 000000000000000626c6168313131, 0x00000000000000000000000000626c6168323232 due to \ - SignConversion(9999999999999)" - ); - } - - #[test] - fn payable_scanner_mark_pending_payable_only_panics_all_fingerprints_found() { - init_test_logging(); - let test_name = "payable_scanner_mark_pending_payable_only_panics_all_fingerprints_found"; - let hash_1 = make_tx_hash(248); - let hash_2 = make_tx_hash(139); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(7879, hash_1), (7881, hash_2)], - no_rowid_results: vec![], - }); - - assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name, - pending_payable_dao, - hash_1, - hash_2, - ); - - // Missing fingerprints, being an additional issue, would provoke an error log, but not here. - TestLogHandler::new().exists_no_log_containing(&format!("ERROR: {test_name}:")); + subject.finish_scan(sent_payables, &Logger::new(test_name)); } #[test] - fn payable_scanner_mark_pending_payable_panics_nonexistent_fingerprints_also_found() { + fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist() { init_test_logging(); let test_name = - "payable_scanner_mark_pending_payable_panics_nonexistent_fingerprints_also_found"; - let hash_1 = make_tx_hash(0xff); - let hash_2 = make_tx_hash(0xf8); - let pending_payable_dao = - PendingPayableDaoMock::default().fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(7881, hash_1)], - no_rowid_results: vec![hash_2], - }); - - assert_panic_from_failing_to_mark_pending_payable_rowid( - test_name, - pending_payable_dao, - hash_1, - hash_2, - ); - - TestLogHandler::new().exists_log_containing(&format!("ERROR: {test_name}: Expected pending payable \ - fingerprints for (tx: 0x00000000000000000000000000000000000000000000000000000000000000f8, to wallet: \ - 0x00000000000000000000000000626c6168323232) were not found; system unreliable")); - } - - #[test] - fn payable_scanner_is_facing_failed_transactions_and_their_fingerprints_exist() { - init_test_logging(); - let test_name = - "payable_scanner_is_facing_failed_transactions_and_their_fingerprints_exist"; - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + "payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist"; + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); let hash_tx_1 = make_tx_hash(0x15b3); let hash_tx_2 = make_tx_hash(0x3039); - let first_fingerprint_rowid = 3; - let second_fingerprint_rowid = 5; + let first_sent_tx_rowid = 3; + let second_sent_tx_rowid = 5; let system = System::new(test_name); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (first_fingerprint_rowid, hash_tx_1), - (second_fingerprint_rowid, hash_tx_2), - ], - no_rowid_results: vec![], - }) - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + .get_tx_identifiers_result( + hashmap!(hash_tx_1 => first_sent_tx_rowid, hash_tx_2 => second_sent_tx_rowid), + ) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); let payable_scanner = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let logger = Logger::new(test_name); let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "Attempt failed".to_string(), - hashes: vec![hash_tx_1, hash_tx_2], + hashes: hashset![hash_tx_1, hash_tx_2], }), response_skeleton_opt: None, }; @@ -1760,26 +1811,25 @@ mod tests { ); assert_eq!(aware_of_unresolved_pending_payable_before, false); assert_eq!(aware_of_unresolved_pending_payable_after, false); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); - assert_eq!( - *fingerprints_rowids_params, - vec![vec![hash_tx_1, hash_tx_2]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *delete_fingerprints_params, - vec![vec![first_fingerprint_rowid, second_fingerprint_rowid]] - ); + let sent_tx_rowids_params = get_tx_identifiers_params_arc.lock().unwrap(); + assert_eq!(*sent_tx_rowids_params, vec![hashset!(hash_tx_1, hash_tx_2)]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset!(hash_tx_1, hash_tx_2)]); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!("WARN: {test_name}: \ - Any persisted data from failed process will be deleted. Caused by: Sending phase: \"Attempt failed\". \ - Signed and hashed transactions: 0x000000000000000000000000000000000000000000000000000\ - 00000000015b3, 0x0000000000000000000000000000000000000000000000000000000000003039")); - log_handler.exists_log_containing( - &format!("WARN: {test_name}: \ - Deleting fingerprints for failed transactions 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Any persisted data from the failed process will be deleted. Caused by: Sending phase: \ + \"Attempt failed\". \ + Signed and hashed txs: \ + 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + 0x0000000000000000000000000000000000000000000000000000000000003039" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Deleting sent payable records for \ + 0x00000000000000000000000000000000000000000000000000000000000015b3, \ 0x0000000000000000000000000000000000000000000000000000000000003039", - )); + )); // we haven't supplied any result for mark_pending_payable() and so it's proved uncalled } @@ -1809,15 +1859,15 @@ mod tests { "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" )); log_handler.exists_log_containing(&format!( - "DEBUG: {test_name}: Ignoring a non-fatal error on our end from before \ - the transactions are hashed: LocallyCausedError(Signing(\"Some error\"))" + "DEBUG: {test_name}: A non-fatal error LocallyCausedError(Signing(\"Some error\")) \ + will be ignored as it is from before any tx could even be hashed" )); } #[test] - fn payable_scanner_finds_fingerprints_for_failed_payments_but_panics_at_their_deletion() { + fn payable_scanner_finds_sent_tx_record_for_failed_payments_but_panics_at_their_deletion() { let test_name = - "payable_scanner_finds_fingerprints_for_failed_payments_but_panics_at_their_deletion"; + "payable_scanner_finds_sent_tx_record_for_failed_payments_but_panics_at_their_deletion"; let rowid_1 = 4; let hash_1 = make_tx_hash(0x7b); let rowid_2 = 6; @@ -1825,20 +1875,17 @@ mod tests { let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "blah".to_string(), - hashes: vec![hash_1, hash_2], + hashes: hashset![hash_1, hash_2], }), response_skeleton_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(rowid_1, hash_1), (rowid_2, hash_2)], - no_rowid_results: vec![], - }) - .delete_fingerprints_result(Err(PendingPayableDaoError::RecordDeletion( - "Gosh, I overslept without an alarm set".to_string(), + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(hash_1 => rowid_1, hash_2 => rowid_2)) + .delete_records_result(Err(SentPayableDaoError::SqlExecutionFailed( + "I overslept since my brain thinks the alarm is just a lullaby".to_string(), ))); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { @@ -1849,37 +1896,34 @@ mod tests { let panic_msg = caught_panic.downcast_ref::().unwrap(); assert_eq!( panic_msg, - "Database corrupt: payable fingerprint deletion for transactions \ + "Database corrupt: sent payable record deletion for txs \ 0x000000000000000000000000000000000000000000000000000000000000007b, 0x00000000000000000000\ - 00000000000000000000000000000000000000000315 failed due to RecordDeletion(\"Gosh, I overslept \ - without an alarm set\")"); + 00000000000000000000000000000000000000000315 failed due to SqlExecutionFailed(\"I overslept \ + since my brain thinks the alarm is just a lullaby\")"); let log_handler = TestLogHandler::new(); - // There is a possible situation when we stumble over missing fingerprints, so we log it. + // There's a possibility that we stumble over missing sent tx records, so we log it. // Here we don't and so any ERROR log shouldn't turn up log_handler.exists_no_log_containing(&format!("ERROR: {}", test_name)) } #[test] - fn payable_scanner_panics_for_missing_fingerprints_but_deletion_of_some_works() { + fn payable_scanner_panics_for_missing_sent_tx_records_but_deletion_of_some_works() { init_test_logging(); let test_name = - "payable_scanner_panics_for_missing_fingerprints_but_deletion_of_some_works"; + "payable_scanner_panics_for_missing_sent_tx_records_but_deletion_of_some_works"; let hash_1 = make_tx_hash(0x1b669); let hash_2 = make_tx_hash(0x3039); let hash_3 = make_tx_hash(0x223d); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(333, hash_1)], - no_rowid_results: vec![hash_2, hash_3], - }) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(hash_1 => 333)) + .delete_records_result(Ok(())); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "SQLite migraine".to_string(), - hashes: vec![hash_1, hash_2, hash_3], + hashes: hashset![hash_1, hash_2, hash_3], }), response_skeleton_opt: None, }; @@ -1890,41 +1934,42 @@ mod tests { let caught_panic = caught_panic_in_err.unwrap_err(); let panic_msg = caught_panic.downcast_ref::().unwrap(); - assert_eq!(panic_msg, "Ran into failed transactions 0x0000000000000000000000000000000000\ - 000000000000000000000000003039, 0x000000000000000000000000000000000000000000000000000000000000223d \ - with missing fingerprints. System no longer reliable"); + assert_eq!( + panic_msg, + "Ran into failed payables \ + 0x000000000000000000000000000000000000000000000000000000000000223d, \ + 0x0000000000000000000000000000000000000000000000000000000000003039 \ + with missing records. The system has become unreliable" + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - &format!("WARN: {test_name}: Any persisted data from failed process will be deleted. Caused by: \ - Sending phase: \"SQLite migraine\". Signed and hashed transactions: \ - 0x000000000000000000000000000000000000000000000000000000000001b669, \ - 0x0000000000000000000000000000000000000000000000000000000000003039, \ - 0x000000000000000000000000000000000000000000000000000000000000223d")); log_handler.exists_log_containing(&format!( - "WARN: {test_name}: Deleting fingerprints for failed transactions {:?}", + "WARN: {test_name}: Any persisted data from the failed process will \ + be deleted. Caused by: Sending phase: \"SQLite migraine\". Signed and hashed txs: \ + 0x000000000000000000000000000000000000000000000000000000000000223d, \ + 0x0000000000000000000000000000000000000000000000000000000000003039, \ + 0x000000000000000000000000000000000000000000000000000000000001b669" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Deleting sent payable records for {:?}", hash_1 )); } #[test] - fn payable_scanner_for_failed_rpcs_one_fingerprint_missing_and_deletion_of_the_other_one_fails() - { - // Two fatal failures at once, missing fingerprints and fingerprint deletion error are both - // legitimate reasons for panic + fn payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails() { + // Two fatal failures at once, missing sent tx records and another record deletion error + // are both legitimate reasons for panic init_test_logging(); - let test_name = "payable_scanner_for_failed_rpcs_one_fingerprint_missing_and_deletion_of_the_other_one_fails"; + let test_name = "payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails"; let existent_record_hash = make_tx_hash(0xb26e); let nonexistent_record_hash = make_tx_hash(0x4d2); - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(45, existent_record_hash)], - no_rowid_results: vec![nonexistent_record_hash], - }) - .delete_fingerprints_result(Err(PendingPayableDaoError::RecordDeletion( + let sent_payable_dao = SentPayableDaoMock::default() + .get_tx_identifiers_result(hashmap!(existent_record_hash => 45)) + .delete_records_result(Err(SentPayableDaoError::SqlExecutionFailed( "Another failure. Really???".to_string(), ))); let mut subject = PayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let failed_payment_1 = RpcPayableFailure { rpc_error: Error::Unreachable, @@ -1952,20 +1997,33 @@ mod tests { let panic_msg = caught_panic.downcast_ref::().unwrap(); assert_eq!( panic_msg, - "Database corrupt: payable fingerprint deletion for transactions 0x00000000000000000000000\ - 0000000000000000000000000000000000000b26e failed due to RecordDeletion(\"Another failure. Really???\")"); + "Database corrupt: sent payable record deletion for txs \ + 0x000000000000000000000000000000000000000000000000000000000000b26e failed due to \ + SqlExecutionFailed(\"Another failure. Really???\")" + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!("WARN: {test_name}: Remote transaction failure: 'Server is unreachable' \ - for payment to 0x0000000000000000000000000000000000616263 and transaction hash 0x00000000000000000000000\ - 0000000000000000000000000000000000000b26e. Please check your blockchain service URL configuration.")); - log_handler.exists_log_containing(&format!("WARN: {test_name}: Remote transaction failure: 'Internal Web3 error' \ - for payment to 0x0000000000000000000000000000000000646566 and transaction hash 0x000000000000000000000000\ - 00000000000000000000000000000000000004d2. Please check your blockchain service URL configuration.")); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Remote sent payable \ + failure 'Server is unreachable' for wallet 0x0000000000000000000000000000000000616263 \ + and tx hash 0x000000000000000000000000000000000000000000000000000000000000b26e" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Remote sent payable \ + failure 'Internal Web3 error' for wallet 0x0000000000000000000000000000000000646566 \ + and tx hash 0x00000000000000000000000000000000000000000000000000000000000004d2" + )); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: \ + Please check your blockchain service URL configuration due to detected remote failures" + )); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of 2 attempts" )); - log_handler.exists_log_containing(&format!("ERROR: {test_name}: Ran into failed transactions 0x0000000000000000\ - 0000000000000000000000000000000000000000000004d2 with missing fingerprints. System no longer reliable")); + log_handler.exists_log_containing(&format!( + "ERROR: {test_name}: Ran into failed \ + payables 0x00000000000000000000000000000000000000000000000000000000000004d2 with missing \ + records. The system has become unreliable" + )); } #[test] @@ -2029,7 +2087,7 @@ mod tests { gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei) )] ) - //no other method was called (absence of panic) and that means we returned early + //no other method was called (absence of panic), and that means we returned early } #[test] @@ -2154,9 +2212,9 @@ mod tests { assert_eq!(result, vec![qualified_payable]); TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {}: Paying qualified debts:\n999,999,999,000,000,\ - 000 wei owed for \\d+ sec exceeds threshold: 500,000,000,000,000,000 wei; creditor: \ - 0x0000000000000000000000000077616c6c657430", + "DEBUG: {}: Paying qualified debts:\n\ + 999,999,999,000,000,000 wei owed for \\d+ sec exceeds the threshold \ + 500,000,000,000,000,000 wei for creditor 0x0000000000000000000000000077616c6c657430", test_name )); } @@ -2194,28 +2252,18 @@ mod tests { let test_name = "pending_payable_scanner_can_initiate_a_scan"; let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); - let payable_fingerprint_1 = PendingPayableFingerprint { - rowid: 555, - timestamp: from_unix_timestamp(210_000_000), - hash: make_tx_hash(45678), - attempt: 1, - amount: 4444, - process_error: None, - }; - let payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 550, - timestamp: from_unix_timestamp(210_000_100), - hash: make_tx_hash(112233), - attempt: 1, - amount: 7999, - process_error: None, - }; - let fingerprints = vec![payable_fingerprint_1, payable_fingerprint_2]; - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(fingerprints.clone()); + let sent_tx = make_sent_tx(456); + let sent_tx_hash = sent_tx.hash; + let failed_tx = make_failed_tx(789); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![sent_tx.clone()]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(vec![failed_tx.clone()]); let mut subject = make_dull_subject(); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -2231,21 +2279,21 @@ mod tests { true, ); - let no_of_pending_payables = fingerprints.len(); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, Ok(RequestTransactionReceipts { - pending_payable_fingerprints: fingerprints, + tx_hashes: vec![ + TxHashByTable::SentPayable(sent_tx_hash), + TxHashByTable::FailedPayable(failed_tx.hash) + ], response_skeleton_opt: None }) ); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!("INFO: {test_name}: Scanning for pending payable"), - &format!( - "DEBUG: {test_name}: Found {no_of_pending_payables} pending payables to process" - ), + &format!("DEBUG: {test_name}: Found 1 pending payables and 1 unfinalized failures to process"), ]) } @@ -2254,10 +2302,15 @@ mod tests { let now = SystemTime::now(); let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = make_dull_subject(); - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let sent_payable_dao = + SentPayableDaoMock::new().retrieve_txs_result(vec![make_sent_tx(123)]); + let failed_payable_dao = + FailedPayableDaoMock::new().retrieve_txs_result(vec![make_failed_tx(456)]); let pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) .build(); // Important subject.aware_of_unresolved_pending_payable = true; @@ -2400,24 +2453,6 @@ mod tests { ); } - #[test] - fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { - let now = SystemTime::now(); - let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let pending_payable_dao = - PendingPayableDaoMock::new().return_all_errorless_fingerprints_result(vec![]); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - - let result = - pending_payable_scanner.start_scan(&consuming_wallet, now, None, &Logger::new("test")); - - let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); - assert_eq!(result, Err(StartScanError::NothingToProcess)); - assert_eq!(is_scan_running, false); - } - #[test] fn check_general_conditions_for_pending_payable_scan_if_it_is_initial_pending_payable_scan() { let mut subject = make_dull_subject(); @@ -2430,60 +2465,110 @@ mod tests { } #[test] - fn pending_payable_scanner_handles_report_transaction_receipts_message() { + fn pending_payable_scanner_handles_tx_receipts_message() { + // Note: the choice of those hashes isn't random; I tried to make sure I will know the order, + // in which these records will be processed, because they are in an ordered map. + // It is important because otherwise preparation of results with the mocks would become + // chaotic, as long as you care about the exact receiver of the mock call among these records init_test_logging(); - let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message"; + let test_name = "pending_payable_scanner_handles_tx_receipts_message"; + // Normal confirmation let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); + // FailedTx reclaim + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + // New tx failure + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + // Validation failures + let update_statuses_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_failed_payable_params_arc = Arc::new(Mutex::new(vec![])); + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_millis(12)); + let timestamp_c = SystemTime::now().sub(Duration::from_millis(1234)); let payable_dao = PayableDaoMock::new() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::new().delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::new() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .update_statuses_params(&update_statuses_pending_payable_params_arc) + .update_statuses_result(Ok(())) + .replace_records_result(Ok(())) + .delete_records_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::new() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())) + .update_statuses_params(&update_statuses_failed_payable_params_arc) + .update_statuses_result(Ok(())) + .delete_records_result(Ok(())); + let tx_hash_1 = make_tx_hash(0x111); + let mut sent_tx_1 = make_sent_tx(123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(333), + block_number: U64::from(1234), + }; + let tx_status_1 = StatusReadFromReceiptCheck::Succeeded(tx_block_1); + let tx_hash_2 = make_tx_hash(0x222); + let mut failed_tx_2 = make_failed_tx(789); + failed_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(222), + block_number: U64::from(2345), + }; + let tx_status_2 = StatusReadFromReceiptCheck::Succeeded(tx_block_2); + let tx_hash_3 = make_tx_hash(0x333); + let mut sent_tx_3 = make_sent_tx(456); + sent_tx_3.hash = tx_hash_3; + let tx_status_3 = StatusReadFromReceiptCheck::Pending; + let tx_hash_4 = make_tx_hash(0x444); + let mut sent_tx_4 = make_sent_tx(4567); + sent_tx_4.hash = tx_hash_4; + sent_tx_4.status = TxStatus::Pending(ValidationStatus::Waiting); + let tx_receipt_rpc_error_4 = AppRpcError::Remote(RemoteError::Unreachable); + let tx_hash_5 = make_tx_hash(0x555); + let mut failed_tx_5 = make_failed_tx(888); + failed_tx_5.hash = tx_hash_5; + failed_tx_5.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ))); + let tx_receipt_rpc_error_5 = + AppRpcError::Remote(RemoteError::InvalidResponse("game over".to_string())); + let tx_hash_6 = make_tx_hash(0x666); + let mut sent_tx_6 = make_sent_tx(789); + sent_tx_6.hash = tx_hash_6; + let tx_status_6 = StatusReadFromReceiptCheck::Reverted; + let sent_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(sent_tx_1.clone())) + .get_record_by_hash_result(Some(sent_tx_3.clone())) + .get_record_by_hash_result(Some(sent_tx_4)) + .get_record_by_hash_result(Some(sent_tx_6.clone())); + let failed_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_result(Some(failed_tx_2.clone())) + .get_record_by_hash_result(Some(failed_tx_5)); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b); let mut pending_payable_scanner = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .validation_failure_clock(Box::new(validation_failure_clock)) .build(); - let transaction_hash_1 = make_tx_hash(4545); - let transaction_receipt_1 = TxReceipt { - transaction_hash: transaction_hash_1, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(1234), - }), - }; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, - }; - let transaction_hash_2 = make_tx_hash(1234); - let transaction_receipt_2 = TxReceipt { - transaction_hash: transaction_hash_2, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(2345), - }), - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 10, - timestamp: from_unix_timestamp(199_780_000), - hash: transaction_hash_2, - attempt: 15, - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_1), - fingerprint_1.clone(), - ), - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_2), - fingerprint_2.clone(), - ), + let msg = TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(tx_hash_1) => Ok(tx_status_1), + TxHashByTable::FailedPayable(tx_hash_2) => Ok(tx_status_2), + TxHashByTable::SentPayable(tx_hash_3) => Ok(tx_status_3), + TxHashByTable::SentPayable(tx_hash_4) => Err(tx_receipt_rpc_error_4), + TxHashByTable::FailedPayable(tx_hash_5) => Err(tx_receipt_rpc_error_5), + TxHashByTable::SentPayable(tx_hash_6) => Ok(tx_status_6), ], response_skeleton_opt: None, }; @@ -2493,54 +2578,75 @@ mod tests { let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); assert_eq!( result, - PendingPayableScanResult::NoPendingPayablesLeft(None) + PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) ); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + assert_eq!(*transactions_confirmed_params, vec![vec![sent_tx_1]]); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); + assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); + let sent_tx_2 = SentTx::from((failed_tx_2, tx_block_2)); + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + let expected_failure_for_tx_3 = FailedTx::from((sent_tx_3, FailureReason::PendingTooLong)); + let expected_failure_for_tx_6 = FailedTx::from((sent_tx_6, FailureReason::Reverted)); assert_eq!( - *transactions_confirmed_params, - vec![vec![fingerprint_1, fingerprint_2]] + *insert_new_records_params, + vec![vec![expected_failure_for_tx_3, expected_failure_for_tx_6]] + ); + let update_statuses_pending_payable_params = + update_statuses_pending_payable_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_pending_payable_params, + vec![ + hashmap!(tx_hash_4 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_a))))) + ] + ); + let update_statuses_failed_payable_params = + update_statuses_failed_payable_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_failed_payable_params, + vec![ + hashmap!(tx_hash_5 => FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_c)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_b))))) + ] ); assert_eq!(subject.scan_started_at(ScanType::PendingPayables), None); - TestLogHandler::new().assert_logs_match_in_order(vec![ - &format!( - "INFO: {}: Transactions {:?}, {:?} completed their confirmation process succeeding", - test_name, transaction_hash_1, transaction_hash_2 - ), - &format!("INFO: {test_name}: The PendingPayables scan ended in \\d+ms."), - ]); + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Processing receipts for 6 txs" + )); + test_log_handler.exists_log_containing(&format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000000000000000000000000000000000000000000000000000000444): Remote(Unreachable). Will retry receipt retrieval next cycle")); + test_log_handler.exists_log_containing(&format!("WARN: {test_name}: Failed to retrieve tx receipt for FailedPayable(0x0000000000000000000000000000000000000000000000000000000000000555): Remote(InvalidResponse(\"game over\")). Will retry receipt retrieval next cycle")); + test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000222 (block 2345) as confirmed on-chain")); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000111 (block 1234) was confirmed", + )); + test_log_handler.exists_log_containing(&format!("INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000333, 0x0000000000000000000000000000000000000000000000000000000000000666 were processed in the db")); } #[test] + #[should_panic( + expected = "We should never receive an empty list of results. Even receipts that could not \ + be retrieved can be interpreted" + )] fn pending_payable_scanner_handles_empty_report_transaction_receipts_message() { - init_test_logging(); - let test_name = - "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], + let msg = TxReceiptsMessage { + results: hashmap![], response_skeleton_opt: None, }; pending_payable_scanner.mark_as_started(SystemTime::now()); let mut subject = make_dull_subject(); subject.pending_payable = Box::new(pending_payable_scanner); - let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - - let is_scan_running = subject.scan_started_at(ScanType::PendingPayables).is_some(); - assert_eq!( - result, - PendingPayableScanResult::NoPendingPayablesLeft(None) - ); - assert_eq!(is_scan_running, false); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!( - "WARN: {test_name}: No transaction receipts found." - )); - tlh.exists_log_matching(&format!( - "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." - )); + let _ = subject.finish_pending_payable_scan(msg, &Logger::new("test")); } #[test] @@ -2726,8 +2832,10 @@ mod tests { } #[test] - #[should_panic(expected = "Attempt to set new start block to 6709 failed due to: \ - UninterpretableValue(\"Illiterate database manager\")")] + #[should_panic( + expected = "Attempt to advance the start block to 6709 failed due to: \ + UninterpretableValue(\"Illiterate database manager\")" + )] fn no_transactions_received_but_start_block_setting_fails() { init_test_logging(); let test_name = "no_transactions_received_but_start_block_setting_fails"; @@ -2972,7 +3080,7 @@ mod tests { subject.signal_scanner_completion(ScanType::Receivables, SystemTime::now(), &logger); TestLogHandler::new().exists_log_containing(&format!( - "ERROR: {test_name}: Called scan_finished() for Receivables scanner but timestamp was not found" + "ERROR: {test_name}: Called scan_finished() for Receivables scanner but could not find any timestamp" )); } @@ -3023,7 +3131,7 @@ mod tests { &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PendingPayableScannerBuilder::new().build(), "PendingPayables", test_name, diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index cfb874f19..f501a7be2 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -1,37 +1,67 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod test_utils; +mod tx_receipt_interpreter; pub mod utils; -use std::cell::RefCell; -use std::rc::Rc; -use std::time::SystemTime; -use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; -use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; -use crate::accountant::db_access_objects::payable_dao::PayableDao; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; -use crate::accountant::{comma_joined_stringifiable, PendingPayableId, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables}; -use crate::accountant::scanners::{PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner}; -use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; +use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDao, FailedTx, FailureRetrieveCondition, FailureStatus, +}; +use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDao, SentPayableDaoError, SentTx, TxStatus, +}; +use crate::accountant::db_access_objects::utils::{TxHash, TxRecordWithHash}; +use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, MismatchReport, PendingPayableCache, PendingPayableScanResult, + PresortedTxFailure, ReceiptScanReport, RecheckRequiringFailures, Retry, TxByTable, + TxCaseToBeInterpreted, TxHashByTable, UpdatableValidationStatus, +}; +use crate::accountant::scanners::{ + PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, +}; +use crate::accountant::{ + comma_joined_stringifiable, RequestTransactionReceipts, ResponseSkeleton, + ScanForPendingPayables, TxReceiptResult, TxReceiptsMessage, +}; +use crate::blockchain::blockchain_interface::data_structures::TxBlock; +use crate::blockchain::errors::validation_status::{ + ValidationFailureClock, ValidationFailureClockReal, +}; use crate::sub_lib::accountant::{FinancialStatistics, PaymentThresholds}; use crate::sub_lib::wallet::Wallet; use crate::time_marking_methods; +use itertools::{Either, Itertools}; +use masq_lib::logger::Logger; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::rc::Rc; +use std::str::FromStr; +use std::time::SystemTime; +use thousands::Separable; +use web3::types::H256; pub struct PendingPayableScanner { pub common: ScannerCommon, pub payable_dao: Box, - pub pending_payable_dao: Box, - pub when_pending_too_long_sec: u64, + pub sent_payable_dao: Box, + pub failed_payable_dao: Box, pub financial_statistics: Rc>, + pub current_sent_payables: Box>, + pub yet_unproven_failed_payables: Box>, + pub clock: Box, } impl PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, - ReportTransactionReceipts, + TxReceiptsMessage, PendingPayableScanResult, > for PendingPayableScanner { @@ -49,756 +79,1975 @@ impl StartableScanner ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for pending payable"); - let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); - match filtered_pending_payable.is_empty() { - true => { - self.mark_as_ended(logger); - Err(StartScanError::NothingToProcess) - } - false => { - debug!( - logger, - "Found {} pending payables to process", - filtered_pending_payable.len() - ); - Ok(RequestTransactionReceipts { - pending_payable_fingerprints: filtered_pending_payable, - response_skeleton_opt, - }) - } + + let pending_tx_hashes_opt = self.handle_pending_payables(); + let failure_hashes_opt = self.handle_unproven_failures(); + + if pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() { + self.mark_as_ended(logger); + return Err(StartScanError::NothingToProcess); } + + Self::log_records_found_for_receipt_check( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + logger, + ); + + let all_hashes = pending_tx_hashes_opt + .unwrap_or_default() + .into_iter() + .chain(failure_hashes_opt.unwrap_or_default()) + .collect_vec(); + + Ok(RequestTransactionReceipts { + tx_hashes: all_hashes, + response_skeleton_opt, + }) } } -impl Scanner for PendingPayableScanner { +impl Scanner for PendingPayableScanner { fn finish_scan( &mut self, - message: ReportTransactionReceipts, + message: TxReceiptsMessage, logger: &Logger, ) -> PendingPayableScanResult { let response_skeleton_opt = message.response_skeleton_opt; - let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { - true => { - warning!(logger, "No transaction receipts found."); - todo!("This requires the payment retry. GH-631 must be completed first"); - } - false => { - debug!( - logger, - "Processing receipts for {} transactions", - message.fingerprints_with_receipts.len() - ); - let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - let requires_payment_retry = - self.process_transactions_by_reported_state(scan_report, logger); + let scan_report = self.interpret_tx_receipts(message, logger); - self.mark_as_ended(logger); + let retry_opt = scan_report.requires_payments_retry(); - requires_payment_retry - } - }; + self.process_txs_by_state(scan_report, logger); - if requires_payment_retry { - PendingPayableScanResult::PaymentRetryRequired - } else { - let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }); - PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) - } + self.mark_as_ended(logger); + + Self::compose_scan_result(retry_opt, response_skeleton_opt) } time_marking_methods!(PendingPayables); as_any_ref_in_trait_impl!(); + + as_any_mut_in_trait_impl!(); } impl PendingPayableScanner { pub fn new( payable_dao: Box, - pending_payable_dao: Box, + sent_payable_dao: Box, + failed_payable_dao: Box, payment_thresholds: Rc, - when_pending_too_long_sec: u64, financial_statistics: Rc>, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, - pending_payable_dao, - when_pending_too_long_sec, + sent_payable_dao, + failed_payable_dao, financial_statistics, + current_sent_payables: Box::new(CurrentPendingPayables::default()), + yet_unproven_failed_payables: Box::new(RecheckRequiringFailures::default()), + clock: Box::new(ValidationFailureClockReal::default()), } } - fn handle_receipts_for_pending_transactions( - &self, - msg: ReportTransactionReceipts, + fn handle_pending_payables(&mut self) -> Option> { + let pending_txs = self + .sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::IsPending)); + + if pending_txs.is_empty() { + return None; + } + + let pending_tx_hashes = Self::get_wrapped_hashes(&pending_txs, TxHashByTable::SentPayable); + self.current_sent_payables.load_cache(pending_txs); + Some(pending_tx_hashes) + } + + fn handle_unproven_failures(&mut self) -> Option> { + let failures = self + .failed_payable_dao + .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)); + + if failures.is_empty() { + return None; + } + + let failure_hashes = Self::get_wrapped_hashes(&failures, TxHashByTable::FailedPayable); + self.yet_unproven_failed_payables.load_cache(failures); + Some(failure_hashes) + } + + fn get_wrapped_hashes( + records: &[Record], + wrap_the_hash: fn(TxHash) -> TxHashByTable, + ) -> Vec + where + Record: TxRecordWithHash, + { + records + .iter() + .map(|record| wrap_the_hash(record.hash())) + .collect_vec() + } + + fn emptiness_check(&self, msg: &TxReceiptsMessage) { + if msg.results.is_empty() { + panic!( + "We should never receive an empty list of results. \ + Even receipts that could not be retrieved can be interpreted" + ) + } + } + + fn compose_scan_result( + retry_opt: Option, + response_skeleton_opt: Option, + ) -> PendingPayableScanResult { + if let Some(retry) = retry_opt { + if let Some(response_skeleton) = response_skeleton_opt { + let ui_msg = NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }; + PendingPayableScanResult::PaymentRetryRequired(Either::Right(ui_msg)) + } else { + PendingPayableScanResult::PaymentRetryRequired(Either::Left(retry)) + } + } else { + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) + } + } + + fn interpret_tx_receipts( + &mut self, + msg: TxReceiptsMessage, + logger: &Logger, + ) -> ReceiptScanReport { + self.emptiness_check(&msg); + + debug!(logger, "Processing receipts for {} txs", msg.results.len()); + + let interpretable_data = self.prepare_cases_to_interpret(msg, logger); + TxReceiptInterpreter::default().compose_receipt_scan_report( + interpretable_data, + &self, + logger, + ) + } + + fn prepare_cases_to_interpret( + &mut self, + msg: TxReceiptsMessage, logger: &Logger, - ) -> PendingPayableScanReport { - let scan_report = PendingPayableScanReport::default(); - msg.fingerprints_with_receipts.into_iter().fold( - scan_report, - |scan_report_so_far, (receipt_result, fingerprint)| match receipt_result { - TransactionReceiptResult::RpcResponse(tx_receipt) => match tx_receipt.status { - TxStatus::Pending => handle_none_receipt( - scan_report_so_far, - fingerprint, - "none was given", - logger, - ), - TxStatus::Failed => { - handle_status_with_failure(scan_report_so_far, fingerprint, logger) + ) -> Vec { + let init: Either, MismatchReport> = Either::Left(vec![]); + let either = msg + .results + .into_iter() + // This must be in for predictability in tests + .sorted_by_key(|(hash_by_table, _)| hash_by_table.hash()) + .fold( + init, + |acc, (tx_hash_by_table, tx_receipt_result)| match acc { + Either::Left(cases) => { + self.resolve_real_query(cases, tx_receipt_result, tx_hash_by_table) } - TxStatus::Succeeded(_) => { - handle_status_with_success(scan_report_so_far, fingerprint, logger) + Either::Right(mut mismatch_report) => { + mismatch_report.remaining_hashes.push(tx_hash_by_table); + Either::Right(mismatch_report) } }, - TransactionReceiptResult::LocalError(e) => handle_none_receipt( - scan_report_so_far, - fingerprint, - &format!("failed due to {}", e), - logger, - ), - }, + ); + + let cases = match either { + Either::Left(cases) => cases, + Either::Right(mismatch_report) => self.panic_dump(mismatch_report), + }; + + self.current_sent_payables.ensure_empty_cache(logger); + self.yet_unproven_failed_payables.ensure_empty_cache(logger); + + cases + } + + fn resolve_real_query( + &mut self, + mut cases: Vec, + receipt_result: TxReceiptResult, + looked_up_hash: TxHashByTable, + ) -> Either, MismatchReport> { + match looked_up_hash { + TxHashByTable::SentPayable(tx_hash) => { + match self.current_sent_payables.get_record_by_hash(tx_hash) { + Some(sent_tx) => { + cases.push(TxCaseToBeInterpreted::new( + TxByTable::SentPayable(sent_tx), + receipt_result, + )); + Either::Left(cases) + } + None => Either::Right(MismatchReport { + noticed_with: looked_up_hash, + remaining_hashes: vec![], + }), + } + } + TxHashByTable::FailedPayable(tx_hash) => { + match self + .yet_unproven_failed_payables + .get_record_by_hash(tx_hash) + { + Some(failed_tx) => { + cases.push(TxCaseToBeInterpreted::new( + TxByTable::FailedPayable(failed_tx), + receipt_result, + )); + Either::Left(cases) + } + None => Either::Right(MismatchReport { + noticed_with: looked_up_hash, + remaining_hashes: vec![], + }), + } + } + } + } + + fn panic_dump(&mut self, mismatch_report: MismatchReport) -> ! { + fn rearrange(hashmap: HashMap) -> Vec { + hashmap + .into_iter() + .sorted_by_key(|(tx_hash, _)| *tx_hash) + .map(|(_, record)| record) + .collect_vec() + } + + panic!( + "Looking up '{:?}' in the cache, the record could not be found. Dumping \ + the remaining values. Pending payables: {:?}. Unproven failures: {:?}. \ + Hashes yet not looked up: {:?}.", + mismatch_report.noticed_with, + rearrange(self.current_sent_payables.dump_cache()), + rearrange(self.yet_unproven_failed_payables.dump_cache()), + mismatch_report.remaining_hashes ) } - fn process_transactions_by_reported_state( + fn process_txs_by_state(&mut self, scan_report: ReceiptScanReport, logger: &Logger) { + self.handle_confirmed_transactions(scan_report.confirmations, logger); + self.handle_failed_transactions(scan_report.failures, logger); + } + + fn handle_confirmed_transactions( &mut self, - scan_report: PendingPayableScanReport, + confirmed_txs: DetectedConfirmations, logger: &Logger, - ) -> bool { - let requires_payments_retry = scan_report.requires_payments_retry(); + ) { + self.handle_tx_failure_reclaims(confirmed_txs.reclaims, logger); + self.handle_normal_confirmations(confirmed_txs.normal_confirmations, logger); + } + + fn handle_tx_failure_reclaims(&mut self, reclaimed: Vec, logger: &Logger) { + if reclaimed.is_empty() { + return; + } + + let hashes_and_blocks = Self::collect_and_sort_hashes_and_blocks(&reclaimed); + + self.replace_sent_tx_records(&reclaimed, &hashes_and_blocks, logger); + + self.delete_failed_tx_records(&hashes_and_blocks, logger); + + self.add_to_the_total_of_paid_payable(&reclaimed, logger) + } + + fn isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> HashSet { + reclaimed.iter().map(|(tx_hash, _)| *tx_hash).collect() + } + + fn collect_and_sort_hashes_and_blocks(sent_txs: &[SentTx]) -> Vec<(TxHash, TxBlock)> { + Self::collect_hashes_and_blocks(sent_txs) + .into_iter() + .sorted() + .collect_vec() + } - self.confirm_transactions(scan_report.confirmed, logger); - self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger); + fn collect_hashes_and_blocks(reclaimed: &[SentTx]) -> HashMap { + reclaimed + .iter() + .map(|reclaim| { + let tx_block = if let TxStatus::Confirmed { block_hash, block_number, .. } = + &reclaim.status + { + TxBlock{ + block_hash: H256::from_str(&block_hash[2..]).expect("Failed to construct hash from str"), + block_number: (*block_number).into() + } + } else { + panic!( + "Processing a reclaim for tx {:?} which isn't filled with the confirmation details", + reclaim.hash + ) + }; + (reclaim.hash, tx_block) + }) + .collect() + } - requires_payments_retry + fn replace_sent_tx_records( + &self, + sent_txs_to_reclaim: &[SentTx], + hashes_and_blocks: &[(TxHash, TxBlock)], + logger: &Logger, + ) { + match self.sent_payable_dao.replace_records(sent_txs_to_reclaim) { + Ok(_) => { + debug!(logger, "Replaced records for txs being reclaimed") + } + Err(e) => { + panic!( + "Unable to proceed in a reclaim as the replacement of sent tx records \ + {} failed due to: {:?}", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + format!("{:?}", tx_hash) + }), + e + ) + } + } } - fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.increment_scan_attempts(&rowids) { - Ok(_) => trace!( + fn delete_failed_tx_records(&self, hashes_and_blocks: &[(TxHash, TxBlock)], logger: &Logger) { + let hashes = Self::isolate_hashes(hashes_and_blocks); + match self.failed_payable_dao.delete_records(&hashes) { + Ok(_) => { + info!( logger, - "Updated records for rowids: {} ", - comma_joined_stringifiable(&rowids, |id| id.to_string()) - ), - Err(e) => panic!( - "Failure on incrementing scan attempts for fingerprints of {} due to {:?}", - PendingPayableId::serialize_hashes_to_string(&ids), + "Reclaimed txs {} as confirmed on-chain", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, tx_block)| { + format!("{:?} (block {})", tx_hash, tx_block.block_number) + }) + ) + } + Err(e) => { + panic!( + "Unable to delete failed tx records {} to finish the reclaims due to: {:?}", + comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + format!("{:?}", tx_hash) + }), e - ), + ) + } + } + } + + fn handle_normal_confirmations(&mut self, confirmed_txs: Vec, logger: &Logger) { + if confirmed_txs.is_empty() { + return; + } + + self.confirm_transactions(&confirmed_txs); + + self.update_tx_blocks(&confirmed_txs, logger); + + self.add_to_the_total_of_paid_payable(&confirmed_txs, logger); + } + + fn confirm_transactions(&self, confirmed_sent_txs: &[SentTx]) { + if let Err(e) = self.payable_dao.transactions_confirmed(confirmed_sent_txs) { + Self::transaction_confirmed_panic(confirmed_sent_txs, e); + } + } + + fn update_tx_blocks(&self, confirmed_sent_txs: &[SentTx], logger: &Logger) { + let tx_confirmations = Self::collect_hashes_and_blocks(confirmed_sent_txs); + + if let Err(e) = self.sent_payable_dao.confirm_txs(&tx_confirmations) { + Self::update_tx_blocks_panic(&tx_confirmations, e); + } else { + Self::log_tx_success(logger, &tx_confirmations); + } + } + + fn log_tx_success(logger: &Logger, tx_hashes_and_tx_blocks: &HashMap) { + logger.info(|| { + let pretty_pairs = tx_hashes_and_tx_blocks + .iter() + .sorted() + .map(|(hash, tx_confirmation)| { + format!("{:?} (block {})", hash, tx_confirmation.block_number) + }) + .join(", "); + match tx_hashes_and_tx_blocks.len() { + 1 => format!("Tx {} was confirmed", pretty_pairs), + _ => format!("Txs {} were confirmed", pretty_pairs), + } + }); + } + + fn transaction_confirmed_panic(confirmed_txs: &[SentTx], e: PayableDaoError) -> ! { + panic!( + "Unable to complete the tx confirmation by the adjustment of the payable accounts \ + {} due to: {:?}", + comma_joined_stringifiable( + &confirmed_txs + .iter() + .map(|tx| tx.receiver_address) + .collect_vec(), + |wallet| format!("{:?}", wallet) + ), + e + ) + } + fn update_tx_blocks_panic( + tx_hashes_and_tx_blocks: &HashMap, + e: SentPayableDaoError, + ) -> ! { + panic!( + "Unable to update sent payable records {} by their tx blocks due to: {:?}", + comma_joined_stringifiable( + &tx_hashes_and_tx_blocks.keys().sorted().collect_vec(), + |tx_hash| format!("{:?}", tx_hash) + ), + e + ) + } + + fn add_to_the_total_of_paid_payable(&mut self, confirmed_payments: &[SentTx], logger: &Logger) { + let to_be_added: u128 = confirmed_payments + .iter() + .map(|sent_tx| sent_tx.amount_minor) + .sum(); + + let total_paid_payable = &mut self + .financial_statistics + .borrow_mut() + .total_paid_payable_wei; + + *total_paid_payable += to_be_added; + + debug!( + logger, + "The total paid payables increased by {} to {} wei", + to_be_added.separate_with_commas(), + total_paid_payable.separate_with_commas() + ); + } + + fn handle_failed_transactions(&self, failures: DetectedFailures, logger: &Logger) { + self.handle_tx_failures(failures.tx_failures, logger); + self.handle_rpc_failures(failures.tx_receipt_rpc_failures, logger); + } + + fn handle_tx_failures(&self, failures: Vec, logger: &Logger) { + #[derive(Default)] + struct GroupedFailures { + new_failures: Vec, + rechecks_completed: Vec, + } + + let grouped_failures = + failures + .into_iter() + .fold(GroupedFailures::default(), |mut acc, failure| { + match failure { + PresortedTxFailure::NewEntry(failed_tx) => { + acc.new_failures.push(failed_tx); + } + PresortedTxFailure::RecheckCompleted(tx_hash) => { + acc.rechecks_completed.push(tx_hash); + } + } + acc + }); + + self.add_new_failures(grouped_failures.new_failures, logger); + self.finalize_unproven_failures(grouped_failures.rechecks_completed, logger); + } + + fn add_new_failures(&self, new_failures: Vec, logger: &Logger) { + fn prepare_hashset(failures: &[FailedTx]) -> HashSet { + failures.iter().map(|failure| failure.hash).collect() + } + fn log_procedure_finished(logger: &Logger, new_failures: &[FailedTx]) { + info!( + logger, + "Failed txs {} were processed in the db", + comma_joined_stringifiable(new_failures, |failure| format!("{:?}", failure.hash)) + ) + } + + if new_failures.is_empty() { + return; + } + + if let Err(e) = self.failed_payable_dao.insert_new_records(&new_failures) { + panic!( + "Unable to persist failed txs {} due to: {:?}", + comma_joined_stringifiable(&new_failures, |failure| format!("{:?}", failure.hash)), + e + ) + } + + match self + .sent_payable_dao + .delete_records(&prepare_hashset(&new_failures)) + { + Ok(_) => { + log_procedure_finished(logger, &new_failures); + } + Err(e) => { + panic!( + "Unable to purge sent payable records for failed txs {} due to: {:?}", + comma_joined_stringifiable(&new_failures, |failure| format!( + "{:?}", + failure.hash + )), + e + ) } } } - fn cancel_failed_transactions(&self, ids: Vec, logger: &Logger) { - if !ids.is_empty() { - //TODO this function is imperfect. It waits for GH-663 - let rowids = PendingPayableId::rowids(&ids); - match self.pending_payable_dao.mark_failures(&rowids) { - Ok(_) => warning!( + fn finalize_unproven_failures(&self, rechecks_completed: Vec, logger: &Logger) { + fn prepare_hashmap(rechecks_completed: &[TxHash]) -> HashMap { + rechecks_completed + .iter() + .map(|tx_hash| (tx_hash.clone(), FailureStatus::Concluded)) + .collect() + } + + if rechecks_completed.is_empty() { + return; + } + + match self + .failed_payable_dao + .update_statuses(&prepare_hashmap(&rechecks_completed)) + { + Ok(_) => { + debug!( logger, - "Broken transactions {} marked as an error. You should take over the care \ - of those to make sure your debts are going to be settled properly. At the moment, \ - there is no automated process fixing that without your assistance", - PendingPayableId::serialize_hashes_to_string(&ids) - ), - Err(e) => panic!( - "Unsuccessful attempt for transactions {} \ - to mark fatal error at payable fingerprint due to {:?}; database unreliable", - PendingPayableId::serialize_hashes_to_string(&ids), + "Concluded failures that had required rechecks: {}.", + comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( + "{:?}", + tx_hash + )) + ); + } + Err(e) => { + panic!( + "Unable to conclude rechecks for failed txs {} due to: {:?}", + comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( + "{:?}", + tx_hash + )), e - ), + ) } } } - fn confirm_transactions( - &mut self, - fingerprints: Vec, + fn handle_rpc_failures(&self, failures: Vec, logger: &Logger) { + if failures.is_empty() { + return; + } + + let (sent_payable_failures, failed_payable_failures): ( + Vec>, + Vec>, + ) = failures.into_iter().partition_map(|failure| match failure { + FailedValidationByTable::SentPayable(failed_validation) => { + Either::Left(failed_validation) + } + FailedValidationByTable::FailedPayable(failed_validation) => { + Either::Right(failed_validation) + } + }); + + self.update_validation_status_for_sent_txs(sent_payable_failures, logger); + + self.update_validation_status_for_failed_txs(failed_payable_failures, logger); + } + + fn update_validation_status_for_sent_txs( + &self, + sent_payable_failures: Vec>, logger: &Logger, ) { - fn serialize_hashes(fingerprints: &[PendingPayableFingerprint]) -> String { - comma_joined_stringifiable(fingerprints, |fgp| format!("{:?}", fgp.hash)) + if !sent_payable_failures.is_empty() { + let updatable = + Self::prepare_statuses_for_update(&sent_payable_failures, &*self.clock, logger); + if !updatable.is_empty() { + match self.sent_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Pending-tx statuses were processed in the db for validation failure \ + of txs {}", + comma_joined_stringifiable(&sent_payable_failures, |failure| { + format!("{:?}", failure.tx_hash) + }) + ) + } + Err(e) => { + panic!( + "Unable to update pending-tx statuses for validation failures '{:?}' \ + due to: {:?}", + sent_payable_failures, e + ) + } + } + } } + } - if !fingerprints.is_empty() { - if let Err(e) = self.payable_dao.transactions_confirmed(&fingerprints) { - panic!( - "Unable to cast confirmed pending payables {} into adjustment in the corresponding payable \ - records due to {:?}", serialize_hashes(&fingerprints), e - ) - } else { - self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); - let rowids = fingerprints - .iter() - .map(|fingerprint| fingerprint.rowid) - .collect::>(); - if let Err(e) = self.pending_payable_dao.delete_fingerprints(&rowids) { - panic!("Unable to delete payable fingerprints {} of verified transactions due to {:?}", - serialize_hashes(&fingerprints), e) - } else { - info!( - logger, - "Transactions {} completed their confirmation process succeeding", - serialize_hashes(&fingerprints) - ) + fn update_validation_status_for_failed_txs( + &self, + failed_txs_validation_failures: Vec>, + logger: &Logger, + ) { + if !failed_txs_validation_failures.is_empty() { + let updatable = Self::prepare_statuses_for_update( + &failed_txs_validation_failures, + &*self.clock, + logger, + ); + if !updatable.is_empty() { + match self.failed_payable_dao.update_statuses(&updatable) { + Ok(_) => { + info!( + logger, + "Failed-tx statuses were processed in the db for validation failure \ + of txs {}", + comma_joined_stringifiable( + &failed_txs_validation_failures, + |failure| { format!("{:?}", failure.tx_hash) } + ) + ) + } + Err(e) => { + panic!( + "Unable to update failed-tx statuses for validation failures '{:?}' \ + due to: {:?}", + failed_txs_validation_failures, e + ) + } } } } } - fn add_to_the_total_of_paid_payable( - &mut self, - fingerprints: &[PendingPayableFingerprint], - serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, + fn prepare_statuses_for_update( + failures: &[FailedValidation], + clock: &dyn ValidationFailureClock, + logger: &Logger, + ) -> HashMap { + failures + .iter() + .flat_map(|failure| { + failure + .new_status(clock) + .map(|tx_status| (failure.tx_hash, tx_status)) + .or_else(|| { + debug!( + logger, + "{}", + PendingPayableScanner::status_not_updatable_log_msg( + &failure.current_status + ) + ); + None + }) + }) + .collect() + } + + fn status_not_updatable_log_msg(status: &dyn Display) -> String { + format!( + "Handling a validation failure, but the status {} cannot be updated.", + status + ) + } + + fn log_records_found_for_receipt_check( + pending_tx_hashes_opt: Option<&Vec>, + failure_hashes_opt: Option<&Vec>, logger: &Logger, ) { - fingerprints.iter().for_each(|fingerprint| { - self.financial_statistics - .borrow_mut() - .total_paid_payable_wei += fingerprint.amount - }); + fn resolve_optional_vec(vec_opt: Option<&Vec>) -> usize { + vec_opt.map(|hashes| hashes.len()).unwrap_or_default() + } + debug!( logger, - "Confirmation of transactions {}; record for total paid payable was modified", - serialize_hashes(fingerprints) + "Found {} pending payables and {} unfinalized failures to process", + resolve_optional_vec(pending_tx_hashes_opt), + resolve_optional_vec(failure_hashes_opt) ); } } #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedPayableDaoError, FailureStatus, + }; + use crate::accountant::db_access_objects::payable_dao::PayableDaoError; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, SentPayableDaoError, TxStatus, + }; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PendingPayableScanResult, PresortedTxFailure, + RecheckRequiringFailures, Retry, TxHashByTable, + }; + use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; + use crate::accountant::scanners::test_utils::PendingPayableCacheMock; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner}; + use crate::accountant::test_utils::{ + make_failed_tx, make_sent_tx, make_transaction_block, FailedPayableDaoMock, PayableDaoMock, + PendingPayableScannerBuilder, SentPayableDaoMock, + }; + use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; + use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, + }; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteErrorKind, + }; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; + use crate::test_utils::{make_paying_wallet, make_wallet}; + use itertools::{Either, Itertools}; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use regex::Regex; + use std::collections::HashMap; use std::ops::Sub; + use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use ethereum_types::{H256, U64}; - use regex::Regex; - use web3::types::TransactionReceipt; - use masq_lib::logger::Logger; - use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use crate::accountant::{PendingPayableId, ReportTransactionReceipts, DEFAULT_PENDING_TOO_LONG_SEC}; - use crate::accountant::db_access_objects::payable_dao::PayableDaoError; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoError; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::scanners::pending_payable_scanner::utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::test_utils::{make_pending_payable_fingerprint, PayableDaoMock, PendingPayableDaoMock, PendingPayableScannerBuilder}; - use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxReceipt, TxStatus}; - use crate::blockchain::test_utils::make_tx_hash; - - fn assert_interpreting_none_status_for_pending_payable( - test_name: &str, - when_pending_too_long_sec: u64, - pending_payable_age_sec: u64, - rowid: u64, - hash: H256, - ) -> PendingPayableScanReport { - init_test_logging(); - let when_sent = SystemTime::now().sub(Duration::from_secs(pending_payable_age_sec)); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: when_sent, - hash, - attempt: 1, - amount: 123, - process_error: None, - }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - - handle_none_status(scan_report, fingerprint, when_pending_too_long_sec, &logger) - } - fn assert_log_msg_and_elapsed_time_in_log_makes_sense( - expected_msg: &str, - elapsed_after: u64, - capture_regex: &str, - ) { - let log_handler = TestLogHandler::default(); - let log_idx = log_handler.exists_log_matching(expected_msg); - let log = log_handler.get_log_at(log_idx); - let capture = captures_for_regex_time_in_sec(&log, capture_regex); - assert!(capture <= elapsed_after) - } + #[test] + fn start_scan_fills_in_caches_and_returns_msg() { + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(890); + let failed_tx_hash_2 = failed_tx_2.hash; + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_result(vec![sent_tx_1.clone(), sent_tx_2.clone()]); + let failed_payable_dao = FailedPayableDaoMock::new() + .retrieve_txs_result(vec![failed_tx_1.clone(), failed_tx_2.clone()]); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(CurrentPendingPayables::default())) + .failed_payable_cache(Box::new(RecheckRequiringFailures::default())) + .build(); + let logger = Logger::new("start_scan_fills_in_caches_and_returns_msg"); + let pending_payable_cache_before = subject.current_sent_payables.dump_cache(); + let failed_payable_cache_before = subject.yet_unproven_failed_payables.dump_cache(); - fn captures_for_regex_time_in_sec(stack: &str, capture_regex: &str) -> u64 { - let capture_regex = Regex::new(capture_regex).unwrap(); - let time_str = capture_regex - .captures(stack) - .unwrap() - .get(1) - .unwrap() - .as_str(); - time_str.parse().unwrap() - } + let result = subject.start_scan(&make_wallet("bluh"), SystemTime::now(), None, &logger); - fn elapsed_since_secs_back(sec: u64) -> u64 { - SystemTime::now() - .sub(Duration::from_secs(sec)) - .elapsed() - .unwrap() - .as_secs() + assert_eq!( + result, + Ok(RequestTransactionReceipts { + tx_hashes: vec![ + TxHashByTable::SentPayable(sent_tx_hash_1), + TxHashByTable::SentPayable(sent_tx_hash_2), + TxHashByTable::FailedPayable(failed_tx_hash_1), + TxHashByTable::FailedPayable(failed_tx_hash_2) + ], + response_skeleton_opt: None + }) + ); + assert!( + pending_payable_cache_before.is_empty(), + "Should have been empty but {:?}", + pending_payable_cache_before + ); + assert!( + failed_payable_cache_before.is_empty(), + "Should have been empty but {:?}", + failed_payable_cache_before + ); + let pending_payable_cache_after = subject.current_sent_payables.dump_cache(); + let failed_payable_cache_after = subject.yet_unproven_failed_payables.dump_cache(); + assert_eq!( + pending_payable_cache_after, + hashmap!(sent_tx_hash_1 => sent_tx_1, sent_tx_hash_2 => sent_tx_2) + ); + assert_eq!( + failed_payable_cache_after, + hashmap!(failed_tx_hash_1 => failed_tx_1, failed_tx_hash_2 => failed_tx_2) + ); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval() - { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_outside_waiting_interval"; - let hash = make_tx_hash(0x237); - let rowid = 466; + fn finish_scan_operates_caches_and_clears_them_after_use() { + let get_record_by_hash_failed_payable_cache_params_arc = Arc::new(Mutex::new(vec![])); + let get_record_by_hash_sent_payable_cache_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_payable_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_sent_payable_params_arc = Arc::new(Mutex::new(vec![])); + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(890); + let failed_tx_hash_2 = failed_tx_2.hash; + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::new() + .confirm_tx_result(Ok(())) + .replace_records_result(Ok(())) + .delete_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::new() + .insert_new_records_result(Ok(())) + .delete_records_result(Ok(())); + let sent_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_params(&get_record_by_hash_sent_payable_cache_params_arc) + .get_record_by_hash_result(Some(sent_tx_1.clone())) + .get_record_by_hash_result(Some(sent_tx_2)) + .ensure_empty_cache_params(&ensure_empty_cache_sent_payable_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .get_record_by_hash_params(&get_record_by_hash_failed_payable_cache_params_arc) + .get_record_by_hash_result(Some(failed_tx_1)) + .get_record_by_hash_result(Some(failed_tx_2)) + .ensure_empty_cache_params(&ensure_empty_cache_failed_payable_params_arc); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + let logger = Logger::new("test"); + let confirmed_tx_block_sent_tx = make_transaction_block(901); + let confirmed_tx_block_failed_tx = make_transaction_block(902); + let msg = TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_sent_tx)), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_failed_tx)) + ], + response_skeleton_opt: None, + }; - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - DEFAULT_PENDING_TOO_LONG_SEC + 1, - rowid, - hash, - ); + let result = subject.finish_scan(msg, &logger); - let elapsed_after = elapsed_since_secs_back(DEFAULT_PENDING_TOO_LONG_SEC + 1); assert_eq!( result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(rowid, hash)], - confirmed: vec![] - } + PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) + ); + let get_record_by_hash_failed_payable_cache_params = + get_record_by_hash_failed_payable_cache_params_arc + .lock() + .unwrap(); + assert_eq!( + *get_record_by_hash_failed_payable_cache_params, + vec![failed_tx_hash_1, failed_tx_hash_2] + ); + let get_record_by_hash_sent_payable_cache_params = + get_record_by_hash_sent_payable_cache_params_arc + .lock() + .unwrap(); + assert_eq!( + *get_record_by_hash_sent_payable_cache_params, + vec![sent_tx_hash_1, sent_tx_hash_2] ); - let capture_regex = "(\\d+){2}sec"; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "ERROR: {}: Pending transaction 0x00000000000000000000000000000000000000\ - 00000000000000000000000237 has exceeded the maximum pending time \\({}sec\\) with the age \ - \\d+sec and the confirmation process is going to be aborted now at the final attempt 1; manual \ - resolution is required from the user to complete the transaction" - , test_name, DEFAULT_PENDING_TOO_LONG_SEC, ), elapsed_after, capture_regex) + let pending_payable_ensure_empty_cache_params = + ensure_empty_cache_sent_payable_params_arc.lock().unwrap(); + assert_eq!(*pending_payable_ensure_empty_cache_params, vec![()]); + let failed_payable_ensure_empty_cache_params = + ensure_empty_cache_failed_payable_params_arc.lock().unwrap(); + assert_eq!(*failed_payable_ensure_empty_cache_params, vec![()]); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_within_waiting_interval"; - let hash = make_tx_hash(0x7b); - let rowid = 333; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC - 1; + fn finish_scan_with_missing_records_inside_caches_noticed_on_missing_sent_tx() { + // Note: the ordering of the hashes matters in this test + let sent_tx_hash_1 = make_tx_hash(0x123); + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.hash = sent_tx_hash_1; + let sent_tx_hash_2 = make_tx_hash(0x876); + let failed_tx_hash_1 = make_tx_hash(0x987); + let mut failed_tx_1 = make_failed_tx(567); + failed_tx_1.hash = failed_tx_hash_1; + let failed_tx_hash_2 = make_tx_hash(0x789); + let mut failed_tx_2 = make_failed_tx(890); + failed_tx_2.hash = failed_tx_hash_2; + let mut pending_payable_cache = CurrentPendingPayables::default(); + pending_payable_cache.load_cache(vec![sent_tx_1]); + let mut failed_payable_cache = RecheckRequiringFailures::default(); + failed_payable_cache.load_cache(vec![failed_tx_1, failed_tx_2]); + let mut subject = PendingPayableScannerBuilder::new().build(); + subject.current_sent_payables = Box::new(pending_payable_cache); + subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + let logger = Logger::new("test"); + let msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok( + StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, + }; - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let regex_str_in_pieces = vec![ + r#"Looking up 'SentPayable\(0x0000000000000000000000000000000000000000000000000000000000000876\)'"#, + r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, + r#" Unproven failures: \[FailedTx \{ hash:"#, + r#" 0x0000000000000000000000000000000000000000000000000000000000000987, receiver_address:"#, + r#" 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \d*,"#, + r#" gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: RetryRequired \}\]."#, + r#" Hashes yet not looked up: \[FailedPayable\(0x000000000000000000000000000000000000000"#, + r#"0000000000000000000000987\)\]"#, + ]; + let regex_str = regex_str_in_pieces.join(""); + let expected_msg_regex = Regex::new(®ex_str).unwrap(); + assert!( + expected_msg_regex.is_match(panic_msg), + "Expected string that matches this regex '{}' but it couldn't with '{}'", + regex_str, + panic_msg ); + } - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } + #[test] + fn finish_scan_with_missing_records_inside_caches_noticed_on_missing_failed_tx() { + let sent_tx_1 = make_sent_tx(456); + let sent_tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(789); + let sent_tx_hash_2 = sent_tx_2.hash; + let failed_tx_1 = make_failed_tx(567); + let failed_tx_hash_1 = failed_tx_1.hash; + let failed_tx_hash_2 = make_tx_hash(901); + let mut pending_payable_cache = CurrentPendingPayables::default(); + pending_payable_cache.load_cache(vec![sent_tx_1, sent_tx_2]); + let mut failed_payable_cache = RecheckRequiringFailures::default(); + failed_payable_cache.load_cache(vec![failed_tx_1]); + let mut subject = PendingPayableScannerBuilder::new().build(); + subject.current_sent_payables = Box::new(pending_payable_cache); + subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); + let logger = Logger::new("test"); + let msg = TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), + TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), + TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), + ], + response_skeleton_opt: None, + }; + + let panic = + catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let regex_str_in_pieces = vec![ + r#"Looking up 'FailedPayable\(0x0000000000000000000000000000000000000000000000000000000000000385\)'"#, + r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, + r#" Unproven failures: \[\]. Hashes yet not looked up: \[\]."#, + ]; + let regex_str = regex_str_in_pieces.join(""); + let expected_msg_regex = Regex::new(®ex_str).unwrap(); + assert!( + expected_msg_regex.is_match(panic_msg), + "Expected string that matches this regex '{}' but it couldn't with '{}'", + regex_str, + panic_msg ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 00000000000007b couldn't be confirmed at attempt 1 at \\d+ms after its sending"), elapsed_after_ms, capture_regex); } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit() { - let test_name = "interpret_transaction_receipt_when_transaction_status_is_none_and_time_equals_the_limit"; - let hash = make_tx_hash(0x237); - let rowid = 466; - let pending_payable_age = DEFAULT_PENDING_TOO_LONG_SEC; + fn throws_an_error_when_no_records_to_process_were_found() { + let now = SystemTime::now(); + let consuming_wallet = make_paying_wallet(b"consuming_wallet"); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(vec![]); + let mut subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); - let result = assert_interpreting_none_status_for_pending_payable( - test_name, - DEFAULT_PENDING_TOO_LONG_SEC, - pending_payable_age, - rowid, - hash, - ); + let result = subject.start_scan(&consuming_wallet, now, None, &Logger::new("test")); - let elapsed_after_ms = elapsed_since_secs_back(pending_payable_age) * 1000; - assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } - ); - let capture_regex = r#"\s(\d+)ms"#; - assert_log_msg_and_elapsed_time_in_log_makes_sense(&format!( - "INFO: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000000000000\ - 000000000000237 couldn't be confirmed at attempt 1 at \\d+ms after its sending", - ), elapsed_after_ms, capture_regex); + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(result, Err(StartScanError::NothingToProcess)); + assert_eq!(is_scan_running, false); + } + + #[test] + fn handle_failed_transactions_does_nothing_if_no_failure_detected() { + let subject = PendingPayableScannerBuilder::new().build(); + let detected_failures = DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")) + + // Mocked pending payable DAO without prepared results didn't panic which means none of its + // methods was used in this test } #[test] - fn interpret_transaction_receipt_when_transaction_status_is_a_failure() { + fn handle_failed_transactions_can_process_standard_tx_failures() { init_test_logging(); - let test_name = "interpret_transaction_receipt_when_transaction_status_is_a_failure"; - let mut tx_receipt = TransactionReceipt::default(); - tx_receipt.status = Some(U64::from(0)); //failure - let hash = make_tx_hash(0xd7); - let fingerprint = PendingPayableFingerprint { - rowid: 777777, - timestamp: SystemTime::now().sub(Duration::from_millis(150000)), - hash, - attempt: 5, - amount: 2222, - process_error: None, + let test_name = "handle_failed_transactions_can_process_standard_tx_failures"; + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1.clone()), + PresortedTxFailure::NewEntry(failed_tx_2.clone()), + ], + tx_receipt_rpc_failures: vec![], }; - let logger = Logger::new(test_name); - let scan_report = PendingPayableScanReport::default(); - let result = handle_status_with_failure(scan_report, fingerprint, &logger); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(777777, hash,)], - confirmed: vec![] - } + *insert_new_records_params, + vec![vec![failed_tx_1, failed_tx_2]] ); - TestLogHandler::new().exists_log_matching(&format!( - "ERROR: {test_name}: Pending transaction 0x0000000000000000000000000000000000000000\ - 0000000000000000000000d7 announced as a failure, interpreting attempt 5 after \ - 1500\\d\\dms from the sending" + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![hash_1, hash_2]]); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 were processed in the db" )); } #[test] - fn handle_pending_txs_with_receipts_handles_none_for_receipt() { + fn handle_failed_transactions_can_process_receipt_retrieval_rpc_failures() { init_test_logging(); - let test_name = "handle_pending_txs_with_receipts_handles_none_for_receipt"; - let subject = PendingPayableScannerBuilder::new().build(); - let rowid = 455; - let hash = make_tx_hash(0x913); - let fingerprint = PendingPayableFingerprint { - rowid, - timestamp: SystemTime::now().sub(Duration::from_millis(10000)), - hash, - attempt: 3, - amount: 111, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }), - fingerprint.clone(), - )], - response_skeleton_opt: None, + let test_name = "handle_failed_transactions_can_process_receipt_retrieval_rpc_failures"; + let retrieve_failed_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let retrieve_sent_txs_params_arc = Arc::new(Mutex::new(vec![])); + let update_statuses_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let hash_3 = make_tx_hash(0x987); + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(1)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(2)); + let timestamp_d = SystemTime::now().sub(Duration::from_secs(3)); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = hash_1; + failed_tx_1.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + failed_tx_2.status = + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ))); + let failed_payable_dao = FailedPayableDaoMock::default() + .retrieve_txs_params(&retrieve_failed_txs_params_arc) + .retrieve_txs_result(vec![failed_tx_1, failed_tx_2]) + .update_statuses_params(&update_statuses_failed_tx_params_arc) + .update_statuses_result(Ok(())); + let mut sent_tx = make_sent_tx(789); + sent_tx.hash = hash_3; + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let sent_payable_dao = SentPayableDaoMock::default() + .retrieve_txs_params(&retrieve_sent_txs_params_arc) + .retrieve_txs_result(vec![sent_tx.clone()]) + .update_statuses_params(&update_statuses_sent_tx_params_arc) + .update_statuses_result(Ok(())); + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b) + .now_result(timestamp_c); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new(validation_failure_clock)) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + )), + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_d), + ), + )), + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_3, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Pending(ValidationStatus::Waiting), + )), + ], }; - let result = subject.handle_receipts_for_pending_transactions(msg, &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new(test_name)); + let update_statuses_sent_tx_params = update_statuses_sent_tx_params_arc.lock().unwrap(); assert_eq!( - result, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(rowid, hash)], - failures: vec![], - confirmed: vec![] - } + *update_statuses_sent_tx_params, + vec![ + hashmap![hash_3 => TxStatus::Pending(ValidationStatus::Reattempting (PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &ValidationFailureClockMock::default().now_result(timestamp_a))))] + ] + ); + let mut update_statuses_failed_tx_params = + update_statuses_failed_tx_params_arc.lock().unwrap(); + let actual_params = update_statuses_failed_tx_params + .remove(0) + .into_iter() + .sorted_by_key(|(key, _)| *key) + .collect::>(); + let expected_params = hashmap!( + hash_1 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &ValidationFailureClockMock::default().now_result(timestamp_b))) + ), + hash_2 => FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp_d)).add_attempt(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockReal::default()))) + ).into_iter().sorted_by_key(|(key,_)|*key).collect::>(); + assert_eq!(actual_params, expected_params); + assert!( + update_statuses_failed_tx_params.is_empty(), + "Should be empty but: {:?}", + update_statuses_sent_tx_params ); - TestLogHandler::new().exists_log_matching(&format!( - "DEBUG: {test_name}: Interpreting a receipt for transaction \ - 0x0000000000000000000000000000000000000000000000000000000000000913 \ - but none was given; attempt 3, 100\\d\\dms since sending" + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Pending-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000987" + )); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Failed-tx statuses were processed in the db for validation failure \ + of txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654" )); + let expectedly_missing_log_msg_fragment = "Handling a validation failure, but the status"; + let otherwise_possible_log_msg = + PendingPayableScanner::status_not_updatable_log_msg(&"Something"); + assert!( + otherwise_possible_log_msg.contains(expectedly_missing_log_msg_fragment), + "We expected to select a true log fragment '{}', but it is not included in '{}'", + expectedly_missing_log_msg_fragment, + otherwise_possible_log_msg + ); + test_log_handler.exists_no_log_containing(&format!( + "DEBUG: {test_name}: {}", + expectedly_missing_log_msg_fragment + )) } #[test] - fn increment_scan_attempts_happy_path() { - let update_remaining_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let hash_1 = make_tx_hash(444888); - let rowid_1 = 3456; - let hash_2 = make_tx_hash(444888); - let rowid_2 = 3456; - let pending_payable_dao = PendingPayableDaoMock::default() - .increment_scan_attempts_params(&update_remaining_fingerprints_params_arc) - .increment_scan_attempts_result(Ok(())); - let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) - .build(); - let transaction_id_1 = PendingPayableId::new(rowid_1, hash_1); - let transaction_id_2 = PendingPayableId::new(rowid_2, hash_2); + fn handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated() { + init_test_logging(); + let test_name = "handle_rpc_failures_when_requested_for_a_status_which_cannot_be_updated"; + let hash_1 = make_tx_hash(0x321); + let hash_2 = make_tx_hash(0x654); + let subject = PendingPayableScannerBuilder::new().build(); - let _ = subject.update_remaining_fingerprints( - vec![transaction_id_1, transaction_id_2], - &Logger::new("test"), + subject.handle_rpc_failures( + vec![ + FailedValidationByTable::FailedPayable(FailedValidation::new( + hash_1, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RetryRequired, + )), + FailedValidationByTable::SentPayable(FailedValidation::new( + hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + TxStatus::Confirmed { + block_hash: "abc".to_string(), + block_number: 0, + detection: Detection::Normal, + }, + )), + ], + &Logger::new(test_name), ); - let update_remaining_fingerprints_params = - update_remaining_fingerprints_params_arc.lock().unwrap(); - assert_eq!( - *update_remaining_fingerprints_params, - vec![vec![rowid_1, rowid_2]] - ) + let test_log_handler = TestLogHandler::new(); + test_log_handler.exists_no_log_containing(&format!("INFO: {test_name}: ")); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \ + {{\"Confirmed\":{{\"block_hash\":\"abc\",\"block_number\":0,\"detection\":\"Normal\"}}}} \ + cannot be updated.", + )); + test_log_handler.exists_log_containing(&format!( + "DEBUG: {test_name}: Handling a validation failure, but the status \"RetryRequired\" \ + cannot be updated." + )); + // It didn't panic, which means none of the DAO methods was called because the DAOs are + // mocked in this test } #[test] #[should_panic( - expected = "Failure on incrementing scan attempts for fingerprints of \ - 0x000000000000000000000000000000000000000000000000000000000006c9d8 \ - due to UpdateFailed(\"yeah, bad\")" + expected = "Unable to update pending-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"bluh\")" )] - fn increment_scan_attempts_sad_path() { - let hash = make_tx_hash(0x6c9d8); - let rowid = 3456; - let pending_payable_dao = - PendingPayableDaoMock::default().increment_scan_attempts_result(Err( - PendingPayableDaoError::UpdateFailed("yeah, bad".to_string()), - )); + fn update_validation_status_for_sent_txs_panics_on_update_statuses() { + let failed_validation = FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ); + let sent_payable_dao = SentPayableDaoMock::default() + .update_statuses_result(Err(SentPayableDaoError::InvalidInput("bluh".to_string()))); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) .build(); - let logger = Logger::new("test"); - let transaction_id = PendingPayableId::new(rowid, hash); - let _ = subject.update_remaining_fingerprints(vec![transaction_id], &logger); + let _ = subject + .update_validation_status_for_sent_txs(vec![failed_validation], &Logger::new("test")); } #[test] - fn update_remaining_fingerprints_does_nothing_if_no_still_pending_transactions_remain() { - let subject = PendingPayableScannerBuilder::new().build(); + #[should_panic( + expected = "Unable to update failed-tx statuses for validation failures '[FailedValidation \ + { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ + AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"bluh\")" + )] + fn update_validation_status_for_failed_txs_panics_on_update_statuses() { + let failed_validation = FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ); + let failed_payable_dao = FailedPayableDaoMock::default() + .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("bluh".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) + .build(); + + let _ = subject + .update_validation_status_for_failed_txs(vec![failed_validation], &Logger::new("test")); + } + + #[test] + fn handle_failed_transactions_can_process_mixed_failures() { + let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let update_status_params_arc = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let timestamp = SystemTime::now(); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = tx_hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = tx_hash_2; + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_params(&insert_new_records_params_arc) + .insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .update_statuses_params(&update_status_params_arc) + .update_statuses_result(Ok(())) + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .validation_failure_clock(Box::new( + ValidationFailureClockMock::default().now_result(timestamp), + )) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(failed_tx_1.clone())], + tx_receipt_rpc_failures: vec![FailedValidationByTable::SentPayable( + FailedValidation::new( + tx_hash_2, + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ), + )], + }; - subject.update_remaining_fingerprints(vec![], &Logger::new("test")) + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); + assert_eq!(*insert_new_records_params, vec![vec![failed_tx_1]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_1]]); + let update_statuses_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_statuses_params, + vec![ + hashmap!(tx_hash_2 => TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), &ValidationFailureClockMock::default().now_result(timestamp))))) + ] + ); } #[test] - fn cancel_failed_transactions_works() { - init_test_logging(); - let test_name = "cancel_failed_transactions_works"; - let mark_failures_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = PendingPayableDaoMock::default() - .mark_failures_params(&mark_failures_params_arc) - .mark_failures_result(Ok(())); + #[should_panic(expected = "Unable to persist failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: NoChange")] + fn handle_failed_transactions_panics_when_it_fails_to_insert_failed_tx_record() { + let failed_payable_dao = FailedPayableDaoMock::default() + .insert_new_records_result(Err(FailedPayableDaoError::NoChange)); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let id_1 = PendingPayableId::new(2, make_tx_hash(0x7b)); - let id_2 = PendingPayableId::new(3, make_tx_hash(0x1c8)); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(vec![id_1, id_2], &Logger::new(test_name)); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + } - let mark_failures_params = mark_failures_params_arc.lock().unwrap(); - assert_eq!(*mark_failures_params, vec![vec![2, 3]]); - TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Broken transactions 0x000000000000000000000000000000000000000000000000000000000000007b, \ - 0x00000000000000000000000000000000000000000000000000000000000001c8 marked as an error. You should take over \ - the care of those to make sure your debts are going to be settled properly. At the moment, there is no automated \ - process fixing that without your assistance", - )); + #[test] + #[should_panic(expected = "Unable to purge sent payable records for failed txs \ + 0x000000000000000000000000000000000000000000000000000000000000014d, \ + 0x00000000000000000000000000000000000000000000000000000000000001bc due to: \ + InvalidInput(\"Booga\")")] + fn handle_failed_transactions_panics_when_it_fails_to_delete_obsolete_sent_tx_records() { + let failed_payable_dao = FailedPayableDaoMock::default().insert_new_records_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .delete_records_result(Err(SentPayableDaoError::InvalidInput("Booga".to_string()))); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let hash_1 = make_tx_hash(0x14d); + let hash_2 = make_tx_hash(0x1bc); + let mut failed_tx_1 = make_failed_tx(789); + failed_tx_1.hash = hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = hash_2; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::NewEntry(failed_tx_1), + PresortedTxFailure::NewEntry(failed_tx_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - #[should_panic( - expected = "Unsuccessful attempt for transactions 0x00000000000000000000000000000000000\ - 0000000000000000000000000014d, 0x000000000000000000000000000000000000000000000000000000\ - 00000001bc to mark fatal error at payable fingerprint due to UpdateFailed(\"no no no\"); \ - database unreliable" - )] - fn cancel_failed_transactions_panics_when_it_fails_to_mark_failure() { - let pending_payable_dao = PendingPayableDaoMock::default().mark_failures_result(Err( - PendingPayableDaoError::UpdateFailed("no no no".to_string()), + fn handle_failed_transactions_can_conclude_rechecked_failures() { + let update_status_params_arc = Arc::new(Mutex::new(vec![])); + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let mut failed_tx_1 = make_failed_tx(123); + failed_tx_1.hash = tx_hash_1; + let mut failed_tx_2 = make_failed_tx(456); + failed_tx_2.hash = tx_hash_2; + let failed_payable_dao = FailedPayableDaoMock::default() + .update_statuses_params(&update_status_params_arc) + .update_statuses_result(Ok(())); + let subject = PendingPayableScannerBuilder::new() + .failed_payable_dao(failed_payable_dao) + .build(); + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; + + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); + + let update_status_params = update_status_params_arc.lock().unwrap(); + assert_eq!( + *update_status_params, + vec![ + hashmap!(tx_hash_1 => FailureStatus::Concluded, tx_hash_2 => FailureStatus::Concluded), + ] + ); + } + + #[test] + #[should_panic(expected = "Unable to conclude rechecks for failed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000321, \ + 0x0000000000000000000000000000000000000000000000000000000000000654 due to: \ + InvalidInput(\"Booga\")")] + fn concluding_rechecks_fails_on_updating_statuses() { + let tx_hash_1 = make_tx_hash(0x321); + let tx_hash_2 = make_tx_hash(0x654); + let failed_payable_dao = FailedPayableDaoMock::default().update_statuses_result(Err( + FailedPayableDaoError::InvalidInput("Booga".to_string()), )); let subject = PendingPayableScannerBuilder::new() - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) .build(); - let transaction_id_1 = PendingPayableId::new(2, make_tx_hash(333)); - let transaction_id_2 = PendingPayableId::new(3, make_tx_hash(444)); - let transaction_ids = vec![transaction_id_1, transaction_id_2]; + let detected_failures = DetectedFailures { + tx_failures: vec![ + PresortedTxFailure::RecheckCompleted(tx_hash_1), + PresortedTxFailure::RecheckCompleted(tx_hash_2), + ], + tx_receipt_rpc_failures: vec![], + }; - subject.cancel_failed_transactions(transaction_ids, &Logger::new("test")); + subject.handle_failed_transactions(detected_failures, &Logger::new("test")); } #[test] - fn cancel_failed_transactions_does_nothing_if_no_tx_failures_detected() { - let subject = PendingPayableScannerBuilder::new().build(); + fn handle_confirmed_transactions_does_nothing_if_no_confirmation_found_on_the_blockchain() { + let mut subject = PendingPayableScannerBuilder::new().build(); + + subject + .handle_confirmed_transactions(DetectedConfirmations::default(), &Logger::new("test")) + + // Mocked payable DAO without prepared results didn't panic, which means none of its methods + // was used in this test + } + + #[test] + fn handles_failure_reclaims_alone() { + init_test_logging(); + let test_name = "handles_failure_reclaims_alone"; + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.cancel_failed_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &logger, + ); - //mocked pending payable DAO didn't panic which means we skipped the actual process + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) as confirmed on-chain", + )); } #[test] #[should_panic( - expected = "Unable to delete payable fingerprints 0x000000000000000000000000000000000\ - 0000000000000000000000000000315, 0x00000000000000000000000000000000000000000000000000\ - 0000000000021a of verified transactions due to RecordDeletion(\"the database \ - is fooling around with us\")" + expected = "Unable to proceed in a reclaim as the replacement of sent tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + failed due to: NoChange" )] - fn confirm_transactions_panics_while_deleting_pending_payable_fingerprint() { - let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Err( - PendingPayableDaoError::RecordDeletion( - "the database is fooling around with us".to_string(), - ), - )); + fn failure_reclaim_fails_on_replace_sent_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default() + .replace_records_result(Err(SentPayableDaoError::NoChange)); let mut subject = PendingPayableScannerBuilder::new() - .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.rowid = 1; - fingerprint_1.hash = make_tx_hash(0x315); - let mut fingerprint_2 = make_pending_payable_fingerprint(); - fingerprint_2.rowid = 1; - fingerprint_2.hash = make_tx_hash(0x21a); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![fingerprint_1, fingerprint_2], &Logger::new("test")); + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_does_nothing_if_none_found_on_the_blockchain() { - let mut subject = PendingPayableScannerBuilder::new().build(); + #[should_panic(expected = "Unable to delete failed tx records \ + 0x0000000000000000000000000000000000000000000000000000000000000123, \ + 0x0000000000000000000000000000000000000000000000000000000000000567 \ + to finish the reclaims due to: EmptyInput")] + fn failure_reclaim_fails_on_delete_failed_tx_record() { + let sent_payable_dao = SentPayableDaoMock::default().replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_result(Err(FailedPayableDaoError::EmptyInput)); + let mut subject = PendingPayableScannerBuilder::new() + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; - subject.confirm_transactions(vec![], &Logger::new("test")) + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx_1.clone(), sent_tx_2.clone()], + }, + &Logger::new("test"), + ); + } - //mocked payable DAO didn't panic which means we skipped the actual process + #[test] + #[should_panic( + expected = "Processing a reclaim for tx 0x0000000000000000000000000000000000000000000000000\ + 000000000000123 which isn't filled with the confirmation details" + )] + fn handle_failure_reclaim_meets_a_record_without_confirmation_details() { + let mut subject = PendingPayableScannerBuilder::new().build(); + let tx_hash = make_tx_hash(0x123); + let mut sent_tx = make_sent_tx(123_123); + sent_tx.hash = tx_hash; + // Here, it should be confirmed already in this status + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx.clone()], + }, + &Logger::new("test"), + ); } #[test] - fn confirm_transactions_works() { + fn handles_normal_confirmations_alone() { init_test_logging(); + let test_name = "handles_normal_confirmations_alone"; let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let pending_payable_dao = PendingPayableDaoMock::default() - .delete_fingerprints_params(&delete_fingerprints_params_arc) - .delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())); + let logger = Logger::new(test_name); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); - let rowid_1 = 2; - let rowid_2 = 5; - let pending_payable_fingerprint_1 = PendingPayableFingerprint { - rowid: rowid_1, - timestamp: from_unix_timestamp(199_000_000), - hash: make_tx_hash(0x123), - attempt: 1, - amount: 4567, - process_error: None, - }; - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: rowid_2, - timestamp: from_unix_timestamp(200_000_000), - hash: make_tx_hash(0x567), - attempt: 1, - amount: 5555, - process_error: None, - }; - - subject.confirm_transactions( - vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), - ], - &Logger::new("confirm_transactions_works"), + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(987_987); + sent_tx_2.hash = tx_hash_2; + let tx_block_2 = TxBlock { + block_hash: make_block_hash(67), + block_number: 6_789_898_789_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_2.block_hash), + block_number: tx_block_2.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone(), sent_tx_2.clone()], + reclaims: vec![], + }, + &logger, ); - let confirm_transactions_params = transactions_confirmed_params_arc.lock().unwrap(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!( + *transactions_confirmed_params, + vec![vec![sent_tx_1, sent_tx_2]] + ); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); assert_eq!( - *confirm_transactions_params, - vec![vec![ - pending_payable_fingerprint_1, - pending_payable_fingerprint_2 - ]] - ); - let delete_fingerprints_params = delete_fingerprints_params_arc.lock().unwrap(); - assert_eq!(*delete_fingerprints_params, vec![vec![rowid_1, rowid_2]]); + *confirm_tx_params, + vec![hashmap![tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2]] + ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "DEBUG: confirm_transactions_works: \ - Confirmation of transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567; \ - record for total paid payable was modified", + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 6789898789) were confirmed", + )); + } + + #[test] + fn mixed_tx_confirmations_work() { + init_test_logging(); + let test_name = "mixed_tx_confirmations_work"; + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let confirm_tx_params_arc = Arc::new(Mutex::new(vec![])); + let replace_records_params_arc = Arc::new(Mutex::new(vec![])); + let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transactions_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_params(&confirm_tx_params_arc) + .confirm_tx_result(Ok(())) + .replace_records_params(&replace_records_params_arc) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default() + .delete_records_params(&delete_records_params_arc) + .delete_records_result(Ok(())); + let logger = Logger::new(test_name); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .failed_payable_dao(failed_payable_dao) + .build(); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x913); + let mut sent_tx_1 = make_sent_tx(123_123); + sent_tx_1.hash = tx_hash_1; + let tx_block_1 = TxBlock { + block_hash: make_block_hash(45), + block_number: 4_578_989_878_u64.into(), + }; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_1.block_hash), + block_number: tx_block_1.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(567_567); + sent_tx_2.hash = tx_hash_2; + let tx_block_3 = TxBlock { + block_hash: make_block_hash(78), + block_number: 7_898_989_878_u64.into(), + }; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block_3.block_hash), + block_number: tx_block_3.block_number.as_u64(), + detection: Detection::Reclaim, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1.clone()], + reclaims: vec![sent_tx_2.clone()], + }, + &logger, ); - log_handler.exists_log_containing( - "INFO: confirm_transactions_works: \ - Transactions \ - 0x0000000000000000000000000000000000000000000000000000000000000123, \ - 0x0000000000000000000000000000000000000000000000000000000000000567 \ - completed their confirmation process succeeding", + + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![vec![sent_tx_1]]); + let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); + assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); + let replace_records_params = replace_records_params_arc.lock().unwrap(); + assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + let delete_records_params = delete_records_params_arc.lock().unwrap(); + assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Reclaimed txs \ + 0x0000000000000000000000000000000000000000000000000000000000000913 (block 7898989878) \ + as confirmed on-chain", + )); + log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 4578989878) was confirmed", + )); + } + + #[test] + #[should_panic( + expected = "Unable to update sent payable records 0x000000000000000000000000000000000000000\ + 000000000000000000000021a, 0x0000000000000000000000000000000000000000000000000000000000000315 \ + by their tx blocks due to: SqlExecutionFailed(\"The database manager is \ + a funny guy, he's fooling around with us\")" + )] + fn handle_confirmed_transactions_panics_while_updating_sent_payable_records_with_the_tx_blocks() + { + let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default().confirm_tx_result(Err( + SentPayableDaoError::SqlExecutionFailed( + "The database manager is a funny guy, he's fooling around with us".to_string(), + ), + )); + let mut subject = PendingPayableScannerBuilder::new() + .payable_dao(payable_dao) + .sent_payable_dao(sent_payable_dao) + .build(); + let mut sent_tx_1 = make_sent_tx(456); + let block = make_transaction_block(678); + sent_tx_1.hash = make_tx_hash(0x315); + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.hash = make_tx_hash(0x21a); + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", block.block_hash), + block_number: block.block_number.as_u64(), + detection: Detection::Normal, + }; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![], + }, + &Logger::new("test"), ); } #[test] #[should_panic( - expected = "Unable to cast confirmed pending payables 0x0000000000000000000000000000000000000000000\ - 000000000000000000315 into adjustment in the corresponding payable records due to RusqliteError\ - (\"record change not successful\")" + expected = "Unable to complete the tx confirmation by the adjustment of the payable accounts \ + 0x000000000000000000000077616c6c6574343536 due to: \ + RusqliteError(\"record change not successful\")" )] - fn confirm_transactions_panics_on_unchecking_payable_table() { + fn handle_confirmed_transactions_panics_on_unchecking_payable_table() { let hash = make_tx_hash(0x315); - let rowid = 3; let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( PayableDaoError::RusqliteError("record change not successful".to_string()), )); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let mut fingerprint = make_pending_payable_fingerprint(); - fingerprint.rowid = rowid; - fingerprint.hash = hash; + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = hash; + + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx], + reclaims: vec![], + }, + &Logger::new("test"), + ); + } + + #[test] + fn log_tx_success_is_agnostic_to_singular_or_plural_form() { + init_test_logging(); + let test_name = "log_tx_success_is_agnostic_to_singular_or_plural_form"; + let plural_case_name = format!("{}_testing_plural_case", test_name); + let singular_case_name = format!("{}_testing_singular_case", test_name); + let logger_plural = Logger::new(&plural_case_name); + let logger_singular = Logger::new(&singular_case_name); + let tx_hash_1 = make_tx_hash(0x123); + let tx_hash_2 = make_tx_hash(0x567); + let mut tx_block_1 = make_transaction_block(456); + tx_block_1.block_number = 1_234_501_u64.into(); + let mut tx_block_2 = make_transaction_block(789); + tx_block_2.block_number = 1_234_502_u64.into(); + let mut tx_hashes_and_blocks = hashmap!(tx_hash_1 => tx_block_1, tx_hash_2 => tx_block_2); + + PendingPayableScanner::log_tx_success(&logger_plural, &tx_hashes_and_blocks); + + tx_hashes_and_blocks.remove(&tx_hash_2); + + PendingPayableScanner::log_tx_success(&logger_singular, &tx_hashes_and_blocks); - subject.confirm_transactions(vec![fingerprint], &Logger::new("test")); + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "INFO: {plural_case_name}: Txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501), 0x0000000000000000000000000000000000000000000000000000000000000567 \ + (block 1234502) were confirmed", + )); + log_handler.exists_log_containing(&format!( + "INFO: {singular_case_name}: Tx 0x0000000000000000000000000000000000000000000000000000000000000123 \ + (block 1234501) was confirmed", + )); } #[test] fn total_paid_payable_rises_with_each_bill_paid() { + init_test_logging(); let test_name = "total_paid_payable_rises_with_each_bill_paid"; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(189_999_888), - hash: make_tx_hash(56789), - attempt: 1, - amount: 5478, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 6, - timestamp: from_unix_timestamp(200_000_011), - hash: make_tx_hash(33333), - attempt: 1, - amount: 6543, - process_error: None, + let mut sent_tx_1 = make_sent_tx(456); + sent_tx_1.amount_minor = 5478; + sent_tx_1.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(123)), + block_number: 89898, + detection: Detection::Normal, + }; + let mut sent_tx_2 = make_sent_tx(789); + sent_tx_2.amount_minor = 3344; + sent_tx_2.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(234)), + block_number: 66312, + detection: Detection::Normal, + }; + let mut sent_tx_3 = make_sent_tx(789); + sent_tx_3.amount_minor = 6543; + sent_tx_3.status = TxStatus::Confirmed { + block_hash: format!("{:?}", make_block_hash(321)), + block_number: 67676, + detection: Detection::Reclaim, }; let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let sent_payable_dao = SentPayableDaoMock::default() + .confirm_tx_result(Ok(())) + .replace_records_result(Ok(())); + let failed_payable_dao = FailedPayableDaoMock::default().delete_records_result(Ok(())); let mut subject = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) - .pending_payable_dao(pending_payable_dao) + .failed_payable_dao(failed_payable_dao) + .sent_payable_dao(sent_payable_dao) .build(); let mut financial_statistics = subject.financial_statistics.borrow().clone(); financial_statistics.total_paid_payable_wei += 1111; subject.financial_statistics.replace(financial_statistics); - subject.confirm_transactions( - vec![fingerprint_1.clone(), fingerprint_2.clone()], + subject.handle_confirmed_transactions( + DetectedConfirmations { + normal_confirmations: vec![sent_tx_1, sent_tx_2], + reclaims: vec![sent_tx_3], + }, &Logger::new(test_name), ); let total_paid_payable = subject.financial_statistics.borrow().total_paid_payable_wei; - assert_eq!(total_paid_payable, 1111 + 5478 + 6543); + assert_eq!(total_paid_payable, 1111 + 5478 + 3344 + 6543); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!("DEBUG: {test_name}: The total paid payables increased by 6,543 to 7,654 wei"), + &format!( + "DEBUG: {test_name}: The total paid payables increased by 8,822 to 16,476 wei" + ), + ]); } } diff --git a/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs new file mode 100644 index 000000000..473fd28cb --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/test_utils.rs @@ -0,0 +1,23 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::validation_status::ValidationFailureClock; +use std::cell::RefCell; +use std::time::SystemTime; + +#[derive(Default)] +pub struct ValidationFailureClockMock { + now_results: RefCell>, +} + +impl ValidationFailureClock for ValidationFailureClockMock { + fn now(&self) -> SystemTime { + self.now_results.borrow_mut().remove(0) + } +} + +impl ValidationFailureClockMock { + pub fn now_result(self, result: SystemTime) -> Self { + self.now_results.borrow_mut().push(result); + self + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs new file mode 100644 index 000000000..4bf96bf1e --- /dev/null +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -0,0 +1,706 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureReason}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentTx, TxStatus, +}; +use crate::accountant::db_access_objects::utils::from_unix_timestamp; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + ConfirmationType, FailedValidation, FailedValidationByTable, ReceiptScanReport, TxByTable, + TxCaseToBeInterpreted, TxHashByTable, +}; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, +}; +use crate::blockchain::errors::internal_errors::InternalErrorKind; +use crate::blockchain::errors::rpc_errors::AppRpcError; +use crate::blockchain::errors::BlockchainErrorKind; +use itertools::Either; +use masq_lib::logger::Logger; +use std::time::SystemTime; +use thousands::Separable; + +#[derive(Default)] +pub struct TxReceiptInterpreter {} + +impl TxReceiptInterpreter { + pub fn compose_receipt_scan_report( + &self, + tx_cases: Vec, + pending_payable_scanner: &PendingPayableScanner, + logger: &Logger, + ) -> ReceiptScanReport { + let scan_report = ReceiptScanReport::default(); + tx_cases + .into_iter() + .fold(scan_report, |scan_report_so_far, tx_case| { + match tx_case.tx_receipt_result { + Ok(tx_status) => match tx_status { + StatusReadFromReceiptCheck::Succeeded(tx_block) => { + Self::handle_tx_confirmation( + scan_report_so_far, + tx_case.tx_by_table, + tx_block, + logger, + ) + } + StatusReadFromReceiptCheck::Reverted => Self::handle_reverted_tx( + scan_report_so_far, + tx_case.tx_by_table, + logger, + ), + StatusReadFromReceiptCheck::Pending => Self::handle_still_pending_tx( + scan_report_so_far, + tx_case.tx_by_table, + &*pending_payable_scanner.sent_payable_dao, + logger, + ), + }, + Err(e) => { + Self::handle_rpc_failure(scan_report_so_far, tx_case.tx_by_table, e, logger) + } + } + }) + } + + fn handle_still_pending_tx( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + sent_payable_dao: &dyn SentPayableDao, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + info!( + logger, + "Tx {:?} not confirmed within {} ms. Will resubmit with higher gas price", + sent_tx.hash, + Self::elapsed_in_ms(from_unix_timestamp(sent_tx.timestamp)) + .separate_with_commas() + ); + let failed_tx = FailedTx::from((sent_tx, FailureReason::PendingTooLong)); + scan_report.register_new_failure(failed_tx); + } + TxByTable::FailedPayable(failed_tx) => { + if failed_tx.reason != FailureReason::PendingTooLong { + unreachable!( + "Transaction is both pending and failed (failure reason: '{:?}'). Should be \ + possible only with the reason 'PendingTooLong'", + failed_tx.reason + ) + } + let replacement_tx = sent_payable_dao + .retrieve_txs(Some(RetrieveCondition::ByNonce(vec![failed_tx.nonce]))); + let replacement_tx_hash = replacement_tx + .get(0) + .unwrap_or_else(|| { + panic!( + "Attempted to display a replacement tx for {:?} but couldn't find \ + one in the database", + failed_tx.hash + ) + }) + .hash; + warning!( + logger, + "Failed tx {:?} on a recheck was found pending on its receipt unexpectedly. \ + It was supposed to be replaced by {:?}", + failed_tx.hash, + replacement_tx_hash + ); + scan_report.register_rpc_failure(FailedValidationByTable::FailedPayable( + FailedValidation::new( + failed_tx.hash, + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + failed_tx.status, + ), + )) + } + } + scan_report + } + + fn elapsed_in_ms(timestamp: SystemTime) -> u128 { + timestamp + .elapsed() + .expect("time calculation for elapsed failed") + .as_millis() + } + + fn handle_tx_confirmation( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + tx_block: TxBlock, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + info!( + logger, + "Pending tx {:?} was confirmed on-chain", sent_tx.hash, + ); + + let completed_sent_tx = SentTx { + status: TxStatus::Confirmed { + block_hash: format!("{:?}", tx_block.block_hash), + block_number: tx_block.block_number.as_u64(), + detection: Detection::Normal, + }, + ..sent_tx + }; + scan_report.register_confirmed_tx(completed_sent_tx, ConfirmationType::Normal); + } + TxByTable::FailedPayable(failed_tx) => { + info!( + logger, + "Failed tx {:?} was later confirmed on-chain and will be reclaimed", + failed_tx.hash + ); + + let sent_tx = SentTx::from((failed_tx, tx_block)); + scan_report.register_confirmed_tx(sent_tx, ConfirmationType::Reclaim); + } + } + scan_report + } + + //TODO: failures handling might need enhancement suggested by GH-693 + fn handle_reverted_tx( + mut scan_report: ReceiptScanReport, + tx: TxByTable, + logger: &Logger, + ) -> ReceiptScanReport { + match tx { + TxByTable::SentPayable(sent_tx) => { + let failure_reason = FailureReason::Reverted; + let failed_tx = FailedTx::from((sent_tx, failure_reason)); + + warning!(logger, "Pending tx {:?} was reverted", failed_tx.hash,); + + scan_report.register_new_failure(failed_tx); + } + TxByTable::FailedPayable(failed_tx) => { + debug!( + logger, + "Reverted tx {:?} on a recheck after {}. Status will be changed to \"Concluded\"", + failed_tx.hash, + failed_tx.reason, + ); + + scan_report.register_finalization_of_unproven_failure(failed_tx.hash); + } + } + scan_report + } + + fn handle_rpc_failure( + mut scan_report: ReceiptScanReport, + tx_by_table: TxByTable, + rpc_error: AppRpcError, + logger: &Logger, + ) -> ReceiptScanReport { + warning!( + logger, + "Failed to retrieve tx receipt for {:?}: {:?}. Will retry receipt retrieval next cycle", + TxHashByTable::from(&tx_by_table), + rpc_error + ); + let hash = tx_by_table.hash(); + let validation_status_update = match tx_by_table { + TxByTable::SentPayable(sent_tx) => { + FailedValidationByTable::new(hash, rpc_error, Either::Left(sent_tx.status)) + } + TxByTable::FailedPayable(failed_tx) => { + FailedValidationByTable::new(hash, rpc_error, Either::Right(failed_tx.status)) + } + }; + scan_report.register_rpc_failure(validation_status_update); + scan_report + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentTx, TxStatus, + }; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + DetectedConfirmations, DetectedFailures, FailedValidation, FailedValidationByTable, + PresortedTxFailure, ReceiptScanReport, TxByTable, + }; + use crate::accountant::test_utils::{ + make_failed_tx, make_sent_tx, make_transaction_block, SentPayableDaoMock, + }; + use crate::blockchain::errors::internal_errors::InternalErrorKind; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, + }; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::make_tx_hash; + use crate::test_utils::unshared_test_utils::capture_digits_with_separators_from_str; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; + + #[test] + fn interprets_receipt_for_pending_tx_if_it_is_a_success() { + init_test_logging(); + let test_name = "interprets_tx_receipt_if_it_is_a_success"; + let hash = make_tx_hash(0xcdef); + let mut sent_tx = make_sent_tx(2244); + sent_tx.hash = hash; + sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); + let tx_block = make_transaction_block(1234); + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_tx_confirmation( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + tx_block, + &logger, + ); + + let mut updated_tx = sent_tx; + updated_tx.status = TxStatus::Confirmed { + block_hash: "0x000000000000000000000000000000000000000000000000000000003b9aced2" + .to_string(), + block_number: 1879080904, + detection: Detection::Normal, + }; + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures::default(), + confirmations: DetectedConfirmations { + normal_confirmations: vec![updated_tx], + reclaims: vec![] + } + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ + 00000cdef was confirmed on-chain", + )); + } + + #[test] + fn interprets_receipt_for_failed_tx_being_rechecked_when_it_is_a_success() { + init_test_logging(); + let test_name = "interprets_receipt_for_failed_tx_being_rechecked_when_it_is_a_success"; + let hash = make_tx_hash(0xcdef); + let mut failed_tx = make_failed_tx(2244); + failed_tx.hash = hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let tx_block = make_transaction_block(1234); + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_tx_confirmation( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + tx_block, + &logger, + ); + + let sent_tx = SentTx::from((failed_tx, tx_block)); + assert!( + matches!( + sent_tx.status, + TxStatus::Confirmed { + detection: Detection::Reclaim, + .. + } + ), + "We expected reclaimed tx, but it says: {:?}", + sent_tx + ); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures::default(), + confirmations: DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![sent_tx] + } + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ + 00000cdef was later confirmed on-chain and will be reclaimed", + )); + } + + #[test] + fn interprets_tx_receipt_for_pending_tx_when_tx_status_says_reverted() { + init_test_logging(); + let test_name = "interprets_tx_receipt_for_pending_tx_when_tx_status_says_reverted"; + let hash = make_tx_hash(0xabc); + let mut sent_tx = make_sent_tx(2244); + sent_tx.hash = hash; + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_reverted_tx( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + &logger, + ); + + let failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(failed_tx)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Pending tx 0x0000000000000000000000000000000000000000000000000000000\ + 000000abc was reverted", + )); + } + + #[test] + fn interprets_tx_receipt_for_failed_tx_when_newly_fetched_tx_status_says_reverted() { + init_test_logging(); + let test_name = "interprets_tx_receipt_for_failed_tx_when_tx_status_reveals_failure"; + let tx_hash = make_tx_hash(0xabc); + let mut failed_tx = make_failed_tx(2244); + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let logger = Logger::new(test_name); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_reverted_tx( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + &logger, + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::RecheckCompleted(tx_hash)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "DEBUG: {test_name}: Reverted tx 0x000000000000000000000000000000000000000000000000000000\ + 0000000abc on a recheck after \"PendingTooLong\". Status will be changed to \"Concluded\"", + )); + } + + #[test] + fn interprets_tx_receipt_for_pending_payable_if_the_tx_keeps_pending() { + init_test_logging(); + let retrieve_txs_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "interprets_tx_receipt_for_pending_payable_if_the_tx_keeps_pending"; + let newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_params(&retrieve_txs_params_arc) + .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + let hash = make_tx_hash(0x913); + let sent_tx_timestamp = to_unix_timestamp( + SystemTime::now() + .checked_sub(Duration::from_secs(120)) + .unwrap(), + ); + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = hash; + sent_tx.timestamp = sent_tx_timestamp; + let scan_report = ReceiptScanReport::default(); + let before = SystemTime::now(); + + let result = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::SentPayable(sent_tx.clone()), + &sent_payable_dao, + &Logger::new(test_name), + ); + + let after = SystemTime::now(); + let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::PendingTooLong)); + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![PresortedTxFailure::NewEntry(expected_failed_tx)], + tx_receipt_rpc_failures: vec![] + }, + confirmations: DetectedConfirmations::default() + } + ); + let log_handler = TestLogHandler::new(); + let log_idx = log_handler.exists_log_matching(&format!( + "INFO: {test_name}: Tx \ + 0x0000000000000000000000000000000000000000000000000000000000000913 not confirmed within \ + \\d{{1,3}}(,\\d{{3}})* ms. Will resubmit with higher gas price" + )); + let log_msg = log_handler.get_log_at(log_idx); + let str_elapsed_ms = capture_digits_with_separators_from_str(&log_msg, 3, ','); + let elapsed_ms = str_elapsed_ms[0].replace(",", "").parse::().unwrap(); + let elapsed_ms_when_before = before + .duration_since(from_unix_timestamp(sent_tx_timestamp)) + .unwrap() + .as_millis(); + let elapsed_ms_when_after = after + .duration_since(from_unix_timestamp(sent_tx_timestamp)) + .unwrap() + .as_millis(); + assert!( + elapsed_ms_when_before <= elapsed_ms && elapsed_ms <= elapsed_ms_when_after, + "we expected the elapsed time {} ms to be between {} and {}.", + elapsed_ms, + elapsed_ms_when_before, + elapsed_ms_when_after + ); + } + + #[test] + fn interprets_tx_receipt_for_supposedly_failed_tx_if_the_tx_keeps_pending() { + init_test_logging(); + let retrieve_txs_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "interprets_tx_receipt_for_supposedly_failed_tx_if_the_tx_keeps_pending"; + let mut newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + newer_sent_tx_for_older_failed_tx.hash = make_tx_hash(0x7c6); + let sent_payable_dao = SentPayableDaoMock::new() + .retrieve_txs_params(&retrieve_txs_params_arc) + .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + let tx_hash = make_tx_hash(0x913); + let mut failed_tx = make_failed_tx(789); + let failed_tx_nonce = failed_tx.nonce; + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::PendingTooLong; + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(failed_tx.clone()), + &sent_payable_dao, + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::Internal( + InternalErrorKind::PendingTooLongNotReplaced + ), + FailureStatus::RecheckRequired(ValidationStatus::Waiting) + ) + )] + }, + confirmations: DetectedConfirmations::default() + } + ); + let retrieve_txs_params = retrieve_txs_params_arc.lock().unwrap(); + assert_eq!( + *retrieve_txs_params, + vec![Some(RetrieveCondition::ByNonce(vec![failed_tx_nonce]))] + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Failed tx 0x0000000000000000000000000000000000000000000000000000000\ + 000000913 on a recheck was found pending on its receipt unexpectedly. It was supposed \ + to be replaced by 0x00000000000000000000000000000000000000000000000000000000000007c6" + )); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Transaction is both pending \ + and failed (failure reason: 'Reverted'). Should be possible only with the reason 'PendingTooLong'" + )] + fn interprets_failed_tx_recheck_as_still_pending_while_the_failure_reason_wasnt_pending_too_long( + ) { + let mut newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); + newer_sent_tx_for_older_failed_tx.hash = make_tx_hash(0x7c6); + let sent_payable_dao = SentPayableDaoMock::new(); + let tx_hash = make_tx_hash(0x913); + let mut failed_tx = make_failed_tx(789); + failed_tx.hash = tx_hash; + failed_tx.status = FailureStatus::RecheckRequired(ValidationStatus::Waiting); + failed_tx.reason = FailureReason::Reverted; + let scan_report = ReceiptScanReport::default(); + + let _ = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(failed_tx), + &sent_payable_dao, + &Logger::new("test"), + ); + } + + #[test] + #[should_panic( + expected = "Attempted to display a replacement tx for 0x000000000000000000000000000\ + 00000000000000000000000000000000001c8 but couldn't find one in the database" + )] + fn handle_still_pending_tx_if_unexpected_behavior_due_to_already_failed_tx_and_db_retrieval_fails( + ) { + let scan_report = ReceiptScanReport::default(); + let still_pending_tx = make_failed_tx(456); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + + let _ = TxReceiptInterpreter::handle_still_pending_tx( + scan_report, + TxByTable::FailedPayable(still_pending_tx), + &sent_payable_dao, + &Logger::new("test"), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_pending_payable_in_first_attempt() { + let test_name = + "interprets_failed_retrieval_of_receipt_for_pending_payable_in_first_attempt"; + + test_failed_retrieval_of_receipt_for_pending_payable( + test_name, + TxStatus::Pending(ValidationStatus::Waiting), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_pending_payable_as_reattempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_pending_payable_as_reattempt"; + + test_failed_retrieval_of_receipt_for_pending_payable( + test_name, + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockReal::default(), + ))), + ); + } + + fn test_failed_retrieval_of_receipt_for_pending_payable( + test_name: &str, + current_tx_status: TxStatus, + ) { + init_test_logging(); + let tx_hash = make_tx_hash(913); + let mut sent_tx = make_sent_tx(456); + sent_tx.hash = tx_hash; + sent_tx.status = current_tx_status.clone(); + let rpc_error = AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_rpc_failure( + scan_report, + TxByTable::SentPayable(sent_tx), + rpc_error.clone(), + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::SentPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&rpc_error).into()), + current_tx_status + ) + ),] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing( + &format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000\ + 000000000000000000000000000000000000000000000000000391): Remote(InvalidResponse(\"bluh\")). \ + Will retry receipt retrieval next cycle")); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_failed_tx_in_first_attempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_failed_tx_in_first_attempt"; + + test_failed_retrieval_of_receipt_for_failed_tx( + test_name, + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ); + } + + #[test] + fn interprets_failed_retrieval_of_receipt_for_failed_tx_as_reattempt() { + let test_name = "interprets_failed_retrieval_of_receipt_for_failed_tx_as_reattempt"; + + test_failed_retrieval_of_receipt_for_failed_tx( + test_name, + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + &ValidationFailureClockReal::default(), + ))), + ); + } + + fn test_failed_retrieval_of_receipt_for_failed_tx( + test_name: &str, + current_failure_status: FailureStatus, + ) { + init_test_logging(); + let tx_hash = make_tx_hash(914); + let mut failed_tx = make_failed_tx(456); + failed_tx.hash = tx_hash; + failed_tx.status = current_failure_status.clone(); + let rpc_error = AppRpcError::Local(LocalError::Internal); + let scan_report = ReceiptScanReport::default(); + + let result = TxReceiptInterpreter::handle_rpc_failure( + scan_report, + TxByTable::FailedPayable(failed_tx), + rpc_error.clone(), + &Logger::new(test_name), + ); + + assert_eq!( + result, + ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&rpc_error).into()), + current_failure_status + ) + )] + }, + confirmations: DetectedConfirmations::default() + } + ); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Failed to retrieve tx receipt for FailedPayable(0x0000000000\ + 000000000000000000000000000000000000000000000000000392): Local(Internal). \ + Will retry receipt retrieval next cycle" + )); + } +} diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index f277a1c91..d08808d75 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -1,191 +1,1160 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::PendingPayableId; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::TxReceiptResult; +use crate::blockchain::errors::rpc_errors::AppRpcError; +use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClock, ValidationStatus, +}; +use crate::blockchain::errors::BlockchainErrorKind; +use itertools::Either; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; -use std::time::SystemTime; +use std::collections::HashMap; #[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct PendingPayableScanReport { - pub still_pending: Vec, - pub failures: Vec, - pub confirmed: Vec, +pub struct ReceiptScanReport { + pub failures: DetectedFailures, + pub confirmations: DetectedConfirmations, } -impl PendingPayableScanReport { - pub fn requires_payments_retry(&self) -> bool { - todo!("complete my within GH-642") +impl ReceiptScanReport { + pub fn requires_payments_retry(&self) -> Option { + match ( + self.failures.requires_retry(), + self.confirmations.is_empty(), + ) { + (None, true) => unreachable!("reading tx receipts gave no results, but always should"), + (None, _) => None, + (Some(retry), _) => Some(retry), + } + } + + pub(super) fn register_confirmed_tx( + &mut self, + confirmed_tx: SentTx, + confirmation_type: ConfirmationType, + ) { + match confirmation_type { + ConfirmationType::Normal => self.confirmations.normal_confirmations.push(confirmed_tx), + ConfirmationType::Reclaim => self.confirmations.reclaims.push(confirmed_tx), + } + } + + pub(super) fn register_new_failure(&mut self, failed_tx: FailedTx) { + self.failures + .tx_failures + .push(PresortedTxFailure::NewEntry(failed_tx)); + } + + pub(super) fn register_finalization_of_unproven_failure(&mut self, tx_hash: TxHash) { + self.failures + .tx_failures + .push(PresortedTxFailure::RecheckCompleted(tx_hash)); + } + + pub(super) fn register_rpc_failure(&mut self, status_update: FailedValidationByTable) { + self.failures.tx_receipt_rpc_failures.push(status_update); + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedConfirmations { + pub normal_confirmations: Vec, + pub reclaims: Vec, +} + +impl DetectedConfirmations { + pub(super) fn is_empty(&self) -> bool { + self.normal_confirmations.is_empty() && self.reclaims.is_empty() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ConfirmationType { + Normal, + Reclaim, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct DetectedFailures { + pub tx_failures: Vec, + pub tx_receipt_rpc_failures: Vec, +} + +impl DetectedFailures { + fn requires_retry(&self) -> Option { + if self.tx_failures.is_empty() && self.tx_receipt_rpc_failures.is_empty() { + None + } else if !self.tx_failures.is_empty() { + Some(Retry::RetryPayments) + } else { + Some(Retry::RetryTxStatusCheckOnly) + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PresortedTxFailure { + NewEntry(FailedTx), + RecheckCompleted(TxHash), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum FailedValidationByTable { + SentPayable(FailedValidation), + FailedPayable(FailedValidation), +} + +impl FailedValidationByTable { + pub fn new( + tx_hash: TxHash, + error: AppRpcError, + status: Either, + ) -> Self { + match status { + Either::Left(tx_status) => Self::SentPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + tx_status, + )), + Either::Right(failure_reason) => Self::FailedPayable(FailedValidation::new( + tx_hash, + BlockchainErrorKind::AppRpc((&error).into()), + failure_reason, + )), + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct FailedValidation { + pub tx_hash: TxHash, + pub validation_failure: BlockchainErrorKind, + pub current_status: RecordStatus, +} + +impl FailedValidation +where + RecordStatus: UpdatableValidationStatus, +{ + pub fn new( + tx_hash: TxHash, + validation_failure: BlockchainErrorKind, + current_status: RecordStatus, + ) -> Self { + Self { + tx_hash, + validation_failure, + current_status, + } + } + + pub fn new_status(&self, clock: &dyn ValidationFailureClock) -> Option { + self.current_status + .update_after_failure(self.validation_failure, clock) + } +} + +pub trait UpdatableValidationStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option + where + Self: Sized; +} + +impl UpdatableValidationStatus for TxStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + TxStatus::Pending(ValidationStatus::Waiting) => Some(TxStatus::Pending( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )), + TxStatus::Pending(ValidationStatus::Reattempting(previous_attempts)) => { + Some(TxStatus::Pending(ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error, clock), + ))) + } + TxStatus::Confirmed { .. } => None, + } + } +} + +impl UpdatableValidationStatus for FailureStatus { + fn update_after_failure( + &self, + error: BlockchainErrorKind, + clock: &dyn ValidationFailureClock, + ) -> Option { + match self { + FailureStatus::RecheckRequired(ValidationStatus::Waiting) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new(error, clock)), + )) + } + FailureStatus::RecheckRequired(ValidationStatus::Reattempting(previous_attempts)) => { + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting( + previous_attempts.clone().add_attempt(error.into(), clock), + ), + )) + } + FailureStatus::RetryRequired | FailureStatus::Concluded => None, + } + } +} + +pub struct MismatchReport { + pub noticed_with: TxHashByTable, + pub remaining_hashes: Vec, +} + +pub trait PendingPayableCache { + fn load_cache(&mut self, records: Vec); + fn get_record_by_hash(&mut self, hash: TxHash) -> Option; + fn ensure_empty_cache(&mut self, logger: &Logger); + fn dump_cache(&mut self) -> HashMap; +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct CurrentPendingPayables { + pub(super) sent_payables: HashMap, +} + +impl PendingPayableCache for CurrentPendingPayables { + fn load_cache(&mut self, records: Vec) { + self.sent_payables + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.sent_payables.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.sent_payables.is_empty() { + debug!( + logger, + "Cache misuse - some pending payables left unprocessed: {:?}. Dumping.", + self.sent_payables + ); + } + self.sent_payables.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.sent_payables.drain().collect() + } +} + +impl CurrentPendingPayables { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct RecheckRequiringFailures { + pub(super) failures: HashMap, +} + +impl PendingPayableCache for RecheckRequiringFailures { + fn load_cache(&mut self, records: Vec) { + self.failures + .extend(records.into_iter().map(|tx| (tx.hash, tx))); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.failures.remove(&hash) + } + + fn ensure_empty_cache(&mut self, logger: &Logger) { + if !self.failures.is_empty() { + debug!( + logger, + "Cache misuse - some tx failures left unprocessed: {:?}. Dumping.", self.failures + ); + } + self.failures.clear() + } + + fn dump_cache(&mut self) -> HashMap { + self.failures.drain().collect() + } +} + +impl RecheckRequiringFailures { + pub fn new() -> Self { + Self::default() } } #[derive(Debug, PartialEq, Eq)] pub enum PendingPayableScanResult { NoPendingPayablesLeft(Option), - PaymentRetryRequired, -} - -pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { - timestamp - .elapsed() - .expect("time calculation for elapsed failed") - .as_millis() -} - -pub fn handle_none_status( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - max_pending_interval: u64, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Pending transaction {:?} couldn't be confirmed at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - let elapsed = fingerprint - .timestamp - .elapsed() - .expect("we should be older now"); - let elapsed = elapsed.as_secs(); - if elapsed > max_pending_interval { - error!( - logger, - "Pending transaction {:?} has exceeded the maximum pending time \ - ({}sec) with the age {}sec and the confirmation process is going to be aborted now \ - at the final attempt {}; manual resolution is required from the \ - user to complete the transaction.", - fingerprint.hash, - max_pending_interval, - elapsed, - fingerprint.attempt - ); - scan_report.failures.push(fingerprint.into()) - } else { - scan_report.still_pending.push(fingerprint.into()) - } - scan_report -} - -pub fn handle_status_with_success( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - info!( - logger, - "Transaction {:?} has been added to the blockchain; detected locally at attempt \ - {} at {}ms after its sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.confirmed.push(fingerprint); - scan_report -} - -//TODO: failures handling is going to need enhancement suggested by GH-693 -pub fn handle_status_with_failure( - mut scan_report: PendingPayableScanReport, - fingerprint: PendingPayableFingerprint, - logger: &Logger, -) -> PendingPayableScanReport { - error!( - logger, - "Pending transaction {:?} announced as a failure, interpreting attempt \ - {} after {}ms from the sending", - fingerprint.hash, - fingerprint.attempt, - elapsed_in_ms(fingerprint.timestamp) - ); - scan_report.failures.push(fingerprint.into()); - scan_report -} - -pub fn handle_none_receipt( - mut scan_report: PendingPayableScanReport, - payable: PendingPayableFingerprint, - error_msg: &str, - logger: &Logger, -) -> PendingPayableScanReport { - debug!( - logger, - "Interpreting a receipt for transaction {:?} but {}; attempt {}, {}ms since sending", - payable.hash, - error_msg, - payable.attempt, - elapsed_in_ms(payable.timestamp) - ); - - scan_report - .still_pending - .push(PendingPayableId::new(payable.rowid, payable.hash)); - scan_report + PaymentRetryRequired(Either), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Retry { + RetryPayments, + RetryTxStatusCheckOnly, +} + +pub struct TxCaseToBeInterpreted { + pub tx_by_table: TxByTable, + pub tx_receipt_result: TxReceiptResult, +} + +impl TxCaseToBeInterpreted { + pub fn new(tx_by_table: TxByTable, tx_receipt_result: TxReceiptResult) -> Self { + Self { + tx_by_table, + tx_receipt_result, + } + } +} + +#[derive(Debug)] +pub enum TxByTable { + SentPayable(SentTx), + FailedPayable(FailedTx), +} + +impl TxByTable { + pub fn hash(&self) -> TxHash { + match self { + TxByTable::SentPayable(tx) => tx.hash, + TxByTable::FailedPayable(tx) => tx.hash, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum TxHashByTable { + SentPayable(TxHash), + FailedPayable(TxHash), +} + +impl TxHashByTable { + pub fn hash(&self) -> TxHash { + match self { + TxHashByTable::SentPayable(hash) => *hash, + TxHashByTable::FailedPayable(hash) => *hash, + } + } +} + +impl From<&TxByTable> for TxHashByTable { + fn from(tx: &TxByTable) -> Self { + match tx { + TxByTable::SentPayable(tx) => TxHashByTable::SentPayable(tx.hash), + TxByTable::FailedPayable(tx) => TxHashByTable::FailedPayable(tx.hash), + } + } } #[cfg(test)] mod tests { + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, TxStatus}; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, + FailedValidationByTable, PendingPayableCache, PresortedTxFailure, ReceiptScanReport, + RecheckRequiringFailures, Retry, TxByTable, TxHashByTable, + }; + use crate::accountant::test_utils::{make_failed_tx, make_sent_tx}; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, ValidationStatus, + }; + use crate::blockchain::errors::BlockchainErrorKind; + use crate::blockchain::test_utils::make_tx_hash; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::ops::Sub; + use std::time::{Duration, SystemTime}; + use std::vec; + + #[test] + fn detected_confirmations_is_empty_works() { + let subject = DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }; + + assert_eq!(subject.is_empty(), true); + } + + #[test] + fn requires_payments_retry() { + // Maximalist approach: exhaustive set of tested variants: + let tx_failures_feedings = vec![ + vec![PresortedTxFailure::NewEntry(make_failed_tx(456))], + vec![PresortedTxFailure::RecheckCompleted(make_tx_hash(123))], + vec![ + PresortedTxFailure::NewEntry(make_failed_tx(123)), + PresortedTxFailure::NewEntry(make_failed_tx(456)), + ], + vec![ + PresortedTxFailure::RecheckCompleted(make_tx_hash(654)), + PresortedTxFailure::RecheckCompleted(make_tx_hash(321)), + ], + vec![ + PresortedTxFailure::NewEntry(make_failed_tx(456)), + PresortedTxFailure::RecheckCompleted(make_tx_hash(654)), + ], + ]; + let tx_receipt_rpc_failures_feeding = vec![ + vec![], + vec![FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ))], + vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + make_tx_hash(12121), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + )], + ]; + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(456)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for tx_failures in &tx_failures_feedings { + for rpc_failures in &tx_receipt_rpc_failures_feeding { + for detected_confirmations in &detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: tx_failures.clone(), + tx_receipt_rpc_failures: rpc_failures.clone(), + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, + Some(Retry::RetryPayments), + "Expected Some(Retry::RetryPayments) but got {:?} for case {:?}", + result, + case + ); + } + } + } + } #[test] - fn requires_payments_retry_says_yes() { - todo!("complete this test with GH-604") - // let cases = vec![ - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // ]; - // - // cases.into_iter().enumerate().for_each(|(idx, case)| { - // let result = case.requires_payments_retry(); - // assert_eq!( - // result, true, - // "We expected true, but got false for case of idx {}", - // idx - // ) - // }) + fn requires_only_receipt_retrieval_retry() { + let rpc_failure_feedings = vec![ + vec![FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ))], + vec![FailedValidationByTable::FailedPayable( + FailedValidation::new( + make_tx_hash(1234), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ), + )], + vec![ + FailedValidationByTable::SentPayable(FailedValidation::new( + make_tx_hash(2222), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ))), + )), + FailedValidationByTable::FailedPayable(FailedValidation::new( + make_tx_hash(1234), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + )), + ], + ]; + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for rpc_failures in &rpc_failure_feedings { + for detected_confirmations in &detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], // This is the determinant + tx_receipt_rpc_failures: rpc_failures.clone(), + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, + Some(Retry::RetryTxStatusCheckOnly), + "Expected Some(Retry::RetryTxStatusCheckOnly) but got {:?} for case {:?}", + result, + case + ); + } + } } #[test] fn requires_payments_retry_says_no() { - todo!("complete this test with GH-604") - // let report = PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }; - // - // let result = report.requires_payments_retry(); - // - // assert_eq!(result, false) + let detected_confirmations_feeding = vec![ + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![make_sent_tx(999)], + }, + DetectedConfirmations { + normal_confirmations: vec![make_sent_tx(777)], + reclaims: vec![], + }, + DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![make_sent_tx(999)], + }, + ]; + + for detected_confirmations in detected_confirmations_feeding { + let case = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: detected_confirmations.clone(), + }; + + let result = case.requires_payments_retry(); + + assert_eq!( + result, None, + "We expected None but got {:?} for case {:?}", + result, case + ); + } + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: reading tx receipts gave no results, \ + but always should" + )] + fn requires_payments_retry_with_no_results_in_whole_summary() { + let report = ReceiptScanReport { + failures: DetectedFailures { + tx_failures: vec![], + tx_receipt_rpc_failures: vec![], + }, + confirmations: DetectedConfirmations { + normal_confirmations: vec![], + reclaims: vec![], + }, + }; + + let _ = report.requires_payments_retry(); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_single_record() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(123); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let state_before = subject.sent_payables.clone(); + subject.load_cache(records); + + let first_attempt = subject.get_record_by_hash(tx_hash); + let second_attempt = subject.get_record_by_hash(tx_hash); + + assert_eq!(state_before, hashmap!()); + assert_eq!(first_attempt, Some(sent_tx)); + assert_eq!(second_attempt, None); + assert!( + subject.sent_payables.is_empty(), + "Should be empty but was {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_insert_and_get_methods_multiple_records() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx_1 = make_sent_tx(123); + let tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(456); + let tx_hash_2 = sent_tx_2.hash; + let sent_tx_3 = make_sent_tx(789); + let tx_hash_3 = sent_tx_3.hash; + let sent_tx_4 = make_sent_tx(101); + let tx_hash_4 = sent_tx_4.hash; + let nonexistent_tx_hash = make_tx_hash(234); + let records = vec![ + sent_tx_1.clone(), + sent_tx_2.clone(), + sent_tx_3.clone(), + sent_tx_4.clone(), + ]; + + let first_query = subject.get_record_by_hash(tx_hash_1); + subject.load_cache(records); + let second_query = subject.get_record_by_hash(nonexistent_tx_hash); + let third_query = subject.get_record_by_hash(tx_hash_2); + let fourth_query = subject.get_record_by_hash(tx_hash_1); + let fifth_query = subject.get_record_by_hash(tx_hash_4); + let sixth_query = subject.get_record_by_hash(tx_hash_1); + let seventh_query = subject.get_record_by_hash(tx_hash_1); + let eighth_query = subject.get_record_by_hash(tx_hash_3); + + assert_eq!(first_query, None); + assert_eq!(second_query, None); + assert_eq!(third_query, Some(sent_tx_2)); + assert_eq!(fourth_query, Some(sent_tx_1)); + assert_eq!(fifth_query, Some(sent_tx_4)); + assert_eq!(sixth_query, None); + assert_eq!(seventh_query, None); + assert_eq!(eighth_query, Some(sent_tx_3)); + assert!( + subject.sent_payables.is_empty(), + "Expected empty cache, but got {:?}", + subject.sent_payables + ); + } + + #[test] + fn pending_payable_cache_ensure_empty_happy_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_happy_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(567); + let tx_hash = sent_tx.hash; + let records = vec![sent_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + let _ = subject.get_record_by_hash(tx_hash); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.sent_payables.is_empty(), + "Should be empty by now but was {:?}", + subject.sent_payables + ); + TestLogHandler::default().exists_no_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some pending payables left unprocessed:" + )); + } + + #[test] + fn pending_payable_cache_ensure_empty_sad_path() { + init_test_logging(); + let test_name = "pending_payable_cache_ensure_empty_sad_path"; + let mut subject = CurrentPendingPayables::new(); + let sent_tx = make_sent_tx(567); + let tx_timestamp = sent_tx.timestamp; + let records = vec![sent_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.sent_payables.is_empty(), + "Should be empty by now but was {:?}", + subject.sent_payables + ); + TestLogHandler::default().exists_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some pending payables left unprocessed: \ + {{0x0000000000000000000000000000000000000000000000000000000000000237: SentTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ + 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ + {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, status: Pending(Waiting) }}}}. \ + Dumping." + )); + } + + #[test] + fn pending_payable_cache_dump_works() { + let mut subject = CurrentPendingPayables::new(); + let sent_tx_1 = make_sent_tx(567); + let tx_hash_1 = sent_tx_1.hash; + let sent_tx_2 = make_sent_tx(456); + let tx_hash_2 = sent_tx_2.hash; + let sent_tx_3 = make_sent_tx(789); + let tx_hash_3 = sent_tx_3.hash; + let records = vec![sent_tx_1.clone(), sent_tx_2.clone(), sent_tx_3.clone()]; + subject.load_cache(records); + + let result = subject.dump_cache(); + + assert_eq!( + result, + hashmap! ( + tx_hash_1 => sent_tx_1, + tx_hash_2 => sent_tx_2, + tx_hash_3 => sent_tx_3 + ) + ); + } + + #[test] + fn failure_cache_insert_and_get_methods_single_record() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_hash = failed_tx.hash; + let records = vec![failed_tx.clone()]; + let state_before = subject.failures.clone(); + subject.load_cache(records); + + let first_attempt = subject.get_record_by_hash(tx_hash); + let second_attempt = subject.get_record_by_hash(tx_hash); + + assert_eq!(state_before, hashmap!()); + assert_eq!(first_attempt, Some(failed_tx)); + assert_eq!(second_attempt, None); + assert!( + subject.failures.is_empty(), + "Should be empty but was {:?}", + subject.failures + ); + } + + #[test] + fn failure_cache_insert_and_get_methods_multiple_records() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx_1 = make_failed_tx(123); + let tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(456); + let tx_hash_2 = failed_tx_2.hash; + let failed_tx_3 = make_failed_tx(789); + let tx_hash_3 = failed_tx_3.hash; + let failed_tx_4 = make_failed_tx(101); + let tx_hash_4 = failed_tx_4.hash; + let nonexistent_tx_hash = make_tx_hash(234); + let records = vec![ + failed_tx_1.clone(), + failed_tx_2.clone(), + failed_tx_3.clone(), + failed_tx_4.clone(), + ]; + + let first_query = subject.get_record_by_hash(tx_hash_1); + subject.load_cache(records); + let second_query = subject.get_record_by_hash(nonexistent_tx_hash); + let third_query = subject.get_record_by_hash(tx_hash_2); + let fourth_query = subject.get_record_by_hash(tx_hash_1); + let fifth_query = subject.get_record_by_hash(tx_hash_4); + let sixth_query = subject.get_record_by_hash(tx_hash_1); + let seventh_query = subject.get_record_by_hash(tx_hash_1); + let eighth_query = subject.get_record_by_hash(tx_hash_3); + + assert_eq!(first_query, None); + assert_eq!(second_query, None); + assert_eq!(third_query, Some(failed_tx_2)); + assert_eq!(fourth_query, Some(failed_tx_1)); + assert_eq!(fifth_query, Some(failed_tx_4)); + assert_eq!(sixth_query, None); + assert_eq!(seventh_query, None); + assert_eq!(eighth_query, Some(failed_tx_3)); + assert!( + subject.failures.is_empty(), + "Expected empty cache, but got {:?}", + subject.failures + ); + } + + #[test] + fn failure_cache_ensure_empty_happy_path() { + init_test_logging(); + let test_name = "failure_cache_ensure_empty_happy_path"; + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_hash = failed_tx.hash; + let records = vec![failed_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + let _ = subject.get_record_by_hash(tx_hash); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.failures.is_empty(), + "Should be empty by now but was {:?}", + subject.failures + ); + TestLogHandler::default().exists_no_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some tx failures left unprocessed:" + )); + } + + #[test] + fn failure_cache_ensure_empty_sad_path() { + init_test_logging(); + let test_name = "failure_cache_ensure_empty_sad_path"; + let mut subject = RecheckRequiringFailures::new(); + let failed_tx = make_failed_tx(567); + let tx_timestamp = failed_tx.timestamp; + let records = vec![failed_tx.clone()]; + let logger = Logger::new(test_name); + subject.load_cache(records); + + subject.ensure_empty_cache(&logger); + + assert!( + subject.failures.is_empty(), + "Should be empty by now but was {:?}", + subject.failures + ); + TestLogHandler::default().exists_log_containing(&format!( + "DEBUG: {test_name}: \ + Cache misuse - some tx failures left unprocessed: \ + {{0x0000000000000000000000000000000000000000000000000000000000000237: FailedTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ + 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ + {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: \ + RetryRequired }}}}. Dumping." + )); + } + + #[test] + fn failure_cache_dump_works() { + let mut subject = RecheckRequiringFailures::new(); + let failed_tx_1 = make_failed_tx(567); + let tx_hash_1 = failed_tx_1.hash; + let failed_tx_2 = make_failed_tx(456); + let tx_hash_2 = failed_tx_2.hash; + let failed_tx_3 = make_failed_tx(789); + let tx_hash_3 = failed_tx_3.hash; + let records = vec![ + failed_tx_1.clone(), + failed_tx_2.clone(), + failed_tx_3.clone(), + ]; + subject.load_cache(records); + + let result = subject.dump_cache(); + + assert_eq!( + result, + hashmap! ( + tx_hash_1 => failed_tx_1, + tx_hash_2 => failed_tx_2, + tx_hash_3 => failed_tx_3 + ) + ); + } + + #[test] + fn failed_validation_new_status_works_for_tx_statuses() { + let timestamp_a = SystemTime::now(); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(11)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(22)); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_c); + let cases = vec![ + ( + FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Pending(ValidationStatus::Waiting), + ), + Some(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_a), + ), + ))), + ), + ( + FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + )), + ), + Some(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockReal::default(), + ), + ))), + ), + ]; + + cases.into_iter().for_each(|(input, expected)| { + assert_eq!(input.new_status(&clock), expected); + }); + } + + #[test] + fn failed_validation_new_status_works_for_failure_statuses() { + let timestamp_a = SystemTime::now().sub(Duration::from_secs(222)); + let timestamp_b = SystemTime::now().sub(Duration::from_secs(3333)); + let timestamp_c = SystemTime::now().sub(Duration::from_secs(44444)); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp_a) + .now_result(timestamp_b); + let cases = vec![ + ( + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RecheckRequired(ValidationStatus::Waiting), + ), + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Internal, + )), + &ValidationFailureClockMock::default().now_result(timestamp_a), + )), + )), + ), + ( + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ), + )), + ), + Some(FailureStatus::RecheckRequired( + ValidationStatus::Reattempting( + PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockMock::default().now_result(timestamp_b), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::InvalidResponse, + )), + &ValidationFailureClockMock::default().now_result(timestamp_c), + ) + .add_attempt( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &ValidationFailureClockReal::default(), + ), + ), + )), + ), + ]; + + cases.into_iter().for_each(|(input, expected)| { + assert_eq!(input.new_status(&clock), expected); + }) + } + + #[test] + fn failed_validation_new_status_has_no_effect_on_unexpected_tx_status() { + let validation_failure_clock = ValidationFailureClockMock::default(); + let mal_validated_tx_status = FailedValidation::new( + make_tx_hash(123), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + TxStatus::Confirmed { + block_hash: "".to_string(), + block_number: 0, + detection: Detection::Normal, + }, + ); + + assert_eq!( + mal_validated_tx_status.new_status(&validation_failure_clock), + None + ); + } + + #[test] + fn failed_validation_new_status_has_no_effect_on_unexpected_failure_status() { + let validation_failure_clock = ValidationFailureClockMock::default(); + let mal_validated_failure_statuses = vec![ + FailedValidation::new( + make_tx_hash(456), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), + FailureStatus::RetryRequired, + ), + FailedValidation::new( + make_tx_hash(789), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + FailureStatus::Concluded, + ), + ]; + + mal_validated_failure_statuses + .into_iter() + .enumerate() + .for_each(|(idx, failed_validation)| { + let result = failed_validation.new_status(&validation_failure_clock); + assert_eq!( + result, None, + "Failed validation should evaluate to 'None' but was '{:?}' for idx: {}", + result, idx + ) + }); + } + + #[test] + fn tx_hash_by_table_provides_plain_hash() { + let expected_hash_a = make_tx_hash(123); + let a = TxHashByTable::SentPayable(expected_hash_a); + let expected_hash_b = make_tx_hash(654); + let b = TxHashByTable::FailedPayable(expected_hash_b); + + let result_a = a.hash(); + let result_b = b.hash(); + + assert_eq!(result_a, expected_hash_a); + assert_eq!(result_b, expected_hash_b); + } + + #[test] + fn tx_by_table_can_provide_hash() { + let sent_tx = make_sent_tx(123); + let expected_hash_a = sent_tx.hash; + let a = TxByTable::SentPayable(sent_tx); + let failed_tx = make_failed_tx(654); + let expected_hash_b = failed_tx.hash; + let b = TxByTable::FailedPayable(failed_tx); + + let result_a = a.hash(); + let result_b = b.hash(); + + assert_eq!(result_a, expected_hash_a); + assert_eq!(result_b, expected_hash_b); + } + + #[test] + fn tx_by_table_can_be_converted_into_tx_hash_by_table() { + let sent_tx = make_sent_tx(123); + let expected_hash_a = sent_tx.hash; + let a = TxByTable::SentPayable(sent_tx); + let failed_tx = make_failed_tx(654); + let expected_hash_b = failed_tx.hash; + let b = TxByTable::FailedPayable(failed_tx); + + let result_a = TxHashByTable::from(&a); + let result_b = TxHashByTable::from(&b); + + assert_eq!(result_a, TxHashByTable::SentPayable(expected_hash_a)); + assert_eq!(result_b, TxHashByTable::FailedPayable(expected_hash_b)); } } diff --git a/node/src/accountant/scanners/receivable_scanner/mod.rs b/node/src/accountant/scanners/receivable_scanner/mod.rs index b7222df0d..eff0df95e 100644 --- a/node/src/accountant/scanners/receivable_scanner/mod.rs +++ b/node/src/accountant/scanners/receivable_scanner/mod.rs @@ -111,7 +111,7 @@ impl ReceivableScanner { { Ok(()) => debug!(logger, "Start block updated to {}", start_block_number), Err(e) => panic!( - "Attempt to set new start block to {} failed due to: {:?}", + "Attempt to advance the start block to {} failed due to: {:?}", start_block_number, e ), } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 7a99605b0..03dad9942 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -44,7 +44,7 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq, Eq)] -pub enum ScanRescheduleAfterEarlyStop { +pub enum ScanReschedulingAfterEarlyStop { Schedule(ScanType), DoNotSchedule, } @@ -245,7 +245,7 @@ pub trait RescheduleScanOnErrorResolver { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop; + ) -> ScanReschedulingAfterEarlyStop; } #[derive(Default)] @@ -258,7 +258,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { let reschedule_hint = match scanner { PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) @@ -285,16 +285,16 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } } @@ -304,9 +304,9 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else { unreachable!( "{:?} should be impossible with RetryPayableScanner in automatic mode", @@ -319,12 +319,12 @@ impl RescheduleScanOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { if is_externally_triggered { - ScanRescheduleAfterEarlyStop::DoNotSchedule + ScanReschedulingAfterEarlyStop::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -340,7 +340,7 @@ impl RescheduleScanOnErrorResolverReal { // the user. // TODO Correctly, a check-point during the bootstrap that wouldn't allow to come // this far should be the solution. Part of the issue mentioned in GH-799 - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables) } else { unreachable!( "PendingPayableScanner called later than the initial attempt, but \ @@ -359,7 +359,7 @@ impl RescheduleScanOnErrorResolverReal { scanner: PayableSequenceScanner, is_externally_triggered: bool, logger: &Logger, - reschedule_hint: &ScanRescheduleAfterEarlyStop, + reschedule_hint: &ScanReschedulingAfterEarlyStop, ) { let scan_mode = if is_externally_triggered { "Manual" @@ -381,7 +381,7 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, + PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, }; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; @@ -634,7 +634,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::DoNotSchedule, + ScanReschedulingAfterEarlyStop::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, @@ -669,7 +669,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); @@ -722,7 +722,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::PendingPayables), "We expected Schedule(PendingPayables) but got {:?} for {:?}", result, scanner @@ -904,7 +904,7 @@ mod tests { assert_eq!( result, - ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), + ScanReschedulingAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, ); diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 3747728ab..c459f7226 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -1,30 +1,29 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod payable_scanner_utils { - use crate::accountant::db_access_objects::utils::ThresholdUtils; - use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; + use crate::accountant::db_access_objects::utils::{ThresholdUtils, TxHash}; + use crate::accountant::db_access_objects::payable_dao::{PayableAccount}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; - use crate::accountant::{comma_joined_stringifiable, SentPayables}; + use crate::accountant::{PendingPayable, SentPayables}; use crate::sub_lib::accountant::PaymentThresholds; - use crate::sub_lib::wallet::Wallet; use itertools::Itertools; use masq_lib::logger::Logger; use std::cmp::Ordering; + use std::collections::HashSet; use std::ops::Not; use std::time::SystemTime; use thousands::Separable; - use web3::types::H256; + use web3::types::{Address, H256}; use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; #[derive(Debug, PartialEq, Eq)] pub enum PayableTransactingErrorEnum { LocallyCausedError(PayableTransactionError), - RemotelyCausedErrors(Vec), + RemotelyCausedErrors(HashSet), } #[derive(Debug, PartialEq)] @@ -102,6 +101,7 @@ pub mod payable_scanner_utils { oldest.balance_wei, oldest.age) } + // TODO lifetimes simplification??? pub fn separate_errors<'a, 'b>( sent_payables: &'a SentPayables, logger: &'b Logger, @@ -111,15 +111,28 @@ pub mod payable_scanner_utils { if individual_batch_responses.is_empty() { panic!("Broken code: An empty vector of processed payments claiming to be an Ok value") } - let (oks, err_hashes_opt) = + + let separated_txs_by_result = separate_rpc_results(individual_batch_responses, logger); - let remote_errs_opt = err_hashes_opt.map(RemotelyCausedErrors); + + let remote_errs_opt = if separated_txs_by_result.err_results.is_empty() { + None + } else { + warning!( + logger, + "Please check your blockchain service URL configuration due \ + to detected remote failures" + ); + Some(RemotelyCausedErrors(separated_txs_by_result.err_results)) + }; + let oks = separated_txs_by_result.ok_results; + (oks, remote_errs_opt) } Err(e) => { warning!( logger, - "Any persisted data from failed process will be deleted. Caused by: {}", + "Any persisted data from the failed process will be deleted. Caused by: {}", e ); @@ -128,55 +141,49 @@ pub mod payable_scanner_utils { } } - fn separate_rpc_results<'a, 'b>( + fn separate_rpc_results<'a>( batch_request_responses: &'a [ProcessedPayableFallible], - logger: &'b Logger, - ) -> (Vec<&'a PendingPayable>, Option>) { + logger: &Logger, + ) -> SeparatedTxsByResult<'a> { //TODO maybe we can return not tuple but struct with remote_errors_opt member - let (oks, errs) = batch_request_responses + let init = SeparatedTxsByResult::default(); + batch_request_responses .iter() - .fold((vec![], vec![]), |acc, rpc_result| { - fold_guts(acc, rpc_result, logger) - }); - - let errs_opt = if !errs.is_empty() { Some(errs) } else { None }; - - (oks, errs_opt) - } - - fn add_pending_payable<'a>( - (mut oks, errs): (Vec<&'a PendingPayable>, Vec), - pending_payable: &'a PendingPayable, - ) -> SeparateTxsByResult<'a> { - oks.push(pending_payable); - (oks, errs) + .fold(init, |acc, rpc_result| { + separate_rpc_results_fold_guts(acc, rpc_result, logger) + }) } - fn add_rpc_failure((oks, mut errs): SeparateTxsByResult, hash: H256) -> SeparateTxsByResult { - errs.push(hash); - (oks, errs) + #[derive(Default)] + pub struct SeparatedTxsByResult<'a> { + pub ok_results: Vec<&'a PendingPayable>, + pub err_results: HashSet, } - type SeparateTxsByResult<'a> = (Vec<&'a PendingPayable>, Vec); - - fn fold_guts<'a, 'b>( - acc: SeparateTxsByResult<'a>, + fn separate_rpc_results_fold_guts<'a>( + mut acc: SeparatedTxsByResult<'a>, rpc_result: &'a ProcessedPayableFallible, - logger: &'b Logger, - ) -> SeparateTxsByResult<'a> { + logger: &Logger, + ) -> SeparatedTxsByResult<'a> { match rpc_result { ProcessedPayableFallible::Correct(pending_payable) => { - add_pending_payable(acc, pending_payable) + acc.ok_results.push(pending_payable); + acc } ProcessedPayableFallible::Failed(RpcPayableFailure { rpc_error, recipient_wallet, hash, }) => { - warning!(logger, "Remote transaction failure: '{}' for payment to {} and transaction hash {:?}. \ - Please check your blockchain service URL configuration.", rpc_error, recipient_wallet, hash + warning!( + logger, + "Remote sent payable failure '{}' for wallet {} and tx hash {:?}", + rpc_error, + recipient_wallet, + hash ); - add_rpc_failure(acc, *hash) + acc.err_results.insert(*hash); + acc } } } @@ -194,14 +201,14 @@ pub mod payable_scanner_utils { .duration_since(payable.last_paid_timestamp) .expect("Payable time is corrupt"); format!( - "{} wei owed for {} sec exceeds threshold: {} wei; creditor: {}", + "{} wei owed for {} sec exceeds the threshold {} wei for creditor {}", payable.balance_wei.separate_with_commas(), p_age.as_secs(), threshold_point.separate_with_commas(), payable.wallet ) }) - .join("\n") + .join(".\n") }) } @@ -234,51 +241,23 @@ pub mod payable_scanner_utils { } #[derive(Debug, PartialEq, Eq)] - pub struct PendingPayableMetadata<'a> { - pub recipient: &'a Wallet, + pub struct PendingPayableMissingInDb { + pub recipient: Address, pub hash: H256, - pub rowid_opt: Option, } - impl<'a> PendingPayableMetadata<'a> { - pub fn new( - recipient: &'a Wallet, - hash: H256, - rowid_opt: Option, - ) -> PendingPayableMetadata<'a> { - PendingPayableMetadata { - recipient, - hash, - rowid_opt, - } + impl PendingPayableMissingInDb { + pub fn new(recipient: Address, hash: H256) -> PendingPayableMissingInDb { + PendingPayableMissingInDb { recipient, hash } } } - pub fn mark_pending_payable_fatal_error( - sent_payments: &[&PendingPayable], - nonexistent: &[PendingPayableMetadata], - error: PayableDaoError, - missing_fingerprints_msg_maker: fn(&[PendingPayableMetadata]) -> String, - logger: &Logger, - ) { - if !nonexistent.is_empty() { - error!(logger, "{}", missing_fingerprints_msg_maker(nonexistent)) - }; - panic!( - "Unable to create a mark in the payable table for wallets {} due to {:?}", - comma_joined_stringifiable(sent_payments, |pending_p| pending_p - .recipient_wallet - .to_string()), - error - ) - } - - pub fn err_msg_for_failure_with_expected_but_missing_fingerprints( + pub fn err_msg_for_failure_with_expected_but_missing_sent_tx_record( nonexistent: Vec, serialize_hashes: fn(&[H256]) -> String, ) -> Option { nonexistent.is_empty().not().then_some(format!( - "Ran into failed transactions {} with missing fingerprints. System no longer reliable", + "Ran into failed payables {} with missing records. The system has become unreliable", serialize_hashes(&nonexistent), )) } @@ -333,15 +312,14 @@ mod tests { payables_debug_summary, separate_errors, PayableThresholdsGauge, PayableThresholdsGaugeReal, }; - use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; + use crate::accountant::{checked_conversion, gwei_to_wei, PendingPayable, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; use crate::test_utils::make_wallet; use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use std::time::SystemTime; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; + use std::time::{SystemTime}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -414,7 +392,7 @@ mod tests { init_test_logging(); let error = PayableTransactionError::Sending { msg: "Bad luck".to_string(), - hashes: vec![make_tx_hash(0x7b)], + hashes: hashset![make_tx_hash(0x7b)], }; let sent_payable = SentPayables { payment_procedure_result: Err(error.clone()), @@ -427,8 +405,8 @@ mod tests { assert_eq!(errs, Some(LocallyCausedError(error))); TestLogHandler::new().exists_log_containing( "WARN: test_logger: Any persisted data from \ - failed process will be deleted. Caused by: Sending phase: \"Bad luck\". Signed and hashed \ - transactions: 0x000000000000000000000000000000000000000000000000000000000000007b", + the failed process will be deleted. Caused by: Sending phase: \"Bad luck\". Signed and hashed txs: \ + 0x000000000000000000000000000000000000000000000000000000000000007b", ); } @@ -455,11 +433,14 @@ mod tests { let (oks, errs) = separate_errors(&sent_payable, &Logger::new("test_logger")); assert_eq!(oks, vec![&payable_ok]); - assert_eq!(errs, Some(RemotelyCausedErrors(vec![make_tx_hash(0x315)]))); - TestLogHandler::new().exists_log_containing("WARN: test_logger: Remote transaction failure: \ - 'Got invalid response: That jackass screwed it up' for payment to 0x000000000000000000000000\ - 00000077686f6f61 and transaction hash 0x0000000000000000000000000000000000000000000000000000\ - 000000000315. Please check your blockchain service URL configuration."); + assert_eq!( + errs, + Some(RemotelyCausedErrors(hashset![make_tx_hash(0x315)])) + ); + TestLogHandler::new().exists_log_containing("WARN: test_logger: Remote sent payable \ + failure 'Got invalid response: That jackass screwed it up' for wallet 0x00000000000000000000\ + 000000000077686f6f61 and tx hash 0x000000000000000000000000000000000000000000000000000000000\ + 0000315"); } #[test] @@ -522,10 +503,10 @@ mod tests { payables_debug_summary(&qualified_payables_and_threshold_points, &logger); TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ - 10,002,000,000,000,000 wei owed for 2678400 sec exceeds threshold: \ - 10,000,000,001,152,000 wei; creditor: 0x0000000000000000000000000077616c6c657430\n\ - 999,999,999,000,000,000 wei owed for 86455 sec exceeds threshold: \ - 999,978,993,055,555,580 wei; creditor: 0x0000000000000000000000000077616c6c657431"); + 10,002,000,000,000,000 wei owed for 2678400 sec exceeds the threshold \ + 10,000,000,001,152,000 wei for creditor 0x0000000000000000000000000077616c6c657430.\n\ + 999,999,999,000,000,000 wei owed for 86455 sec exceeds the threshold \ + 999,978,993,055,555,580 wei for creditor 0x0000000000000000000000000077616c6c657431"); } #[test] @@ -665,7 +646,7 @@ mod tests { fn count_total_errors_works_correctly_for_local_error_after_signing() { let error = PayableTransactionError::Sending { msg: "Ouuuups".to_string(), - hashes: vec![make_tx_hash(333), make_tx_hash(666)], + hashes: hashset![make_tx_hash(333), make_tx_hash(666)], }; let sent_payable = Some(LocallyCausedError(error)); @@ -676,7 +657,7 @@ mod tests { #[test] fn count_total_errors_works_correctly_for_remote_errors() { - let sent_payable = Some(RemotelyCausedErrors(vec![ + let sent_payable = Some(RemotelyCausedErrors(hashset![ make_tx_hash(123), make_tx_hash(456), ])); diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 637325091..ecd0781fe 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,16 +2,19 @@ #![cfg(test)] +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::scanners::payable_scanner_extension::{ MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, }; -use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableCache, PendingPayableScanResult, +}; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, - ScanRescheduleAfterEarlyStop, + ScanReschedulingAfterEarlyStop, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::{ @@ -19,8 +22,7 @@ use crate::accountant::scanners::{ Scanner, StartScanError, StartableScanner, }; use crate::accountant::{ - ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, - SentPayables, + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, SentPayables, TxReceiptsMessage, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, OutboundPaymentsInstructions}; @@ -32,6 +34,7 @@ use masq_lib::ui_gateway::NodeToUiMessage; use regex::Regex; use std::any::type_name; use std::cell::RefCell; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use time::{format_description, PrimitiveDateTime}; @@ -367,11 +370,7 @@ pub enum ScannerReplacement { PendingPayable( ReplacementType< PendingPayableScanner, - ScannerMock< - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - >, + ScannerMock, >, ), Receivable( @@ -387,8 +386,8 @@ pub enum MarkScanner<'a> { Started(SystemTime), } -// Cautious: Don't compare to another timestamp on a full match; this timestamp is trimmed in -// nanoseconds down to three digits +// Cautious: Don't compare to another timestamp on an exact match. This timestamp is trimmed in +// nanoseconds down to three digits. Works only for the format bound by TIME_FORMATTING_STRING pub fn parse_system_time_from_str(examined_str: &str) -> Vec { let regex = Regex::new(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})").unwrap(); let captures = regex.captures_iter(examined_str); @@ -444,7 +443,7 @@ pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec>>, - resolve_rescheduling_on_error_results: RefCell>, + resolve_rescheduling_on_error_results: RefCell>, } impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { @@ -454,7 +453,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { error: &StartScanError, is_externally_triggered: bool, logger: &Logger, - ) -> ScanRescheduleAfterEarlyStop { + ) -> ScanReschedulingAfterEarlyStop { self.resolve_rescheduling_on_error_params .lock() .unwrap() @@ -480,7 +479,7 @@ impl RescheduleScanOnErrorResolverMock { } pub fn resolve_rescheduling_on_error_result( self, - result: ScanRescheduleAfterEarlyStop, + result: ScanReschedulingAfterEarlyStop, ) -> Self { self.resolve_rescheduling_on_error_results .borrow_mut() @@ -492,3 +491,70 @@ impl RescheduleScanOnErrorResolverMock { pub fn make_zeroed_consuming_wallet_balances() -> ConsumingWalletBalances { ConsumingWalletBalances::new(0.into(), 0.into()) } + +pub struct PendingPayableCacheMock { + load_cache_params: Arc>>>, + load_cache_results: RefCell>>, + get_record_by_hash_params: Arc>>, + get_record_by_hash_results: RefCell>>, + ensure_empty_cache_params: Arc>>, +} + +impl Default for PendingPayableCacheMock { + fn default() -> Self { + Self { + load_cache_params: Arc::new(Mutex::new(vec![])), + load_cache_results: RefCell::new(vec![]), + get_record_by_hash_params: Arc::new(Mutex::new(vec![])), + get_record_by_hash_results: RefCell::new(vec![]), + ensure_empty_cache_params: Arc::new(Mutex::new(vec![])), + } + } +} + +impl PendingPayableCache for PendingPayableCacheMock { + fn load_cache(&mut self, records: Vec) { + self.load_cache_params.lock().unwrap().push(records); + self.load_cache_results.borrow_mut().remove(0); + } + + fn get_record_by_hash(&mut self, hash: TxHash) -> Option { + self.get_record_by_hash_params.lock().unwrap().push(hash); + self.get_record_by_hash_results.borrow_mut().remove(0) + } + + fn ensure_empty_cache(&mut self, _logger: &Logger) { + self.ensure_empty_cache_params.lock().unwrap().push(()); + } + + fn dump_cache(&mut self) -> HashMap { + unimplemented!("not needed yet") + } +} + +impl PendingPayableCacheMock { + pub fn load_cache_params(mut self, params: &Arc>>>) -> Self { + self.load_cache_params = params.clone(); + self + } + + pub fn load_cache_result(self, result: HashMap) -> Self { + self.load_cache_results.borrow_mut().push(result); + self + } + + pub fn get_record_by_hash_params(mut self, params: &Arc>>) -> Self { + self.get_record_by_hash_params = params.clone(); + self + } + + pub fn get_record_by_hash_result(self, result: Option) -> Self { + self.get_record_by_hash_results.borrow_mut().push(result); + self + } + + pub fn ensure_empty_cache_params(mut self, params: &Arc>>) -> Self { + self.ensure_empty_cache_params = params.clone(); + self + } +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 44c888ff7..81b612e47 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -4,18 +4,21 @@ use crate::accountant::db_access_objects::banned_dao::{BannedDao, BannedDaoFactory}; use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureReason, FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::payable_dao::{ - PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, -}; -use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayableDao, PendingPayableDaoError, PendingPayableDaoFactory, TransactionHashes, + MarkPendingPayableID, PayableAccount, PayableDao, PayableDaoError, PayableDaoFactory, }; use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; +use crate::accountant::db_access_objects::sent_payable_dao::{ + RetrieveCondition, SentPayableDaoError, SentTx, +}; +use crate::accountant::db_access_objects::sent_payable_dao::{ + SentPayableDao, SentPayableDaoFactory, TxStatus, +}; use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, TxHash, TxIdentifiers, }; @@ -25,15 +28,17 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, }; use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; +use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableCache; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; +use crate::accountant::scanners::test_utils::PendingPayableCacheMock; use crate::accountant::scanners::PayableScanner; -use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; -use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; -use crate::blockchain::test_utils::make_tx_hash; +use crate::accountant::{gwei_to_wei, Accountant}; +use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, TxBlock}; +use crate::blockchain::errors::validation_status::{ValidationFailureClock, ValidationStatus}; +use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::bootstrapper::BootstrapperConfig; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; @@ -45,8 +50,7 @@ use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::make_bc_with_defaults; -use actix::System; -use ethereum_types::H256; +use ethereum_types::U64; use masq_lib::logger::Logger; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; @@ -57,6 +61,7 @@ use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::SystemTime; +use web3::types::Address; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_unix_timestamp(SystemTime::now()); @@ -94,13 +99,73 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( } } +pub fn make_sent_tx(num: u64) -> SentTx { + if num == 0 { + panic!("num for generating must be greater than 0"); + } + let params = TxRecordCommonParts::new(num); + SentTx { + hash: params.hash, + receiver_address: params.receiver_address, + amount_minor: params.amount_minor, + timestamp: params.timestamp, + gas_price_minor: params.gas_price_minor, + nonce: params.nonce, + status: TxStatus::Pending(ValidationStatus::Waiting), + } +} + +pub fn make_failed_tx(num: u64) -> FailedTx { + let params = TxRecordCommonParts::new(num); + FailedTx { + hash: params.hash, + receiver_address: params.receiver_address, + amount_minor: params.amount_minor, + timestamp: params.timestamp, + gas_price_minor: params.gas_price_minor, + nonce: params.nonce, + reason: FailureReason::PendingTooLong, + status: FailureStatus::RetryRequired, + } +} + +pub fn make_transaction_block(num: u64) -> TxBlock { + TxBlock { + block_hash: make_block_hash(num as u32), + block_number: U64::from(num * num * num), + } +} + +struct TxRecordCommonParts { + hash: TxHash, + receiver_address: Address, + amount_minor: u128, + timestamp: i64, + gas_price_minor: u128, + nonce: u64, +} + +impl TxRecordCommonParts { + fn new(num: u64) -> Self { + Self { + hash: make_tx_hash(num as u32), + receiver_address: make_wallet(&format!("wallet{}", num)).address(), + amount_minor: gwei_to_wei(num * num), + timestamp: to_unix_timestamp(SystemTime::now()) - (num as i64 * 60), + gas_price_minor: gwei_to_wei(num), + nonce: num, + } + } +} + pub struct AccountantBuilder { config_opt: Option, consuming_wallet_opt: Option, logger_opt: Option, payable_dao_factory_opt: Option, receivable_dao_factory_opt: Option, - pending_payable_dao_factory_opt: Option, + sent_payable_dao_factory_opt: Option, + failed_payable_dao_factory_opt: Option, banned_dao_factory_opt: Option, config_dao_factory_opt: Option, } @@ -113,7 +178,8 @@ impl Default for AccountantBuilder { logger_opt: None, payable_dao_factory_opt: None, receivable_dao_factory_opt: None, - pending_payable_dao_factory_opt: None, + sent_payable_dao_factory_opt: None, + failed_payable_dao_factory_opt: None, banned_dao_factory_opt: None, config_dao_factory_opt: None, } @@ -251,7 +317,11 @@ const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::PendingPayableScanner, ]; -const PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ +//TODO Utkarsh should also update this +const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 1] = + [DestinationMarker::PendingPayableScanner]; + +const SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, DestinationMarker::PayableScanner, DestinationMarker::PendingPayableScanner, @@ -278,16 +348,16 @@ impl AccountantBuilder { self } - pub fn pending_payable_daos( + pub fn sent_payable_daos( mut self, - specially_configured_daos: Vec>, + specially_configured_daos: Vec>, ) -> Self { create_or_update_factory!( specially_configured_daos, - PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - pending_payable_dao_factory_opt, - PendingPayableDaoFactoryMock, - PendingPayableDao, + SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, + sent_payable_dao_factory_opt, + SentPayableDaoFactoryMock, + SentPayableDao, self ) } @@ -306,6 +376,27 @@ impl AccountantBuilder { ) } + pub fn failed_payable_daos( + mut self, + mut specially_configured_daos: Vec>, + ) -> Self { + specially_configured_daos.iter_mut().for_each(|dao| { + if let DaoWithDestination::ForPendingPayableScanner(dao) = dao { + let mut extended_queue = vec![vec![]]; + extended_queue.append(&mut dao.retrieve_txs_results.borrow_mut()); + dao.retrieve_txs_results.replace(extended_queue); + } + }); + create_or_update_factory!( + specially_configured_daos, + FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, + failed_payable_dao_factory_opt, + FailedPayableDaoFactoryMock, + FailedPayableDao, + self + ) + } + pub fn receivable_daos( mut self, specially_configured_daos: Vec>, @@ -352,12 +443,15 @@ impl AccountantBuilder { .make_result(ReceivableDaoMock::new()) .make_result(ReceivableDaoMock::new()), ); - let pending_payable_dao_factory = self.pending_payable_dao_factory_opt.unwrap_or( - PendingPayableDaoFactoryMock::new() - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()) - .make_result(PendingPayableDaoMock::new()), + let sent_payable_dao_factory = self.sent_payable_dao_factory_opt.unwrap_or( + SentPayableDaoFactoryMock::new() + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()) + .make_result(SentPayableDaoMock::new()), ); + let failed_payable_dao_factory = self + .failed_payable_dao_factory_opt + .unwrap_or(FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new())); let banned_dao_factory = self .banned_dao_factory_opt .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); @@ -368,7 +462,8 @@ impl AccountantBuilder { config, DaoFactories { payable_dao_factory: Box::new(payable_dao_factory), - pending_payable_dao_factory: Box::new(pending_payable_dao_factory), + sent_payable_dao_factory: Box::new(sent_payable_dao_factory), + failed_payable_dao_factory: Box::new(failed_payable_dao_factory), receivable_dao_factory: Box::new(receivable_dao_factory), banned_dao_factory: Box::new(banned_dao_factory), config_dao_factory: Box::new(config_dao_factory), @@ -394,7 +489,8 @@ impl PayableDaoFactory for PayableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "PayableDao Missing. This problem mostly occurs when PayableDao is only supplied \ + for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -430,7 +526,8 @@ impl ReceivableDaoFactory for ReceivableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "ReceivableDao Missing. This problem mostly occurs when ReceivableDao is only \ + supplied for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -530,7 +627,7 @@ pub struct PayableDaoMock { non_pending_payables_results: RefCell>>, mark_pending_payables_rowids_params: Arc>>>, mark_pending_payables_rowids_results: RefCell>>, - transactions_confirmed_params: Arc>>>, + transactions_confirmed_params: Arc>>>, transactions_confirmed_results: RefCell>>, custom_query_params: Arc>>>, custom_query_result: RefCell>>>, @@ -542,37 +639,36 @@ impl PayableDao for PayableDaoMock { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), PayableDaoError> { - self.more_money_payable_parameters - .lock() - .unwrap() - .push((now, wallet.clone(), amount)); + self.more_money_payable_parameters.lock().unwrap().push(( + now, + wallet.clone(), + amount_minor, + )); self.more_money_payable_results.borrow_mut().remove(0) } fn mark_pending_payables_rowids( &self, - wallets_and_rowids: &[(&Wallet, u64)], - ) -> Result<(), PayableDaoError> { - self.mark_pending_payables_rowids_params - .lock() - .unwrap() - .push( - wallets_and_rowids - .iter() - .map(|(wallet, id)| ((*wallet).clone(), *id)) - .collect(), - ); - self.mark_pending_payables_rowids_results - .borrow_mut() - .remove(0) - } - - fn transactions_confirmed( - &self, - confirmed_payables: &[PendingPayableFingerprint], + _mark_instructions: &[MarkPendingPayableID], ) -> Result<(), PayableDaoError> { + todo!("will be removed in the associated card - GH-662") + // self.mark_pending_payables_rowids_params + // .lock() + // .unwrap() + // .push( + // mark_instructions + // .iter() + // .map(|(wallet, id)| ((*wallet).clone(), *id)) + // .collect(), + // ); + // self.mark_pending_payables_rowids_results + // .borrow_mut() + // .remove(0) + } + + fn transactions_confirmed(&self, confirmed_payables: &[SentTx]) -> Result<(), PayableDaoError> { self.transactions_confirmed_params .lock() .unwrap() @@ -643,10 +739,7 @@ impl PayableDaoMock { self } - pub fn transactions_confirmed_params( - mut self, - params: &Arc>>>, - ) -> Self { + pub fn transactions_confirmed_params(mut self, params: &Arc>>>) -> Self { self.transactions_confirmed_params = params.clone(); self } @@ -694,12 +787,13 @@ impl ReceivableDao for ReceivableDaoMock { &self, now: SystemTime, wallet: &Wallet, - amount: u128, + amount_minor: u128, ) -> Result<(), ReceivableDaoError> { - self.more_money_receivable_parameters - .lock() - .unwrap() - .push((now, wallet.clone(), amount)); + self.more_money_receivable_parameters.lock().unwrap().push(( + now, + wallet.clone(), + amount_minor, + )); self.more_money_receivable_results.borrow_mut().remove(0) } @@ -886,178 +980,169 @@ pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> Boot } #[derive(Default)] -pub struct PendingPayableDaoMock { - fingerprints_rowids_params: Arc>>>, - fingerprints_rowids_results: RefCell>, - delete_fingerprints_params: Arc>>>, - delete_fingerprints_results: RefCell>>, - insert_new_fingerprints_params: Arc, SystemTime)>>>, - insert_new_fingerprints_results: RefCell>>, - increment_scan_attempts_params: Arc>>>, - increment_scan_attempts_result: RefCell>>, - mark_failures_params: Arc>>>, - mark_failures_results: RefCell>>, - return_all_errorless_fingerprints_params: Arc>>, - return_all_errorless_fingerprints_results: RefCell>>, - pub have_return_all_errorless_fingerprints_shut_down_the_system: bool, -} - -impl PendingPayableDao for PendingPayableDaoMock { - fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes { - self.fingerprints_rowids_params +pub struct SentPayableDaoMock { + get_tx_identifiers_params: Arc>>>, + get_tx_identifiers_results: RefCell>, + insert_new_records_params: Arc>>>, + insert_new_records_results: RefCell>>, + retrieve_txs_params: Arc>>>, + retrieve_txs_results: RefCell>>, + confirm_tx_params: Arc>>>, + confirm_tx_results: RefCell>>, + update_statuses_params: Arc>>>, + update_statuses_results: RefCell>>, + replace_records_params: Arc>>>, + replace_records_results: RefCell>>, + delete_records_params: Arc>>>, + delete_records_results: RefCell>>, +} + +impl SentPayableDao for SentPayableDaoMock { + fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + self.get_tx_identifiers_params .lock() .unwrap() - .push(hashes.to_vec()); - self.fingerprints_rowids_results.borrow_mut().remove(0) + .push(hashes.clone()); + self.get_tx_identifiers_results.borrow_mut().remove(0) } - - fn return_all_errorless_fingerprints(&self) -> Vec { - self.return_all_errorless_fingerprints_params + fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + self.insert_new_records_params .lock() .unwrap() - .push(()); - if self.have_return_all_errorless_fingerprints_shut_down_the_system - && self - .return_all_errorless_fingerprints_results - .borrow() - .is_empty() - { - System::current().stop(); - return vec![]; - } - self.return_all_errorless_fingerprints_results - .borrow_mut() - .remove(0) + .push(txs.to_vec()); + self.insert_new_records_results.borrow_mut().remove(0) } - - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError> { - self.insert_new_fingerprints_params + fn retrieve_txs(&self, condition: Option) -> Vec { + self.retrieve_txs_params.lock().unwrap().push(condition); + self.retrieve_txs_results.borrow_mut().remove(0) + } + fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError> { + self.confirm_tx_params .lock() .unwrap() - .push((hashes_and_amounts.to_vec(), batch_wide_timestamp)); - self.insert_new_fingerprints_results.borrow_mut().remove(0) + .push(hash_map.clone()); + self.confirm_tx_results.borrow_mut().remove(0) } - - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.delete_fingerprints_params + fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + self.replace_records_params .lock() .unwrap() - .push(ids.to_vec()); - self.delete_fingerprints_results.borrow_mut().remove(0) + .push(new_txs.to_vec()); + self.replace_records_results.borrow_mut().remove(0) } - fn increment_scan_attempts(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.increment_scan_attempts_params + fn update_statuses( + &self, + hash_map: &HashMap, + ) -> Result<(), SentPayableDaoError> { + self.update_statuses_params .lock() .unwrap() - .push(ids.to_vec()); - self.increment_scan_attempts_result.borrow_mut().remove(0) + .push(hash_map.clone()); + self.update_statuses_results.borrow_mut().remove(0) } - fn mark_failures(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - self.mark_failures_params.lock().unwrap().push(ids.to_vec()); - self.mark_failures_results.borrow_mut().remove(0) + fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + self.delete_records_params + .lock() + .unwrap() + .push(hashes.clone()); + self.delete_records_results.borrow_mut().remove(0) } } -impl PendingPayableDaoMock { +impl SentPayableDaoMock { pub fn new() -> Self { - PendingPayableDaoMock::default() + SentPayableDaoMock::default() } - pub fn fingerprints_rowids_params(mut self, params: &Arc>>>) -> Self { - self.fingerprints_rowids_params = params.clone(); + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + self.get_tx_identifiers_params = params.clone(); self } - pub fn fingerprints_rowids_result(self, result: TransactionHashes) -> Self { - self.fingerprints_rowids_results.borrow_mut().push(result); + pub fn get_tx_identifiers_result(self, result: TxIdentifiers) -> Self { + self.get_tx_identifiers_results.borrow_mut().push(result); self } - pub fn insert_fingerprints_params( + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + self.insert_new_records_params = params.clone(); + self + } + + pub fn insert_new_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.insert_new_records_results.borrow_mut().push(result); + self + } + + pub fn retrieve_txs_params( mut self, - params: &Arc, SystemTime)>>>, + params: &Arc>>>, ) -> Self { - self.insert_new_fingerprints_params = params.clone(); + self.retrieve_txs_params = params.clone(); self } - pub fn insert_fingerprints_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.insert_new_fingerprints_results - .borrow_mut() - .push(result); + pub fn retrieve_txs_result(self, result: Vec) -> Self { + self.retrieve_txs_results.borrow_mut().push(result); self } - pub fn delete_fingerprints_params(mut self, params: &Arc>>>) -> Self { - self.delete_fingerprints_params = params.clone(); + pub fn confirm_tx_params(mut self, params: &Arc>>>) -> Self { + self.confirm_tx_params = params.clone(); self } - pub fn delete_fingerprints_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.delete_fingerprints_results.borrow_mut().push(result); + pub fn confirm_tx_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.confirm_tx_results.borrow_mut().push(result); self } - pub fn return_all_errorless_fingerprints_params( - mut self, - params: &Arc>>, - ) -> Self { - self.return_all_errorless_fingerprints_params = params.clone(); + pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { + self.replace_records_params = params.clone(); self } - pub fn return_all_errorless_fingerprints_result( - self, - result: Vec, - ) -> Self { - self.return_all_errorless_fingerprints_results - .borrow_mut() - .push(result); + pub fn replace_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.replace_records_results.borrow_mut().push(result); self } - pub fn mark_failures_params(mut self, params: &Arc>>>) -> Self { - self.mark_failures_params = params.clone(); + pub fn update_statuses_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.update_statuses_params = params.clone(); self } - pub fn mark_failures_result(self, result: Result<(), PendingPayableDaoError>) -> Self { - self.mark_failures_results.borrow_mut().push(result); + pub fn update_statuses_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.update_statuses_results.borrow_mut().push(result); self } - pub fn increment_scan_attempts_params(mut self, params: &Arc>>>) -> Self { - self.increment_scan_attempts_params = params.clone(); + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + self.delete_records_params = params.clone(); self } - pub fn increment_scan_attempts_result( - self, - result: Result<(), PendingPayableDaoError>, - ) -> Self { - self.increment_scan_attempts_result - .borrow_mut() - .push(result); + pub fn delete_records_result(self, result: Result<(), SentPayableDaoError>) -> Self { + self.delete_records_results.borrow_mut().push(result); self } } -pub struct PendingPayableDaoFactoryMock { +pub struct SentPayableDaoFactoryMock { make_params: Arc>>, - make_results: RefCell>>, + make_results: RefCell>>, } -impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { - fn make(&self) -> Box { +impl SentPayableDaoFactory for SentPayableDaoFactoryMock { + fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { panic!( - "PendingPayableDao Missing. This problem mostly occurs when PendingPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + "SentPayableDao Missing. This problem mostly occurs when SentPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." ) }; self.make_params.lock().unwrap().push(()); @@ -1065,7 +1150,7 @@ impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { } } -impl PendingPayableDaoFactoryMock { +impl SentPayableDaoFactoryMock { pub fn new() -> Self { Self { make_params: Arc::new(Mutex::new(vec![])), @@ -1078,7 +1163,7 @@ impl PendingPayableDaoFactoryMock { self } - pub fn make_result(self, result: PendingPayableDaoMock) -> Self { + pub fn make_result(self, result: SentPayableDaoMock) -> Self { self.make_results.borrow_mut().push(Box::new(result)); self } @@ -1122,12 +1207,12 @@ impl FailedPayableDao for FailedPayableDaoMock { fn update_statuses( &self, - status_updates: HashMap, + status_updates: &HashMap, ) -> Result<(), FailedPayableDaoError> { self.update_statuses_params .lock() .unwrap() - .push(status_updates); + .push(status_updates.clone()); self.update_statuses_results.borrow_mut().remove(0) } @@ -1209,9 +1294,6 @@ pub struct FailedPayableDaoFactoryMock { impl FailedPayableDaoFactory for FailedPayableDaoFactoryMock { fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!("FailedPayableDao Missing.") - }; self.make_params.lock().unwrap().push(()); self.make_results.borrow_mut().remove(0) } @@ -1238,7 +1320,7 @@ impl FailedPayableDaoFactoryMock { pub struct PayableScannerBuilder { payable_dao: PayableDaoMock, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, payment_thresholds: PaymentThresholds, payment_adjuster: PaymentAdjusterMock, } @@ -1247,7 +1329,7 @@ impl PayableScannerBuilder { pub fn new() -> Self { Self { payable_dao: PayableDaoMock::new(), - pending_payable_dao: PendingPayableDaoMock::new(), + sent_payable_dao: SentPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), payment_adjuster: PaymentAdjusterMock::default(), } @@ -1271,18 +1353,18 @@ impl PayableScannerBuilder { self } - pub fn pending_payable_dao( + pub fn sent_payable_dao( mut self, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, ) -> PayableScannerBuilder { - self.pending_payable_dao = pending_payable_dao; + self.sent_payable_dao = sent_payable_dao; self } pub fn build(self) -> PayableScanner { PayableScanner::new( Box::new(self.payable_dao), - Box::new(self.pending_payable_dao), + Box::new(self.sent_payable_dao), Rc::new(self.payment_thresholds), Box::new(self.payment_adjuster), ) @@ -1291,20 +1373,26 @@ impl PayableScannerBuilder { pub struct PendingPayableScannerBuilder { payable_dao: PayableDaoMock, - pending_payable_dao: PendingPayableDaoMock, + sent_payable_dao: SentPayableDaoMock, + failed_payable_dao: FailedPayableDaoMock, payment_thresholds: PaymentThresholds, - when_pending_too_long_sec: u64, financial_statistics: FinancialStatistics, + current_sent_payables: Box>, + yet_unproven_failed_payables: Box>, + clock: Box, } impl PendingPayableScannerBuilder { pub fn new() -> Self { Self { payable_dao: PayableDaoMock::new(), - pending_payable_dao: PendingPayableDaoMock::new(), + sent_payable_dao: SentPayableDaoMock::new(), + failed_payable_dao: FailedPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), - when_pending_too_long_sec: DEFAULT_PENDING_TOO_LONG_SEC, financial_statistics: FinancialStatistics::default(), + current_sent_payables: Box::new(PendingPayableCacheMock::default()), + yet_unproven_failed_payables: Box::new(PendingPayableCacheMock::default()), + clock: Box::new(ValidationFailureClockMock::default()), } } @@ -1313,24 +1401,46 @@ impl PendingPayableScannerBuilder { self } - pub fn pending_payable_dao(mut self, pending_payable_dao: PendingPayableDaoMock) -> Self { - self.pending_payable_dao = pending_payable_dao; + pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { + self.sent_payable_dao = sent_payable_dao; self } - pub fn when_pending_too_long_sec(mut self, interval: u64) -> Self { - self.when_pending_too_long_sec = interval; + pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { + self.failed_payable_dao = failed_payable_dao; + self + } + + pub fn sent_payable_cache(mut self, cache: Box>) -> Self { + self.current_sent_payables = cache; + self + } + + pub fn failed_payable_cache( + mut self, + failures: Box>, + ) -> Self { + self.yet_unproven_failed_payables = failures; + self + } + + pub fn validation_failure_clock(mut self, clock: Box) -> Self { + self.clock = clock; self } pub fn build(self) -> PendingPayableScanner { - PendingPayableScanner::new( + let mut scanner = PendingPayableScanner::new( Box::new(self.payable_dao), - Box::new(self.pending_payable_dao), + Box::new(self.sent_payable_dao), + Box::new(self.failed_payable_dao), Rc::new(self.payment_thresholds), - self.when_pending_too_long_sec, Rc::new(RefCell::new(self.financial_statistics)), - ) + ); + scanner.current_sent_payables = self.current_sent_payables; + scanner.yet_unproven_failed_payables = self.yet_unproven_failed_payables; + scanner.clock = self.clock; + scanner } } @@ -1398,17 +1508,6 @@ pub fn make_custom_payment_thresholds() -> PaymentThresholds { } } -pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { - PendingPayableFingerprint { - rowid: 33, - timestamp: from_unix_timestamp(222_222_222), - hash: make_tx_hash(456), - attempt: 1, - amount: 12345, - process_error: None, - } -} - pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, @@ -1490,10 +1589,10 @@ where { let conn = Connection::open_in_memory().unwrap(); let execute = |sql: &str| conn.execute(sql, []).unwrap(); - execute("create table whatever (exclamations text)"); - execute("insert into whatever (exclamations) values ('Gosh')"); + execute("create table whatever (exclamation text)"); + execute("insert into whatever (exclamation) values ('Gosh')"); - conn.query_row("select exclamations from whatever", [], tested_fn) + conn.query_row("select exclamation from whatever", [], tested_fn) .unwrap(); } diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 9e15f9c5d..8b24da722 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -473,7 +473,8 @@ impl ActorFactory for ActorFactoryReal { ) -> AccountantSubs { let data_directory = config.data_directory.as_path(); let payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); - let pending_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let sent_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); + let failed_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let receivable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let banned_dao_factory = Box::new(Accountant::dao_factory(data_directory)); let config_dao_factory = Box::new(Accountant::dao_factory(data_directory)); @@ -484,7 +485,8 @@ impl ActorFactory for ActorFactoryReal { config, DaoFactories { payable_dao_factory, - pending_payable_dao_factory, + sent_payable_dao_factory, + failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, config_dao_factory, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 421fc6bd5..d1d41337b 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,17 +1,21 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, PricedQualifiedPayables}; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayablesMessage, +}; use crate::accountant::{ - ReceivedPayments, ResponseSkeleton, ScanError, - SentPayables, SkeletonOptHolder, + ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, TxReceiptResult, }; -use crate::accountant::{ReportTransactionReceipts, RequestTransactionReceipts}; +use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; use crate::actor_system_factory::SubsFactory; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainInterfaceError, PayableTransactionError, }; -use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible; +use crate::blockchain::blockchain_interface::data_structures::{ + ProcessedPayableFallible, StatusReadFromReceiptCheck, +}; use crate::blockchain::blockchain_interface::BlockchainInterface; use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; use crate::database::db_initializer::{DbInitializationConfig, DbInitializer, DbInitializerReal}; @@ -19,12 +23,10 @@ use crate::db_config::config_dao::ConfigDaoReal; use crate::db_config::persistent_configuration::{ PersistentConfiguration, PersistentConfigurationReal, }; -use crate::sub_lib::blockchain_bridge::{ - BlockchainBridgeSubs, OutboundPaymentsInstructions, -}; +use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, OutboundPaymentsInstructions}; use crate::sub_lib::peer_actors::BindMessage; use crate::sub_lib::utils::{db_connection_launch_panic, handle_ui_crash_request}; -use crate::sub_lib::wallet::{Wallet}; +use crate::sub_lib::wallet::Wallet; use actix::Actor; use actix::Context; use actix::Handler; @@ -33,19 +35,16 @@ use actix::{Addr, Recipient}; use futures::Future; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; +use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; +use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; use std::string::ToString; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -use ethabi::Hash; use web3::types::H256; -use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; -use masq_lib::messages::ScanType; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; pub const DEFAULT_BLOCKCHAIN_SERVICE_URL: &str = "https://0.0.0.0"; @@ -59,12 +58,12 @@ pub struct BlockchainBridge { received_payments_subs_opt: Option>, scan_error_subs_opt: Option>, crashable: bool, - pending_payable_confirmation: TransactionConfirmationTools, + pending_payable_confirmation: TxConfirmationTools, } -struct TransactionConfirmationTools { - new_pp_fingerprints_sub_opt: Option>, - report_transaction_receipts_sub_opt: Option>, +struct TxConfirmationTools { + register_new_pending_payables_sub_opt: Option>, + report_tx_receipts_sub_opt: Option>, } #[derive(PartialEq, Eq)] @@ -88,11 +87,10 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: BindMessage, _ctx: &mut Self::Context) -> Self::Result { self.pending_payable_confirmation - .new_pp_fingerprints_sub_opt = - Some(msg.peer_actors.accountant.init_pending_payable_fingerprints); - self.pending_payable_confirmation - .report_transaction_receipts_sub_opt = - Some(msg.peer_actors.accountant.report_transaction_receipts); + .register_new_pending_payables_sub_opt = + Some(msg.peer_actors.accountant.register_new_pending_payables); + self.pending_payable_confirmation.report_tx_receipts_sub_opt = + Some(msg.peer_actors.accountant.report_transaction_status); self.payable_payments_setup_subs_opt = Some(msg.peer_actors.accountant.report_payable_payments_setup); self.sent_payable_subs_opt = Some(msg.peer_actors.accountant.report_sent_payments); @@ -164,21 +162,14 @@ impl Handler for BlockchainBridge { } #[derive(Debug, Clone, PartialEq, Eq, Message)] -pub struct PendingPayableFingerprintSeeds { - pub batch_wide_timestamp: SystemTime, - pub hashes_and_balances: Vec, +pub struct RegisterNewPendingPayables { + pub new_sent_txs: Vec, } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct PendingPayableFingerprint { - // Sqlite begins counting from 1 - pub rowid: u64, - pub timestamp: SystemTime, - pub hash: H256, - // We have Sqlite begin counting from 1 - pub attempt: u16, - pub amount: u128, - pub process_error: Option, +impl RegisterNewPendingPayables { + pub fn new(new_sent_txs: Vec) -> Self { + Self { new_sent_txs } + } } impl Handler for BlockchainBridge { @@ -204,9 +195,9 @@ impl BlockchainBridge { scan_error_subs_opt: None, crashable, logger: Logger::new("BlockchainBridge"), - pending_payable_confirmation: TransactionConfirmationTools { - new_pp_fingerprints_sub_opt: None, - report_transaction_receipts_sub_opt: None, + pending_payable_confirmation: TxConfirmationTools { + register_new_pending_payables_sub_opt: None, + report_tx_receipts_sub_opt: None, }, } } @@ -394,21 +385,21 @@ impl BlockchainBridge { fn log_status_of_tx_receipts( logger: &Logger, - transaction_receipts_results: &[TransactionReceiptResult], + transaction_receipts_results: &[&TxReceiptResult], ) { logger.debug(|| { let (successful_count, failed_count, pending_count) = transaction_receipts_results.iter().fold( (0, 0, 0), |(success, fail, pending), transaction_receipt| match transaction_receipt { - TransactionReceiptResult::RpcResponse(tx_receipt) => { - match tx_receipt.status { - TxStatus::Failed => (success, fail + 1, pending), - TxStatus::Pending => (success, fail, pending + 1), - TxStatus::Succeeded(_) => (success + 1, fail, pending), + Ok(tx_status) => match tx_status { + StatusReadFromReceiptCheck::Reverted => (success, fail + 1, pending), + StatusReadFromReceiptCheck::Succeeded(_) => { + (success + 1, fail, pending) } - } - TransactionReceiptResult::LocalError(_) => (success, fail, pending + 1), + StatusReadFromReceiptCheck::Pending => (success, fail, pending + 1), + }, + Err(_) => (success, fail, pending + 1), }, ); format!( @@ -425,30 +416,21 @@ impl BlockchainBridge { let logger = self.logger.clone(); let accountant_recipient = self .pending_payable_confirmation - .report_transaction_receipts_sub_opt + .report_tx_receipts_sub_opt .clone() .expect("Accountant is unbound"); - - let transaction_hashes = msg - .pending_payable_fingerprints - .iter() - .map(|finger_print| finger_print.hash) - .collect::>(); Box::new( self.blockchain_interface - .process_transaction_receipts(transaction_hashes) + .process_transaction_receipts(msg.tx_hashes) .map_err(move |e| e.to_string()) - .and_then(move |transaction_receipts_results| { - Self::log_status_of_tx_receipts(&logger, &transaction_receipts_results); - - let pairs = transaction_receipts_results - .into_iter() - .zip(msg.pending_payable_fingerprints.into_iter()) - .collect_vec(); - + .and_then(move |tx_receipt_results| { + Self::log_status_of_tx_receipts( + &logger, + tx_receipt_results.values().collect_vec().as_slice(), + ); accountant_recipient - .try_send(ReportTransactionReceipts { - fingerprints_with_receipts: pairs, + .try_send(TxReceiptsMessage { + results: tx_receipt_results, response_skeleton_opt: msg.response_skeleton_opt, }) .expect("Accountant is dead"); @@ -488,19 +470,19 @@ impl BlockchainBridge { affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { - let new_fingerprints_recipient = self.new_fingerprints_recipient(); + let recipient = self.new_pending_payables_recipient(); let logger = self.logger.clone(); self.blockchain_interface.submit_payables_in_batch( logger, agent, - new_fingerprints_recipient, + recipient, affordable_accounts, ) } - fn new_fingerprints_recipient(&self) -> Recipient { + fn new_pending_payables_recipient(&self) -> Recipient { self.pending_payable_confirmation - .new_pp_fingerprints_sub_opt + .register_new_pending_payables_sub_opt .clone() .expect("Accountant unbound") } @@ -552,18 +534,26 @@ impl SubsFactory for BlockchainBridgeSub mod tests { use super::*; use crate::accountant::db_access_objects::payable_dao::PayableAccount; - use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; + use crate::accountant::db_access_objects::sent_payable_dao::TxStatus; + use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, + }; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint, make_priced_qualified_payables}; + use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::accountant::test_utils::make_priced_qualified_payables; + use crate::accountant::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainAgentBuildError, PayableTransactionError, }; use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible::Correct; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, RetrievedBlockchainTransactions, + BlockchainTransaction, RetrievedBlockchainTransactions, TxBlock, }; + use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; + use crate::blockchain::errors::validation_status::ValidationStatus; use crate::blockchain::test_utils::{ make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; @@ -584,6 +574,7 @@ mod tests { use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; + use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; @@ -597,9 +588,6 @@ mod tests { use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160}; - use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; - use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, QualifiedPayableWithGasPrice}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt}; impl Handler> for BlockchainBridge { type Result = (); @@ -897,18 +885,18 @@ mod tests { system.run(); let time_after = SystemTime::now(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let pending_payable_fingerprint_seeds_msg = - accountant_recording.get_record::(0); + let register_new_pending_payables_msg = + accountant_recording.get_record::(0); let sent_payables_msg = accountant_recording.get_record::(1); + let expected_hash = + H256::from_str("81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c") + .unwrap(); assert_eq!( sent_payables_msg, &SentPayables { payment_procedure_result: Ok(vec![Correct(PendingPayable { - recipient_wallet: account.wallet, - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap() + recipient_wallet: account.wallet.clone(), + hash: expected_hash })]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -916,17 +904,26 @@ mod tests { }) } ); - assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp >= time_before); - assert!(pending_payable_fingerprint_seeds_msg.batch_wide_timestamp <= time_after); + let first_actual_sent_tx = ®ister_new_pending_payables_msg.new_sent_txs[0]; assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap(), - amount: account.balance_wei - }] + first_actual_sent_tx.receiver_address, + account.wallet.address() + ); + assert_eq!(first_actual_sent_tx.hash, expected_hash); + assert_eq!(first_actual_sent_tx.amount_minor, account.balance_wei); + assert_eq!(first_actual_sent_tx.gas_price_minor, 111_222_333); + assert_eq!(first_actual_sent_tx.nonce, 0x20); + assert_eq!( + first_actual_sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting) + ); + assert!( + to_unix_timestamp(time_before) <= first_actual_sent_tx.timestamp + && first_actual_sent_tx.timestamp <= to_unix_timestamp(time_after), + "We thought the timestamp was between {:?} and {:?}, but it was {:?}", + time_before, + time_after, + from_unix_timestamp(first_actual_sent_tx.timestamp) ); assert_eq!(accountant_recording.len(), 2); } @@ -945,7 +942,7 @@ mod tests { let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); - let wallet_account = make_wallet("blah"); + let account_wallet = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); let persistent_configuration_mock = PersistentConfigurationMock::default(); let subject = BlockchainBridge::new( @@ -958,7 +955,7 @@ mod tests { let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); let account = PayableAccount { - wallet: wallet_account, + wallet: account_wallet.clone(), balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, @@ -986,8 +983,8 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let pending_payable_fingerprint_seeds_msg = - accountant_recording.get_record::(0); + let actual_register_new_pending_payables_msg = + accountant_recording.get_record::(0); let sent_payables_msg = accountant_recording.get_record::(1); let scan_error_msg = accountant_recording.get_record::(2); assert_sending_error( @@ -998,14 +995,23 @@ mod tests { "Transport error: Error(IncompleteMessage)", ); assert_eq!( - pending_payable_fingerprint_seeds_msg.hashes_and_balances, - vec![HashAndAmount { - hash: H256::from_str( - "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - .unwrap(), - amount: account.balance_wei - }] + actual_register_new_pending_payables_msg.new_sent_txs[0].receiver_address, + account_wallet.address() + ); + assert_eq!( + actual_register_new_pending_payables_msg.new_sent_txs[0].hash, + H256::from_str("81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c") + .unwrap() + ); + assert_eq!( + actual_register_new_pending_payables_msg.new_sent_txs[0].amount_minor, + account.balance_wei + ); + let number_of_requested_txs = actual_register_new_pending_payables_msg.new_sent_txs.len(); + assert_eq!( + number_of_requested_txs, 1, + "We expected only one sent tx, but got {}", + number_of_requested_txs ); assert_eq!( *scan_error_msg, @@ -1016,7 +1022,8 @@ mod tests { context_id: 4321 }), msg: format!( - "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" + "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". \ + Signed and hashed txs: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) } ); @@ -1058,7 +1065,7 @@ mod tests { let (accountant, _, accountant_recording) = make_recorder(); subject .pending_payable_confirmation - .new_pp_fingerprints_sub_opt = Some(accountant.start().recipient()); + .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject .process_payments(msg.agent, msg.affordable_accounts) @@ -1119,7 +1126,7 @@ mod tests { let (accountant, _, accountant_recording) = make_recorder(); subject .pending_payable_confirmation - .new_pp_fingerprints_sub_opt = Some(accountant.start().recipient()); + .register_new_pending_payables_sub_opt = Some(accountant.start().recipient()); let result = subject .process_payments(msg.agent, msg.affordable_accounts) @@ -1154,21 +1161,13 @@ mod tests { #[test] fn blockchain_bridge_processes_requests_for_a_complete_and_null_transaction_receipt() { let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); - let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); - let hash_1 = pending_payable_fingerprint_1.hash; - let hash_2 = make_tx_hash(78989); - let pending_payable_fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 4565, - process_error: None, - }; + let accountant = + accountant.system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)); + let tx_hash_1 = make_tx_hash(123); + let tx_hash_2 = make_tx_hash(456); let first_response = ReceiptResponseBuilder::default() .status(U64::from(1)) - .transaction_hash(hash_1) + .transaction_hash(tx_hash_1) .build(); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) @@ -1189,9 +1188,9 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - pending_payable_fingerprint_1.clone(), - pending_payable_fingerprint_2.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1205,26 +1204,20 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 1); - let report_transaction_receipt_message = - accountant_recording.get_record::(0); + let tx_receipts_message = accountant_recording.get_record::(0); let mut expected_receipt = TransactionReceipt::default(); - expected_receipt.transaction_hash = hash_1; + expected_receipt.transaction_hash = tx_hash_1; expected_receipt.status = Some(U64::from(1)); assert_eq!( - report_transaction_receipt_message, - &ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(expected_receipt.into()), - pending_payable_fingerprint_1 - ), - ( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash_2, - status: TxStatus::Pending - }), - pending_payable_fingerprint_2 + tx_receipts_message, + &TxReceiptsMessage { + results: hashmap![ + TxHashByTable::SentPayable(tx_hash_1) => Ok( + expected_receipt.into() ), + TxHashByTable::FailedPayable(tx_hash_2) => Ok( + StatusReadFromReceiptCheck::Pending + ) ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1286,8 +1279,7 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_on_failure_from_remote_process_sends_back_all_good_results_and_logs_abort( - ) { + fn handle_request_transaction_receipts_sends_back_results() { init_test_logging(); let port = find_free_port(); let block_number = U64::from(4545454); @@ -1302,62 +1294,26 @@ mod tests { .begin_batch() .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .raw_response(tx_receipt_response) - .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .err_response( 429, "The requests per second (RPS) of your requests are higher than your plan allows." .to_string(), 7, ) + .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) .end_batch() .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!( - ReportTransactionReceipts, - ScanError - )) + .system_stop_conditions(match_lazily_every_type_id!(TxReceiptsMessage)) .start(); - let report_transaction_receipt_recipient: Recipient = + let report_transaction_receipt_recipient: Recipient = accountant_addr.clone().recipient(); let scan_error_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(111334); - let hash_2 = make_tx_hash(100000); - let hash_3 = make_tx_hash(0x1348d); - let hash_4 = make_tx_hash(11111); - let mut fingerprint_1 = make_pending_payable_fingerprint(); - fingerprint_1.hash = hash_1; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_2, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_3 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: hash_3, - attempt: 3, - amount: 4565, - process_error: None, - }; - let fingerprint_4 = PendingPayableFingerprint { - rowid: 450, - timestamp: from_unix_timestamp(230_000_000), - hash: hash_4, - attempt: 1, - amount: 7879, - process_error: None, - }; - let transaction_receipt = TxReceipt { - transaction_hash: Default::default(), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number, - }), - }; + let tx_hash_1 = make_tx_hash(1334); + let tx_hash_2 = make_tx_hash(1000); + let tx_hash_3 = make_tx_hash(1212); + let tx_hash_4 = make_tx_hash(1111); let blockchain_interface = make_blockchain_interface_web3(port); let system = System::new("test_transaction_receipts"); let mut subject = BlockchainBridge::new( @@ -1367,14 +1323,14 @@ mod tests { ); subject .pending_payable_confirmation - .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); + .report_tx_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![ - fingerprint_1.clone(), - fingerprint_2.clone(), - fingerprint_3.clone(), - fingerprint_4.clone(), + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::SentPayable(tx_hash_2), + TxHashByTable::SentPayable(tx_hash_3), + TxHashByTable::SentPayable(tx_hash_4), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1388,15 +1344,18 @@ mod tests { assert_eq!(system.run(), 0); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 1); - let report_receipts_msg = accountant_recording.get_record::(0); + let report_receipts_msg = accountant_recording.get_record::(0); assert_eq!( *report_receipts_msg, - ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - (TransactionReceiptResult::RpcResponse(TxReceipt{ transaction_hash: hash_1, status: TxStatus::Pending }), fingerprint_1), - (TransactionReceiptResult::RpcResponse(transaction_receipt), fingerprint_2), - (TransactionReceiptResult::RpcResponse(TxReceipt{ transaction_hash: hash_3, status: TxStatus::Pending }), fingerprint_3), - (TransactionReceiptResult::LocalError("RPC error: Error { code: ServerError(429), message: \"The requests per second (RPS) of your requests are higher than your plan allows.\", data: None }".to_string()), fingerprint_4) + TxReceiptsMessage { + results: hashmap![TxHashByTable::SentPayable(tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + TxHashByTable::SentPayable(tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash: Default::default(), + block_number, + })), + TxHashByTable::SentPayable(tx_hash_3) => Err( + AppRpcError:: Remote(RemoteError::Web3RpcError { code: 429, message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string()})), + TxHashByTable::SentPayable(tx_hash_4) => Ok(StatusReadFromReceiptCheck::Pending), ], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1410,32 +1369,17 @@ mod tests { } #[test] - fn handle_request_transaction_receipts_short_circuits_if_submit_batch_fails() { + fn handle_request_transaction_receipts_failing_submit_the_batch() { init_test_logging(); let (accountant, _, accountant_recording) = make_recorder(); let accountant_addr = accountant .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); - let report_transaction_recipient: Recipient = + let report_transaction_recipient: Recipient = accountant_addr.recipient(); - let hash_1 = make_tx_hash(0x1b2e6); - let fingerprint_1 = PendingPayableFingerprint { - rowid: 454, - timestamp: SystemTime::now(), - hash: hash_1, - attempt: 3, - amount: 3333, - process_error: None, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 456, - timestamp: SystemTime::now(), - hash: make_tx_hash(222444), - attempt: 3, - amount: 4565, - process_error: None, - }; + let tx_hash_1 = make_tx_hash(10101); + let tx_hash_2 = make_tx_hash(10102); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); let blockchain_interface = make_blockchain_interface_web3(port); @@ -1446,10 +1390,13 @@ mod tests { ); subject .pending_payable_confirmation - .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); + .report_tx_receipts_sub_opt = Some(report_transaction_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable_fingerprints: vec![fingerprint_1, fingerprint_2], + tx_hashes: vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::FailedPayable(tx_hash_2), + ], response_skeleton_opt: None, }; let system = System::new("test"); @@ -2033,10 +1980,11 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_lazily_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); + subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, ScanType::Receivables, @@ -2045,7 +1993,9 @@ mod tests { system.run(); let accountant_recording = accountant_recording_arc.lock().unwrap(); - let msg_opt = accountant_recording.get_record_opt::(0); + let received_msg = accountant_recording.get_record::(0); + assert_eq!(received_msg.new_start_block, BlockMarker::Value(0xc8 + 1)); + let msg_opt = accountant_recording.get_record_opt::(1); assert_eq!(msg_opt, None, "We didnt expect a scan error: {:?}", msg_opt); } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index c93c07b53..7a4d6ddfb 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -6,104 +6,12 @@ use crate::blockchain::blockchain_interface::data_structures::errors::Blockchain use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use ethereum_types::{H256, U256, U64}; use futures::Future; -use serde_derive::{Deserialize, Serialize}; use serde_json::Value; -use std::fmt::Display; -use std::str::FromStr; use web3::contract::{Contract, Options}; use web3::transports::{Batch, Http}; -use web3::types::{Address, BlockNumber, Filter, Log, TransactionReceipt}; +use web3::types::{Address, BlockNumber, Filter, Log}; use web3::{Error, Web3}; -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TransactionReceiptResult { - RpcResponse(TxReceipt), - LocalError(String), -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TxStatus { - Failed, - Pending, - Succeeded(TransactionBlock), -} - -impl FromStr for TxStatus { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "Pending" => Ok(TxStatus::Pending), - "Failed" => Ok(TxStatus::Failed), // TODO: GH-631: This should be removed - s if s.starts_with("Succeeded") => { - // The format is "Succeeded(block_number, block_hash)" - let parts: Vec<&str> = s[10..s.len() - 1].split(',').collect(); - if parts.len() != 2 { - return Err("Invalid Succeeded format".to_string()); - } - let block_number: u64 = parts[0] - .parse() - .map_err(|_| "Invalid block number".to_string())?; - let block_hash = - H256::from_str(&parts[1][2..]).map_err(|_| "Invalid block hash".to_string())?; - Ok(TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number: U64::from(block_number), - })) - } - _ => Err(format!("Unknown status: {}", s)), - } - } -} - -impl Display for TxStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TxStatus::Failed => write!(f, "Failed"), - TxStatus::Pending => write!(f, "Pending"), - TxStatus::Succeeded(block) => { - write!( - f, - "Succeeded({},{:?})", - block.block_number, block.block_hash - ) - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct TxReceipt { - pub transaction_hash: H256, - pub status: TxStatus, -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct TransactionBlock { - pub block_hash: H256, - pub block_number: U64, -} - -impl From for TxReceipt { - fn from(receipt: TransactionReceipt) -> Self { - let status = match (receipt.status, receipt.block_hash, receipt.block_number) { - (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { - TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }) - } - (Some(status), _, _) if status == U64::from(0) => TxStatus::Failed, - _ => TxStatus::Pending, - }; - - TxReceipt { - transaction_hash: receipt.transaction_hash, - status, - } - } -} - pub struct LowBlockchainIntWeb3 { web3: Web3, web3_batch: Web3>, @@ -222,7 +130,7 @@ mod tests { use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_LITERAL; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::{BlockchainInterfaceError, BlockchainInterface}; - use crate::blockchain::test_utils::make_blockchain_interface_web3; + use crate::blockchain::test_utils::{make_block_hash, make_blockchain_interface_web3, make_tx_hash, TransactionReceiptBuilder}; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use ethereum_types::{H256, U64}; @@ -230,8 +138,8 @@ mod tests { use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::utils::find_free_port; use std::str::FromStr; - use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, TransactionReceipt, U256}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use web3::types::{BlockNumber, Bytes, FilterBuilder, Log, U256}; + use crate::blockchain::blockchain_interface::data_structures::StatusReadFromReceiptCheck; #[test] fn get_transaction_fee_balance_works() { @@ -601,17 +509,17 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_successful_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - Some(U64::from(10)), - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - match tx_receipt.status { - TxStatus::Succeeded(ref block) => { - assert_eq!(block.block_hash, H256::from_low_u64_be(0x1234)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .block_hash(make_block_hash(0x1234)) + .block_number(10.into()) + .build() + .into(); + + match tx_status { + StatusReadFromReceiptCheck::Succeeded(ref block) => { + assert_eq!(block.block_hash, make_block_hash(0x1234)); assert_eq!(block.block_number, U64::from(10)); } _ => panic!("Expected status to be Succeeded"), @@ -620,139 +528,43 @@ mod tests { #[test] fn transaction_receipt_can_be_converted_to_failed_transaction() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(0)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(0)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Failed); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Reverted); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status() { - let tx_receipt: TxReceipt = - create_tx_receipt(None, None, None, H256::from_low_u64_be(0x5678)); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - None, - None, - H256::from_low_u64_be(0x5678), - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .status(U64::from(1)) + .build() + .into(); - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } #[test] fn transaction_receipt_can_be_converted_to_pending_transaction_no_status_and_block_info() { - let tx_receipt: TxReceipt = create_tx_receipt( - Some(U64::from(1)), - Some(H256::from_low_u64_be(0x1234)), - None, - H256::from_low_u64_be(0x5678), - ); - - assert_eq!(tx_receipt.transaction_hash, H256::from_low_u64_be(0x5678)); - assert_eq!(tx_receipt.status, TxStatus::Pending); - } - - #[test] - fn tx_status_display_works() { - // Test Failed - assert_eq!(TxStatus::Failed.to_string(), "Failed"); - - // Test Pending - assert_eq!(TxStatus::Pending.to_string(), "Pending"); - - // Test Succeeded - let block_number = U64::from(12345); - let block_hash = H256::from_low_u64_be(0xabcdef); - let succeeded = TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }); - assert_eq!( - succeeded.to_string(), - format!("Succeeded({},0x{:x})", block_number, block_hash) - ); - } - - #[test] - fn tx_status_from_str_works() { - // Test Pending - assert_eq!(TxStatus::from_str("Pending"), Ok(TxStatus::Pending)); - - // Test Failed - assert_eq!(TxStatus::from_str("Failed"), Ok(TxStatus::Failed)); - - // Test Succeeded with valid input - let block_number = 123456789; - let block_hash = H256::from_low_u64_be(0xabcdef); - let input = format!("Succeeded({},0x{:x})", block_number, block_hash); - assert_eq!( - TxStatus::from_str(&input), - Ok(TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number: U64::from(block_number), - })) - ); + let tx_status: StatusReadFromReceiptCheck = + TransactionReceiptBuilder::new(make_tx_hash(0x5678)) + .build() + .into(); - // Test Succeeded with invalid format - assert_eq!( - TxStatus::from_str("Succeeded(123)"), - Err("Invalid Succeeded format".to_string()) - ); - - // Test Succeeded with invalid block number - assert_eq!( - TxStatus::from_str( - "Succeeded(abc,0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef)" - ), - Err("Invalid block number".to_string()) - ); - - // Test Succeeded with invalid block hash - assert_eq!( - TxStatus::from_str("Succeeded(123,0xinvalidhash)"), - Err("Invalid block hash".to_string()) - ); - - // Test unknown status - assert_eq!( - TxStatus::from_str("InProgress"), - Err("Unknown status: InProgress".to_string()) - ); - } - - fn create_tx_receipt( - status: Option, - block_hash: Option, - block_number: Option, - transaction_hash: H256, - ) -> TxReceipt { - let receipt = TransactionReceipt { - status, - root: None, - block_hash, - block_number, - cumulative_gas_used: Default::default(), - gas_used: None, - contract_address: None, - transaction_hash, - transaction_index: Default::default(), - logs: vec![], - logs_bloom: Default::default(), - }; - receipt.into() + assert_eq!(tx_status, StatusReadFromReceiptCheck::Pending); } } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index bb9cde491..7178d9d90 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,8 +4,9 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; +use std::collections::{HashMap}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; +use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible, StatusReadFromReceiptCheck}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainInterface}; @@ -20,12 +21,15 @@ use actix::Recipient; use ethereum_types::U64; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, PricedQualifiedPayables}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{LowBlockchainIntWeb3, TransactionReceiptResult, TxReceipt, TxStatus}; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, RegisterNewPendingPayables}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::LowBlockchainIntWeb3; use crate::blockchain::blockchain_interface::blockchain_interface_web3::utils::{create_blockchain_agent_web3, send_payables_within_batch, BlockchainAgentFutureResult}; - +use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; // TODO We should probably begin to attach these constants to the interfaces more tightly, so that // we aren't baffled by which interface they belong with. I suggest to declare them inside // their inherent impl blocks. They will then need to be preceded by the class name @@ -182,7 +186,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { Box::new( get_gas_price .map_err(BlockchainAgentBuildError::GasPrice) - .and_then(move |gas_price_wei| { + .and_then(move |gas_price_minor| { get_transaction_fee_balance .map_err(move |e| { BlockchainAgentBuildError::TransactionFeeBalance(wallet_address, e) @@ -195,7 +199,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { .and_then(move |masq_token_balance| { let blockchain_agent_future_result = BlockchainAgentFutureResult { - gas_price_wei, + gas_price_minor, transaction_fee_balance, masq_token_balance, }; @@ -213,38 +217,44 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>> - { + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = HashMap, + Error = BlockchainInterfaceError, + >, + > { Box::new( self.lower_interface() - .get_transaction_receipt_in_batch(transaction_hashes.clone()) + .get_transaction_receipt_in_batch(Self::collect_plain_hashes(&tx_hashes)) .map_err(move |e| e) .and_then(move |batch_response| { Ok(batch_response .into_iter() - .zip(transaction_hashes) - .map(|(response, hash)| match response { + .zip(tx_hashes.into_iter()) + .map(|(response, tx_hash)| match response { Ok(result) => { match serde_json::from_value::(result) { Ok(receipt) => { - TransactionReceiptResult::RpcResponse(receipt.into()) + (tx_hash, Ok(StatusReadFromReceiptCheck::from(receipt))) } Err(e) => { if e.to_string().contains("invalid type: null") { - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: hash, - status: TxStatus::Pending, - }) + (tx_hash, Ok(StatusReadFromReceiptCheck::Pending)) } else { - TransactionReceiptResult::LocalError(e.to_string()) + ( + tx_hash, + Err(AppRpcError::Remote( + RemoteError::InvalidResponse(e.to_string()), + )), + ) } } } } - Err(e) => TransactionReceiptResult::LocalError(e.to_string()), + Err(e) => (tx_hash, Err(AppRpcError::from(e))), }) - .collect::>()) + .collect::>()) }), ) } @@ -253,7 +263,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { @@ -274,7 +284,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { &web3_batch, consuming_wallet, pending_nonce, - fingerprints_recipient, + new_pending_payables_recipient, affordable_accounts, ) }), @@ -285,7 +295,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub struct HashAndAmount { pub hash: H256, - pub amount: u128, + pub amount_minor: u128, } impl BlockchainInterfaceWeb3 { @@ -431,22 +441,43 @@ impl BlockchainInterfaceWeb3 { Ok(transactions) } } + + fn collect_plain_hashes(hashes_by_table: &[TxHashByTable]) -> Vec { + hashes_by_table + .iter() + .map(|hash_by_table| match hash_by_table { + TxHashByTable::SentPayable(hash) => *hash, + TxHashByTable::FailedPayable(hash) => *hash, + }) + .collect() + } } #[cfg(test)] mod tests { use super::*; + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + QualifiedPayableWithGasPrice, QualifiedPayablesBeforeGasPriceSelection, + UnpricedQualifiedPayables, + }; + use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; - use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; + use crate::blockchain::blockchain_interface::data_structures::{ + BlockchainTransaction, TxBlock, + }; use crate::blockchain::blockchain_interface::{ BlockchainAgentBuildError, BlockchainInterfaceError, BlockchainInterface, RetrievedBlockchainTransactions, }; - use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; + use crate::blockchain::test_utils::{ + all_chains, make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, + }; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; @@ -462,10 +493,7 @@ mod tests { use std::str::FromStr; use web3::transports::Http; use web3::types::{H256, U256}; - use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayableWithGasPrice}; - use crate::accountant::test_utils::make_payable_account; - use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; #[test] fn constants_are_correct() { @@ -1046,27 +1074,19 @@ mod tests { #[test] fn process_transaction_receipts_works() { let port = find_free_port(); - let tx_hash_1 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0e") - .unwrap(); - let tx_hash_2 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0f") - .unwrap(); - let tx_hash_3 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0a") - .unwrap(); - let tx_hash_4 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0b") - .unwrap(); - let tx_hash_5 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0c") - .unwrap(); - let tx_hash_6 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0d") - .unwrap(); - let tx_hash_vec = vec![ - tx_hash_1, tx_hash_2, tx_hash_3, tx_hash_4, tx_hash_5, tx_hash_6, - ]; + let tx_hash_1 = make_tx_hash(3300); + let tx_hash_2 = make_tx_hash(3401); + let tx_hash_3 = make_tx_hash(3502); + let tx_hash_4 = make_tx_hash(3603); + let tx_hash_5 = make_tx_hash(3704); + let tx_hash_6 = make_tx_hash(3805); + let tx_hbt_1 = TxHashByTable::FailedPayable(tx_hash_1); + let tx_hbt_2 = TxHashByTable::FailedPayable(tx_hash_2); + let tx_hbt_3 = TxHashByTable::SentPayable(tx_hash_3); + let tx_hbt_4 = TxHashByTable::SentPayable(tx_hash_4); + let tx_hbt_5 = TxHashByTable::SentPayable(tx_hash_5); + let tx_hbt_6 = TxHashByTable::SentPayable(tx_hash_6); + let sent_tx_vec = vec![tx_hbt_1, tx_hbt_2, tx_hbt_3, tx_hbt_4, tx_hbt_5, tx_hbt_6]; let block_hash = H256::from_str("6d0abccae617442c26104c2bc63d1bc05e1e002e555aec4ab62a46e826b18f18") .unwrap(); @@ -1108,48 +1128,45 @@ mod tests { let subject = make_blockchain_interface_web3(port); let result = subject - .process_transaction_receipts(tx_hash_vec) + .process_transaction_receipts(sent_tx_vec.clone()) .wait() .unwrap(); - assert_eq!(result[0], TransactionReceiptResult::LocalError("RPC error: Error { code: ServerError(429), message: \"The requests per second (RPS) of your requests are higher than your plan allows.\", data: None }".to_string())); + assert_eq!(result.get(&tx_hbt_1).unwrap(), &Err( + AppRpcError::Remote( + RemoteError::Web3RpcError { + code: 429, + message: + "The requests per second (RPS) of your requests are higher than your plan allows." + .to_string() + } + )) + ); assert_eq!( - result[1], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_2, - status: TxStatus::Pending - }) + result.get(&tx_hbt_2).unwrap(), + &Ok(StatusReadFromReceiptCheck::Pending) ); assert_eq!( - result[2], - TransactionReceiptResult::LocalError( + result.get(&tx_hbt_3).unwrap(), + &Err(AppRpcError::Remote(RemoteError::InvalidResponse( "invalid type: string \"trash\", expected struct Receipt".to_string() - ) + ))) ); assert_eq!( - result[3], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_4, - status: TxStatus::Pending - }) + result.get(&tx_hbt_4).unwrap(), + &Ok(StatusReadFromReceiptCheck::Pending) ); assert_eq!( - result[4], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_5, - status: TxStatus::Failed, - }) + result.get(&tx_hbt_5).unwrap(), + &Ok(StatusReadFromReceiptCheck::Reverted) ); assert_eq!( - result[5], - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: tx_hash_6, - status: TxStatus::Succeeded(TransactionBlock { - block_hash, - block_number, - }), - }) - ); + result.get(&tx_hbt_6).unwrap(), + &Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }),) + ) } #[test] @@ -1157,13 +1174,12 @@ mod tests { let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); let subject = make_blockchain_interface_web3(port); - let tx_hash_1 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0e") - .unwrap(); - let tx_hash_2 = - H256::from_str("a128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0f") - .unwrap(); - let tx_hash_vec = vec![tx_hash_1, tx_hash_2]; + let tx_hash_1 = make_tx_hash(789); + let tx_hash_2 = make_tx_hash(123); + let tx_hash_vec = vec![ + TxHashByTable::SentPayable(tx_hash_1), + TxHashByTable::SentPayable(tx_hash_2), + ]; let error = subject .process_transaction_receipts(tx_hash_vec) @@ -1346,4 +1362,33 @@ mod tests { BlockMarker::Uninitialized ); } + + #[test] + fn collect_plain_hashes_works() { + let hash_sent_tx_1 = make_tx_hash(456); + let hash_sent_tx_2 = make_tx_hash(789); + let hash_sent_tx_3 = make_tx_hash(234); + let hash_failed_tx_1 = make_tx_hash(123); + let hash_failed_tx_2 = make_tx_hash(345); + let inputs = vec![ + TxHashByTable::SentPayable(hash_sent_tx_1), + TxHashByTable::FailedPayable(hash_failed_tx_1), + TxHashByTable::SentPayable(hash_sent_tx_2), + TxHashByTable::SentPayable(hash_sent_tx_3), + TxHashByTable::FailedPayable(hash_failed_tx_2), + ]; + + let result = BlockchainInterfaceWeb3::collect_plain_hashes(&inputs); + + assert_eq!( + result, + vec![ + hash_sent_tx_1, + hash_failed_tx_1, + hash_sent_tx_2, + hash_sent_tx_3, + hash_failed_tx_2 + ] + ); + } } diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index 00489febc..d8e1729f9 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -1,18 +1,21 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; +use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; +use crate::accountant::db_access_objects::utils::{to_unix_timestamp, TxHash}; use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::accountant::PendingPayable; use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ - BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, + BlockchainInterfaceWeb3, TRANSFER_METHOD_ID, }; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ ProcessedPayableFallible, RpcPayableFailure, }; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use actix::Recipient; @@ -22,66 +25,47 @@ use masq_lib::constants::WALLET_ADDRESS_LENGTH; use masq_lib::logger::Logger; use secp256k1secrets::SecretKey; use serde_json::Value; +use std::collections::HashSet; use std::iter::once; use std::time::SystemTime; use thousands::Separable; use web3::transports::{Batch, Http}; use web3::types::{Bytes, SignedTransaction, TransactionParameters, U256}; -use web3::Error as Web3Error; use web3::Web3; #[derive(Debug)] pub struct BlockchainAgentFutureResult { - pub gas_price_wei: U256, + pub gas_price_minor: U256, pub transaction_fee_balance: U256, pub masq_token_balance: U256, } -pub fn advance_used_nonce(current_nonce: U256) -> U256 { - current_nonce - .checked_add(U256::one()) - .expect("unexpected limits") -} - -fn error_with_hashes( - error: Web3Error, - hashes_and_paid_amounts: Vec, -) -> PayableTransactionError { - let hashes = hashes_and_paid_amounts - .into_iter() - .map(|hash_and_amount| hash_and_amount.hash) - .collect(); - PayableTransactionError::Sending { - msg: error.to_string(), - hashes, - } -} +// TODO using these three vectors like this is dangerous; who guarantees that all three have their +// items sorted in the right order? pub fn merged_output_data( responses: Vec>, - hashes_and_paid_amounts: Vec, + sent_tx_hashes: Vec, accounts: Vec, ) -> Vec { let iterator_with_all_data = responses .into_iter() - .zip(hashes_and_paid_amounts.into_iter()) + .zip(sent_tx_hashes.into_iter()) .zip(accounts.iter()); iterator_with_all_data - .map( - |((rpc_result, hash_and_amount), account)| match rpc_result { - Ok(_rpc_result) => { - // TODO: GH-547: This rpc_result should be validated - ProcessedPayableFallible::Correct(PendingPayable { - recipient_wallet: account.wallet.clone(), - hash: hash_and_amount.hash, - }) - } - Err(rpc_error) => ProcessedPayableFallible::Failed(RpcPayableFailure { - rpc_error, + .map(|((rpc_result, hash), account)| match rpc_result { + Ok(_rpc_result) => { + // TODO: GH-547: This rpc_result should be validated + ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: account.wallet.clone(), - hash: hash_and_amount.hash, - }), - }, - ) + hash, + }) + } + Err(rpc_error) => ProcessedPayableFallible::Failed(RpcPayableFailure { + rpc_error, + recipient_wallet: account.wallet.clone(), + hash, + }), + }) .collect() } @@ -143,11 +127,11 @@ pub fn transmission_log( introduction.chain(body).collect() } -pub fn sign_transaction_data(amount: u128, recipient_wallet: Wallet) -> [u8; 68] { +pub fn sign_transaction_data(amount_minor: u128, recipient_wallet: Wallet) -> [u8; 68] { let mut data = [0u8; 4 + 32 + 32]; data[0..4].copy_from_slice(&TRANSFER_METHOD_ID); data[16..36].copy_from_slice(&recipient_wallet.address().0[..]); - U256::from(amount).to_big_endian(&mut data[36..68]); + U256::from(amount_minor).to_big_endian(&mut data[36..68]); data } @@ -164,11 +148,11 @@ pub fn sign_transaction( web3_batch: &Web3>, recipient_wallet: Wallet, consuming_wallet: Wallet, - amount: u128, + amount_minor: u128, nonce: U256, gas_price_in_wei: u128, ) -> SignedTransaction { - let data = sign_transaction_data(amount, recipient_wallet); + let data = sign_transaction_data(amount_minor, recipient_wallet); let gas_limit = gas_limit(data, chain); // Warning: If you set gas_price or nonce to None in transaction_parameters, sign_transaction // will start making RPC calls which we don't want (Do it at your own risk). @@ -215,7 +199,7 @@ pub fn sign_and_append_payment( consuming_wallet: Wallet, nonce: U256, gas_price_in_wei: u128, -) -> HashAndAmount { +) -> TxHash { let signed_tx = sign_transaction( chain, web3_batch, @@ -227,10 +211,7 @@ pub fn sign_and_append_payment( ); append_signed_transaction_to_batch(web3_batch, signed_tx.raw_transaction); - HashAndAmount { - hash: signed_tx.transaction_hash, - amount: recipient.balance_wei, - } + signed_tx.transaction_hash } pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_transaction: Bytes) { @@ -239,37 +220,51 @@ pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_tr } pub fn sign_and_append_multiple_payments( + now: SystemTime, logger: &Logger, chain: Chain, web3_batch: &Web3>, consuming_wallet: Wallet, - mut pending_nonce: U256, + initial_pending_nonce: U256, accounts: &PricedQualifiedPayables, -) -> Vec { - let mut hash_and_amount_list = vec![]; - accounts.payables.iter().for_each(|payable_pack| { - let payable = &payable_pack.payable; - debug!( - logger, - "Preparing payable future of {} wei to {} with nonce {}", - payable.balance_wei.separate_with_commas(), - payable.wallet, - pending_nonce - ); - - let hash_and_amount = sign_and_append_payment( - chain, - web3_batch, - payable, - consuming_wallet.clone(), - pending_nonce, - payable_pack.gas_price_minor, - ); - - pending_nonce = advance_used_nonce(pending_nonce); - hash_and_amount_list.push(hash_and_amount); - }); - hash_and_amount_list +) -> Vec { + let unix_mow = to_unix_timestamp(now); + accounts + .payables + .iter() + .enumerate() + .map(|(idx, payable_pack)| { + let current_pending_nonce = initial_pending_nonce + U256::from(idx); + let payable = &payable_pack.payable; + + debug!( + logger, + "Preparing tx of {} wei to {} with nonce {}", + payable.balance_wei.separate_with_commas(), + payable.wallet, + current_pending_nonce + ); + + let hash = sign_and_append_payment( + chain, + web3_batch, + payable, + consuming_wallet.clone(), + current_pending_nonce, + payable_pack.gas_price_minor, + ); + + SentTx { + hash, + receiver_address: payable.wallet.address(), + amount_minor: payable.balance_wei, + timestamp: unix_mow, + gas_price_minor: payable_pack.gas_price_minor, + nonce: current_pending_nonce.as_u64(), + status: TxStatus::Pending(ValidationStatus::Waiting), + } + }) + .collect() } #[allow(clippy::too_many_arguments)] @@ -279,7 +274,7 @@ pub fn send_payables_within_batch( web3_batch: &Web3>, consuming_wallet: Wallet, pending_nonce: U256, - new_fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError> + 'static> { @@ -291,7 +286,10 @@ pub fn send_payables_within_batch( chain.rec().num_chain_id, ); - let hashes_and_paid_amounts = sign_and_append_multiple_payments( + let common_timestamp = SystemTime::now(); + + let prepared_sent_txs_records = sign_and_append_multiple_payments( + common_timestamp, logger, chain, web3_batch, @@ -300,15 +298,16 @@ pub fn send_payables_within_batch( &accounts, ); - let timestamp = SystemTime::now(); - let hashes_and_paid_amounts_error = hashes_and_paid_amounts.clone(); - let hashes_and_paid_amounts_ok = hashes_and_paid_amounts.clone(); + let sent_txs_hashes: Vec = prepared_sent_txs_records + .iter() + .map(|sent_tx| sent_tx.hash) + .collect(); + let planned_sent_txs_hashes = HashSet::from_iter(sent_txs_hashes.clone().into_iter()); - new_fingerprints_recipient - .try_send(PendingPayableFingerprintSeeds { - batch_wide_timestamp: timestamp, - hashes_and_balances: hashes_and_paid_amounts, - }) + let new_pending_payables_message = RegisterNewPendingPayables::new(prepared_sent_txs_records); + + new_pending_payables_recipient + .try_send(new_pending_payables_message) .expect("Accountant is dead"); info!( @@ -321,11 +320,14 @@ pub fn send_payables_within_batch( web3_batch .transport() .submit_batch() - .map_err(|e| error_with_hashes(e, hashes_and_paid_amounts_error)) + .map_err(move |e| PayableTransactionError::Sending { + msg: e.to_string(), + hashes: planned_sent_txs_hashes, + }) .and_then(move |batch_response| { Ok(merged_output_data( batch_response, - hashes_and_paid_amounts_ok, + sent_txs_hashes, accounts.into(), )) }), @@ -346,7 +348,7 @@ pub fn create_blockchain_agent_web3( masq_token_balance_in_minor_units, ); Box::new(BlockchainAgentWeb3::new( - blockchain_agent_future_result.gas_price_wei.as_u128(), + blockchain_agent_future_result.gas_price_minor.as_u128(), gas_limit_const_part, wallet, cons_wallet_balances, @@ -431,13 +433,8 @@ mod tests { let mut batch_result = web3_batch.eth().transport().submit_batch().wait().unwrap(); assert_eq!( result, - HashAndAmount { - hash: H256::from_str( - "94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2" - ) - .unwrap(), - amount: account.balance_wei - } + H256::from_str("94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2") + .unwrap() ); assert_eq!( batch_result.pop().unwrap().unwrap(), @@ -448,9 +445,10 @@ mod tests { } #[test] - fn send_and_append_multiple_payments_works() { + fn sign_and_append_multiple_payments_works() { + let now = SystemTime::now(); let port = find_free_port(); - let logger = Logger::new("send_and_append_multiple_payments_works"); + let logger = Logger::new("sign_and_append_multiple_payments_works"); let (_event_loop_handle, transport) = Http::with_max_parallel( &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), REQUESTS_IN_PARALLEL, @@ -463,11 +461,12 @@ mod tests { let account_1 = make_payable_account(1); let account_2 = make_payable_account(2); let accounts = make_priced_qualified_payables(vec![ - (account_1, 111_111_111), - (account_2, 222_222_222), + (account_1.clone(), 111_234_111), + (account_2.clone(), 222_432_222), ]); - let result = sign_and_append_multiple_payments( + let mut result = sign_and_append_multiple_payments( + now, &logger, chain, &web3_batch, @@ -476,25 +475,47 @@ mod tests { &accounts, ); + let first_actual_sent_tx = result.remove(0); + let second_actual_sent_tx = result.remove(0); + assert_prepared_sent_tx_record( + first_actual_sent_tx, + now, + account_1, + "0x6b85347ff8edf8b126dffb85e7517ac7af1b23eace4ed5ad099d783fd039b1ee", + 1, + 111_234_111, + ); + assert_prepared_sent_tx_record( + second_actual_sent_tx, + now, + account_2, + "0x3dac025697b994920c9cd72ab0d2df82a7caaa24d44e78b7c04e223299819d54", + 2, + 222_432_222, + ); + } + + fn assert_prepared_sent_tx_record( + actual_sent_tx: SentTx, + now: SystemTime, + account_1: PayableAccount, + expected_tx_hash_including_prefix: &str, + expected_nonce: u64, + expected_gas_price_minor: u128, + ) { + assert_eq!(actual_sent_tx.receiver_address, account_1.wallet.address()); assert_eq!( - result, - vec![ - HashAndAmount { - hash: H256::from_str( - "374b7d023f4ac7d99e612d82beda494b0747116e9b9dc975b33b865f331ee934" - ) - .unwrap(), - amount: 1000000000 - }, - HashAndAmount { - hash: H256::from_str( - "5708afd876bc2573f9db984ec6d0e7f8ef222dd9f115643c9b9056d8bef8bbd9" - ) - .unwrap(), - amount: 2000000000 - } - ] + actual_sent_tx.hash, + H256::from_str(&expected_tx_hash_including_prefix[2..]).unwrap() + ); + assert_eq!(actual_sent_tx.amount_minor, account_1.balance_wei); + assert_eq!(actual_sent_tx.gas_price_minor, expected_gas_price_minor); + assert_eq!(actual_sent_tx.nonce, expected_nonce); + assert_eq!( + actual_sent_tx.status, + TxStatus::Pending(ValidationStatus::Waiting) ); + assert_eq!(actual_sent_tx.timestamp, to_unix_timestamp(now)); } #[test] @@ -626,16 +647,7 @@ mod tests { pending_payable_opt: None, }, ]; - let fingerprint_inputs = vec![ - HashAndAmount { - hash: make_tx_hash(444), - amount: 2_345_678, - }, - HashAndAmount { - hash: make_tx_hash(333), - amount: 6_543_210, - }, - ]; + let tx_hashes = vec![make_tx_hash(444), make_tx_hash(333)]; let responses = vec![ Ok(Value::String(String::from("blah"))), Err(web3::Error::Rpc(Error { @@ -645,7 +657,7 @@ mod tests { })), ]; - let result = merged_output_data(responses, fingerprint_inputs, accounts.to_vec()); + let result = merged_output_data(responses, tx_hashes, accounts.to_vec()); assert_eq!( result, @@ -679,13 +691,13 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let pending_nonce: U256 = 1.into(); + let pending_nonce: U256 = 3.into(); let web3_batch = Web3::new(Batch::new(transport)); let (accountant, _, accountant_recording) = make_recorder(); let logger = Logger::new(test_name); let chain = DEFAULT_CHAIN; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let new_fingerprints_recipient = accountant.start().recipient(); + let new_pending_payables_recipient = accountant.start().recipient(); let system = System::new(test_name); let timestamp_before = SystemTime::now(); @@ -695,7 +707,7 @@ mod tests { &web3_batch, consuming_wallet.clone(), pending_nonce, - new_fingerprints_recipient, + new_pending_payables_recipient, accounts.clone(), ) .wait(); @@ -703,12 +715,30 @@ mod tests { System::current().stop(); system.run(); let timestamp_after = SystemTime::now(); + assert_eq!(result, expected_result); let accountant_recording_result = accountant_recording.lock().unwrap(); - let ppfs_message = - accountant_recording_result.get_record::(0); + let rnpp_message = accountant_recording_result.get_record::(0); assert_eq!(accountant_recording_result.len(), 1); - assert!(timestamp_before <= ppfs_message.batch_wide_timestamp); - assert!(timestamp_after >= ppfs_message.batch_wide_timestamp); + let nonces = 3_64..(accounts.payables.len() as u64 + 3); + rnpp_message + .new_sent_txs + .iter() + .zip(accounts.payables.iter()) + .zip(nonces) + .for_each(|((tx, payable_account), nonce)| { + assert_eq!( + tx.receiver_address, + payable_account.payable.wallet.address() + ); + assert_eq!(tx.amount_minor, payable_account.payable.balance_wei); + assert_eq!(tx.gas_price_minor, payable_account.gas_price_minor); + assert_eq!(tx.nonce, nonce); + assert_eq!(tx.status, TxStatus::Pending(ValidationStatus::Waiting)); + assert!( + timestamp_before <= from_unix_timestamp(tx.timestamp) + && from_unix_timestamp(tx.timestamp) <= timestamp_after + ); + }); let tlh = TestLogHandler::new(); tlh.exists_log_containing( &format!("DEBUG: {test_name}: Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", @@ -721,7 +751,6 @@ mod tests { "INFO: {test_name}: {}", transmission_log(chain, &accounts, pending_nonce) )); - assert_eq!(result, expected_result); } #[test] @@ -740,14 +769,14 @@ mod tests { Correct(PendingPayable { recipient_wallet: account_1.wallet.clone(), hash: H256::from_str( - "6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2", + "0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3", ) .unwrap(), }), Correct(PendingPayable { recipient_wallet: account_2.wallet.clone(), hash: H256::from_str( - "b67a61b29c0c48d8b63a64fda73b3247e8e2af68082c710325675d4911e113d4", + "6b485dbd4d769b5a19fa57058d612fad99cdd78769db6b3be129f981c42657ac", ) .unwrap(), }), @@ -775,9 +804,9 @@ mod tests { let port = find_free_port(); let expected_result = Err(Sending { msg: format!("Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", os_code, os_msg).to_string(), - hashes: vec![ - H256::from_str("ec7ac48060b75889f949f5e8d301b386198218e60e2635c95cb6b0934a0887ea").unwrap(), - H256::from_str("c2d5059db0ec2fbf15f83d9157eeb0d793d6242de5e73a607935fb5660e7e925").unwrap() + hashes: hashset![ + H256::from_str("5bbe90ad19d86b69ee49879cec4b3f8b769223e6a872aae0be88773de2fc3beb").unwrap(), + H256::from_str("a1b609dbe9cc77ad586dbe4e5c1079d6ad76020a353c960928d6daeafd43f366").unwrap() ], }); @@ -818,7 +847,7 @@ mod tests { data: None, }), recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), + hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -827,7 +856,7 @@ mod tests { data: None, }), recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), + hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), }), ]); @@ -861,7 +890,7 @@ mod tests { let expected_result = Ok(vec![ Correct(PendingPayable { recipient_wallet: account_1.wallet.clone(), - hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), + hash: H256::from_str("0f054a18b49f5c2172acab061e7f4e6f91d1586de1b010d5cb3090b93bae0da3").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -870,7 +899,7 @@ mod tests { data: None, }), recipient_wallet: account_2.wallet.clone(), - hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), + hash: H256::from_str("d2749ac321b8701d4aba3417ef23482c4792b19d534dccb2834667f5f52fd6c4").unwrap(), }), ]); @@ -885,15 +914,6 @@ mod tests { ); } - #[test] - fn advance_used_nonce_works() { - let initial_nonce = U256::from(55); - - let result = advance_used_nonce(initial_nonce); - - assert_eq!(result, U256::from(56)) - } - #[test] #[should_panic( expected = "Consuming wallet doesn't contain a secret key: Signature(\"Cannot sign with non-keypair wallet: Address(0x000000000000000000006261645f77616c6c6574).\")" diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index ffdfa4c20..1d01532ec 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -1,11 +1,13 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::comma_joined_stringifiable; -use itertools::Either; +use crate::accountant::db_access_objects::utils::TxHash; +use itertools::{Either, Itertools}; +use std::collections::HashSet; use std::fmt; use std::fmt::{Display, Formatter}; use variant_count::VariantCount; -use web3::types::{Address, H256}; +use web3::types::Address; const BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED: &str = "Uninitialized blockchain interface. To avoid \ being delinquency-banned, you should restart the Node with a value for blockchain-service-url"; @@ -39,7 +41,10 @@ pub enum PayableTransactionError { TransactionID(BlockchainInterfaceError), UnusableWallet(String), Signing(String), - Sending { msg: String, hashes: Vec }, + Sending { + msg: String, + hashes: HashSet, + }, UninitializedInterface, } @@ -61,12 +66,15 @@ impl Display for PayableTransactionError { msg ), Self::Signing(msg) => write!(f, "Signing phase: \"{}\"", msg), - Self::Sending { msg, hashes } => write!( - f, - "Sending phase: \"{}\". Signed and hashed transactions: {}", - msg, - comma_joined_stringifiable(hashes, |hash| format!("{:?}", hash)) - ), + Self::Sending { msg, hashes } => { + let hashes = hashes.iter().map(|hash| *hash).sorted().collect_vec(); + write!( + f, + "Sending phase: \"{}\". Signed and hashed txs: {}", + msg, + comma_joined_stringifiable(&hashes, |hash| format!("{:?}", hash)) + ) + } Self::UninitializedInterface => { write!(f, "{}", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED) } @@ -180,7 +188,7 @@ mod tests { ), PayableTransactionError::Sending { msg: "Sending to cosmos belongs elsewhere".to_string(), - hashes: vec![make_tx_hash(0x6f), make_tx_hash(0xde)], + hashes: hashset![make_tx_hash(0x6f), make_tx_hash(0xde)], }, PayableTransactionError::UninitializedInterface, ]; @@ -202,7 +210,7 @@ mod tests { LEDGER wallet, stupid.\"", "Signing phase: \"You cannot sign with just three crosses here, clever boy\"", "Sending phase: \"Sending to cosmos belongs elsewhere\". Signed and hashed \ - transactions: 0x000000000000000000000000000000000000000000000000000000000000006f, \ + txs: 0x000000000000000000000000000000000000000000000000000000000000006f, \ 0x00000000000000000000000000000000000000000000000000000000000000de", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED ]) diff --git a/node/src/blockchain/blockchain_interface/data_structures/mod.rs b/node/src/blockchain/blockchain_interface/data_structures/mod.rs index a33a1f889..1e8c918de 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -2,12 +2,16 @@ pub mod errors; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; +use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::PendingPayable; use crate::blockchain::blockchain_bridge::BlockMarker; use crate::sub_lib::wallet::Wallet; +use ethereum_types::U64; +use serde_derive::{Deserialize, Serialize}; use std::fmt; -use std::fmt::Formatter; -use web3::types::H256; +use std::fmt::{Display, Formatter}; +use web3::types::{TransactionReceipt, H256}; use web3::Error; #[derive(Clone, Debug, Eq, PartialEq)] @@ -37,7 +41,7 @@ pub struct RetrievedBlockchainTransactions { pub struct RpcPayableFailure { pub rpc_error: Error, pub recipient_wallet: Wallet, - pub hash: H256, + pub hash: TxHash, } #[derive(Debug, PartialEq, Clone)] @@ -45,3 +49,90 @@ pub enum ProcessedPayableFallible { Correct(PendingPayable), Failed(RpcPayableFailure), } + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RetrievedTxStatus { + pub tx_hash: TxHashByTable, + pub status: StatusReadFromReceiptCheck, +} + +impl RetrievedTxStatus { + pub fn new(tx_hash: TxHashByTable, status: StatusReadFromReceiptCheck) -> Self { + Self { tx_hash, status } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum StatusReadFromReceiptCheck { + Reverted, + Succeeded(TxBlock), + Pending, +} + +impl Display for StatusReadFromReceiptCheck { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StatusReadFromReceiptCheck::Reverted => { + write!(f, "Reverted") + } + StatusReadFromReceiptCheck::Succeeded(block) => { + write!( + f, + "Succeeded({},{:?})", + block.block_number, block.block_hash + ) + } + StatusReadFromReceiptCheck::Pending => write!(f, "Pending"), + } + } +} + +impl From for StatusReadFromReceiptCheck { + fn from(receipt: TransactionReceipt) -> Self { + match (receipt.status, receipt.block_hash, receipt.block_number) { + (Some(status), Some(block_hash), Some(block_number)) if status == U64::from(1) => { + StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }) + } + (Some(status), _, _) if status == U64::from(0) => StatusReadFromReceiptCheck::Reverted, + _ => StatusReadFromReceiptCheck::Pending, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Ord, PartialOrd, Serialize, Deserialize)] +pub struct TxBlock { + pub block_hash: H256, + pub block_number: U64, +} + +#[cfg(test)] +mod tests { + use crate::blockchain::blockchain_interface::data_structures::{ + StatusReadFromReceiptCheck, TxBlock, + }; + use ethereum_types::{H256, U64}; + + #[test] + fn tx_status_display_works() { + // Test Failed + assert_eq!(StatusReadFromReceiptCheck::Reverted.to_string(), "Reverted"); + + // Test Pending + assert_eq!(StatusReadFromReceiptCheck::Pending.to_string(), "Pending"); + + // Test Succeeded + let block_number = U64::from(12345); + let block_hash = H256::from_low_u64_be(0xabcdef); + let succeeded = StatusReadFromReceiptCheck::Succeeded(TxBlock { + block_hash, + block_number, + }); + assert_eq!( + succeeded.to_string(), + format!("Succeeded({},0x{:x})", block_number, block_hash) + ); + } +} diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index eb736b2a3..09961776e 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -4,20 +4,27 @@ pub mod blockchain_interface_web3; pub mod data_structures; pub mod lower_level_interface; -use actix::Recipient; -use ethereum_types::H256; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; +use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; +use crate::accountant::TxReceiptResult; +use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_bridge::{ + BlockMarker, BlockScanRange, RegisterNewPendingPayables, +}; +use crate::blockchain::blockchain_interface::data_structures::errors::{ + BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError, +}; +use crate::blockchain::blockchain_interface::data_structures::{ + ProcessedPayableFallible, RetrievedBlockchainTransactions, +}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::sub_lib::wallet::Wallet; +use actix::Recipient; use futures::Future; use masq_lib::blockchains::chains::Chain; -use web3::types::Address; use masq_lib::logger::Logger; -use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; +use std::collections::HashMap; +use web3::types::Address; pub trait BlockchainInterface { fn contract_address(&self) -> Address; @@ -40,14 +47,19 @@ pub trait BlockchainInterface { fn process_transaction_receipts( &self, - transaction_hashes: Vec, - ) -> Box, Error = BlockchainInterfaceError>>; + tx_hashes: Vec, + ) -> Box< + dyn Future< + Item = HashMap, + Error = BlockchainInterfaceError, + >, + >; fn submit_payables_in_batch( &self, logger: Logger, agent: Box, - fingerprints_recipient: Recipient, + new_pending_payables_recipient: Recipient, affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>>; diff --git a/node/src/blockchain/errors/internal_errors.rs b/node/src/blockchain/errors/internal_errors.rs index fb6a4bf63..9982d0667 100644 --- a/node/src/blockchain/errors/internal_errors.rs +++ b/node/src/blockchain/errors/internal_errors.rs @@ -7,7 +7,7 @@ pub enum InternalError { PendingTooLongNotReplaced, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum InternalErrorKind { PendingTooLongNotReplaced, } diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index 5cd1a6f3c..e406a96b1 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -14,7 +14,7 @@ pub enum BlockchainError { Internal(InternalError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum BlockchainErrorKind { AppRpc(AppRpcErrorKind), Internal(InternalErrorKind), diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index e717fbf25..bf78fa53b 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -53,13 +53,13 @@ impl From for AppRpcError { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AppRpcErrorKind { Local(LocalErrorKind), Remote(RemoteErrorKind), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum LocalErrorKind { Decoder, Internal, @@ -68,7 +68,7 @@ pub enum LocalErrorKind { Transport, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum RemoteErrorKind { InvalidResponse, Unreachable, diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index f3b354931..2ce57f261 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -229,6 +229,55 @@ pub fn transport_error_message() -> String { } } +pub struct TransactionReceiptBuilder { + status_opt: Option, + block_hash_opt: Option, + block_number_opt: Option, + transaction_hash: H256, +} + +impl TransactionReceiptBuilder { + pub fn new(transaction_hash: H256) -> Self { + Self { + status_opt: None, + block_hash_opt: None, + block_number_opt: None, + transaction_hash, + } + } + + pub fn status(mut self, status: U64) -> Self { + self.status_opt = Some(status); + self + } + + pub fn block_hash(mut self, block_hash: H256) -> Self { + self.block_hash_opt = Some(block_hash); + self + } + + pub fn block_number(mut self, block_number: U64) -> Self { + self.block_number_opt = Some(block_number); + self + } + + pub fn build(self) -> TransactionReceipt { + TransactionReceipt { + status: self.status_opt, + root: None, + block_hash: self.block_hash_opt, + block_number: self.block_number_opt, + cumulative_gas_used: Default::default(), + gas_used: None, + contract_address: None, + transaction_hash: self.transaction_hash, + transaction_index: Default::default(), + logs: vec![], + logs_bloom: Default::default(), + } + } +} + #[derive(Default)] pub struct ValidationFailureClockMock { now_results: RefCell>, diff --git a/node/src/database/db_migrations/migrations/migration_4_to_5.rs b/node/src/database/db_migrations/migrations/migration_4_to_5.rs index 4b5bbb50a..204ab49a5 100644 --- a/node/src/database/db_migrations/migrations/migration_4_to_5.rs +++ b/node/src/database/db_migrations/migrations/migration_4_to_5.rs @@ -150,7 +150,7 @@ mod tests { conn: &dyn ConnectionWrapper, transaction_hash_opt: Option, wallet: &Wallet, - amount: i64, + amount_minor: i64, timestamp: SystemTime, ) { let hash_str = transaction_hash_opt @@ -159,7 +159,7 @@ mod tests { let mut stm = conn.prepare("insert into payable (wallet_address, balance, last_paid_timestamp, pending_payment_transaction) values (?,?,?,?)").unwrap(); let params: &[&dyn ToSql] = &[ &wallet, - &amount, + &amount_minor, &to_unix_timestamp(timestamp), if !hash_str.is_empty() { &hash_str diff --git a/node/src/database/rusqlite_wrappers.rs b/node/src/database/rusqlite_wrappers.rs index ec867482f..2177a250b 100644 --- a/node/src/database/rusqlite_wrappers.rs +++ b/node/src/database/rusqlite_wrappers.rs @@ -5,15 +5,15 @@ use crate::masq_lib::utils::ExpectValue; use rusqlite::{Connection, Error, Statement, ToSql, Transaction}; use std::fmt::Debug; -// We were challenged multiple times to device mocks for testing stubborn, hard to tame, data +// We were challenged multiple times to devise mocks for testing stubborn, hard to tame, data // structures from the 'rusqlite' library. After all, we've adopted two of them, the Connection, // that came first, and the Transaction to come much later. Of these, only the former complies // with the standard policy we follow for mock designs. // // The delay until the second one became a thing, even though we would've been glad having it -// on hand much earlier, was caused by vacuum of ideas on how we could create a mock of these +// on hand much earlier, was caused by a vacuum of ideas on how we could create a mock of these // parameters and have it accepted by the compiler. Passing a lot of time, we came up with a hybrid, -// at least. That said, it has costed us a considerably high price of giving up on simplicity. +// at least. That said, it has cost us a considerably high price of giving up on simplicity. // // The firmest blocker of the design has always rooted in a relationship of serialized lifetimes, // affecting each other, that has been so hard to maintain right. Yet the choices made @@ -74,12 +74,12 @@ impl ConnectionWrapperReal { } } -// Whole point of this outer wrapper, that is common to both the real and mock transactions, is to +// The whole point of this outer wrapper that is common to both the real and mock transactions is to // make a chance to deconstruct all components of a transaction in place. It plays a crucial role -// during the final commit. Note that an usual mock based on the direct use of a trait object +// during the final commit. Note that a usual mock based on the direct use of a trait object // cannot be consumed by any of its methods because of the Rust rules for trait objects. They say // clearly that we can access it via '&self', '&mut self' but not 'self'. However, to have a thing -// consume itself we need to be provided with the full ownership. +// consume itself, we need to be provided with the full ownership. // // Leaving remains of an already committed transaction around would expose us to a risk. Let's // imagine somebody trying to make use of it the second time, while the inner element providing diff --git a/node/src/database/test_utils/transaction_wrapper_mock.rs b/node/src/database/test_utils/transaction_wrapper_mock.rs index d0577c72f..5b9a717e9 100644 --- a/node/src/database/test_utils/transaction_wrapper_mock.rs +++ b/node/src/database/test_utils/transaction_wrapper_mock.rs @@ -137,7 +137,7 @@ impl TransactionInnerWrapper for TransactionInnerWrapperMock { // is to be formed. // With that said, we're relieved to have at least one working solution now. Speaking of the 'prepare' -// method, an error would be hardly needed because the production code simply unwraps the results by +// method, an error would hardly be needed because the production code simply unwraps the results by // using 'expect'. That is a function excluded from the requirement of writing tests for. // The 'Statement' produced by this method must be better understood. The 'prepare' method has @@ -199,12 +199,12 @@ impl SetupForProdCodeAndAlteredStmts { // necessary base. If the continuity is broken the later statement might not work. If // we record some changes on the transaction, other changes tried to be done from // a different connection might meet a different state of the database and thwart the - // efforts. (This behaviour probably depends on the global setup of the db). + // efforts. (This behavior probably depends on the global setup of the db). // // // Also imagine a 'Statement' that wouldn't cause an error whereupon any potential // rollback of this txn should best drag off both the prod code and altered statements - // all together, disappearing. If we did not use this txn some of the changes would stay. + // all together, disappearing. If we did not use this txn some changes would stay. { self.txn_bearing_prod_code_stmts_opt .as_ref() diff --git a/node/src/stream_handler_pool.rs b/node/src/stream_handler_pool.rs index 470f0c44f..5772e9cf8 100644 --- a/node/src/stream_handler_pool.rs +++ b/node/src/stream_handler_pool.rs @@ -1760,7 +1760,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); @@ -1927,7 +1927,7 @@ mod tests { }) .unwrap(); - tx.send(subject_subs).expect("Tx failure"); + tx.send(subject_subs).expect("SentTx failure"); system.run(); }); diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index f1f174e6e..e8d3477e6 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -1,15 +1,15 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; +use crate::accountant::db_access_objects::failed_payable_dao::FailedPayableDaoFactory; use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoFactory; +use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoFactory; use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::{ - checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, - SentPayables, + checked_conversion, Accountant, ReceivedPayments, ScanError, SentPayables, TxReceiptsMessage, }; use crate::actor_system_factory::SubsFactory; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::db_config::config_dao::ConfigDaoFactory; use crate::sub_lib::neighborhood::ConfigChangeMsg; use crate::sub_lib::peer_actors::{BindMessage, StartMessage}; @@ -71,7 +71,8 @@ impl PaymentThresholds { pub struct DaoFactories { pub payable_dao_factory: Box, - pub pending_payable_dao_factory: Box, + pub sent_payable_dao_factory: Box, + pub failed_payable_dao_factory: Box, pub receivable_dao_factory: Box, pub banned_dao_factory: Box, pub config_dao_factory: Box, @@ -100,8 +101,8 @@ pub struct AccountantSubs { pub report_services_consumed: Recipient, pub report_payable_payments_setup: Recipient, pub report_inbound_payments: Recipient, - pub init_pending_payable_fingerprints: Recipient, - pub report_transaction_receipts: Recipient, + pub register_new_pending_payables: Recipient, + pub report_transaction_status: Recipient, pub report_sent_payments: Recipient, pub scan_errors: Recipient, pub ui_message_sub: Recipient, diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 588eb87e6..546149ae6 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -570,6 +570,18 @@ pub mod unshared_test_utils { pub assertions: Box, } + pub fn capture_digits_with_separators_from_str( + surveyed_str: &str, + length_between_separators: usize, + separator: char, + ) -> Vec { + let regex = + format!("(\\d{{1,{length_between_separators}}}(?:{separator}\\d{{{length_between_separators}}})+)"); + let re = regex::Regex::new(®ex).unwrap(); + let captures = re.captures_iter(surveyed_str); + captures.map(|capture| capture[1].to_string()).collect() + } + pub fn assert_on_initialization_with_panic_on_migration(data_dir: &Path, act: &A) where A: Fn(&Path) + ?Sized, @@ -934,8 +946,7 @@ pub mod unshared_test_utils { ) -> Box { if self.panic_on_schedule_attempt { panic!( - "Message scheduling request for {:?} and interval {}ms, thought not \ - expected", + "Message scheduling request for {:?} and interval {}ms, thought not expected", msg, interval.as_millis() ); diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 6633ee948..ed35378c2 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -7,8 +7,8 @@ use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::accountant::{ReportTransactionReceipts, ScanForPendingPayables, ScanForRetryPayables}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; +use crate::accountant::{ScanForPendingPayables, ScanForRetryPayables, TxReceiptsMessage}; +use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::daemon::crash_notification::CrashNotification; use crate::daemon::DaemonBindMessage; @@ -153,7 +153,7 @@ recorder_message_handler_t_m_p!(NodeFromUiMessage); recorder_message_handler_t_m_p!(NodeToUiMessage); recorder_message_handler_t_m_p!(NoLookupIncipientCoresPackage); recorder_message_handler_t_p!(OutboundPaymentsInstructions); -recorder_message_handler_t_m_p!(PendingPayableFingerprintSeeds); +recorder_message_handler_t_m_p!(RegisterNewPendingPayables); recorder_message_handler_t_m_p!(PoolBindMessage); recorder_message_handler_t_m_p!(QualifiedPayablesMessage); recorder_message_handler_t_m_p!(ReceivedPayments); @@ -162,7 +162,7 @@ recorder_message_handler_t_m_p!(RemoveStreamMsg); recorder_message_handler_t_m_p!(ReportExitServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportRoutingServiceProvidedMessage); recorder_message_handler_t_m_p!(ReportServicesConsumedMessage); -recorder_message_handler_t_m_p!(ReportTransactionReceipts); +recorder_message_handler_t_m_p!(TxReceiptsMessage); recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); @@ -529,8 +529,8 @@ pub fn make_accountant_subs_from_recorder(addr: &Addr) -> AccountantSu report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), report_payable_payments_setup: recipient!(addr, BlockchainAgentWithContextMessage), report_inbound_payments: recipient!(addr, ReceivedPayments), - init_pending_payable_fingerprints: recipient!(addr, PendingPayableFingerprintSeeds), - report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), + register_new_pending_payables: recipient!(addr, RegisterNewPendingPayables), + report_transaction_status: recipient!(addr, TxReceiptsMessage), report_sent_payments: recipient!(addr, SentPayables), scan_errors: recipient!(addr, ScanError), ui_message_sub: recipient!(addr, NodeFromUiMessage), From 6e020c7235bb16599cb1aac3847dc39b2adb97b1 Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:10:03 +0200 Subject: [PATCH 03/48] GH-689: Amend scanner scheduling: Handling ScanError msg (#691) * GH-689: finished * GH-689: fixes after auto-review --------- Co-authored-by: Bert --- node/src/accountant/mod.rs | 357 +++++++++++++----- node/src/accountant/scanners/mod.rs | 37 +- .../scanners/pending_payable_scanner/mod.rs | 10 +- .../tx_receipt_interpreter.rs | 4 +- node/src/blockchain/blockchain_bridge.rs | 77 ++-- node/src/sub_lib/accountant.rs | 27 +- .../test_utils/recorder_stop_conditions.rs | 8 +- 7 files changed, 363 insertions(+), 157 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f54a7dbd6..b8651d990 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -43,12 +43,12 @@ use crate::blockchain::blockchain_interface::data_structures::{ use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::bootstrapper::BootstrapperConfig; use crate::database::db_initializer::DbInitializationConfig; -use crate::sub_lib::accountant::AccountantSubs; use crate::sub_lib::accountant::DaoFactories; use crate::sub_lib::accountant::FinancialStatistics; use crate::sub_lib::accountant::ReportExitServiceProvidedMessage; use crate::sub_lib::accountant::ReportRoutingServiceProvidedMessage; use crate::sub_lib::accountant::ReportServicesConsumedMessage; +use crate::sub_lib::accountant::{AccountantSubs, DetailedScanType}; use crate::sub_lib::accountant::{MessageIdGenerator, MessageIdGeneratorReal}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::neighborhood::{ConfigChange, ConfigChangeMsg}; @@ -177,7 +177,7 @@ pub struct ScanForReceivables { #[derive(Debug, Clone, Message, PartialEq, Eq)] pub struct ScanError { - pub scan_type: ScanType, + pub scan_type: DetailedScanType, pub response_skeleton_opt: Option, pub msg: String, } @@ -417,31 +417,51 @@ impl Handler for Accountant { impl Handler for Accountant { type Result = (); - fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, scan_error: ScanError, ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); self.scanners .acknowledge_scan_error(&scan_error, &self.logger); - if let Some(response_skeleton) = scan_error.response_skeleton_opt { - let error_msg = NodeToUiMessage { - target: ClientId(response_skeleton.client_id), - body: MessageBody { - opcode: "scan".to_string(), - path: MessagePath::Conversation(response_skeleton.context_id), - payload: Err(( - SCAN_ERROR, - format!( - "{:?} scan failed: '{}'", - scan_error.scan_type, scan_error.msg - ), - )), - }, - }; - error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway not bound") - .try_send(error_msg) - .expect("UiGateway is dead"); + + match scan_error.response_skeleton_opt { + None => match scan_error.scan_type { + DetailedScanType::NewPayables => self + .scan_schedulers + .payable + .schedule_new_payable_scan(ctx, &self.logger), + DetailedScanType::RetryPayables => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, None, &self.logger), + DetailedScanType::PendingPayables => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + DetailedScanType::Receivables => { + self.scan_schedulers.receivable.schedule(ctx, &self.logger) + } + }, + Some(response_skeleton) => { + let error_msg = NodeToUiMessage { + target: ClientId(response_skeleton.client_id), + body: MessageBody { + opcode: "scan".to_string(), + path: MessagePath::Conversation(response_skeleton.context_id), + payload: Err(( + SCAN_ERROR, + format!( + "{:?} scan failed: '{}'", + scan_error.scan_type, scan_error.msg + ), + )), + }, + }; + error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway not bound") + .try_send(error_msg) + .expect("UiGateway is dead"); + } } } } @@ -5563,29 +5583,205 @@ mod tests { const EXAMPLE_ERROR_MSG: &str = "My tummy hurts"; + fn do_setup_and_prepare_assertions_for_new_payables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = SystemTime::now(); + scan_schedulers.payable.dyn_interval_computer = Box::new( + NewPayableScanDynIntervalComputerMock::default() + .compute_interval_result(Some(Duration::from_secs(152))), + ); + scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_later_params = notify_later_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => assert_eq!( + *notify_later_params, + vec![(ScanForNewPayables::default(), Duration::from_secs(152))] + ), + Some(_) => { + assert!( + notify_later_params.is_empty(), + "Should be empty but contained {:?}", + notify_later_params + ) + } + } + }) + }, + ) + } + + fn do_setup_and_prepare_assertions_for_retry_payables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(¬ify_params_arc)); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_params = notify_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => { + // Response skeleton must be None + assert_eq!( + *notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt: None + }] + ) + } + Some(_) => { + assert!( + notify_params.is_empty(), + "Should be empty but contained {:?}", + notify_params + ) + } + } + }) + }, + ) + } + + fn do_setup_and_prepare_assertions_for_pending_payables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); + let ensure_empty_cache_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers.pending_payable.interval = Duration::from_secs(600); + scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + let sent_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_sent_tx_params_arc); + let failed_payable_cache = PendingPayableCacheMock::default() + .ensure_empty_cache_params(&ensure_empty_cache_failed_tx_params_arc); + let scanner = PendingPayableScannerBuilder::new() + .sent_payable_cache(Box::new(sent_payable_cache)) + .failed_payable_cache(Box::new(failed_payable_cache)) + .build(); + scanners.replace_scanner(ScannerReplacement::PendingPayable( + ReplacementType::Real(scanner), + )); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_later_params = notify_later_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => { + assert_eq!( + *notify_later_params, + vec![(ScanForPendingPayables::default(), Duration::from_secs(600))] + ) + } + Some(_) => { + assert!( + notify_later_params.is_empty(), + "Should be empty but contained {:?}", + notify_later_params + ) + } + } + let ensure_empty_cache_sent_tx_params = + ensure_empty_cache_sent_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_sent_tx_params, vec![()]); + let ensure_empty_cache_failed_tx_params = + ensure_empty_cache_failed_tx_params_arc.lock().unwrap(); + assert_eq!(*ensure_empty_cache_failed_tx_params, vec![()]); + }) + }, + ) + } + + fn do_setup_and_prepare_assertions_for_receivables( + ) -> Box RunSchedulersAssertions> { + Box::new( + |_scanners: &mut Scanners, scan_schedulers: &mut ScanSchedulers| { + // Setup + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + scan_schedulers.receivable.interval = Duration::from_secs(600); + scan_schedulers.receivable.handle = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + + // Assertions + Box::new(move |response_skeleton_opt| { + let notify_later_params = notify_later_params_arc.lock().unwrap(); + match response_skeleton_opt { + None => { + assert_eq!( + *notify_later_params, + vec![(ScanForReceivables::default(), Duration::from_secs(600))] + ) + } + Some(_) => { + assert!( + notify_later_params.is_empty(), + "Should be empty but contained {:?}", + notify_later_params + ) + } + } + }) + }, + ) + } + #[test] - fn handling_scan_error_for_externally_triggered_payables() { + fn handling_scan_error_for_externally_triggered_new_payables() { test_scan_error_is_handled_properly( - "handling_scan_error_for_externally_triggered_payables", + "handling_scan_error_for_externally_triggered_new_payables", ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_new_payables(), ); } + #[test] + fn handling_scan_error_for_externally_triggered_retry_payables() { + test_scan_error_is_handled_properly( + "handling_scan_error_for_externally_triggered_retry_payables", + ScanError { + scan_type: DetailedScanType::RetryPayables, + response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), + msg: EXAMPLE_ERROR_MSG.to_string(), + }, + do_setup_and_prepare_assertions_for_retry_payables(), + ) + } + #[test] fn handling_scan_error_for_externally_triggered_pending_payables() { - let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); - test_scan_error_is_handled_properly_more_specifically( + test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_pending_payables", ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, - Some(additional_test_setup_and_assertions), + do_setup_and_prepare_assertions_for_pending_payables(), ); } @@ -5594,36 +5790,50 @@ mod tests { test_scan_error_is_handled_properly( "handling_scan_error_for_externally_triggered_receivables", ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: Some(EXAMPLE_RESPONSE_SKELETON), msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_receivables(), ); } #[test] - fn handling_scan_error_for_internally_triggered_payables() { + fn handling_scan_error_for_internally_triggered_new_payables() { test_scan_error_is_handled_properly( - "handling_scan_error_for_internally_triggered_payables", + "handling_scan_error_for_internally_triggered_new_payables", ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_new_payables(), + ); + } + + #[test] + fn handling_scan_error_for_internally_triggered_retry_payables() { + test_scan_error_is_handled_properly( + "handling_scan_error_for_internally_triggered_retry_payables", + ScanError { + scan_type: DetailedScanType::RetryPayables, + response_skeleton_opt: None, + msg: EXAMPLE_ERROR_MSG.to_string(), + }, + do_setup_and_prepare_assertions_for_retry_payables(), ); } #[test] fn handling_scan_error_for_internally_triggered_pending_payables() { - let additional_test_setup_and_assertions = prepare_setup_and_assertion_fns(); - test_scan_error_is_handled_properly_more_specifically( + test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_pending_payables", ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, - Some(additional_test_setup_and_assertions), + do_setup_and_prepare_assertions_for_pending_payables(), ); } @@ -5632,41 +5842,14 @@ mod tests { test_scan_error_is_handled_properly( "handling_scan_error_for_internally_triggered_receivables", ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: None, msg: EXAMPLE_ERROR_MSG.to_string(), }, + do_setup_and_prepare_assertions_for_receivables(), ); } - fn prepare_setup_and_assertion_fns() -> (Box, Box) { - let ensure_empty_cache_sent_tx_params_arc = Arc::new(Mutex::new(vec![])); - let ensure_empty_cache_failed_tx_params_arc = Arc::new(Mutex::new(vec![])); - let sent_payable_cache = PendingPayableCacheMock::default() - .ensure_empty_cache_params(&ensure_empty_cache_sent_tx_params_arc); - let failed_payable_cache = PendingPayableCacheMock::default() - .ensure_empty_cache_params(&ensure_empty_cache_failed_tx_params_arc); - let scanner = PendingPayableScannerBuilder::new() - .sent_payable_cache(Box::new(sent_payable_cache)) - .failed_payable_cache(Box::new(failed_payable_cache)) - .build(); - ( - Box::new(|scanners: &mut Scanners| { - scanners.replace_scanner(ScannerReplacement::PendingPayable( - ReplacementType::Real(scanner), - )); - }) as Box, - Box::new(move || { - let ensure_empty_cache_sent_tx_params = - ensure_empty_cache_sent_tx_params_arc.lock().unwrap(); - assert_eq!(*ensure_empty_cache_sent_tx_params, vec![()]); - let ensure_empty_cache_failed_tx_params = - ensure_empty_cache_failed_tx_params_arc.lock().unwrap(); - assert_eq!(*ensure_empty_cache_failed_tx_params, vec![()]); - }) as Box, - ) - } - #[test] fn financials_request_with_nothing_to_respond_to_is_refused() { let system = System::new("test"); @@ -6412,32 +6595,29 @@ mod tests { let _: u64 = wei_to_gwei(u128::MAX); } - fn test_scan_error_is_handled_properly(test_name: &str, message: ScanError) { - test_scan_error_is_handled_properly_more_specifically(test_name, message, None) - } - fn test_scan_error_is_handled_properly_more_specifically( + type RunSchedulersAssertions = Box)>; + + fn test_scan_error_is_handled_properly( test_name: &str, message: ScanError, - additional_assertion_opt: Option<(Box, Box)>, + set_up_schedulers_and_prepare_assertions: Box< + dyn FnOnce(&mut Scanners, &mut ScanSchedulers) -> RunSchedulersAssertions, + >, ) { init_test_logging(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("blah")) .logger(Logger::new(test_name)) .build(); - let (adjust_scanner, run_additional_assertion) = match additional_assertion_opt { - Some(two_functions) => two_functions, - None => ( - Box::new(|scanners: &mut Scanners| { - scanners.reset_scan_started( - message.scan_type, - MarkScanner::Started(SystemTime::now()), - ) - }) as Box, - Box::new(|| ()) as Box, - ), - }; - adjust_scanner(&mut subject.scanners); + subject.scanners.reset_scan_started( + message.scan_type.into(), + MarkScanner::Started(SystemTime::now()), + ); + let run_schedulers_assertions = set_up_schedulers_and_prepare_assertions( + &mut subject.scanners, + &mut subject.scan_schedulers, + ); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -6448,13 +6628,15 @@ mod tests { subject_addr .try_send(AssertionsMessage { assertions: Box::new(move |actor: &mut Accountant| { - let scan_started_at_opt = actor.scanners.scan_started_at(message.scan_type); + let scan_started_at_opt = + actor.scanners.scan_started_at(message.scan_type.into()); assert_eq!(scan_started_at_opt, None); }), }) .unwrap(); System::current().stop(); assert_eq!(system.run(), 0); + run_schedulers_assertions(message.response_skeleton_opt); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); match message.response_skeleton_opt { Some(response_skeleton) => { @@ -6494,7 +6676,6 @@ mod tests { )); } } - run_additional_assertion(); } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 71ff0f62c..1d69ab3c9 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -12,7 +12,10 @@ use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_sent_tx_record, investigate_debt_extremes, payables_debug_summary, separate_errors, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMissingInDb}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_sent_tx_record, + investigate_debt_extremes, payables_debug_summary, separate_errors, OperationOutcome, PayableScanResult, + PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMissingInDb}; use crate::accountant::{PendingPayable, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, @@ -20,9 +23,7 @@ use crate::accountant::{ ScanForReceivables, SentPayables, }; use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; -use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, -}; +use crate::sub_lib::accountant::{DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; use actix::{Message}; @@ -271,14 +272,14 @@ impl Scanners { pub fn acknowledge_scan_error(&mut self, error: &ScanError, logger: &Logger) { match error.scan_type { - ScanType::Payables => { - self.payable.mark_as_ended(logger); + DetailedScanType::NewPayables | DetailedScanType::RetryPayables => { + self.payable.mark_as_ended(logger) } - ScanType::PendingPayables => { + DetailedScanType::PendingPayables => { self.empty_caches(logger); self.pending_payable.mark_as_ended(logger); } - ScanType::Receivables => { + DetailedScanType::Receivables => { self.receivable.mark_as_ended(logger); } }; @@ -1072,7 +1073,8 @@ mod tests { use crate::db_config::mocks::ConfigDaoMock; use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, + DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, + DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; @@ -3199,7 +3201,7 @@ mod tests { #[test] fn acknowledge_scan_error_works() { - fn scan_error(scan_type: ScanType) -> ScanError { + fn scan_error(scan_type: DetailedScanType) -> ScanError { ScanError { scan_type, response_skeleton_opt: None, @@ -3210,22 +3212,27 @@ mod tests { init_test_logging(); let test_name = "acknowledge_scan_error_works"; let inputs: Vec<( - ScanType, + DetailedScanType, Box, Box Option>, )> = vec![ ( - ScanType::Payables, + DetailedScanType::NewPayables, + Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.payable.scan_started_at()), + ), + ( + DetailedScanType::RetryPayables, Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.payable.scan_started_at()), ), ( - ScanType::PendingPayables, + DetailedScanType::PendingPayables, Box::new(|subject| subject.pending_payable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.pending_payable.scan_started_at()), ), ( - ScanType::Receivables, + DetailedScanType::Receivables, Box::new(|subject| subject.receivable.mark_as_started(SystemTime::now())), Box::new(|subject| subject.receivable.scan_started_at()), ), @@ -3258,7 +3265,7 @@ mod tests { ); test_log_handler.exists_log_containing(&format!( "INFO: {test_name}: The {:?} scan ended in", - scan_type + ScanType::from(scan_type) )); }) } diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index f501a7be2..70c043909 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -865,7 +865,7 @@ mod tests { let pending_payable_cache_before = subject.current_sent_payables.dump_cache(); let failed_payable_cache_before = subject.yet_unproven_failed_payables.dump_cache(); - let result = subject.start_scan(&make_wallet("bluh"), SystemTime::now(), None, &logger); + let result = subject.start_scan(&make_wallet("blah"), SystemTime::now(), None, &logger); assert_eq!( result, @@ -1344,7 +1344,7 @@ mod tests { #[should_panic( expected = "Unable to update pending-tx statuses for validation failures '[FailedValidation \ { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ - AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"bluh\")" + AppRpc(Local(Internal)), current_status: Pending(Waiting) }]' due to: InvalidInput(\"blah\")" )] fn update_validation_status_for_sent_txs_panics_on_update_statuses() { let failed_validation = FailedValidation::new( @@ -1353,7 +1353,7 @@ mod tests { TxStatus::Pending(ValidationStatus::Waiting), ); let sent_payable_dao = SentPayableDaoMock::default() - .update_statuses_result(Err(SentPayableDaoError::InvalidInput("bluh".to_string()))); + .update_statuses_result(Err(SentPayableDaoError::InvalidInput("blah".to_string()))); let subject = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) @@ -1367,7 +1367,7 @@ mod tests { #[should_panic( expected = "Unable to update failed-tx statuses for validation failures '[FailedValidation \ { tx_hash: 0x00000000000000000000000000000000000000000000000000000000000001c8, validation_failure: \ - AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"bluh\")" + AppRpc(Local(Internal)), current_status: RecheckRequired(Waiting) }]' due to: InvalidInput(\"blah\")" )] fn update_validation_status_for_failed_txs_panics_on_update_statuses() { let failed_validation = FailedValidation::new( @@ -1376,7 +1376,7 @@ mod tests { FailureStatus::RecheckRequired(ValidationStatus::Waiting), ); let failed_payable_dao = FailedPayableDaoMock::default() - .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("bluh".to_string()))); + .update_statuses_result(Err(FailedPayableDaoError::InvalidInput("blah".to_string()))); let subject = PendingPayableScannerBuilder::new() .failed_payable_dao(failed_payable_dao) .validation_failure_clock(Box::new(ValidationFailureClockReal::default())) diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs index 4bf96bf1e..e01425d69 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -607,7 +607,7 @@ mod tests { let mut sent_tx = make_sent_tx(456); sent_tx.hash = tx_hash; sent_tx.status = current_tx_status.clone(); - let rpc_error = AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())); + let rpc_error = AppRpcError::Remote(RemoteError::InvalidResponse("blah".to_string())); let scan_report = ReceiptScanReport::default(); let result = TxReceiptInterpreter::handle_rpc_failure( @@ -635,7 +635,7 @@ mod tests { ); TestLogHandler::new().exists_log_containing( &format!("WARN: {test_name}: Failed to retrieve tx receipt for SentPayable(0x0000000000\ - 000000000000000000000000000000000000000000000000000391): Remote(InvalidResponse(\"bluh\")). \ + 000000000000000000000000000000000000000000000000000391): Remote(InvalidResponse(\"blah\")). \ Will retry receipt retrieval next cycle")); } diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index d1d41337b..1a5cad399 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -23,6 +23,7 @@ use crate::db_config::config_dao::ConfigDaoReal; use crate::db_config::persistent_configuration::{ PersistentConfiguration, PersistentConfigurationReal, }; +use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, OutboundPaymentsInstructions}; use crate::sub_lib::peer_actors::BindMessage; use crate::sub_lib::utils::{db_connection_launch_panic, handle_ui_crash_request}; @@ -123,7 +124,7 @@ impl Handler for BlockchainBridge { ) -> >::Result { self.handle_scan_future( Self::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg, ) } @@ -135,7 +136,7 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: RequestTransactionReceipts, _ctx: &mut Self::Context) { self.handle_scan_future( Self::handle_request_transaction_receipts, - ScanType::PendingPayables, + DetailedScanType::PendingPayables, msg, ) } @@ -145,7 +146,13 @@ impl Handler for BlockchainBridge { type Result = (); fn handle(&mut self, msg: QualifiedPayablesMessage, _ctx: &mut Self::Context) { - self.handle_scan_future(Self::handle_qualified_payable_msg, ScanType::Payables, msg); + self.handle_scan_future( + Self::handle_qualified_payable_msg, + todo!( + "This needs to be decided on GH-605. Look what mode you run and set it accordingly" + ), + msg, + ); } } @@ -155,7 +162,9 @@ impl Handler for BlockchainBridge { fn handle(&mut self, msg: OutboundPaymentsInstructions, _ctx: &mut Self::Context) { self.handle_scan_future( Self::handle_outbound_payments_instructions, - ScanType::Payables, + todo!( + "This needs to be decided on GH-605. Look what mode you run and set it accordingly" + ), msg, ) } @@ -440,7 +449,7 @@ impl BlockchainBridge { ) } - fn handle_scan_future(&mut self, handler: F, scan_type: ScanType, msg: M) + fn handle_scan_future(&mut self, handler: F, scan_type: DetailedScanType, msg: M) where F: FnOnce(&mut BlockchainBridge, M) -> Box>, M: SkeletonOptHolder, @@ -562,15 +571,10 @@ mod tests { use crate::node_test_utils::check_timestamp; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; - use crate::test_utils::recorder::{ - make_accountant_subs_from_recorder, make_recorder, peer_actors_builder, - }; + use crate::test_utils::recorder::{make_accountant_subs_from_recorder, make_blockchain_bridge_subs_from_recorder, make_recorder, peer_actors_builder}; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use crate::test_utils::unshared_test_utils::{ - assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, - prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, ZERO, - }; + use crate::test_utils::unshared_test_utils::{assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, SubsFactoryTestAddrLeaker, ZERO}; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; @@ -601,6 +605,17 @@ mod tests { } } + impl SubsFactory + for SubsFactoryTestAddrLeaker + { + fn make(&self, addr: &Addr) -> BlockchainBridgeSubs { + self.send_leaker_msg_and_return_meaningless_subs( + addr, + make_blockchain_bridge_subs_from_recorder, + ) + } + } + #[test] fn constants_have_correct_values() { assert_eq!(CRASH_KEY, "BLOCKCHAINBRIDGE"); @@ -1016,7 +1031,7 @@ mod tests { assert_eq!( *scan_error_msg, ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 @@ -1267,7 +1282,7 @@ mod tests { assert_eq!( scan_error, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: None, msg: "Error while retrieving transactions: QueryFailed(\"Transport error: Error(IncompleteMessage)\")".to_string() } @@ -1403,7 +1418,7 @@ mod tests { let _ = subject.handle_scan_future( BlockchainBridge::handle_request_transaction_receipts, - ScanType::PendingPayables, + DetailedScanType::PendingPayables, msg, ); @@ -1412,7 +1427,7 @@ mod tests { assert_eq!( recording.get_record::(0), &ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: None, msg: "Blockchain error: Query failed: Transport error: Error(IncompleteMessage)" .to_string() @@ -1781,7 +1796,7 @@ mod tests { assert_eq!( scan_error_msg, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 @@ -1841,7 +1856,7 @@ mod tests { assert_eq!( scan_error_msg, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321 @@ -1987,7 +2002,7 @@ mod tests { subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, retrieve_transactions, ); @@ -2041,7 +2056,7 @@ mod tests { subject.handle_scan_future( BlockchainBridge::handle_retrieve_transactions, - ScanType::Receivables, + DetailedScanType::Receivables, msg.clone(), ); @@ -2051,7 +2066,7 @@ mod tests { assert_eq!( message, &ScanError { - scan_type: ScanType::Receivables, + scan_type: DetailedScanType::Receivables, response_skeleton_opt: msg.response_skeleton_opt, msg: "Error while retrieving transactions: QueryFailed(\"RPC error: Error { code: ServerError(-32005), message: \\\"My tummy hurts\\\", data: None }\")" .to_string() @@ -2190,22 +2205,4 @@ mod tests { assert_eq!(increase_gas_price_by_margin(1_000_000_000), 1_300_000_000); assert_eq!(increase_gas_price_by_margin(9_000_000_000), 11_700_000_000); } -} - -#[cfg(test)] -pub mod exportable_test_parts { - use super::*; - use crate::test_utils::recorder::make_blockchain_bridge_subs_from_recorder; - use crate::test_utils::unshared_test_utils::SubsFactoryTestAddrLeaker; - - impl SubsFactory - for SubsFactoryTestAddrLeaker - { - fn make(&self, addr: &Addr) -> BlockchainBridgeSubs { - self.send_leaker_msg_and_return_meaningless_subs( - addr, - make_blockchain_bridge_subs_from_recorder, - ) - } - } -} +} \ No newline at end of file diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index e8d3477e6..78379b890 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -192,23 +192,44 @@ impl MessageIdGenerator for MessageIdGeneratorReal { as_any_ref_in_trait_impl!(); } +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum DetailedScanType { + NewPayables, + RetryPayables, + PendingPayables, + Receivables, +} + #[cfg(test)] mod tests { use crate::accountant::test_utils::AccountantBuilder; use crate::accountant::{checked_conversion, Accountant}; use crate::sub_lib::accountant::{ - AccountantSubsFactoryReal, MessageIdGenerator, MessageIdGeneratorReal, PaymentThresholds, - ScanIntervals, SubsFactory, DEFAULT_EARNING_WALLET, DEFAULT_PAYMENT_THRESHOLDS, - DEFAULT_SCAN_INTERVALS, MSG_ID_INCREMENTER, TEMPORARY_CONSUMING_WALLET, + AccountantSubsFactoryReal, DetailedScanType, MessageIdGenerator, MessageIdGeneratorReal, + PaymentThresholds, ScanIntervals, SubsFactory, DEFAULT_EARNING_WALLET, + DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS, MSG_ID_INCREMENTER, + TEMPORARY_CONSUMING_WALLET, }; use crate::sub_lib::wallet::Wallet; use crate::test_utils::recorder::{make_accountant_subs_from_recorder, Recorder}; use actix::Actor; + use masq_lib::messages::ScanType; use std::str::FromStr; use std::sync::atomic::Ordering; use std::sync::Mutex; use std::time::Duration; + impl From for ScanType { + fn from(scan_type: DetailedScanType) -> Self { + match scan_type { + DetailedScanType::NewPayables => ScanType::Payables, + DetailedScanType::RetryPayables => ScanType::Payables, + DetailedScanType::PendingPayables => ScanType::PendingPayables, + DetailedScanType::Receivables => ScanType::Receivables, + } + } + } + static MSG_ID_GENERATOR_TEST_GUARD: Mutex<()> = Mutex::new(()); impl PaymentThresholds { diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 9a3214eea..f10e0e4a6 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -184,9 +184,9 @@ macro_rules! match_lazily_every_type_id{ mod tests { use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; + use crate::sub_lib::accountant::DetailedScanType; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; - use masq_lib::messages::ScanType; use std::any::TypeId; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::vec; @@ -249,16 +249,16 @@ mod tests { let mut cond_set = StopConditions::AllGreedily(vec![MsgIdentification::ByPredicate { predicate: Box::new(|msg| { let scan_err_msg: &ScanError = msg.downcast_ref().unwrap(); - scan_err_msg.scan_type == ScanType::PendingPayables + scan_err_msg.scan_type == DetailedScanType::PendingPayables }), }]); let wrong_msg = ScanError { - scan_type: ScanType::Payables, + scan_type: DetailedScanType::NewPayables, response_skeleton_opt: None, msg: "booga".to_string(), }; let good_msg = ScanError { - scan_type: ScanType::PendingPayables, + scan_type: DetailedScanType::PendingPayables, response_skeleton_opt: None, msg: "blah".to_string(), }; From 24396b1cfe4e65f25e2a01ff02a674b0ddb9cfbf Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Tue, 16 Sep 2025 12:39:05 +0530 Subject: [PATCH 04/48] GH-605: add TODO for the test --- .../blockchain_interface/blockchain_interface_web3/utils.rs | 1 + 1 file changed, 1 insertion(+) 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 c25460ac6..40a47f1ab 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -588,6 +588,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), From 647d61ac0ff0dd91a00a1757d6767246b6396627 Mon Sep 17 00:00:00 2001 From: Bert <65427484+bertllll@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:11:29 +0200 Subject: [PATCH 05/48] GH-606: Sweeping for ending the first stage (#706) * GH-606-v2: db and help msgs + the interface document * GH-606-v2: constraint/raletion between scan intervals established * GH-606: final clean-up * GH-606-v2: after review * GH-606-v2: formatting --------- Co-authored-by: Bert --- USER-INTERFACE-INTERFACE.md | 19 +- .../src/blockchains/blockchain_records.rs | 29 +- masq_lib/src/blockchains/chains.rs | 1 + masq_lib/src/constants.rs | 42 +- masq_lib/src/shared_schema.rs | 57 +- .../db_access_objects/payable_dao.rs | 98 +- .../db_access_objects/pending_payable_dao.rs | 933 ------------------ node/src/accountant/mod.rs | 19 +- .../accountant/scanners/scan_schedulers.rs | 23 +- node/src/accountant/test_utils.rs | 9 +- node/src/actor_system_factory.rs | 2 +- node/src/blockchain/blockchain_bridge.rs | 16 +- node/src/bootstrapper.rs | 5 +- node/src/daemon/setup_reporter.rs | 45 +- node/src/database/config_dumper.rs | 37 +- node/src/database/db_initializer.rs | 73 +- .../migrations/migration_10_to_11.rs | 9 +- .../migrations/migration_5_to_6.rs | 38 +- node/src/db_config/config_dao_null.rs | 11 +- .../unprivileged_parse_args_configuration.rs | 74 +- node/src/sub_lib/accountant.rs | 58 +- node/src/sub_lib/combined_parameters.rs | 9 +- node/src/test_utils/database_utils.rs | 7 +- node/src/test_utils/mod.rs | 15 +- 24 files changed, 381 insertions(+), 1248 deletions(-) delete mode 100644 node/src/accountant/db_access_objects/pending_payable_dao.rs diff --git a/USER-INTERFACE-INTERFACE.md b/USER-INTERFACE-INTERFACE.md index 443abc65a..3a5cacffa 100644 --- a/USER-INTERFACE-INTERFACE.md +++ b/USER-INTERFACE-INTERFACE.md @@ -358,8 +358,8 @@ Another reason the secrets might be missing is that there are not yet any secret "exitServiceRate: " }, "scanIntervals": { - "pendingPayableSec": , "payableSec": , + "pendingPayableSec": , "receivableSec": }, } @@ -453,20 +453,21 @@ database password. If you want to know whether the password you have is the corr * `scanIntervals`: These three intervals describe the length of three different scan cycles running automatically in the background since the Node has connected to a qualified neighborhood that consists of neighbors enabling a complete - 3-hop route. Each parameter can be set independently, but by default are all the same which currently is most desirable - for the consistency of service payments to and from your Node. Technically, there doesn't have to be any lower limit - for the minimum of time you can set; two scans of the same sort would never run at the same time but the next one is + 3-hop route. Each parameter can be set independently. Technically, there doesn't have to be any lower limit for +* the minimum of time you can set; two scans of the same sort would never run at the same time but the next one is always scheduled not earlier than the end of the previous one. These are ever present values, no matter if the user's set any value, because defaults are prepared. -* `pendingPayableSec`: Amount of seconds between two sequential cycles of scanning for payments that are marked as currently - pending; the payments were sent to pay our debts, the payable. The purpose of this process is to confirm the status of - the pending payment; either the payment transaction was written on blockchain as successful or failed. - -* `payableSec`: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts of that meet +* `payableSec`: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question. +* `pendingPayableSec`: The time elapsed since the last payable transaction was processed. This scan operates + on an irregular schedule and is triggered after new transactions are sent or when failed transactions need + to be replaced. The scanner monitors pending transactions and verifies their blockchain status, determining whether + each payment was successfully recorded or failed. Any failed transaction is automatically resubmitted as soon + as the failure is detected. + * `receivableSec`: Amount of seconds between two sequential cycles of scanning for payments on the blockchain that have been sent by our creditors to us, which are credited against receivables recorded for services provided. diff --git a/masq_lib/src/blockchains/blockchain_records.rs b/masq_lib/src/blockchains/blockchain_records.rs index 00ac1bb66..821b0d7f1 100644 --- a/masq_lib/src/blockchains/blockchain_records.rs +++ b/masq_lib/src/blockchains/blockchain_records.rs @@ -4,7 +4,9 @@ use crate::blockchains::chains::Chain; use crate::constants::{ BASE_GAS_PRICE_CEILING_WEI, BASE_MAINNET_CHAIN_ID, BASE_MAINNET_CONTRACT_CREATION_BLOCK, BASE_MAINNET_FULL_IDENTIFIER, BASE_SEPOLIA_CHAIN_ID, BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, - BASE_SEPOLIA_FULL_IDENTIFIER, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, + BASE_SEPOLIA_FULL_IDENTIFIER, DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, DEV_GAS_PRICE_CEILING_WEI, ETH_GAS_PRICE_CEILING_WEI, ETH_MAINNET_CHAIN_ID, ETH_MAINNET_CONTRACT_CREATION_BLOCK, ETH_MAINNET_FULL_IDENTIFIER, ETH_ROPSTEN_CHAIN_ID, ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, ETH_ROPSTEN_FULL_IDENTIFIER, @@ -15,14 +17,13 @@ use crate::constants::{ }; use ethereum_types::{Address, H160}; -// TODO these should probably be a static (it's a shame that we construct the data every time anew -// when we ask for the chain specs), and dynamic initialization should be allowed as well -pub const CHAINS: [BlockchainRecord; 7] = [ +pub static CHAINS: [BlockchainRecord; 7] = [ BlockchainRecord { self_id: Chain::PolyMainnet, num_chain_id: POLYGON_MAINNET_CHAIN_ID, literal_identifier: POLYGON_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -31,6 +32,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: ETH_MAINNET_CHAIN_ID, literal_identifier: ETH_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -39,6 +41,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: BASE_MAINNET_CHAIN_ID, literal_identifier: BASE_MAINNET_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, }, @@ -47,6 +50,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: BASE_SEPOLIA_CHAIN_ID, literal_identifier: BASE_SEPOLIA_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, }, @@ -55,6 +59,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: POLYGON_AMOY_CHAIN_ID, literal_identifier: POLYGON_AMOY_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, }, @@ -63,6 +68,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: ETH_ROPSTEN_CHAIN_ID, literal_identifier: ETH_ROPSTEN_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, }, @@ -71,6 +77,7 @@ pub const CHAINS: [BlockchainRecord; 7] = [ num_chain_id: DEV_CHAIN_ID, literal_identifier: DEV_CHAIN_FULL_IDENTIFIER, gas_price_safe_ceiling_minor: DEV_GAS_PRICE_CEILING_WEI, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, }, @@ -82,6 +89,7 @@ pub struct BlockchainRecord { pub num_chain_id: u64, pub literal_identifier: &'static str, pub gas_price_safe_ceiling_minor: u128, + pub default_pending_payable_interval_sec: u64, pub contract: Address, pub contract_creation_block: u64, } @@ -128,7 +136,11 @@ const POLYGON_MAINNET_CONTRACT_ADDRESS: Address = H160([ mod tests { use super::*; use crate::blockchains::chains::chain_from_chain_identifier_opt; - use crate::constants::{BASE_MAINNET_CONTRACT_CREATION_BLOCK, WEIS_IN_GWEI}; + use crate::constants::{ + BASE_MAINNET_CONTRACT_CREATION_BLOCK, DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, + DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, WEIS_IN_GWEI, + }; use std::collections::HashSet; use std::iter::FromIterator; @@ -209,6 +221,7 @@ mod tests { self_id: examined_chain, literal_identifier: "eth-mainnet", gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -226,6 +239,7 @@ mod tests { self_id: examined_chain, literal_identifier: "eth-ropsten", gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, } @@ -243,6 +257,7 @@ mod tests { self_id: examined_chain, literal_identifier: "polygon-mainnet", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -260,6 +275,7 @@ mod tests { self_id: examined_chain, literal_identifier: "polygon-amoy", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, } @@ -277,6 +293,7 @@ mod tests { self_id: examined_chain, literal_identifier: "base-mainnet", gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -294,6 +311,7 @@ mod tests { self_id: examined_chain, literal_identifier: "base-sepolia", gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, } @@ -311,6 +329,7 @@ mod tests { self_id: examined_chain, literal_identifier: "dev", gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, + default_pending_payable_interval_sec: DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, } diff --git a/masq_lib/src/blockchains/chains.rs b/masq_lib/src/blockchains/chains.rs index b7061d899..cedde32d1 100644 --- a/masq_lib/src/blockchains/chains.rs +++ b/masq_lib/src/blockchains/chains.rs @@ -142,6 +142,7 @@ mod tests { self_id: Chain::PolyMainnet, literal_identifier: "", gas_price_safe_ceiling_minor: 0, + default_pending_payable_interval_sec: 0, contract: Default::default(), contract_creation_block: 0, } diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index 67338f5a3..fcbec6826 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -18,24 +18,16 @@ pub const MASQ_URL_PREFIX: &str = "masq://"; pub const CURRENT_LOGFILE_NAME: &str = "MASQNode_rCURRENT.log"; pub const MASQ_PROMPT: &str = "masq> "; -pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really -pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; - pub const WALLET_ADDRESS_LENGTH: usize = 42; -pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; pub const WEIS_IN_GWEI: i128 = 1_000_000_000; -pub const DEFAULT_MAX_BLOCK_COUNT: u64 = 100_000; +pub const COMBINED_PARAMETERS_DELIMITER: char = '|'; pub const PAYLOAD_ZERO_SIZE: usize = 0usize; -pub const ETH_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 11_170_708; -pub const ETH_ROPSTEN_CONTRACT_CREATION_BLOCK: u64 = 8_688_171; -pub const POLYGON_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 14_863_650; -pub const POLYGON_AMOY_CONTRACT_CREATION_BLOCK: u64 = 5_323_366; -pub const BASE_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 19_711_235; -pub const BASE_SEPOLIA_CONTRACT_CREATION_BLOCK: u64 = 14_732_730; -pub const MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK: u64 = 0; +//descriptor +pub const CENTRAL_DELIMITER: char = '@'; +pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; //Migration versions //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -88,11 +80,11 @@ pub const VALUE_EXCEEDS_ALLOWED_LIMIT: u64 = ACCOUNTANT_PREFIX | 3; //////////////////////////////////////////////////////////////////////////////////////////////////// -pub const COMBINED_PARAMETERS_DELIMITER: char = '|'; +pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; -//descriptor -pub const CENTRAL_DELIMITER: char = '@'; -pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; +pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really +pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; +pub const DEFAULT_MAX_BLOCK_COUNT: u64 = 100_000; //chains pub const POLYGON_MAINNET_CHAIN_ID: u64 = 137; @@ -114,11 +106,25 @@ pub const ETH_ROPSTEN_FULL_IDENTIFIER: &str = concatcp!(ETH_FAMILY, LINK, "ropst pub const BASE_MAINNET_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, MAINNET); pub const BASE_SEPOLIA_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, "sepolia"); pub const DEV_CHAIN_FULL_IDENTIFIER: &str = "dev"; + +pub const ETH_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 11_170_708; +pub const ETH_ROPSTEN_CONTRACT_CREATION_BLOCK: u64 = 8_688_171; +pub const POLYGON_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 14_863_650; +pub const POLYGON_AMOY_CONTRACT_CREATION_BLOCK: u64 = 5_323_366; +pub const BASE_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 19_711_235; +pub const BASE_SEPOLIA_CONTRACT_CREATION_BLOCK: u64 = 14_732_730; +pub const MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK: u64 = 0; + pub const POLYGON_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; pub const ETH_GAS_PRICE_CEILING_WEI: u128 = 100_000_000_000; pub const BASE_GAS_PRICE_CEILING_WEI: u128 = 50_000_000_000; pub const DEV_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC: u64 = 600; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC: u64 = 120; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC: u64 = 180; +pub const DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC: u64 = 120; + #[cfg(test)] mod tests { use super::*; @@ -204,6 +210,10 @@ mod tests { assert_eq!(ETH_GAS_PRICE_CEILING_WEI, 100_000_000_000); assert_eq!(BASE_GAS_PRICE_CEILING_WEI, 50_000_000_000); assert_eq!(DEV_GAS_PRICE_CEILING_WEI, 200_000_000_000); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_ETH_SEC, 600); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_BASE_SEC, 120); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_POLYGON_SEC, 180); + assert_eq!(DEFAULT_PENDING_PAYABLE_INTERVAL_DEV_SEC, 120); assert_eq!( CLIENT_REQUEST_PAYLOAD_CURRENT_VERSION, DataVersion { major: 0, minor: 1 } diff --git a/masq_lib/src/shared_schema.rs b/masq_lib/src/shared_schema.rs index 11dfb865f..3a8b194da 100644 --- a/masq_lib/src/shared_schema.rs +++ b/masq_lib/src/shared_schema.rs @@ -20,8 +20,8 @@ pub const CHAIN_HELP: &str = "The blockchain network MASQ Node will configure itself to use. You must ensure the \ Ethereum client specified by --blockchain-service-url communicates with the same blockchain network."; pub const CONFIG_FILE_HELP: &str = - "Optional TOML file containing configuration that doesn't often change. Should contain only \ - scalar items, string or numeric, whose names are exactly the same as the command-line parameters \ + "Optional TOML file containing configuration that seldom changes. Should contain only \ + scalar items, string, or numeric, whose names are exactly the same as the command-line parameters \ they replace (except no '--' prefix). If you specify a relative path, or no path, the Node will \ look for your config file starting in the --data-directory. If you specify an absolute path, \ --data-directory will be ignored when searching for the config file. A few parameters \ @@ -138,9 +138,9 @@ pub const REAL_USER_HELP: &str = like ::."; pub const SCANS_HELP: &str = "The Node, when running, performs various periodic scans, including scanning for payables that need to be paid, \ - for pending payables that have arrived (and are no longer pending), for incoming receivables that need to be \ - recorded, and for delinquent Nodes that need to be banned. If you don't specify this parameter, or if you give \ - it the value 'on', these scans will proceed normally. But if you give the value 'off', the scans won't be \ + for pending payables that have arrived or happened to fail (and are no longer pending), for incoming receivables \ + that need to be recorded, and for delinquent Nodes that need to be banned. If you don't specify this parameter, \ + or if you give it the value 'on', these scans will proceed normally. But if you give the value 'off', the scans won't be \ started when the Node starts, and will have to be triggered later manually and individually with the \ MASQNode-UIv2 'scan' command. (If you don't, you'll most likely be delinquency-banned by all your neighbors.) \ This parameter is most useful for testing."; @@ -183,19 +183,18 @@ pub const PAYMENT_THRESHOLDS_HELP: &str = "\ pub const SCAN_INTERVALS_HELP:&str = "\ These three intervals describe the length of three different scan cycles running automatically in the background \ since the Node has connected to a qualified neighborhood that consists of neighbors enabling a complete 3-hop \ - route. Each parameter can be set independently, but by default are all the same which currently is most desirable \ - for the consistency of service payments to and from your Node. Technically, there doesn't have to be any lower \ + route. Each parameter can be set independently. Technically, there doesn't have to be any lower \ limit for the minimum of time you can set; two scans of the same sort would never run at the same time but the \ next one is always scheduled not earlier than the end of the previous one. These are ever present values, no matter \ if the user's set any value, they have defaults. The parameters must be always supplied all together, delimited by vertical \ bars and in the right order.\n\n\ - 1. Pending Payable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments that are \ - marked as currently pending; the payments were sent to pay our debts, the payable. The purpose of this process is to \ - confirm the status of the pending payment; either the payment transaction was written on blockchain as successful or \ - failed.\n\n\ - 2. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ - of that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If \ - they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question.\n\n\ + 1. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ + that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. \ + If they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question.\n\n\ + 2. Pending Payable Scan Interval: The time elapsed since the last payable transaction was processed. This scan operates \ + on an irregular schedule and is triggered after new transactions are sent or when failed transactions need to be replaced. \ + The scanner monitors pending transactions and verifies their blockchain status, determining whether each payment was \ + successfully recorded or failed. Any failed transaction is automatically resubmitted as soon as the failure is detected.\n\n\ 3. Receivable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments on the \ blockchain that have been sent by our creditors to us, which are credited against receivables recorded for services \ provided."; @@ -744,8 +743,8 @@ mod tests { ); assert_eq!( CONFIG_FILE_HELP, - "Optional TOML file containing configuration that doesn't often change. Should contain only \ - scalar items, string or numeric, whose names are exactly the same as the command-line parameters \ + "Optional TOML file containing configuration that seldom changes. Should contain only \ + scalar items, string, or numeric, whose names are exactly the same as the command-line parameters \ they replace (except no '--' prefix). If you specify a relative path, or no path, the Node will \ look for your config file starting in the --data-directory. If you specify an absolute path, \ --data-directory will be ignored when searching for the config file. A few parameters \ @@ -883,6 +882,16 @@ mod tests { you start the Node using pkexec or some other method that doesn't populate the SUDO_xxx variables. Use a value \ like ::." ); + assert_eq!( + SCANS_HELP, + "The Node, when running, performs various periodic scans, including scanning for payables that need to be paid, \ + for pending payables that have arrived or happened to fail (and are no longer pending), for incoming receivables \ + that need to be recorded, and for delinquent Nodes that need to be banned. If you don't specify this parameter, \ + or if you give it the value 'on', these scans will proceed normally. But if you give the value 'off', the scans won't be \ + started when the Node starts, and will have to be triggered later manually and individually with the \ + MASQNode-UIv2 'scan' command. (If you don't, you'll most likely be delinquency-banned by all your neighbors.) \ + This parameter is most useful for testing." + ); assert_eq!( DEFAULT_UI_PORT_VALUE.to_string(), @@ -959,19 +968,19 @@ mod tests { SCAN_INTERVALS_HELP, "These three intervals describe the length of three different scan cycles running automatically in the background \ since the Node has connected to a qualified neighborhood that consists of neighbors enabling a complete 3-hop \ - route. Each parameter can be set independently, but by default are all the same which currently is most desirable \ - for the consistency of service payments to and from your Node. Technically, there doesn't have to be any lower \ + route. Each parameter can be set independently. Technically, there doesn't have to be any lower \ limit for the minimum of time you can set; two scans of the same sort would never run at the same time but the \ next one is always scheduled not earlier than the end of the previous one. These are ever present values, no matter \ if the user's set any value, they have defaults. The parameters must be always supplied all together, delimited by \ vertical bars and in the right order.\n\n\ - 1. Pending Payable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments that are \ - marked as currently pending; the payments were sent to pay our debts, the payable. The purpose of this process is to \ - confirm the status of the pending payment; either the payment transaction was written on blockchain as successful or \ - failed.\n\n\ - 2. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ - of that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If \ + 1. Payable Scan Interval: Amount of seconds between two sequential cycles of scanning aimed to find payable accounts \ + that meet the criteria set by the Payment Thresholds; these accounts are tracked on behalf of our creditors. If \ they meet the Payment Threshold criteria, our Node will send a debt payment transaction to the creditor in question.\n\n\ + 2. Pending Payable Scan Interval: The time elapsed since the last payable transaction was processed. This scan operates \ + on an irregular schedule and is triggered after new transactions are sent or when failed transactions need \ + to be replaced. The scanner monitors pending transactions and verifies their blockchain status, determining whether \ + each payment was successfully recorded or failed. Any failed transaction is automatically resubmitted as soon \ + as the failure is detected.\n\n\ 3. Receivable Scan Interval: Amount of seconds between two sequential cycles of scanning for payments on the \ blockchain that have been sent by our creditors to us, which are credited against receivables recorded for services \ provided." diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 0226c0c68..cff264a58 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -24,7 +24,6 @@ use masq_lib::utils::ExpectValue; use rusqlite::OptionalExtension; use rusqlite::{Error, Row}; use std::fmt::Debug; -use std::str::FromStr; use std::time::SystemTime; use web3::types::H256; @@ -313,39 +312,22 @@ impl PayableDaoReal { let balance_high_bytes_result = row.get(1); let balance_low_bytes_result = row.get(2); let last_paid_timestamp_result = row.get(3); - let pending_payable_rowid_result: Result, Error> = row.get(4); - let pending_payable_hash_result: Result, Error> = row.get(5); match ( wallet_result, balance_high_bytes_result, balance_low_bytes_result, last_paid_timestamp_result, - pending_payable_rowid_result, - pending_payable_hash_result, ) { - ( - Ok(wallet), - Ok(high_bytes), - Ok(low_bytes), - Ok(last_paid_timestamp), - Ok(rowid_opt), - Ok(hash_opt), - ) => Ok(PayableAccount { - wallet, - balance_wei: checked_conversion::(BigIntDivider::reconstitute( - high_bytes, low_bytes, - )), - last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), - pending_payable_opt: rowid_opt.map(|rowid| { - let hash_str = - hash_opt.expect("database corrupt; missing hash but existing rowid"); - PendingPayableId::new( - u64::try_from(rowid).unwrap(), - H256::from_str(&hash_str[2..]) - .unwrap_or_else(|_| panic!("wrong form of tx hash {}", hash_str)), - ) - }), - }), + (Ok(wallet), Ok(high_bytes), Ok(low_bytes), Ok(last_paid_timestamp)) => { + Ok(PayableAccount { + wallet, + balance_wei: checked_conversion::(BigIntDivider::reconstitute( + high_bytes, low_bytes, + )), + last_paid_timestamp: utils::from_unix_timestamp(last_paid_timestamp), + pending_payable_opt: None, + }) + } e => panic!( "Database is corrupt: PAYABLE table columns and/or types: {:?}", e @@ -359,13 +341,9 @@ impl PayableDaoReal { wallet_address, balance_high_b, balance_low_b, - last_paid_timestamp, - pending_payable_rowid, - pending_payable.transaction_hash + last_paid_timestamp from payable - left join pending_payable on - pending_payable.rowid = payable.pending_payable_rowid {} {} order by {}, @@ -560,7 +538,6 @@ mod tests { use rusqlite::ToSql; use rusqlite::{Connection, OpenFlags}; use std::path::Path; - use std::str::FromStr; use std::time::Duration; #[test] @@ -1314,9 +1291,9 @@ mod tests { #[test] fn custom_query_in_top_records_mode_with_default_ordering() { - //Accounts of balances smaller than one gwei don't qualify. - //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, - //here by balance and then by age. + // Accounts of balances smaller than one gwei don't qualify. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( @@ -1344,13 +1321,7 @@ mod tests { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x4444444444444444444444444444444444444444"), @@ -1364,9 +1335,9 @@ mod tests { #[test] fn custom_query_in_top_records_mode_ordered_by_age() { - //Accounts of balances smaller than one gwei don't qualify. - //Two accounts differ only in balance but not in the debt's age which allows to check doubled ordering, - //here by age and then by balance. + // Accounts of balances smaller than one gwei don't qualify. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_test_setup = accounts_for_tests_of_top_records(now); let subject = custom_query_test_body_for_payable( @@ -1388,13 +1359,7 @@ mod tests { wallet: Wallet::new("0x5555555555555555555555555555555555555555"), balance_wei: 10_000_000_100, last_paid_timestamp: from_unix_timestamp(now - 86_401), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None }, PayableAccount { wallet: Wallet::new("0x1111111111111111111111111111111111111111"), @@ -1433,8 +1398,8 @@ mod tests { #[test] fn custom_query_in_range_mode() { - //Two accounts differ only in debt's age but not balance which allows to check doubled ordering, - //by balance and then by age. + // Two accounts differ only in the debt age but not the balance which allows to check double + // ordering, primarily by balance and then age. let now = current_unix_timestamp(); let main_setup = |conn: &dyn ConnectionWrapper, insert: InsertPayableHelperFn| { insert( @@ -1518,13 +1483,7 @@ mod tests { wallet: Wallet::new("0x2222222222222222222222222222222222222222"), balance_wei: gwei_to_wei(1_800_456_000_u32), last_paid_timestamp: from_unix_timestamp(now - 55_120), - pending_payable_opt: Some(PendingPayableId::new( - 1, - H256::from_str( - "abc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223" - ) - .unwrap() - )) + pending_payable_opt: None } ] ); @@ -1690,19 +1649,6 @@ mod tests { .initialize(&home_dir, DbInitializationConfig::test_default()) .unwrap(); main_setup_fn(conn.as_ref(), &insert_payable_record_fn); - - let pending_payable_account: &[&dyn ToSql] = &[ - &String::from("0xabc4546cce78230a2312e12f3acb78747340456fe5237896666100143abcd223"), - &40, - &478945, - &177777777, - &1, - ]; - conn - .prepare("insert into pending_payable (transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt) values (?,?,?,?,?)") - .unwrap() - .execute(pending_payable_account) - .unwrap(); PayableDaoReal::new(conn) } } diff --git a/node/src/accountant/db_access_objects/pending_payable_dao.rs b/node/src/accountant/db_access_objects/pending_payable_dao.rs deleted file mode 100644 index 414c364d8..000000000 --- a/node/src/accountant/db_access_objects/pending_payable_dao.rs +++ /dev/null @@ -1,933 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::db_access_objects::utils::{ - from_unix_timestamp, to_unix_timestamp, DaoFactoryReal, VigilantRusqliteFlatten, -}; -use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; -use crate::database::rusqlite_wrappers::ConnectionWrapper; -use crate::sub_lib::wallet::Wallet; -use masq_lib::utils::ExpectValue; -use rusqlite::Row; -use std::collections::HashSet; -use std::fmt::Debug; -use std::str::FromStr; -use std::time::SystemTime; -use web3::types::H256; - -#[derive(Debug, PartialEq, Eq)] -pub enum PendingPayableDaoError { - InsertionFailed(String), - UpdateFailed(String), - SignConversionError(u64), - RecordCannotBeRead, - RecordDeletion(String), - ErrorMarkFailed(String), -} - -#[derive(Debug)] -pub struct TransactionHashes { - pub rowid_results: Vec<(u64, H256)>, - pub no_rowid_results: Vec, -} - -pub trait PendingPayableDao { - // Note that the order of the returned results is not guaranteed - fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes; - // fn return_all_errorless_fingerprints(&self) -> Vec; - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError>; - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError>; - fn increment_scan_attempts(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError>; - fn mark_failures(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError>; -} - -impl PendingPayableDao for PendingPayableDaoReal<'_> { - fn fingerprints_rowids(&self, hashes: &[H256]) -> TransactionHashes { - //Vec<(Option, H256)> { - fn hash_and_rowid_in_single_row(row: &Row) -> rusqlite::Result<(u64, H256)> { - let hash_str: String = row.get(0).expectv("hash"); - let hash = H256::from_str(&hash_str[2..]).expect("hash inserted right turned wrong"); - let sqlite_signed_rowid: i64 = row.get(1).expectv("rowid"); - let rowid = u64::try_from(sqlite_signed_rowid).expect("SQlite goes from 1 to i64:MAX"); - Ok((rowid, hash)) - } - - let sql = format!( - "select transaction_hash, rowid from pending_payable where transaction_hash in ({})", - comma_joined_stringifiable(hashes, |hash| format!("'{:?}'", hash)) - ); - - let all_found_records = self - .conn - .prepare(&sql) - .expect("Internal error") - .query_map([], hash_and_rowid_in_single_row) - .expect("map query failed") - .vigilant_flatten() - .collect::>(); - let hashes_of_found_records = all_found_records - .iter() - .map(|(_, hash)| *hash) - .collect::>(); - let hashes_of_missing_rowids = hashes - .iter() - .filter(|hash| !hashes_of_found_records.contains(hash)) - .cloned() - .collect(); - - TransactionHashes { - rowid_results: all_found_records, - no_rowid_results: hashes_of_missing_rowids, - } - } - - // fn return_all_errorless_fingerprints(&self) -> Vec { - // let mut stm = self - // .conn - // .prepare( - // "select rowid, transaction_hash, amount_high_b, amount_low_b, \ - // payable_timestamp, attempt from pending_payable where process_error is null", - // ) - // .expect("Internal error"); - // stm.query_map([], |row| { - // let rowid: u64 = Self::get_with_expect(row, 0); - // let transaction_hash: String = Self::get_with_expect(row, 1); - // let amount_high_bytes: i64 = Self::get_with_expect(row, 2); - // let amount_low_bytes: i64 = Self::get_with_expect(row, 3); - // let timestamp: i64 = Self::get_with_expect(row, 4); - // let attempt: u16 = Self::get_with_expect(row, 5); - // Ok(SentTx { - // rowid, - // timestamp: from_unix_timestamp(timestamp), - // hash: H256::from_str(&transaction_hash[2..]).unwrap_or_else(|e| { - // panic!( - // "Invalid hash format (\"{}\": {:?}) - database corrupt", - // transaction_hash, e - // ) - // }), - // attempt, - // amount_minor: checked_conversion::(BigIntDivider::reconstitute( - // amount_high_bytes, - // amount_low_bytes, - // )), - // process_error: None, - // }) - // }) - // .expect("rusqlite failure") - // .vigilant_flatten() - // .collect() - // } - - fn insert_new_fingerprints( - &self, - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> Result<(), PendingPayableDaoError> { - fn values_clause_for_fingerprints_to_insert( - hashes_and_amounts: &[HashAndAmount], - batch_wide_timestamp: SystemTime, - ) -> String { - let time_t = to_unix_timestamp(batch_wide_timestamp); - comma_joined_stringifiable(hashes_and_amounts, |hash_and_amount| { - let amount_checked = checked_conversion::(hash_and_amount.amount); - let (high_bytes, low_bytes) = BigIntDivider::deconstruct(amount_checked); - format!( - "('{:?}', {}, {}, {}, 1, null)", - hash_and_amount.hash, high_bytes, low_bytes, time_t - ) - }) - } - - let insert_sql = format!( - "insert into pending_payable (\ - transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error\ - ) values {}", - values_clause_for_fingerprints_to_insert(hashes_and_amounts, batch_wide_timestamp) - ); - match self - .conn - .prepare(&insert_sql) - .expect("Internal error") - .execute([]) - { - Ok(x) if x == hashes_and_amounts.len() => Ok(()), - Ok(x) => panic!( - "expected {} changed rows but got {}", - hashes_and_amounts.len(), - x - ), - Err(e) => Err(PendingPayableDaoError::InsertionFailed(e.to_string())), - } - } - - fn delete_fingerprints(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - let sql = format!( - "delete from pending_payable where rowid in ({})", - Self::serialize_ids(ids) - ); - match self - .conn - .prepare(&sql) - .expect("delete command wrong") - .execute([]) - { - Ok(x) if x == ids.len() => Ok(()), - Ok(num) => panic!( - "deleting sent tx record, expected {} rows to be changed, but the actual number is {}", - ids.len(), - num - ), - Err(e) => Err(PendingPayableDaoError::RecordDeletion(e.to_string())), - } - } - - fn increment_scan_attempts(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - let sql = format!( - "update pending_payable set attempt = attempt + 1 where rowid in ({})", - Self::serialize_ids(ids) - ); - match self.conn.prepare(&sql).expect("Internal error").execute([]) { - Ok(num) if num == ids.len() => Ok(()), - Ok(num) => panic!( - "Database corrupt: updating fingerprints: expected to update {} rows but did {}", - ids.len(), - num - ), - Err(e) => Err(PendingPayableDaoError::UpdateFailed(e.to_string())), - } - } - - fn mark_failures(&self, ids: &[u64]) -> Result<(), PendingPayableDaoError> { - let sql = format!( - "update pending_payable set process_error = 'ERROR' where rowid in ({})", - Self::serialize_ids(ids) - ); - match self - .conn - .prepare(&sql) - .expect("Internal error") - .execute([]) { - Ok(num) if num == ids.len() => Ok(()), - Ok(num) => - panic!( - "Database corrupt: marking failure at fingerprints: expected to change {} rows but did {}", - ids.len(), num - ) - , - Err(e) => Err(PendingPayableDaoError::ErrorMarkFailed(e.to_string())), - } - } -} - -#[derive(Debug)] -pub struct PendingPayableDaoReal<'a> { - conn: Box, -} - -impl<'a> PendingPayableDaoReal<'a> { - pub fn new(conn: Box) -> Self { - Self { conn } - } - - fn get_with_expect(row: &Row, index: usize) -> T { - row.get(index).expect("database is corrupt") - } - - fn serialize_ids(ids: &[u64]) -> String { - comma_joined_stringifiable(ids, |id| id.to_string()) - } -} - -pub trait PendingPayableDaoFactory { - fn make(&self) -> Box; -} - -impl PendingPayableDaoFactory for DaoFactoryReal { - fn make(&self) -> Box { - Box::new(PendingPayableDaoReal::new(self.make_connection())) - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::checked_conversion; - use crate::accountant::db_access_objects::sent_payable_dao::{ - PendingPayableDao, PendingPayableDaoError, PendingPayableDaoReal, - }; - use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; - use crate::blockchain::test_utils::make_tx_hash; - use crate::database::db_initializer::{ - DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, - }; - use crate::database::rusqlite_wrappers::ConnectionWrapperReal; - use crate::database::test_utils::ConnectionWrapperMock; - use masq_lib::test_utils::utils::ensure_node_home_directory_exists; - use rusqlite::{Connection, OpenFlags}; - use std::str::FromStr; - use std::time::SystemTime; - use web3::types::H256; - - // #[test] - // fn insert_new_fingerprints_happy_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "insert_new_fingerprints_happy_path", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let hash_1 = make_tx_hash(4546); - // let amount_1 = 55556; - // let hash_2 = make_tx_hash(6789); - // let amount_2 = 44445; - // let batch_wide_timestamp = from_unix_timestamp(200_000_000); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: amount_2, - // }; - // - // let _ = subject - // .insert_new_fingerprints( - // &[hash_and_amount_1, hash_and_amount_2], - // batch_wide_timestamp, - // ) - // .unwrap(); - // - // let records = subject.return_all_errorless_fingerprints(); - // assert_eq!( - // records, - // vec![ - // SentTx { - // rowid: 1, - // timestamp: batch_wide_timestamp, - // hash: hash_and_amount_1.hash, - // attempt: 1, - // amount_minor: hash_and_amount_1.amount, - // process_error: None - // }, - // SentTx { - // rowid: 2, - // timestamp: batch_wide_timestamp, - // hash: hash_and_amount_2.hash, - // attempt: 1, - // amount_minor: hash_and_amount_2.amount, - // process_error: None - // } - // ] - // ) - // } - // - // #[test] - // fn insert_new_fingerprints_sad_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "insert_new_fingerprints_sad_path", - // ); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let hash = make_tx_hash(45466); - // let amount = 55556; - // let timestamp = from_unix_timestamp(200_000_000); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // let hash_and_amount = HashAndAmount { hash, amount }; - // - // let result = subject.insert_new_fingerprints(&[hash_and_amount], timestamp); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::InsertionFailed( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic(expected = "expected 1 changed rows but got 0")] - // fn insert_new_fingerprints_number_of_returned_rows_different_than_expected() { - // let setup_conn = Connection::open_in_memory().unwrap(); - // // injecting a by-plan failing statement into the mocked connection in order to provoke - // // a reaction that would've been untestable directly on the table the act is closely coupled with - // let statement = { - // setup_conn - // .execute("create table example (id integer)", []) - // .unwrap(); - // setup_conn.prepare("select id from example").unwrap() - // }; - // let wrapped_conn = ConnectionWrapperMock::default().prepare_result(Ok(statement)); - // let hash_1 = make_tx_hash(4546); - // let amount_1 = 55556; - // let batch_wide_timestamp = from_unix_timestamp(200_000_000); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // let hash_and_amount = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // - // let _ = subject.insert_new_fingerprints(&[hash_and_amount], batch_wide_timestamp); - // } - // - // #[test] - // fn fingerprints_rowids_when_records_reachable() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "fingerprints_rowids_when_records_reachable", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let timestamp = from_unix_timestamp(195_000_000); - // // use full range tx hashes because SqLite has tendencies to see the value as a hex and convert it to an integer, - // // then complain about its excessive size if supplied in unquoted strings - // let hash_1 = - // H256::from_str("b4bc263278d3a82a652a8d73a6bfd8ec0ba1a63923bbb4f38147fb8a943da26a") - // .unwrap(); - // let hash_2 = - // H256::from_str("5a2909e7bb71943c82a94d9beb04e230351541fc14619ee8bb9b7372ea88ba39") - // .unwrap(); - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: 4567, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: 6789, - // }; - // let fingerprints_init_input = vec![hash_and_amount_1, hash_and_amount_2]; - // { - // subject - // .insert_new_fingerprints(&fingerprints_init_input, timestamp) - // .unwrap(); - // } - // - // let result = subject.fingerprints_rowids(&[hash_1, hash_2]); - // - // let first_expected_pair = &(1, hash_1); - // assert!( - // result.rowid_results.contains(first_expected_pair), - // "Returned rowid pairs should have contained {:?} but all it did is {:?}", - // first_expected_pair, - // result.rowid_results - // ); - // let second_expected_pair = &(2, hash_2); - // assert!( - // result.rowid_results.contains(second_expected_pair), - // "Returned rowid pairs should have contained {:?} but all it did is {:?}", - // second_expected_pair, - // result.rowid_results - // ); - // assert_eq!(result.rowid_results.len(), 2); - // } - // - // #[test] - // fn fingerprints_rowids_when_nonexistent_records() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "fingerprints_rowids_when_nonexistent_records", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let hash_1 = make_tx_hash(11119); - // let hash_2 = make_tx_hash(22229); - // let hash_3 = make_tx_hash(33339); - // let hash_4 = make_tx_hash(44449); - // // For more illustrative results, I use the official tooling but also generate one extra record before the chief one for - // // this test, and in the end, I delete the first one. It leaves a single record still in but with the rowid 2 instead of - // // just an ambiguous 1 - // subject - // .insert_new_fingerprints( - // &[HashAndAmount { - // hash: hash_2, - // amount_minor: 8901234, - // }], - // SystemTime::now(), - // ) - // .unwrap(); - // subject - // .insert_new_fingerprints( - // &[HashAndAmount { - // hash: hash_3, - // amount_minor: 1234567, - // }], - // SystemTime::now(), - // ) - // .unwrap(); - // subject.delete_fingerprints(&[1]).unwrap(); - // - // let result = subject.fingerprints_rowids(&[hash_1, hash_2, hash_3, hash_4]); - // - // assert_eq!(result.rowid_results, vec![(2, hash_3),]); - // assert_eq!(result.no_rowid_results, vec![hash_1, hash_2, hash_4]); - // } - // - // #[test] - // fn return_all_errorless_fingerprints_works_when_no_records_with_error_marks() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "return_all_errorless_fingerprints_works_when_no_records_with_error_marks", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let batch_wide_timestamp = from_unix_timestamp(195_000_000); - // let hash_1 = make_tx_hash(11119); - // let amount_1 = 787; - // let hash_2 = make_tx_hash(10000); - // let amount_2 = 333; - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: amount_2, - // }; - // - // { - // subject - // .insert_new_fingerprints( - // &[hash_and_amount_1, hash_and_amount_2], - // batch_wide_timestamp, - // ) - // .unwrap(); - // } - // - // let result = subject.return_all_errorless_fingerprints(); - // - // assert_eq!( - // result, - // vec![ - // SentTx { - // rowid: 1, - // timestamp: batch_wide_timestamp, - // hash: hash_1, - // attempt: 1, - // amount_minor: amount_1, - // process_error: None - // }, - // SentTx { - // rowid: 2, - // timestamp: batch_wide_timestamp, - // hash: hash_2, - // attempt: 1, - // amount_minor: amount_2, - // process_error: None - // } - // ] - // ) - // } - // - // #[test] - // fn return_all_errorless_fingerprints_works_when_some_records_with_error_marks() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "return_all_errorless_fingerprints_works_when_some_records_with_error_marks", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // let timestamp = from_unix_timestamp(198_000_000); - // let hash = make_tx_hash(10000); - // let amount = 333; - // let hash_and_amount_1 = HashAndAmount { - // hash: make_tx_hash(11119), - // amount_minor: 2000, - // }; - // let hash_and_amount_2 = HashAndAmount { hash, amount }; - // { - // subject - // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - // .unwrap(); - // subject.mark_failures(&[1]).unwrap(); - // } - // - // let result = subject.return_all_errorless_fingerprints(); - // - // assert_eq!( - // result, - // vec![SentTx { - // rowid: 2, - // timestamp, - // hash, - // attempt: 1, - // amount, - // process_error: None - // }] - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "Invalid hash format (\"silly_hash\": Invalid character 'l' at position 0) - database corrupt" - // )] - // fn return_all_errorless_fingerprints_panics_on_malformed_hash() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "return_all_errorless_fingerprints_panics_on_malformed_hash", - // ); - // let wrapped_conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // { - // wrapped_conn - // .prepare("insert into pending_payable \ - // (rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error) \ - // values (1, 'silly_hash', 4, 111, 10000000000, 1, null)") - // .unwrap() - // .execute([]) - // .unwrap(); - // } - // let subject = PendingPayableDaoReal::new(wrapped_conn); - // - // let _ = subject.return_all_errorless_fingerprints(); - // } - // - // #[test] - // fn delete_fingerprints_happy_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "delete_fingerprints_happy_path", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints( - // &[ - // HashAndAmount { - // hash: make_tx_hash(1234), - // amount_minor: 1111, - // }, - // HashAndAmount { - // hash: make_tx_hash(2345), - // amount_minor: 5555, - // }, - // HashAndAmount { - // hash: make_tx_hash(3456), - // amount_minor: 2222, - // }, - // ], - // SystemTime::now(), - // ) - // .unwrap(); - // } - // - // let result = subject.delete_fingerprints(&[2, 3]); - // - // assert_eq!(result, Ok(())); - // let records_in_the_db = subject.return_all_errorless_fingerprints(); - // let record_left_in = &records_in_the_db[0]; - // assert_eq!(record_left_in.hash, make_tx_hash(1234)); - // assert_eq!(record_left_in.rowid, 1); - // assert_eq!(records_in_the_db.len(), 1); - // } - // - // #[test] - // fn delete_fingerprints_sad_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "delete_fingerprints_sad_path", - // ); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let rowid = 45; - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // - // let result = subject.delete_fingerprints(&[rowid]); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::RecordDeletion( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "deleting sent tx record, expected 2 rows to be changed, but the actual number is 1" - // )] - // fn delete_fingerprints_changed_different_number_of_rows_than_expected() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "delete_fingerprints_changed_different_number_of_rows_than_expected", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let rowid_1 = 1; - // let rowid_2 = 2; - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints( - // &[HashAndAmount { - // hash: make_tx_hash(666666), - // amount_minor: 5555, - // }], - // SystemTime::now(), - // ) - // .unwrap(); - // } - // - // let _ = subject.delete_fingerprints(&[rowid_1, rowid_2]); - // } - // - // #[test] - // fn increment_scan_attempts_works() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "increment_scan_attempts_works", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let hash_1 = make_tx_hash(345); - // let hash_2 = make_tx_hash(456); - // let hash_3 = make_tx_hash(567); - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: 1122, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: 2233, - // }; - // let hash_and_amount_3 = HashAndAmount { - // hash: hash_3, - // amount_minor: 3344, - // }; - // let timestamp = from_unix_timestamp(190_000_000); - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints( - // &[hash_and_amount_1, hash_and_amount_2, hash_and_amount_3], - // timestamp, - // ) - // .unwrap(); - // } - // - // let result = subject.increment_scan_attempts(&[2, 3]); - // - // assert_eq!(result, Ok(())); - // let mut all_records = subject.return_all_errorless_fingerprints(); - // assert_eq!(all_records.len(), 3); - // let record_1 = all_records.remove(0); - // assert_eq!(record_1.hash, hash_1); - // assert_eq!(record_1.attempt, 1); - // let record_2 = all_records.remove(0); - // assert_eq!(record_2.hash, hash_2); - // assert_eq!(record_2.attempt, 2); - // let record_3 = all_records.remove(0); - // assert_eq!(record_3.hash, hash_3); - // assert_eq!(record_3.attempt, 2); - // } - // - // #[test] - // fn increment_scan_attempts_works_sad_path() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "increment_scan_attempts_works_sad_path", - // ); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // - // let result = subject.increment_scan_attempts(&[1]); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::UpdateFailed( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "Database corrupt: updating fingerprints: expected to update 2 rows but did 0" - // )] - // fn increment_scan_attempts_panics_on_unexpected_row_change_count() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "increment_scan_attempts_panics_on_unexpected_row_change_count", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(conn); - // - // let _ = subject.increment_scan_attempts(&[1, 2]); - // } - // - // #[test] - // fn mark_failures_works() { - // let home_dir = - // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_works"); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let hash_1 = make_tx_hash(555); - // let amount_1 = 1234; - // let hash_2 = make_tx_hash(666); - // let amount_2 = 2345; - // let hash_and_amount_1 = HashAndAmount { - // hash: hash_1, - // amount_minor: amount_1, - // }; - // let hash_and_amount_2 = HashAndAmount { - // hash: hash_2, - // amount_minor: amount_2, - // }; - // let timestamp = from_unix_timestamp(190_000_000); - // let subject = PendingPayableDaoReal::new(conn); - // { - // subject - // .insert_new_fingerprints(&[hash_and_amount_1, hash_and_amount_2], timestamp) - // .unwrap(); - // } - // - // let result = subject.mark_failures(&[2]); - // - // assert_eq!(result, Ok(())); - // let assert_conn = Connection::open(home_dir.join(DATABASE_FILE)).unwrap(); - // let mut assert_stm = assert_conn - // .prepare("select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable") - // .unwrap(); - // let found_fingerprints = assert_stm - // .query_map([], |row| { - // let rowid: u64 = row.get(0).unwrap(); - // let transaction_hash: String = row.get(1).unwrap(); - // let amount_high_b: i64 = row.get(2).unwrap(); - // let amount_low_b: i64 = row.get(3).unwrap(); - // let timestamp: i64 = row.get(4).unwrap(); - // let attempt: u16 = row.get(5).unwrap(); - // let process_error: Option = row.get(6).unwrap(); - // Ok(SentTx { - // rowid, - // timestamp: from_unix_timestamp(timestamp), - // hash: H256::from_str(&transaction_hash[2..]).unwrap(), - // attempt, - // amount_minor: checked_conversion::(BigIntDivider::reconstitute( - // amount_high_b, - // amount_low_b, - // )), - // process_error, - // }) - // }) - // .unwrap() - // .flatten() - // .collect::>(); - // assert_eq!( - // *found_fingerprints, - // vec![ - // SentTx { - // rowid: 1, - // timestamp, - // hash: hash_1, - // attempt: 1, - // amount_minor: amount_1, - // process_error: None - // }, - // SentTx { - // rowid: 2, - // timestamp, - // hash: hash_2, - // attempt: 1, - // amount_minor: amount_2, - // process_error: Some("ERROR".to_string()) - // } - // ] - // ) - // } - // - // #[test] - // fn mark_failures_sad_path() { - // let home_dir = - // ensure_node_home_directory_exists("sent_payable_dao", "mark_failures_sad_path"); - // { - // DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // } - // let conn_read_only = Connection::open_with_flags( - // home_dir.join(DATABASE_FILE), - // OpenFlags::SQLITE_OPEN_READ_ONLY, - // ) - // .unwrap(); - // let wrapped_conn = ConnectionWrapperReal::new(conn_read_only); - // let subject = PendingPayableDaoReal::new(Box::new(wrapped_conn)); - // - // let result = subject.mark_failures(&[1]); - // - // assert_eq!( - // result, - // Err(PendingPayableDaoError::ErrorMarkFailed( - // "attempt to write a readonly database".to_string() - // )) - // ) - // } - // - // #[test] - // #[should_panic( - // expected = "Database corrupt: marking failure at fingerprints: expected to change 2 rows but did 0" - // )] - // fn mark_failures_panics_on_wrong_row_change_count() { - // let home_dir = ensure_node_home_directory_exists( - // "sent_payable_dao", - // "mark_failures_panics_on_wrong_row_change_count", - // ); - // let conn = DbInitializerReal::default() - // .initialize(&home_dir, DbInitializationConfig::test_default()) - // .unwrap(); - // let subject = PendingPayableDaoReal::new(conn); - // - // let _ = subject.mark_failures(&[10, 20]); - // } -} diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index b8651d990..b9a3d093b 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1330,7 +1330,7 @@ mod tests { use ethsign_crypto::Keccak256; use log::Level; use masq_lib::constants::{ - REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, + DEFAULT_CHAIN, REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, VALUE_EXCEEDS_ALLOWED_LIMIT, }; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; @@ -1340,7 +1340,7 @@ mod tests { }; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; - use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use masq_lib::test_utils::utils::{ensure_node_home_directory_exists, TEST_DEFAULT_CHAIN}; use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; use std::any::TypeId; @@ -1370,7 +1370,7 @@ mod tests { #[test] fn new_calls_factories_properly() { - let config = make_bc_with_defaults(); + let config = make_bc_with_defaults(DEFAULT_CHAIN); let payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let sent_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let failed_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); @@ -1435,7 +1435,8 @@ mod tests { #[test] fn accountant_have_proper_defaulted_values() { - let bootstrapper_config = make_bc_with_defaults(); + let chain = TEST_DEFAULT_CHAIN; + let bootstrapper_config = make_bc_with_defaults(chain); let payable_dao_factory = Box::new( PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) // For Accountant @@ -1475,7 +1476,7 @@ mod tests { ); let financial_statistics = result.financial_statistics().clone(); - let default_scan_intervals = ScanIntervals::default(); + let default_scan_intervals = ScanIntervals::compute_default(chain); assert_eq!( result.scan_schedulers.payable.new_payable_interval, default_scan_intervals.payable_scan_interval @@ -1558,7 +1559,7 @@ mod tests { { init_test_logging(); let mut subject = AccountantBuilder::default() - .bootstrapper_config(make_bc_with_defaults()) + .bootstrapper_config(make_bc_with_defaults(TEST_DEFAULT_CHAIN)) .build(); subject.logger = Logger::new("ConfigChange"); @@ -4537,7 +4538,7 @@ mod tests { #[test] fn report_services_consumed_message_is_received() { init_test_logging(); - let config = make_bc_with_defaults(); + let config = make_bc_with_defaults(TEST_DEFAULT_CHAIN); let more_money_payable_params_arc = Arc::new(Mutex::new(vec![])); let payable_dao_mock = PayableDaoMock::new() .more_money_payable_params(more_money_payable_params_arc.clone()) @@ -4879,7 +4880,7 @@ mod tests { expected = "panic message (processed with: node_lib::sub_lib::utils::crash_request_analyzer)" )] fn accountant_can_be_crashed_properly_but_not_improperly() { - let mut config = make_bc_with_defaults(); + let mut config = make_bc_with_defaults(TEST_DEFAULT_CHAIN); config.crash_point = CrashPoint::Message; let accountant = AccountantBuilder::default() .bootstrapper_config(config) @@ -5936,7 +5937,7 @@ mod tests { let receivable_dao = ReceivableDaoMock::new().total_result(987_654_328_996); let system = System::new("test"); let subject = AccountantBuilder::default() - .bootstrapper_config(make_bc_with_defaults()) + .bootstrapper_config(make_bc_with_defaults(TEST_DEFAULT_CHAIN)) .payable_daos(vec![ForAccountantBody(payable_dao)]) .receivable_daos(vec![ForAccountantBody(receivable_dao)]) .build(); diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 03dad9942..dd23e05bd 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -385,6 +385,7 @@ mod tests { }; use crate::accountant::scanners::{ManulTriggerError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; + use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; use itertools::Itertools; use lazy_static::lazy_static; use masq_lib::logger::Logger; @@ -596,7 +597,7 @@ mod tests { #[test] fn resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let test_name = "resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered"; @@ -652,7 +653,7 @@ mod tests { fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( ) { init_test_logging(); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let test_name = "resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true"; let logger = Logger::new(test_name); @@ -687,7 +688,7 @@ mod tests { )] fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( ) { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let _ = subject .reschedule_on_error_resolver @@ -706,7 +707,7 @@ mod tests { init_test_logging(); let test_name = "resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan"; let logger = Logger::new(test_name); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let scanner = PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, }; @@ -740,7 +741,7 @@ mod tests { possible" )] fn pending_p_scan_attempt_if_no_consuming_wallet_found_mustnt_happen_if_not_initial_scan() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let scanner = PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }; @@ -795,7 +796,7 @@ mod tests { StartScanError::NothingToProcess, StartScanError::NoConsumingWalletFound, ]); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); test_forbidden_states(&subject, &inputs, false); test_forbidden_states(&subject, &inputs, true); @@ -805,7 +806,7 @@ mod tests { fn resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered() { let test_name = "resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered"; - let subject = ScanSchedulers::new(ScanIntervals::default(), false); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, false); test_what_if_externally_triggered( test_name, @@ -816,7 +817,7 @@ mod tests { #[test] fn any_automatic_scan_with_start_scan_error_is_fatal_for_retry_payables() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); ALL_START_SCAN_ERRORS.iter().for_each(|error| { let panic = catch_unwind(AssertUnwindSafe(|| { @@ -849,7 +850,7 @@ mod tests { fn resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered() { let test_name = "resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered"; - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); test_what_if_externally_triggered( test_name, @@ -864,7 +865,7 @@ mod tests { should never interfere with itself ScanAlreadyRunning { cross_scan_cause_opt: None, started_at:" )] fn resolve_hint_for_new_payables_if_scan_is_already_running_error_and_is_automatic_scan() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); let _ = subject .reschedule_on_error_resolver @@ -890,7 +891,7 @@ mod tests { ]); let logger = Logger::new(test_name); let test_log_handler = TestLogHandler::new(); - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(*TEST_SCAN_INTERVALS, true); inputs.errors.iter().for_each(|error| { let result = subject diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 81b612e47..2f777e57b 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -52,6 +52,7 @@ use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMoc use crate::test_utils::unshared_test_utils::make_bc_with_defaults; use ethereum_types::U64; use masq_lib::logger::Logger; +use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; @@ -431,7 +432,9 @@ impl AccountantBuilder { } pub fn build(self) -> Accountant { - let config = self.config_opt.unwrap_or(make_bc_with_defaults()); + let config = self + .config_opt + .unwrap_or(make_bc_with_defaults(TEST_DEFAULT_CHAIN)); let payable_dao_factory = self.payable_dao_factory_opt.unwrap_or( PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) @@ -967,13 +970,13 @@ impl BannedDaoMock { } pub fn bc_from_earning_wallet(earning_wallet: Wallet) -> BootstrapperConfig { - let mut bc = make_bc_with_defaults(); + let mut bc = make_bc_with_defaults(TEST_DEFAULT_CHAIN); bc.earning_wallet = earning_wallet; bc } pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> BootstrapperConfig { - let mut bc = make_bc_with_defaults(); + let mut bc = make_bc_with_defaults(TEST_DEFAULT_CHAIN); bc.consuming_wallet_opt = Some(consuming_wallet); bc.earning_wallet = earning_wallet; bc diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 8b24da722..61c5ff9c0 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -1167,7 +1167,7 @@ mod tests { log_level: LevelFilter::Off, crash_point: CrashPoint::None, dns_servers: vec![], - scan_intervals_opt: Some(ScanIntervals::default()), + scan_intervals_opt: Some(ScanIntervals::compute_default(TEST_DEFAULT_CHAIN)), automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 1a5cad399..3458a4140 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -38,7 +38,6 @@ use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; @@ -571,10 +570,17 @@ mod tests { use crate::node_test_utils::check_timestamp; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; - use crate::test_utils::recorder::{make_accountant_subs_from_recorder, make_blockchain_bridge_subs_from_recorder, make_recorder, peer_actors_builder}; + use crate::test_utils::recorder::{ + make_accountant_subs_from_recorder, make_blockchain_bridge_subs_from_recorder, + make_recorder, peer_actors_builder, + }; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use crate::test_utils::unshared_test_utils::{assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, SubsFactoryTestAddrLeaker, ZERO}; + use crate::test_utils::unshared_test_utils::{ + assert_on_initialization_with_panic_on_migration, configure_default_persistent_config, + prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, + SubsFactoryTestAddrLeaker, ZERO, + }; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; @@ -606,7 +612,7 @@ mod tests { } impl SubsFactory - for SubsFactoryTestAddrLeaker + for SubsFactoryTestAddrLeaker { fn make(&self, addr: &Addr) -> BlockchainBridgeSubs { self.send_leaker_msg_and_return_meaningless_subs( @@ -2205,4 +2211,4 @@ mod tests { assert_eq!(increase_gas_price_by_margin(1_000_000_000), 1_300_000_000); assert_eq!(increase_gas_price_by_margin(9_000_000_000), 11_700_000_000); } -} \ No newline at end of file +} diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index 71a0751b0..aa3943183 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -1233,6 +1233,7 @@ mod tests { vec![SocketAddr::new(IpAddr::from_str("1.2.3.4").unwrap(), 1111)]; let mut unprivileged_config = BootstrapperConfig::new(); //values from unprivileged config + let chain = unprivileged_config.blockchain_bridge_config.chain; let gas_price = 123; let blockchain_url_opt = Some("some.service@earth.abc".to_string()); let clandestine_port_opt = Some(44444); @@ -1252,7 +1253,7 @@ mod tests { unprivileged_config.earning_wallet = earning_wallet.clone(); unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); - unprivileged_config.scan_intervals_opt = Some(ScanIntervals::default()); + unprivileged_config.scan_intervals_opt = Some(ScanIntervals::compute_default(chain)); unprivileged_config.automatic_scans_enabled = true; unprivileged_config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; @@ -1276,7 +1277,7 @@ mod tests { assert_eq!(privileged_config.db_password_opt, db_password_opt); assert_eq!( privileged_config.scan_intervals_opt, - Some(ScanIntervals::default()) + Some(ScanIntervals::compute_default(chain)) ); assert_eq!(privileged_config.automatic_scans_enabled, true); assert_eq!( diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 3d0a79b6b..b03251842 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -21,7 +21,6 @@ use crate::node_configurator::{ data_directory_from_context, determine_user_specific_data, DirsWrapper, DirsWrapperReal, }; use crate::sub_lib::accountant::PaymentThresholds as PaymentThresholdsFromAccountant; -use crate::sub_lib::accountant::DEFAULT_SCAN_INTERVALS; use crate::sub_lib::neighborhood::NodeDescriptor; use crate::sub_lib::neighborhood::{NeighborhoodMode as NeighborhoodModeEnum, DEFAULT_RATE_PACK}; use crate::sub_lib::utils::make_new_multi_config; @@ -1083,12 +1082,16 @@ impl ValueRetriever for ScanIntervals { fn computed_default( &self, - _bootstrapper_config: &BootstrapperConfig, + bootstrapper_config: &BootstrapperConfig, pc: &dyn PersistentConfiguration, _db_password_opt: &Option, ) -> Option<(String, UiSetupResponseValueStatus)> { let pc_value = pc.scan_intervals().expectv("scan-intervals"); - payment_thresholds_rate_pack_and_scan_intervals(pc_value, *DEFAULT_SCAN_INTERVALS) + let chain = bootstrapper_config.blockchain_bridge_config.chain; + payment_thresholds_rate_pack_and_scan_intervals( + pc_value, + crate::sub_lib::accountant::ScanIntervals::compute_default(chain), + ) } fn is_required(&self, _params: &SetupCluster) -> bool { @@ -1208,7 +1211,9 @@ mod tests { use crate::daemon::dns_inspector::dns_inspector::DnsInspector; use crate::daemon::dns_inspector::DnsInspectionError; use crate::daemon::setup_reporter; - use crate::database::db_initializer::{DbInitializer, DbInitializerReal, DATABASE_FILE}; + use crate::database::db_initializer::{ + DbInitializer, DbInitializerReal, InitializationMode, DATABASE_FILE, + }; use crate::database::rusqlite_wrappers::ConnectionWrapperReal; use crate::db_config::config_dao::{ConfigDao, ConfigDaoReal}; use crate::db_config::persistent_configuration::{ @@ -1229,6 +1234,7 @@ mod tests { use crate::test_utils::unshared_test_utils::{ make_persistent_config_real_with_config_dao_null, make_pre_populated_mocked_directory_wrapper, make_simplified_multi_config, + TEST_SCAN_INTERVALS, }; use crate::test_utils::{assert_string_contains, rate_pack}; use core::option::Option; @@ -1335,15 +1341,19 @@ mod tests { "setup_reporter", "get_modified_setup_database_populated_only_requireds_set", ); + let chain = DEFAULT_CHAIN; + let mut init_config = DbInitializationConfig::test_default(); + if let InitializationMode::CreationAndMigration { external_data } = &mut init_config.mode { + external_data.chain = chain + } else { + panic!("unexpected initialization mode"); + } let data_dir = home_dir.join("data_dir"); - let chain_specific_data_dir = data_dir.join(DEFAULT_CHAIN.rec().literal_identifier); + let chain_specific_data_dir = data_dir.join(chain.rec().literal_identifier); std::fs::create_dir_all(&chain_specific_data_dir).unwrap(); let db_initializer = DbInitializerReal::default(); let conn = db_initializer - .initialize( - &chain_specific_data_dir, - DbInitializationConfig::test_default(), - ) + .initialize(&chain_specific_data_dir, init_config) .unwrap(); let mut config = PersistentConfigurationReal::from(conn); config.change_password(None, "password").unwrap(); @@ -1448,7 +1458,7 @@ mod tests { ), ( "scan-intervals", - &DEFAULT_SCAN_INTERVALS.to_string(), + &accountant::ScanIntervals::compute_default(chain).to_string(), Default, ), ("scans", "on", Default), @@ -3358,6 +3368,7 @@ mod tests { fn rate_pack_computed_default_when_persistent_config_like_default() { assert_computed_default_when_persistent_config_like_default( &RatePack {}, + None, DEFAULT_RATE_PACK.to_string(), ) } @@ -3437,15 +3448,19 @@ mod tests { #[test] fn scan_intervals_computed_default_when_persistent_config_like_default() { + let chain = DEFAULT_CHAIN; + let mut bootstrapper_config = BootstrapperConfig::new(); + bootstrapper_config.blockchain_bridge_config.chain = chain; assert_computed_default_when_persistent_config_like_default( &ScanIntervals {}, - *DEFAULT_SCAN_INTERVALS, + Some(bootstrapper_config), + accountant::ScanIntervals::compute_default(chain), ) } #[test] fn scan_intervals_computed_default_persistent_config_unequal_to_default() { - let mut scan_intervals = *DEFAULT_SCAN_INTERVALS; + let mut scan_intervals = *TEST_SCAN_INTERVALS; scan_intervals.payable_scan_interval = scan_intervals .payable_scan_interval .add(Duration::from_secs(15)); @@ -3469,6 +3484,7 @@ mod tests { fn payment_thresholds_computed_default_when_persistent_config_like_default() { assert_computed_default_when_persistent_config_like_default( &PaymentThresholds {}, + None, DEFAULT_PAYMENT_THRESHOLDS.to_string(), ) } @@ -3491,12 +3507,13 @@ mod tests { fn assert_computed_default_when_persistent_config_like_default( subject: &dyn ValueRetriever, + bootstrapper_config_opt: Option, default: T, ) where T: Display + PartialEq, { - let mut bootstrapper_config = BootstrapperConfig::new(); - //the rate_pack within the mode setting does not determine the result, so I just set a nonsense + let mut bootstrapper_config = bootstrapper_config_opt.unwrap_or(BootstrapperConfig::new()); + //the rate_pack within the mode setting does not affect the result, so I set nonsense bootstrapper_config.neighborhood_config.mode = NeighborhoodModeEnum::OriginateOnly(vec![], rate_pack(0)); let persistent_config = diff --git a/node/src/database/config_dumper.rs b/node/src/database/config_dumper.rs index 17e24899e..a1a435818 100644 --- a/node/src/database/config_dumper.rs +++ b/node/src/database/config_dumper.rs @@ -168,7 +168,8 @@ mod tests { use crate::db_config::typed_config_layer::encode_bytes; use crate::node_configurator::DirsWrapperReal; use crate::node_test_utils::DirsWrapperMock; - use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; + use crate::sub_lib::accountant; + use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::cryptde::PlainData; use crate::sub_lib::neighborhood::{NodeDescriptor, DEFAULT_RATE_PACK}; use crate::test_utils::database_utils::bring_db_0_back_to_life_and_return_connection; @@ -327,6 +328,7 @@ mod tests { .initialize(&database_path, DbInitializationConfig::panic_on_migration()) .unwrap(); let dao = ConfigDaoReal::new(conn); + let chain = Chain::PolyMainnet; assert_value("blockchainServiceUrl", "https://infura.io/ID", &map); assert_value("clandestinePort", "3456", &map); assert_encrypted_value( @@ -340,11 +342,7 @@ mod tests { "0x0123456789012345678901234567890123456789", &map, ); - assert_value( - "chainName", - Chain::PolyMainnet.rec().literal_identifier, - &map, - ); + assert_value("chainName", chain.rec().literal_identifier, &map); assert_value("gasPrice", "1", &map); assert_value( "pastNeighbors", @@ -365,8 +363,12 @@ mod tests { &map, ); assert_value("ratePack", &DEFAULT_RATE_PACK.to_string(), &map); - assert_value("scanIntervals", &DEFAULT_SCAN_INTERVALS.to_string(), &map); - assert!(output.ends_with("\n}\n")) //asserting that there is a blank line at the end + assert_value( + "scanIntervals", + &accountant::ScanIntervals::compute_default(chain).to_string(), + &map, + ); + assert!(output.ends_with("\n}\n")) // To assert a blank line at the end } #[test] @@ -510,7 +512,11 @@ mod tests { &map, ); assert_value("ratePack", &DEFAULT_RATE_PACK.to_string(), &map); - assert_value("scanIntervals", &DEFAULT_SCAN_INTERVALS.to_string(), &map); + assert_value( + "scanIntervals", + &accountant::ScanIntervals::compute_default(Chain::PolyMainnet).to_string(), + &map, + ); } #[test] @@ -586,6 +592,7 @@ mod tests { .initialize(&data_dir, DbInitializationConfig::panic_on_migration()) .unwrap(); let dao = Box::new(ConfigDaoReal::new(conn)); + let chain = Chain::PolyMainnet; assert_value("blockchainServiceUrl", "https://infura.io/ID", &map); assert_value("clandestinePort", "3456", &map); assert_encrypted_value( @@ -599,11 +606,7 @@ mod tests { "0x0123456789012345678901234567890123456789", &map, ); - assert_value( - "chainName", - Chain::PolyMainnet.rec().literal_identifier, - &map, - ); + assert_value("chainName", chain.rec().literal_identifier, &map); assert_value("gasPrice", "1", &map); assert_value( "pastNeighbors", @@ -624,7 +627,11 @@ mod tests { &map, ); assert_value("ratePack", &DEFAULT_RATE_PACK.to_string(), &map); - assert_value("scanIntervals", &DEFAULT_SCAN_INTERVALS.to_string(), &map); + assert_value( + "scanIntervals", + &accountant::ScanIntervals::compute_default(chain).to_string(), + &map, + ); } #[test] diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index 674786766..6eb69b4a6 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -4,7 +4,8 @@ use crate::database::rusqlite_wrappers::{ConnectionWrapper, ConnectionWrapperRea use crate::database::db_migrations::db_migrator::{DbMigrator, DbMigratorReal}; use crate::db_config::secure_config_layer::EXAMPLE_ENCRYPTED; use crate::neighborhood::DEFAULT_MIN_HOPS; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use crate::sub_lib::utils::db_connection_launch_panic; use masq_lib::blockchains::chains::Chain; @@ -137,7 +138,6 @@ impl DbInitializerReal { Self::create_payable_table(conn); Self::create_sent_payable_table(conn); Self::create_failed_payable_table(conn); - Self::create_pending_payable_table(conn); Self::create_receivable_table(conn); Self::create_banned_table(conn); } @@ -253,7 +253,7 @@ impl DbInitializerReal { Self::set_config_value( conn, "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(external_params.chain).to_string()), false, "scan intervals", ); @@ -311,27 +311,6 @@ impl DbInitializerReal { .expect("Can't create transaction hash index in failed payments"); } - pub fn create_pending_payable_table(conn: &Connection) { - conn.execute( - "create table if not exists pending_payable ( - rowid integer primary key, - transaction_hash text not null, - amount_high_b integer not null, - amount_low_b integer not null, - payable_timestamp integer not null, - attempt integer not null, - process_error text null - )", - [], - ) - .expect("Can't create pending_payable table"); - conn.execute( - "CREATE UNIQUE INDEX pending_payable_hash_idx ON pending_payable (transaction_hash)", - [], - ) - .expect("Can't create transaction hash index in pending payments"); - } - pub fn create_payable_table(conn: &Connection) { conn.execute( "create table if not exists payable ( @@ -736,50 +715,6 @@ mod tests { assert_no_index_exists_for_table(conn.as_ref(), "config") } - #[test] - fn db_initialize_creates_pending_payable_table() { - let home_dir = ensure_node_home_directory_does_not_exist( - "db_initializer", - "db_initialize_creates_pending_payable_table", - ); - let subject = DbInitializerReal::default(); - - let conn = subject - .initialize(&home_dir, DbInitializationConfig::test_default()) - .unwrap(); - - let mut stmt = conn - .prepare( - "SELECT rowid, - transaction_hash, - amount_high_b, - amount_low_b, - payable_timestamp, - attempt, - process_error - FROM pending_payable", - ) - .unwrap(); - let result = stmt.execute([]).unwrap(); - assert_eq!(result, 1); - let expected_key_words: &[&[&str]] = &[ - &["rowid", "integer", "primary", "key"], - &["transaction_hash", "text", "not", "null"], - &["amount_high_b", "integer", "not", "null"], - &["amount_low_b", "integer", "not", "null"], - &["payable_timestamp", "integer", "not", "null"], - &["attempt", "integer", "not", "null"], - &["process_error", "text", "null"], - ]; - assert_create_table_stm_contains_all_parts(&*conn, "pending_payable", expected_key_words); - let expected_key_words: &[&[&str]] = &[&["transaction_hash"]]; - assert_index_stm_is_coupled_with_right_parameter( - conn.as_ref(), - "pending_payable_hash_idx", - expected_key_words, - ) - } - #[test] fn db_initialize_creates_sent_payable_table() { let home_dir = ensure_node_home_directory_does_not_exist( @@ -1117,7 +1052,7 @@ mod tests { verify( &mut config_vec, "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(TEST_DEFAULT_CHAIN).to_string()), false, ); verify( diff --git a/node/src/database/db_migrations/migrations/migration_10_to_11.rs b/node/src/database/db_migrations/migrations/migration_10_to_11.rs index 5e4e18368..b3f2a157a 100644 --- a/node/src/database/db_migrations/migrations/migration_10_to_11.rs +++ b/node/src/database/db_migrations/migrations/migration_10_to_11.rs @@ -36,9 +36,12 @@ impl DatabaseMigration for Migrate_10_to_11 { status text not null )"; + let sql_statement_for_pending_payable = "drop table pending_payable"; + declaration_utils.execute_upon_transaction(&[ &sql_statement_for_sent_payable, &sql_statement_for_failed_payable, + &sql_statement_for_pending_payable, ]) } @@ -55,10 +58,7 @@ mod tests { use crate::database::test_utils::{ SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, SQL_ATTRIBUTES_FOR_CREATING_SENT_PAYABLE, }; - use crate::test_utils::database_utils::{ - assert_create_table_stm_contains_all_parts, assert_table_exists, - bring_db_0_back_to_life_and_return_connection, make_external_data, - }; + use crate::test_utils::database_utils::{assert_create_table_stm_contains_all_parts, assert_table_does_not_exist, assert_table_exists, bring_db_0_back_to_life_and_return_connection, make_external_data}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use std::fs::create_dir_all; @@ -103,6 +103,7 @@ mod tests { "failed_payable", SQL_ATTRIBUTES_FOR_CREATING_FAILED_PAYABLE, ); + assert_table_does_not_exist(connection.as_ref(), "pending_payable"); TestLogHandler::new().assert_logs_contain_in_order(vec![ "DbMigrator: Database successfully migrated from version 10 to 11", ]); diff --git a/node/src/database/db_migrations/migrations/migration_5_to_6.rs b/node/src/database/db_migrations/migrations/migration_5_to_6.rs index a5f902cb9..b32e3b2d0 100644 --- a/node/src/database/db_migrations/migrations/migration_5_to_6.rs +++ b/node/src/database/db_migrations/migrations/migration_5_to_6.rs @@ -2,8 +2,10 @@ use crate::database::db_migrations::db_migrator::DatabaseMigration; use crate::database::db_migrations::migrator_utils::DBMigDeclarator; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; +use masq_lib::blockchains::chains::Chain; #[allow(non_camel_case_types)] pub struct Migrate_5_to_6; @@ -19,9 +21,18 @@ impl DatabaseMigration for Migrate_5_to_6 { ); let statement_2 = Self::make_initialization_statement("rate_pack", &DEFAULT_RATE_PACK.to_string()); + let tx = declaration_utils.transaction(); + let chain = tx + .prepare("SELECT value FROM config WHERE name = 'chain_name'") + .expect("internal error") + .query_row([], |row| { + let res_str = row.get::<_, String>(0); + res_str.map(|str| Chain::from(str.as_str())) + }) + .expect("failed to read the chain from db"); let statement_3 = Self::make_initialization_statement( "scan_intervals", - &DEFAULT_SCAN_INTERVALS.to_string(), + &accountant::ScanIntervals::compute_default(chain).to_string(), ); declaration_utils.execute_upon_transaction(&[&statement_1, &statement_2, &statement_3]) } @@ -45,11 +56,13 @@ mod tests { use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; - use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; + use crate::sub_lib::accountant; + use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use crate::test_utils::database_utils::{ bring_db_0_back_to_life_and_return_connection, make_external_data, retrieve_config_row, }; + use masq_lib::blockchains::chains::Chain; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; #[test] @@ -59,15 +72,21 @@ mod tests { let db_path = dir_path.join(DATABASE_FILE); let _ = bring_db_0_back_to_life_and_return_connection(&db_path); let subject = DbInitializerReal::default(); - { - subject + let chain = { + let conn = subject .initialize_to_version( &dir_path, 5, DbInitializationConfig::create_or_migrate(make_external_data()), ) .unwrap(); - } + let chain = conn + .prepare("SELECT value FROM config WHERE name = 'chain_name'") + .unwrap() + .query_row([], |row| row.get::<_, String>(0)) + .unwrap(); + chain + }; let result = subject.initialize_to_version( &dir_path, @@ -88,7 +107,12 @@ mod tests { assert_eq!(encrypted, false); let (scan_intervals, encrypted) = retrieve_config_row(connection.as_ref(), "scan_intervals"); - assert_eq!(scan_intervals, Some(DEFAULT_SCAN_INTERVALS.to_string())); + assert_eq!( + scan_intervals, + Some( + accountant::ScanIntervals::compute_default(Chain::from(chain.as_str())).to_string() + ) + ); assert_eq!(encrypted, false); } } diff --git a/node/src/db_config/config_dao_null.rs b/node/src/db_config/config_dao_null.rs index f1fc58cd4..8cd87c075 100644 --- a/node/src/db_config/config_dao_null.rs +++ b/node/src/db_config/config_dao_null.rs @@ -4,13 +4,13 @@ use crate::database::db_initializer::DbInitializerReal; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoError, ConfigDaoRecord}; use crate::neighborhood::DEFAULT_MIN_HOPS; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::accountant; +use crate::sub_lib::accountant::DEFAULT_PAYMENT_THRESHOLDS; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::{CURRENT_SCHEMA_VERSION, DEFAULT_GAS_PRICE}; use std::collections::HashMap; - /* This class exists because the Daemon uses the same configuration code that the Node uses, and @@ -139,7 +139,10 @@ impl Default for ConfigDaoNull { ); data.insert( "scan_intervals".to_string(), - (Some(DEFAULT_SCAN_INTERVALS.to_string()), false), + ( + Some(accountant::ScanIntervals::compute_default(Chain::default()).to_string()), + false, + ), ); data.insert("max_block_count".to_string(), (None, false)); Self { data } @@ -208,7 +211,7 @@ mod tests { subject.get("scan_intervals").unwrap(), ConfigDaoRecord::new( "scan_intervals", - Some(&DEFAULT_SCAN_INTERVALS.to_string()), + Some(&accountant::ScanIntervals::compute_default(Chain::default()).to_string()), false ) ); diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 801aa4456..a66e74c5f 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -330,7 +330,7 @@ fn get_public_ip(multi_config: &MultiConfig) -> Result match IpAddr::from_str(&ip_str) { Ok(ip_addr) => Ok(ip_addr), - Err(_) => todo!("Drive in a better error message"), //Err(ConfiguratorError::required("ip", &format! ("blockety blip: '{}'", ip_str), + Err(_) => todo!("Drive in a better error message. The multiconfig wouldn't allow a bad format, though."), //Err(ConfiguratorError::required("ip", &format! ("blockety blip: '{}'", ip_str), }, None => Ok(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), // sentinel: means "Try Automap" } @@ -494,7 +494,7 @@ fn configure_accountant_config( |pc: &mut dyn PersistentConfiguration, curves| pc.set_payment_thresholds(curves), )?; - check_payment_thresholds(&payment_thresholds)?; + validate_payment_thresholds(&payment_thresholds)?; let scan_intervals = process_combined_params( "scan-intervals", @@ -505,6 +505,8 @@ fn configure_accountant_config( |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), )?; + validate_scan_intervals(&scan_intervals)?; + let automatic_scans_enabled = value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == "on"; @@ -515,12 +517,13 @@ fn configure_accountant_config( Ok(()) } -fn check_payment_thresholds( +fn validate_payment_thresholds( payment_thresholds: &PaymentThresholds, ) -> Result<(), ConfiguratorError> { if payment_thresholds.debt_threshold_gwei <= payment_thresholds.permanent_debt_allowed_gwei { let msg = format!( - "Value of DebtThresholdGwei ({}) must be bigger than PermanentDebtAllowedGwei ({})", + "Value of DebtThresholdGwei ({}) must be bigger than PermanentDebtAllowedGwei ({}) \ + as the smallest value", payment_thresholds.debt_threshold_gwei, payment_thresholds.permanent_debt_allowed_gwei ); return Err(ConfiguratorError::required("payment-thresholds", &msg)); @@ -534,6 +537,21 @@ fn check_payment_thresholds( Ok(()) } +fn validate_scan_intervals(scan_intervals: &ScanIntervals) -> Result<(), ConfiguratorError> { + if scan_intervals.payable_scan_interval < scan_intervals.pending_payable_scan_interval { + Err(ConfiguratorError::required( + "scan-intervals", + &format!( + "The PendingPayableScanInterval value ({} s) must not exceed the PayableScanInterval \ + value ({} s) and should ideally be approximately half of it", + scan_intervals.pending_payable_scan_interval.as_secs(), + scan_intervals.payable_scan_interval.as_secs()), + )) + } else { + Ok(()) + } +} + fn configure_rate_pack( multi_config: &MultiConfig, persist_config: &mut dyn PersistentConfiguration, @@ -2099,8 +2117,8 @@ mod tests { } #[test] - fn configure_accountant_config_discovers_invalid_payment_thresholds_params_combination_given_from_users_input( - ) { + fn configure_accountant_config_discovers_invalid_payment_thresholds_combination_in_users_input() + { let multi_config = make_simplified_multi_config([ "--payment-thresholds", "19999|10000|1000|20000|1000|20000", @@ -2116,7 +2134,8 @@ mod tests { &mut persistent_config, ); - let expected_msg = "Value of DebtThresholdGwei (19999) must be bigger than PermanentDebtAllowedGwei (20000)"; + let expected_msg = "Value of DebtThresholdGwei (19999) must be bigger than \ + PermanentDebtAllowedGwei (20000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2127,14 +2146,15 @@ mod tests { } #[test] - fn check_payment_thresholds_works_for_equal_debt_parameters() { + fn validate_payment_thresholds_works_for_equal_debt_parameters() { let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; payment_thresholds.permanent_debt_allowed_gwei = 10000; payment_thresholds.debt_threshold_gwei = 10000; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); - let expected_msg = "Value of DebtThresholdGwei (10000) must be bigger than PermanentDebtAllowedGwei (10000)"; + let expected_msg = "Value of DebtThresholdGwei (10000) must be bigger than \ + PermanentDebtAllowedGwei (10000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2145,14 +2165,15 @@ mod tests { } #[test] - fn check_payment_thresholds_works_for_too_small_debt_threshold() { + fn validate_payment_thresholds_works_for_too_small_debt_threshold() { let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; payment_thresholds.permanent_debt_allowed_gwei = 10000; payment_thresholds.debt_threshold_gwei = 9999; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); - let expected_msg = "Value of DebtThresholdGwei (9999) must be bigger than PermanentDebtAllowedGwei (10000)"; + let expected_msg = "Value of DebtThresholdGwei (9999) must be bigger than \ + PermanentDebtAllowedGwei (10000) as the smallest value"; assert_eq!( result, Err(ConfiguratorError::required( @@ -2163,7 +2184,8 @@ mod tests { } #[test] - fn check_payment_thresholds_does_not_permit_threshold_interval_longer_than_1_000_000_000_s() { + fn validate_payment_thresholds_does_not_permit_threshold_interval_longer_than_1_000_000_000_s() + { //this goes to the furthest extreme where the delta of debt limits is just 1 gwei, which, //if divided by the slope interval equal or longer 10^9 and rounded, gives 0 let mut payment_thresholds = *DEFAULT_PAYMENT_THRESHOLDS; @@ -2171,7 +2193,7 @@ mod tests { payment_thresholds.debt_threshold_gwei = 101; payment_thresholds.threshold_interval_sec = 1_000_000_001; - let result = check_payment_thresholds(&payment_thresholds); + let result = validate_payment_thresholds(&payment_thresholds); let expected_msg = "Value of ThresholdIntervalSec must not exceed 1,000,000,000 s"; assert_eq!( @@ -2186,6 +2208,28 @@ mod tests { assert_eq!(last_value_possible, -1) } + #[test] + fn configure_accountant_config_discovers_invalid_scan_intervals_combination_in_users_input() { + let multi_config = make_simplified_multi_config(["--scan-intervals", "600|601|600"]); + let mut bootstrapper_config = BootstrapperConfig::new(); + let mut persistent_config = + configure_default_persistent_config(ACCOUNTANT_CONFIG_PARAMS | MAPPING_PROTOCOL) + .set_scan_intervals_result(Ok(())); + + let result = configure_accountant_config( + &multi_config, + &mut bootstrapper_config, + &mut persistent_config, + ); + + let expected_msg = "The PendingPayableScanInterval value (601 s) must not exceed \ + the PayableScanInterval value (600 s) and should ideally be approximately half of it"; + assert_eq!( + result, + Err(ConfiguratorError::required("scan-intervals", expected_msg)) + ) + } + #[test] fn unprivileged_parse_args_with_invalid_consuming_wallet_private_key_reacts_correctly() { running_test(); diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 78379b890..317070c09 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -17,6 +17,7 @@ use crate::sub_lib::wallet::Wallet; use actix::Recipient; use actix::{Addr, Message}; use lazy_static::lazy_static; +use masq_lib::blockchains::chains::Chain; use masq_lib::ui_gateway::NodeFromUiMessage; use std::fmt::{Debug, Formatter}; use std::str::FromStr; @@ -37,11 +38,6 @@ lazy_static! { threshold_interval_sec: 21600, unban_below_gwei: 500_000_000, }; - pub static ref DEFAULT_SCAN_INTERVALS: ScanIntervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(600), - pending_payable_scan_interval: Duration::from_secs(60), - receivable_scan_interval: Duration::from_secs(600) - }; } //please, alphabetical order @@ -85,9 +81,15 @@ pub struct ScanIntervals { pub receivable_scan_interval: Duration, } -impl Default for ScanIntervals { - fn default() -> Self { - *DEFAULT_SCAN_INTERVALS +impl ScanIntervals { + pub fn compute_default(chain: Chain) -> Self { + Self { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs( + chain.rec().default_pending_payable_interval_sec, + ), + receivable_scan_interval: Duration::from_secs(600), + } } } @@ -207,12 +209,12 @@ mod tests { use crate::sub_lib::accountant::{ AccountantSubsFactoryReal, DetailedScanType, MessageIdGenerator, MessageIdGeneratorReal, PaymentThresholds, ScanIntervals, SubsFactory, DEFAULT_EARNING_WALLET, - DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS, MSG_ID_INCREMENTER, - TEMPORARY_CONSUMING_WALLET, + DEFAULT_PAYMENT_THRESHOLDS, MSG_ID_INCREMENTER, TEMPORARY_CONSUMING_WALLET, }; use crate::sub_lib::wallet::Wallet; use crate::test_utils::recorder::{make_accountant_subs_from_recorder, Recorder}; use actix::Actor; + use masq_lib::blockchains::chains::Chain; use masq_lib::messages::ScanType; use std::str::FromStr; use std::sync::atomic::Ordering; @@ -252,12 +254,6 @@ mod tests { threshold_interval_sec: 21600, unban_below_gwei: 500_000_000, }; - let scan_intervals_expected = ScanIntervals { - payable_scan_interval: Duration::from_secs(600), - pending_payable_scan_interval: Duration::from_secs(60), - receivable_scan_interval: Duration::from_secs(600), - }; - assert_eq!(*DEFAULT_SCAN_INTERVALS, scan_intervals_expected); assert_eq!(*DEFAULT_PAYMENT_THRESHOLDS, payment_thresholds_expected); assert_eq!(*DEFAULT_EARNING_WALLET, default_earning_wallet_expected); assert_eq!( @@ -310,4 +306,34 @@ mod tests { assert_eq!(id, 0) } + + #[test] + fn default_for_scan_intervals_can_be_computed() { + let chain_a = Chain::BaseMainnet; + let chain_b = Chain::PolyMainnet; + + let result_a = ScanIntervals::compute_default(chain_a); + let result_b = ScanIntervals::compute_default(chain_b); + + assert_eq!( + result_a, + ScanIntervals { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs( + chain_a.rec().default_pending_payable_interval_sec + ), + receivable_scan_interval: Duration::from_secs(600), + } + ); + assert_eq!( + result_b, + ScanIntervals { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs( + chain_b.rec().default_pending_payable_interval_sec + ), + receivable_scan_interval: Duration::from_secs(600), + } + ); + } } diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index 53a3e8488..bd26eb627 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -307,6 +307,7 @@ mod tests { use super::*; use crate::sub_lib::combined_parameters::CombinedParamsDataTypes::U128; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; + use crate::test_utils::unshared_test_utils::TEST_SCAN_INTERVALS; use std::panic::catch_unwind; #[test] @@ -455,7 +456,7 @@ mod tests { let panic_3 = catch_unwind(|| { let _: &[(&str, CombinedParamsDataTypes)] = - (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))).into(); + (&CombinedParams::ScanIntervals(Initialized(*TEST_SCAN_INTERVALS))).into(); }) .unwrap_err(); let panic_3_msg = panic_3.downcast_ref::().unwrap(); @@ -464,7 +465,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - ScanIntervals::default() + *TEST_SCAN_INTERVALS ) ); } @@ -502,7 +503,7 @@ mod tests { ); let panic_3 = catch_unwind(|| { - (&CombinedParams::ScanIntervals(Initialized(ScanIntervals::default()))) + (&CombinedParams::ScanIntervals(Initialized(*TEST_SCAN_INTERVALS))) .initialize_objects(HashMap::new()); }) .unwrap_err(); @@ -512,7 +513,7 @@ mod tests { panic_3_msg, &format!( "should be called only on uninitialized object, not: ScanIntervals(Initialized({:?}))", - ScanIntervals::default() + *TEST_SCAN_INTERVALS ) ); } diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index fb8ba3a83..a2b6d9ee1 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -109,9 +109,10 @@ pub fn assert_table_exists(conn: &dyn ConnectionWrapper, table_name: &str) { } pub fn assert_table_does_not_exist(conn: &dyn ConnectionWrapper, table_name: &str) { - let error_stm = conn - .prepare(&format!("select * from {}", table_name)) - .unwrap_err(); + let error_stm = match conn.prepare(&format!("select * from {}", table_name)) { + Ok(_) => panic!("Table {} should not exist, but it does", table_name), + Err(e) => e, + }; let error_msg = match error_stm { Error::SqliteFailure(_, Some(msg)) => msg, x => panic!("we expected SqliteFailure but we got: {:?}", x), diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 546149ae6..601ee7bd1 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -548,6 +548,7 @@ pub mod unshared_test_utils { use crossbeam_channel::{unbounded, Receiver, Sender}; use itertools::Either; use lazy_static::lazy_static; + use masq_lib::blockchains::chains::Chain; use masq_lib::constants::HTTP_PORT; use masq_lib::messages::{ToMessageBody, UiCrashRequest}; use masq_lib::multi_config::MultiConfig; @@ -642,6 +643,14 @@ pub mod unshared_test_utils { MultiConfig::new_test_only(arg_matches) } + lazy_static! { + pub static ref TEST_SCAN_INTERVALS: ScanIntervals = ScanIntervals { + payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(360), + receivable_scan_interval: Duration::from_secs(600), + }; + } + pub const ZERO: u32 = 0b0; pub const MAPPING_PROTOCOL: u32 = 0b000010; pub const ACCOUNTANT_CONFIG_PARAMS: u32 = 0b000100; @@ -686,16 +695,16 @@ pub mod unshared_test_utils { ) -> PersistentConfigurationMock { persistent_config_mock .payment_thresholds_result(Ok(PaymentThresholds::default())) - .scan_intervals_result(Ok(ScanIntervals::default())) + .scan_intervals_result(Ok(*TEST_SCAN_INTERVALS)) } pub fn make_persistent_config_real_with_config_dao_null() -> PersistentConfigurationReal { PersistentConfigurationReal::new(Box::new(ConfigDaoNull::default())) } - pub fn make_bc_with_defaults() -> BootstrapperConfig { + pub fn make_bc_with_defaults(chain: Chain) -> BootstrapperConfig { let mut config = BootstrapperConfig::new(); - config.scan_intervals_opt = Some(ScanIntervals::default()); + config.scan_intervals_opt = Some(ScanIntervals::compute_default(chain)); config.automatic_scans_enabled = true; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; config.payment_thresholds_opt = Some(PaymentThresholds::default()); From d2fd9cde476e2de36c27786a38e4538060f33e58 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 18 Sep 2025 11:35:15 +0530 Subject: [PATCH 06/48] GH-605: more and more changes --- node/src/accountant/scanners/mod.rs | 14 ++++++++++++-- .../src/accountant/scanners/payable_scanner/mod.rs | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index d58f21368..625dae312 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1077,12 +1077,22 @@ mod tests { #[test] fn finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err( + ) { + assert_finish_payable_scan_keeps_aware_flag_false_on_error(PayableScanType::New); + assert_finish_payable_scan_keeps_aware_flag_false_on_error(PayableScanType::Retry); + } + + fn assert_finish_payable_scan_keeps_aware_flag_false_on_error( + payable_scan_type: PayableScanType, ) { 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 = match payable_scan_type { + PayableScanType::New => "finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err_for_new_scan", + PayableScanType::Retry => "finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err_for_retry_scan", + }; 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); diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index c91b3c765..60bd91917 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -21,7 +21,7 @@ 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, }; @@ -171,7 +171,7 @@ impl PayableScanner { } fn handle_new(&self, batch_results: &BatchResults, logger: &Logger) { - let (sent, failed) = calculate_lengths(&batch_results); + let (sent, failed) = calculate_occurences(&batch_results); debug!( logger, "Processed new txs while sending to RPC: {}", @@ -186,7 +186,7 @@ impl PayableScanner { } fn handle_retry(&self, batch_results: &BatchResults, logger: &Logger) { - let (sent, failed) = calculate_lengths(&batch_results); + let (sent, failed) = calculate_occurences(&batch_results); debug!( logger, "Processed retried txs while sending to RPC: {}", From 52411c92b917f7b0f3084d1514de2e39d801c74c Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 18 Sep 2025 14:26:53 +0530 Subject: [PATCH 07/48] GH-605: bit more refactoring --- .../db_access_objects/test_utils.rs | 17 ++++ .../blockchain_interface_web3/utils.rs | 85 ++++++++----------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index 2fa6d1a65..9506282be 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -6,6 +6,7 @@ use crate::accountant::db_access_objects::failed_payable_dao::{ }; use crate::accountant::db_access_objects::sent_payable_dao::{Tx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplate; use crate::blockchain::test_utils::make_tx_hash; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, @@ -51,6 +52,14 @@ impl TxBuilder { self } + pub fn template(mut self, signable_tx_template: SignableTxTemplate) -> Self { + self.receiver_address_opt = Some(signable_tx_template.receiver_address); + self.amount_opt = Some(signable_tx_template.amount_in_wei); + self.gas_price_wei_opt = Some(signable_tx_template.gas_price_wei); + self.nonce_opt = Some(signable_tx_template.nonce); + self + } + pub fn status(mut self, status: TxStatus) -> Self { self.status_opt = Some(status); self @@ -123,6 +132,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 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 40a47f1ab..c6f17c91b 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -319,7 +319,7 @@ pub fn create_blockchain_agent_web3( mod tests { use super::*; 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::{ @@ -398,18 +398,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(), @@ -680,26 +677,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], @@ -881,30 +870,24 @@ 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 { + let failed_tx = FailedTxBuilder::default() + .hash(signed_tx_2.transaction_hash) + .template(template_2) + .timestamp(to_unix_timestamp(SystemTime::now())) + .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, - }; + }))) + .status(FailureStatus::RetryRequired) + .build(); BatchResults { sent_txs: vec![sent_tx], From 398bdd911a7af4aaceb28c0e7aa189b0e2405292 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 18 Sep 2025 15:35:26 +0530 Subject: [PATCH 08/48] GH-605: bit more changes --- .../blockchain_interface_web3/utils.rs | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) 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 c6f17c91b..60dcf4ebf 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -43,7 +43,7 @@ pub struct BlockchainAgentFutureResult { pub masq_token_balance: U256, } -fn return_sending_error(sent_txs: &Vec, error: &Web3Error) -> LocalPayableError { +fn return_sending_error(sent_txs: &[Tx], error: &Web3Error) -> LocalPayableError { LocalPayableError::Sending( sent_txs .iter() @@ -70,18 +70,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\ @@ -208,12 +210,12 @@ 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 { @@ -420,7 +422,7 @@ mod tests { amount: 1,000,000,000 wei, \ to 0x0000000000000000000000000077616c6c657431, \ nonce: 1, \ - gas price: 1 gwei" + gas price: 1,000,000,000 wei" )); } @@ -453,12 +455,34 @@ 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, template.amount_in_wei, + "Transaction {} amount mismatch", + index + ); + assert_eq!( + sent_tx.gas_price_wei, 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 + ) }) } @@ -637,7 +661,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) } }, } From e044dc46ff1735d6036cdec2f1ea378c9f5fdbe5 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 18 Sep 2025 15:43:06 +0530 Subject: [PATCH 09/48] GH-605: changing to & works --- node/src/blockchain/blockchain_agent/agent_web3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/blockchain/blockchain_agent/agent_web3.rs b/node/src/blockchain/blockchain_agent/agent_web3.rs index bbf5d377f..e4cffe637 100644 --- a/node/src/blockchain/blockchain_agent/agent_web3.rs +++ b/node/src/blockchain/blockchain_agent/agent_web3.rs @@ -537,7 +537,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() From 8227dc8341daf86e20959f104fa8b0a2ff46205a Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 18 Sep 2025 16:29:49 +0530 Subject: [PATCH 10/48] GH-605: further changes --- .../tx_templates/priced/new.rs | 2 +- .../tx_templates/priced/retry.rs | 2 +- .../scanners/pending_payable_scanner/utils.rs | 64 ------------------- .../blockchain/blockchain_agent/agent_web3.rs | 42 +++++------- 4 files changed, 18 insertions(+), 92 deletions(-) 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 255ba4b7b..6de54e4c9 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 97db24bf0..d97a1c5ca 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 @@ -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/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index f277a1c91..21909ca21 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -125,67 +125,3 @@ pub fn handle_none_receipt( .push(PendingPayableId::new(payable.rowid, payable.hash)); scan_report } - -#[cfg(test)] -mod tests { - - #[test] - fn requires_payments_retry_says_yes() { - todo!("complete this test with GH-604") - // let cases = vec![ - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - // confirmed: vec![make_pending_payable_fingerprint()], - // }, - // ]; - // - // cases.into_iter().enumerate().for_each(|(idx, case)| { - // let result = case.requires_payments_retry(); - // assert_eq!( - // result, true, - // "We expected true, but got false for case of idx {}", - // idx - // ) - // }) - } - - #[test] - fn requires_payments_retry_says_no() { - todo!("complete this test with GH-604") - // let report = PendingPayableScanReport { - // still_pending: vec![], - // failures: vec![], - // confirmed: vec![make_pending_payable_fingerprint()], - // }; - // - // let result = report.requires_payments_retry(); - // - // assert_eq!(result, false) - } -} diff --git a/node/src/blockchain/blockchain_agent/agent_web3.rs b/node/src/blockchain/blockchain_agent/agent_web3.rs index e4cffe637..d431e6877 100644 --- a/node/src/blockchain/blockchain_agent/agent_web3.rs +++ b/node/src/blockchain/blockchain_agent/agent_web3.rs @@ -322,7 +322,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 +345,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 +390,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 +425,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 +457,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 +492,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" From ff6e400de17680df930935469925c8fa2804c9ab Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 18 Sep 2025 16:56:53 +0530 Subject: [PATCH 11/48] GH-605: more TODOs --- node/src/accountant/test_utils.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index c8d9d89b5..5a14a1af2 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -321,6 +321,7 @@ impl AccountantBuilder { } pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { + // TODO: GH-605: Merge Cleanup match self.sent_payable_dao_factory_opt { None => { self.sent_payable_dao_factory_opt = @@ -336,6 +337,8 @@ impl AccountantBuilder { } pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { + // TODO: GH-605: Merge cleanup + match self.failed_payable_dao_factory_opt { None => { self.failed_payable_dao_factory_opt = From dc863ef54c59f11b9bc8e104e5dadd0cb5254af5 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 14:01:06 +0530 Subject: [PATCH 12/48] GH-605: derive Copy for PayableScanType --- node/src/accountant/mod.rs | 2 +- node/src/blockchain/blockchain_bridge.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 77444a480..d39945c70 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -139,7 +139,7 @@ pub struct ReportTransactionReceipts { pub response_skeleton_opt: Option, } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Copy, Clone)] pub enum PayableScanType { New, Retry, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 3eb71f0ac..0afb6d305 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -312,8 +312,6 @@ impl BlockchainBridge { 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"); }; @@ -326,7 +324,7 @@ impl BlockchainBridge { payment_procedure_result: Self::payment_procedure_result_from_error( e.clone(), ), - payable_scan_type: payable_scan_type_for_err, + payable_scan_type, response_skeleton_opt: skeleton_opt, }); format!("ReportAccountsPayable: {}", e) From 9fd9ee7c7f88e3a361bca8a36ac50f134a82b183 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 14:18:55 +0530 Subject: [PATCH 13/48] GH-605: add scan_type function --- node/src/blockchain/blockchain_bridge.rs | 7 +------ node/src/sub_lib/blockchain_bridge.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 0afb6d305..d101f7568 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -305,12 +305,7 @@ impl BlockchainBridge { .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 = msg.scan_type(); let send_message_if_failure = move |msg: SentPayables| { sent_payable_subs.try_send(msg).expect("Accountant is dead"); diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 8ce62e467..1b2fca21b 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -3,7 +3,9 @@ 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::sub_lib::peer_actors::BindMessage; @@ -60,6 +62,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 { From 9ba398e5a4967666638e336706a29609184554f1 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 14:34:00 +0530 Subject: [PATCH 14/48] GH-605: comment out unused code in test --- node/src/blockchain/blockchain_bridge.rs | 31 ++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index d101f7568..1a21ba66b 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1018,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( @@ -1037,16 +1037,17 @@ mod tests { 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 - }] - ); + // 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, ScanType::Payables); assert_eq!( scan_error_msg.response_skeleton_opt, @@ -1062,7 +1063,7 @@ mod tests { "FailedTx { hash: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c," )); assert!(scan_error_msg.msg.contains("reason: Submission(Local(Transport(\"Error(IncompleteMessage)\"))), status: RetryRequired }")); - assert_eq!(accountant_recording.len(), 3); + assert_eq!(accountant_recording.len(), 2); } #[test] From 80711d4c0814ef2827ef288c91872e0d63c87a26 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 14:50:35 +0530 Subject: [PATCH 15/48] GH-605: refactor the code a bit --- node/src/blockchain/blockchain_bridge.rs | 40 +++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 1a21ba66b..a178d8843 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -300,36 +300,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 sent_payable_subs_err = sent_payable_subs_success.clone(); let payable_scan_type = msg.scan_type(); - 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(); - 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, - response_skeleton_opt: skeleton_opt, - }); + sent_payable_subs_success + .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_err + .try_send(SentPayables { + payment_procedure_result: Ok(batch_results), + payable_scan_type, + response_skeleton_opt: skeleton_opt, + }) + .expect("Accountant is dead"); + Ok(()) }), ) @@ -1144,7 +1146,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] From 4ac6418e78b05553c60834cfb0e37afb6a33e216 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 15:10:15 +0530 Subject: [PATCH 16/48] GH-605: refactor signable_tx_templates_can_be_created_from_priced_retry_tx_templates --- .../payable_scanner/tx_templates/signable/mod.rs | 10 +++++----- .../payable_scanner/tx_templates/test_utils.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) 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 f4a05e85f..1e753866e 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 @@ -146,18 +146,18 @@ 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[index].base.receiver_address + retries[tx_order].base.receiver_address ); assert_eq!(signable.nonce, nonce + i as u64); - assert_eq!(signable.amount_in_wei, retries[index].base.amount_in_wei); + assert_eq!(signable.amount_in_wei, retries[tx_order].base.amount_in_wei); assert_eq!( signable.gas_price_wei, - retries[index].computed_gas_price_wei + retries[tx_order].computed_gas_price_wei ); }); } 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 9dc3fb413..22d1cd792 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 @@ -41,8 +41,8 @@ pub fn make_priced_retry_tx_template(n: u64) -> PricedRetryTxTemplate { pub fn make_signable_tx_template(n: u64) -> SignableTxTemplate { SignableTxTemplate { receiver_address: make_address(1), - amount_in_wei: n as u128 * 1000, - gas_price_wei: n as u128 * 100, + amount_in_wei: n as u128 * 1_000, + gas_price_wei: n as u128 * 1_000_000, nonce: n, } } @@ -50,8 +50,8 @@ pub fn make_signable_tx_template(n: u64) -> SignableTxTemplate { 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() } From 83d615537146d9c528cc6f68213ea49f2d34ed78 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 15:24:41 +0530 Subject: [PATCH 17/48] GH-605: the helper functions have more realistic args name --- .../payable_scanner/tx_templates/signable/mod.rs | 1 - .../payable_scanner/tx_templates/test_utils.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) 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 1e753866e..fa4c8cb1e 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 @@ -132,7 +132,6 @@ mod tests { #[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), 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 22d1cd792..b91eaed76 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,20 +30,20 @@ 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 * 1_000, - gas_price_wei: n as u128 * 1_000_000, - nonce: n, + amount_in_wei: nonce as u128 * 1_000, + gas_price_wei: nonce as u128 * 1_000_000, + nonce, } } From 141d10a77d85733bdcbd39b6ede249eb7789d3d2 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 15:41:41 +0530 Subject: [PATCH 18/48] GH-605: more refactoring of signable_tx_templates_can_be_created_from_priced_retry_tx_templates --- .../tx_templates/signable/mod.rs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) 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 fa4c8cb1e..850973997 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 @@ -149,14 +149,26 @@ mod tests { .enumerate() .for_each(|(i, (signable, tx_order))| { assert_eq!( - signable.receiver_address, - retries[tx_order].base.receiver_address + 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); - assert_eq!(signable.amount_in_wei, retries[tx_order].base.amount_in_wei); assert_eq!( - signable.gas_price_wei, - retries[tx_order].computed_gas_price_wei + signable.nonce, + nonce + i as u64, + "Element {} (tx_order {}): nonce mismatch", + i, + tx_order + ); + assert_eq!( + signable.amount_in_wei, retries[tx_order].base.amount_in_wei, + "Element {} (tx_order {}): amount_in_wei mismatch", + i, tx_order + ); + assert_eq!( + signable.gas_price_wei, retries[tx_order].computed_gas_price_wei, + "Element {} (tx_order {}): gas_price_wei mismatch", + i, tx_order ); }); } From 4f9104541081c6799bdb078de4b4d0d494bf5c91 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 15:57:50 +0530 Subject: [PATCH 19/48] GH-605: more refactored changes --- .../tx_templates/signable/mod.rs | 67 +++++++++++++------ 1 file changed, 48 insertions(+), 19 deletions(-) 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 850973997..0a83e864e 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,6 +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 crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ + PricedNewTxTemplate, PricedNewTxTemplates, +}; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::{ + PricedRetryTxTemplate, PricedRetryTxTemplates, +}; use bytes::Buf; use itertools::{Either, Itertools}; use std::ops::Deref; @@ -14,6 +18,28 @@ pub struct SignableTxTemplate { pub nonce: u64, } +impl From<(&PricedNewTxTemplate, u64)> for SignableTxTemplate { + fn from((template, nonce): (&PricedNewTxTemplate, u64)) -> Self { + SignableTxTemplate { + receiver_address: template.base.receiver_address, + amount_in_wei: template.base.amount_in_wei, + gas_price_wei: template.computed_gas_price_wei, + nonce, + } + } +} + +impl From<(&PricedRetryTxTemplate, u64)> for SignableTxTemplate { + fn from((template, nonce): (&PricedRetryTxTemplate, u64)) -> Self { + SignableTxTemplate { + receiver_address: template.base.receiver_address, + amount_in_wei: template.base.amount_in_wei, + gas_price_wei: template.computed_gas_price_wei, + nonce, + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct SignableTxTemplates(pub Vec); @@ -42,12 +68,7 @@ impl SignableTxTemplates { 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, - }) + .map(|(i, template)| SignableTxTemplate::from((template, latest_nonce + i as u64))) .collect() } @@ -56,12 +77,7 @@ impl SignableTxTemplates { .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, - }) + .map(|(i, template)| SignableTxTemplate::from((template, latest_nonce + i as u64))) .collect() } @@ -121,11 +137,24 @@ 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", + ); }); } From 896f7d0c11e406e613c49bc932d2d4b1115faca0 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 16:13:23 +0530 Subject: [PATCH 20/48] GH-605: further refactoring changes --- .../tx_templates/signable/mod.rs | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) 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 0a83e864e..7bb488a5d 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 @@ -19,22 +19,22 @@ pub struct SignableTxTemplate { } impl From<(&PricedNewTxTemplate, u64)> for SignableTxTemplate { - fn from((template, nonce): (&PricedNewTxTemplate, u64)) -> Self { + fn from((priced_new_tx_template, nonce): (&PricedNewTxTemplate, u64)) -> Self { SignableTxTemplate { - receiver_address: template.base.receiver_address, - amount_in_wei: template.base.amount_in_wei, - gas_price_wei: template.computed_gas_price_wei, + 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((template, nonce): (&PricedRetryTxTemplate, u64)) -> Self { + fn from((priced_retry_tx_template, nonce): (&PricedRetryTxTemplate, u64)) -> Self { SignableTxTemplate { - receiver_address: template.base.receiver_address, - amount_in_wei: template.base.amount_in_wei, - gas_price_wei: template.computed_gas_price_wei, + 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, } } @@ -49,37 +49,41 @@ impl FromIterator for SignableTxTemplates { } } -impl SignableTxTemplates { - pub fn new( - priced_tx_templates: Either, - latest_nonce: u64, - ) -> Self { - match priced_tx_templates { - Either::Left(priced_new_tx_templates) => { - Self::from_new_txs(priced_new_tx_templates, latest_nonce) - } - Either::Right(priced_retry_tx_templates) => { - Self::from_retry_txs(priced_retry_tx_templates, latest_nonce) - } - } - } - - fn from_new_txs(templates: PricedNewTxTemplates, latest_nonce: u64) -> Self { - templates +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() } +} - fn from_retry_txs(templates: PricedRetryTxTemplates, latest_nonce: u64) -> Self { - templates +impl From<(PricedRetryTxTemplates, u64)> for SignableTxTemplates { + fn from((priced_retry_tx_templates, latest_nonce): (PricedRetryTxTemplates, u64)) -> Self { + priced_retry_tx_templates .reorder_by_nonces(latest_nonce) .iter() .enumerate() .map(|(i, template)| SignableTxTemplate::from((template, latest_nonce + i as u64))) .collect() } +} + +impl SignableTxTemplates { + pub fn new( + priced_tx_templates: Either, + latest_nonce: u64, + ) -> Self { + match priced_tx_templates { + Either::Left(priced_new_tx_templates) => { + Self::from((priced_new_tx_templates, latest_nonce)) + } + Either::Right(priced_retry_tx_templates) => { + Self::from((priced_retry_tx_templates, latest_nonce)) + } + } + } pub fn nonce_range(&self) -> (u64, u64) { let sorted: Vec<&SignableTxTemplate> = self From 18e0a540b38b4090347c4f471870cfe57b80b8f3 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 19 Sep 2025 16:43:56 +0530 Subject: [PATCH 21/48] GH-605: handle_batch_results has been renamed --- .../scanners/payable_scanner/mod.rs | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index 60bd91917..22bae8e87 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -163,14 +163,18 @@ 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) { + fn handle_batch_results_for_new_scan(&self, batch_results: &BatchResults, logger: &Logger) { let (sent, failed) = calculate_occurences(&batch_results); debug!( logger, @@ -185,7 +189,7 @@ impl PayableScanner { } } - fn handle_retry(&self, batch_results: &BatchResults, logger: &Logger) { + fn handle_batch_results_for_retry_scan(&self, batch_results: &BatchResults, logger: &Logger) { let (sent, failed) = calculate_occurences(&batch_results); debug!( logger, @@ -572,11 +576,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 +593,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 +623,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 +647,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 +675,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}")); From cf0938a56aabc750890f6d5074561c61421623bc Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Sat, 20 Sep 2025 11:58:45 +0530 Subject: [PATCH 22/48] GH-605: few more changes --- node/src/blockchain/errors/validation_status.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index aaa1c7ee8..3feecd1a1 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -230,12 +230,12 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::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::Io), &clock); attempts2 = attempts2.add_attempt( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Signing), &clock, From 9308d374b2c385bb87aab6623df1290cad68c076 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Sat, 20 Sep 2025 12:12:40 +0530 Subject: [PATCH 23/48] GH-605: few more changes --- node/src/accountant/mod.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index d39945c70..d50bc3410 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -4866,14 +4866,11 @@ 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 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(())); @@ -4890,6 +4887,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 +4919,15 @@ 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(())); @@ -4950,6 +4947,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,9 +4979,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] From 73395d8f386a1d90fea8275145b0870c13a42937 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Mon, 22 Sep 2025 12:37:29 +0530 Subject: [PATCH 24/48] GH-605: Review 4 --- node/src/accountant/scanners/mod.rs | 21 ++++++++++++------- .../tx_templates/test_utils.rs | 1 + node/src/accountant/test_utils.rs | 4 ++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 625dae312..bb24ced96 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1078,24 +1078,29 @@ mod tests { #[test] fn finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err( ) { - assert_finish_payable_scan_keeps_aware_flag_false_on_error(PayableScanType::New); - assert_finish_payable_scan_keeps_aware_flag_false_on_error(PayableScanType::Retry); + 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 assert_finish_payable_scan_keeps_aware_flag_false_on_error( + 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 = match payable_scan_type { - PayableScanType::New => "finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err_for_new_scan", - PayableScanType::Retry => "finish_payable_scan_keeps_the_aware_of_unresolved_pending_payable_flag_as_false_in_case_of_err_for_retry_scan", - }; + 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, 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); 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 b91eaed76..6a95732cc 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 @@ -31,6 +31,7 @@ pub fn make_priced_new_tx_template(n: u64) -> PricedNewTxTemplate { } pub fn make_priced_retry_tx_template(prev_nonce: u64) -> PricedRetryTxTemplate { + // TODO: GH-605: During the merge, check against fns used by Bert and keep only one version of the two. PricedRetryTxTemplate { base: BaseTxTemplate::from(&make_payable_account(prev_nonce)), prev_nonce, diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 5a14a1af2..0a0164891 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -321,7 +321,7 @@ impl AccountantBuilder { } pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { - // TODO: GH-605: Merge Cleanup + // TODO: GH-605: 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 = @@ -337,7 +337,7 @@ impl AccountantBuilder { } pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { - // TODO: GH-605: Merge cleanup + // TODO: GH-605: Merge cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 match self.failed_payable_dao_factory_opt { None => { From 7522708c38da49fad8936371e5eabdc83579f5e0 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Mon, 22 Sep 2025 13:07:29 +0530 Subject: [PATCH 25/48] GH-605: Bug bot error --- node/src/blockchain/blockchain_bridge.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index a178d8843..10f082807 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -311,7 +311,7 @@ impl BlockchainBridge { Box::new( self.process_payments(msg.agent, msg.priced_templates) .map_err(move |e: LocalPayableError| { - sent_payable_subs_success + sent_payable_subs_err .try_send(SentPayables { payment_procedure_result: Self::payment_procedure_result_from_error( e.clone(), @@ -324,7 +324,7 @@ impl BlockchainBridge { format!("ReportAccountsPayable: {}", e) }) .and_then(move |batch_results| { - sent_payable_subs_err + sent_payable_subs_success .try_send(SentPayables { payment_procedure_result: Ok(batch_results), payable_scan_type, From 794d23a8d451462f953fd7d458268f1a8389cc24 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Wed, 24 Sep 2025 14:06:39 +0530 Subject: [PATCH 26/48] GH-605: reduced errors to 97 --- .../db_access_objects/failed_payable_dao.rs | 31 +- .../db_access_objects/payable_dao.rs | 27 +- .../db_access_objects/sent_payable_dao.rs | 129 ++++--- .../db_access_objects/test_utils.rs | 15 +- .../src/accountant/db_access_objects/utils.rs | 12 +- node/src/accountant/mod.rs | 141 +++----- node/src/accountant/scanners/mod.rs | 336 ++++++++---------- .../scanners/payable_scanner/finish_scan.rs | 2 +- .../scanners/payable_scanner/mod.rs | 13 +- .../tx_templates/initial/retry.rs | 8 +- .../scanners/payable_scanner/utils.rs | 6 +- .../scanners/pending_payable_scanner/mod.rs | 32 +- .../tx_receipt_interpreter.rs | 5 +- .../scanners/pending_payable_scanner/utils.rs | 2 +- node/src/accountant/test_utils.rs | 277 +++------------ node/src/actor_system_factory.rs | 2 - node/src/blockchain/blockchain_bridge.rs | 89 ++--- .../blockchain_interface_web3/mod.rs | 28 +- .../blockchain_interface_web3/utils.rs | 25 +- .../data_structures/mod.rs | 10 +- .../blockchain/blockchain_interface/mod.rs | 23 +- node/src/blockchain/errors/rpc_errors.rs | 10 +- .../blockchain/errors/validation_status.rs | 37 +- node/src/blockchain/test_utils.rs | 18 - node/src/sub_lib/accountant.rs | 2 - 25 files changed, 525 insertions(+), 755 deletions(-) diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index f1eb7d586..d8ba7369e 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -1,13 +1,13 @@ // 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, TxRecordWithHash, VigilantRusqliteFlatten, + sql_values_of_failed_tx, DaoFactoryReal, TxHash, TxIdentifiers, VigilantRusqliteFlatten, }; use crate::accountant::db_access_objects::Transaction; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, join_with_separator}; +use crate::accountant::{checked_conversion, comma_joined_stringifiable, join_with_separator}; use crate::blockchain::errors::rpc_errors::{AppRpcError, AppRpcErrorKind}; -use crate::blockchain::errors::validation_status::ValidationStatus; +use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; use crate::database::rusqlite_wrappers::ConnectionWrapper; use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; @@ -29,7 +29,7 @@ pub enum FailedPayableDaoError { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum FailureReason { - Submission(AppRpcError), + Submission(AppRpcErrorKind), Reverted, PendingTooLong, } @@ -76,12 +76,6 @@ 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)] pub struct FailedTx { pub hash: TxHash, @@ -142,14 +136,14 @@ impl Ord for FailedTx { } } -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 { 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()), status: FailureStatus::RetryRequired, @@ -451,13 +445,16 @@ mod tests { }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; use crate::accountant::db_access_objects::Transaction; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::blockchain::errors::rpc_errors::LocalError::Decoder; - use crate::blockchain::errors::rpc_errors::{AppRpcError, AppRpcErrorKind}; + use crate::blockchain::errors::rpc_errors::{ + AppRpcError, 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_tx_hash, ValidationFailureClockMock}; + use crate::blockchain::test_utils::{make_address, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index cb31b6282..2951b73a8 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -1,16 +1,22 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use std::collections::{BTreeSet}; +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, RowId, TopStmConfig, TxHash, VigilantRusqliteFlatten}; +use crate::accountant::db_access_objects::utils::{ + from_unix_timestamp, sum_i128_values_from_table, to_unix_timestamp, AssemblerFeeder, + CustomQuery, DaoFactoryReal, RangeStmConfig, RowId, TopStmConfig, TxHash, + VigilantRusqliteFlatten, +}; 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::{checked_conversion, sign_conversion, PendingPayableId}; -use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; +use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; +use crate::accountant::{ + checked_conversion, join_with_separator, sign_conversion, PendingPayableId, +}; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; use ethabi::Address; @@ -21,10 +27,10 @@ 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::time::SystemTime; -use web3::types::{Address, H256}; -use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; +use web3::types::H256; #[derive(Debug, PartialEq, Eq)] pub enum PayableDaoError { @@ -555,13 +561,15 @@ impl TableNameDAO for PayableDaoReal { #[cfg(test)] mod tests { use super::*; + use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition::ByAddresses; use crate::accountant::db_access_objects::sent_payable_dao::SentTx; + use crate::accountant::db_access_objects::test_utils::make_sent_tx; use crate::accountant::db_access_objects::utils::{ current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, }; use crate::accountant::gwei_to_wei; use crate::accountant::test_utils::{ - assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, make_sent_tx, + assert_account_creation_fn_fails_on_finding_wrong_columns_and_value_types, trick_rusqlite_with_read_only_conn, }; use crate::blockchain::test_utils::make_tx_hash; @@ -569,6 +577,7 @@ mod tests { DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; use crate::database::rusqlite_wrappers::ConnectionWrapperReal; + use crate::database::test_utils::ConnectionWrapperMock; use crate::test_utils::make_wallet; use itertools::Itertools; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; @@ -577,7 +586,7 @@ mod tests { use rusqlite::{Connection, OpenFlags}; use std::path::Path; use std::str::FromStr; - use crate::database::test_utils::ConnectionWrapperMock; + use time::Duration; #[test] fn more_money_payable_works_for_new_address() { @@ -971,7 +980,7 @@ mod tests { // TODO argument will be eliminated in GH-662 None, ); - let mut sent_tx = make_sent_tx((idx as u64 + 1) * 1234); + let mut sent_tx = make_sent_tx((idx as u32 + 1) * 1234); sent_tx.hash = test_inputs.hash; sent_tx.amount_minor = test_inputs.balance_change; sent_tx.receiver_address = test_inputs.receiver_wallet; 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 b991d0f9a..3d964f41a 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -1,19 +1,23 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use std::collections::{HashMap, HashSet}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use ethereum_types::{H256}; -use web3::types::Address; -use masq_lib::utils::ExpectValue; -use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::accountant::db_access_objects::utils::{TxHash, TxIdentifiers}; +use crate::accountant::db_access_objects::utils::{ + 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::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; +use crate::accountant::{checked_conversion, comma_joined_stringifiable, 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 crate::accountant::db_access_objects::failed_payable_dao::ValidationStatus; +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use web3::types::Address; #[derive(Debug, PartialEq, Eq)] pub enum SentPayableDaoError { @@ -35,7 +39,7 @@ pub struct SentTx { pub status: TxStatus, } -impl Transaction for Tx { +impl Transaction for SentTx { fn hash(&self) -> TxHash { self.hash } @@ -45,7 +49,7 @@ impl Transaction for Tx { } fn amount(&self) -> u128 { - self.amount + self.amount_minor } fn timestamp(&self) -> i64 { @@ -53,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 { @@ -65,20 +69,20 @@ impl Transaction for Tx { } } -impl PartialOrd for Tx { +impl PartialOrd for SentTx { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for Tx { +impl Ord for SentTx { 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)) + .then_with(|| other.amount_minor.cmp(&self.amount_minor)) } } @@ -159,13 +163,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, ) -> Result<(), SentPayableDaoError>; - fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError>; + //TODO potentially atomically fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError>; } @@ -220,7 +227,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); } @@ -351,7 +358,7 @@ 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); } @@ -517,28 +524,40 @@ impl SentPayableDaoFactory for DaoFactoryReal { #[cfg(test)] mod tests { - use std::collections::{HashMap, HashSet}; - use std::ops::Add; - use std::str::FromStr; - use std::sync::{Arc, Mutex}; - use std::time::{Duration, UNIX_EPOCH}; - use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, TxConfirmation, TxStatus}; + use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ + ByHash, ByNonce, IsPending, + }; + use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{ + EmptyInput, PartialExecution, + }; + use crate::accountant::db_access_objects::sent_payable_dao::{ + Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, + 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::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 masq_lib::test_utils::utils::ensure_node_home_directory_exists; - use rusqlite::{Connection}; - use crate::accountant::db_access_objects::failed_payable_dao::{ValidationStatus}; - use crate::accountant::db_access_objects::sent_payable_dao::RetrieveCondition::{ByHash, IsPending}; - use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; - use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; - use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; - use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; - use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; + use rusqlite::Connection; + use std::collections::{BTreeSet, HashMap, HashSet}; + use std::ops::{Add, Sub}; + use std::str::FromStr; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] fn insert_new_records_works() { @@ -1188,7 +1207,7 @@ mod tests { let mut tx3 = make_sent_tx(123); tx3.status = TxStatus::Pending(ValidationStatus::Waiting); subject - .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2.clone(), tx3.clone()])) .unwrap(); let hashmap = HashMap::from([ ( @@ -1554,30 +1573,30 @@ mod tests { #[test] fn tx_ordering_works() { - let tx1 = Tx { + let tx1 = SentTx { hash: make_tx_hash(1), receiver_address: make_address(1), - amount: 100, + amount_minor: 100, timestamp: 1000, - gas_price_wei: 10, + gas_price_minor: 10, nonce: 1, status: TxStatus::Pending(ValidationStatus::Waiting), }; - let tx2 = Tx { + let tx2 = SentTx { hash: make_tx_hash(2), receiver_address: make_address(2), - amount: 200, + amount_minor: 200, timestamp: 1000, - gas_price_wei: 20, + gas_price_minor: 20, nonce: 1, status: TxStatus::Pending(ValidationStatus::Waiting), }; - let tx3 = Tx { + let tx3 = SentTx { hash: make_tx_hash(3), receiver_address: make_address(3), - amount: 100, + amount_minor: 100, timestamp: 2000, - gas_price_wei: 30, + gas_price_minor: 30, nonce: 2, status: TxStatus::Pending(ValidationStatus::Waiting), }; @@ -1595,27 +1614,27 @@ mod tests { 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 06bffe7c5..02801b2c8 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -6,6 +6,9 @@ use crate::accountant::db_access_objects::failed_payable_dao::{ }; use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplate; +use crate::blockchain::errors::validation_status::ValidationStatus; +use crate::blockchain::test_utils::make_tx_hash; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; @@ -169,7 +172,7 @@ pub fn make_failed_tx(n: u32) -> FailedTx { .build() } -pub fn make_sent_tx(n: u32) -> Tx { +pub fn make_sent_tx(n: u32) -> SentTx { let n = n * 2; // Always Even TxBuilder::default() .hash(make_tx_hash(n)) @@ -177,14 +180,14 @@ pub fn make_sent_tx(n: u32) -> Tx { .build() } -pub fn assert_on_sent_txs(left: Vec, right: Vec) { +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.amount_minor, t2.amount_minor); + assert_eq!(t1.gas_price_minor, t2.gas_price_minor); assert_eq!(t1.nonce, t2.nonce); assert_eq!(t1.status, t2.status); assert!((t1.timestamp - t2.timestamp).abs() < 10); @@ -197,8 +200,8 @@ pub fn assert_on_failed_txs(left: Vec, right: Vec) { left.iter().zip(right).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 a007fed6b..98c14ac3e 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/mod.rs b/node/src/accountant/mod.rs index 39a1b9a0a..5cac1b70a 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -22,11 +22,26 @@ use crate::accountant::db_access_objects::utils::{ use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, }; -use crate::accountant::scanners::{StartScanError, Scanners}; -use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; +use crate::accountant::scanners::payable_scanner::msgs::{ + InitialTemplatesMessage, PricedTemplatesMessage, +}; +use crate::accountant::scanners::payable_scanner::utils::NextScanToRun; +use crate::accountant::scanners::pending_payable_scanner::utils::{ + PendingPayableScanResult, Retry, TxHashByTable, +}; +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::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError; -use crate::blockchain::blockchain_interface::data_structures::{BatchResults, BlockchainTransaction}; +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::DaoFactories; @@ -62,7 +77,7 @@ use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; #[cfg(test)] use std::default::Default; use std::fmt::Display; @@ -72,11 +87,6 @@ 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 @@ -1167,7 +1177,10 @@ impl Accountant { comma_joined_stringifiable(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } - match self.sent_payable_dao.insert_new_records(&msg.new_sent_txs) { + match self + .sent_payable_dao + .insert_new_records(&BTreeSet::from(msg.new_sent_txs)) + { Ok(_) => debug!( self.logger, "Registered new pending payables for: {}", @@ -1265,7 +1278,7 @@ pub fn wei_to_gwei, S: Display + Copy + Div + From> for Accountant { type Result = (); @@ -4984,12 +5013,14 @@ mod tests { #[test] fn accountant_finishes_processing_of_retry_payables_and_schedules_pending_payable_scanner() { + let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let 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(); let sent_payable_dao = SentPayableDaoMock::new() + .get_tx_identifiers_params(&get_tx_identifiers_params_arc) .insert_new_records_params(&inserted_new_records_params_arc) .insert_new_records_result(Ok(())); let failed_payble_dao = FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::new()); @@ -5044,70 +5075,6 @@ mod tests { ); } - #[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(PayableTransactionError::Sending { - msg: "booga".to_string(), - hashes: vec![make_tx_hash(456)], - }), - 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] fn retry_payable_scan_is_requested_to_be_repeated() { init_test_logging(); diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index ec33b61ac..41aa35802 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -7,26 +7,29 @@ pub mod scan_schedulers; pub mod test_utils; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; -use crate::accountant::db_access_objects::pending_payable_dao::{PendingPayable, PendingPayableDao}; +use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDao; +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::payable_scanner::msgs::{ InitialTemplatesMessage, PricedTemplatesMessage, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::{ScanError, ScanForPendingPayables, ScanForRetryPayables}; +use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::utils::{NextScanToRun, PayableScanResult}; +use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PayableScanner}; +use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; +use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; +use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::{ - comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, - ScanForReceivables, SentPayables, -}; -use crate::accountant::{ - ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, - ScanForNewPayables, ScanForReceivables, SentPayables, + ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, ScanError, ScanForNewPayables, + ScanForPendingPayables, 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, DetailedScanType, 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; @@ -44,8 +47,6 @@ use std::time::SystemTime; use time::format_description::parse; use time::OffsetDateTime; use variant_count::VariantCount; -use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao}; -use crate::accountant::db_access_objects::utils::{TxHash}; // Leave the individual scanner objects private! pub struct Scanners { @@ -589,20 +590,52 @@ impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScann #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; - use crate::accountant::db_access_objects::pending_payable_dao::{ - PendingPayable, PendingPayableDaoError, TransactionHashes, + use crate::accountant::db_access_objects::failed_payable_dao::{ + FailedTx, FailureReason, FailureStatus, }; + use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; + use crate::accountant::db_access_objects::sent_payable_dao::{Detection, 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::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; - use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables}; - use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; - use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; + use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; + use crate::accountant::scanners::payable_scanner::test_utils::PayableScannerBuilder; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::{ + RetryTxTemplate, RetryTxTemplates, + }; + use crate::accountant::scanners::payable_scanner::utils::PayableScanResult; + use crate::accountant::scanners::payable_scanner::PayableScanner; + use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; + use crate::accountant::scanners::pending_payable_scanner::utils::{ + CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, Retry, + TxHashByTable, + }; + use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; + use crate::accountant::scanners::receivable_scanner::ReceivableScanner; + use crate::accountant::scanners::test_utils::ScannerReplacement::PendingPayable; + use crate::accountant::scanners::test_utils::{ + assert_timestamps_from_str, parse_system_time_from_str, + trim_expected_timestamp_to_three_digits_nanos, MarkScanner, NullScanner, + PendingPayableCacheMock, ReplacementType, ScannerReplacement, + }; + use crate::accountant::scanners::{ + ManulTriggerError, Scanner, ScannerCommon, Scanners, StartScanError, StartableScanner, + }; + use crate::accountant::test_utils::{ + make_custom_payment_thresholds, make_payable_account, + make_qualified_and_unqualified_payables, make_receivable_account, BannedDaoFactoryMock, + BannedDaoMock, ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, FailedPayableDaoMock, + PayableDaoFactoryMock, PayableDaoMock, PayableThresholdsGaugeMock, + PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, + ReceivableScannerBuilder, SentPayableDaoFactoryMock, SentPayableDaoMock, + }; + use crate::accountant::{ + gwei_to_wei, PayableScanType, ReceivedPayments, RequestTransactionReceipts, + ResponseSkeleton, ScanError, SentPayables, TxReceiptsMessage, + }; + use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::{ - BlockchainTransaction, ProcessedPayableFallible, RpcPayableFailure, + BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck, TxBlock, }; use crate::blockchain::errors::rpc_errors::{ AppRpcError, AppRpcErrorKind, RemoteError, RemoteErrorKind, @@ -615,13 +648,14 @@ mod tests { use crate::db_config::mocks::ConfigDaoMock; use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, + DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, + DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::{make_paying_wallet, make_wallet}; - use actix::Message; - use itertools::Either; + use actix::{Message, System}; + use ethereum_types::U64; use itertools::Either; use masq_lib::logger::Logger; use masq_lib::messages::ScanType; @@ -630,17 +664,12 @@ mod tests { use regex::Regex; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; + use std::collections::BTreeSet; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use web3::types::{H256}; use web3::Error; - use masq_lib::messages::ScanType; - use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; - use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; impl Scanners { pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { @@ -724,10 +753,6 @@ mod tests { let payable_dao_factory = PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()); - // TODO: GH-605: Remove the pending payable dao factory - 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()); @@ -755,8 +780,6 @@ mod tests { payable_dao_factory: Box::new(payable_dao_factory), sent_payable_dao_factory: Box::new(sent_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), @@ -1208,86 +1231,88 @@ mod tests { .payable_dao(payable_dao) .sent_payable_dao(sent_payable_dao) .build(); - let sent_payables = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Correct(payable_1), - ProcessedPayableFallible::Correct(payable_2), - ]), - response_skeleton_opt: None, - }; - - subject.finish_scan(sent_payables, &Logger::new(test_name)); - } - - #[test] - fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist() { - init_test_logging(); - let test_name = - "payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist"; - let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); - let delete_records_params_arc = Arc::new(Mutex::new(vec![])); - let hash_tx_1 = make_tx_hash(0x15b3); - let hash_tx_2 = make_tx_hash(0x3039); - let first_sent_tx_rowid = 3; - let second_sent_tx_rowid = 5; - let system = System::new(test_name); - let sent_payable_dao = SentPayableDaoMock::default() - .get_tx_identifiers_params(&get_tx_identifiers_params_arc) - .get_tx_identifiers_result( - hashmap!(hash_tx_1 => first_sent_tx_rowid, hash_tx_2 => second_sent_tx_rowid), - ) - .delete_records_params(&delete_records_params_arc) - .delete_records_result(Ok(())); - let payable_scanner = PayableScannerBuilder::new() - .sent_payable_dao(sent_payable_dao) - .build(); - let logger = Logger::new(test_name); - let sent_payable = SentPayables { - payment_procedure_result: Err(PayableTransactionError::Sending { - msg: "Attempt failed".to_string(), - hashes: hashset![hash_tx_1, hash_tx_2], - }), - response_skeleton_opt: None, - }; - 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, false); - let sent_tx_rowids_params = get_tx_identifiers_params_arc.lock().unwrap(); - assert_eq!(*sent_tx_rowids_params, vec![hashset!(hash_tx_1, hash_tx_2)]); - let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset!(hash_tx_1, hash_tx_2)]); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!( - "WARN: {test_name}: \ - Any persisted data from the failed process will be deleted. Caused by: Sending phase: \ - \"Attempt failed\". \ - Signed and hashed txs: \ - 0x00000000000000000000000000000000000000000000000000000000000015b3, \ - 0x0000000000000000000000000000000000000000000000000000000000003039" - )); - log_handler.exists_log_containing(&format!( - "WARN: {test_name}: \ - Deleting sent payable records for \ - 0x00000000000000000000000000000000000000000000000000000000000015b3, \ - 0x0000000000000000000000000000000000000000000000000000000000003039", - )); - // we haven't supplied any result for mark_pending_payable() and so it's proved uncalled - } + todo!("GH-605: Work on it") + // let sent_payables = SentPayables { + // payment_procedure_result: Ok(vec![ + // ProcessedPayableFallible::Correct(payable_1), + // ProcessedPayableFallible::Correct(payable_2), + // ]), + // response_skeleton_opt: None, + // }; + + // subject.finish_scan(sent_payables, &Logger::new(test_name)); + } + + // TODO: GH-605: Verify and remove + // #[test] + // fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist() { + // init_test_logging(); + // let test_name = + // "payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist"; + // let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); + // let delete_records_params_arc = Arc::new(Mutex::new(vec![])); + // let hash_tx_1 = make_tx_hash(0x15b3); + // let hash_tx_2 = make_tx_hash(0x3039); + // let first_sent_tx_rowid = 3; + // let second_sent_tx_rowid = 5; + // let system = System::new(test_name); + // let sent_payable_dao = SentPayableDaoMock::default() + // .get_tx_identifiers_params(&get_tx_identifiers_params_arc) + // .get_tx_identifiers_result( + // hashmap!(hash_tx_1 => first_sent_tx_rowid, hash_tx_2 => second_sent_tx_rowid), + // ) + // .delete_records_params(&delete_records_params_arc) + // .delete_records_result(Ok(())); + // let payable_scanner = PayableScannerBuilder::new() + // .sent_payable_dao(sent_payable_dao) + // .build(); + // let logger = Logger::new(test_name); + // let sent_payable = SentPayables { + // payment_procedure_result: Err(PayableTransactionError::Sending { + // msg: "Attempt failed".to_string(), + // hashes: hashset![hash_tx_1, hash_tx_2], + // }), + // response_skeleton_opt: None, + // }; + // 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, false); + // let sent_tx_rowids_params = get_tx_identifiers_params_arc.lock().unwrap(); + // assert_eq!(*sent_tx_rowids_params, vec![hashset!(hash_tx_1, hash_tx_2)]); + // let delete_records_params = delete_records_params_arc.lock().unwrap(); + // assert_eq!(*delete_records_params, vec![hashset!(hash_tx_1, hash_tx_2)]); + // let log_handler = TestLogHandler::new(); + // log_handler.exists_log_containing(&format!( + // "WARN: {test_name}: \ + // Any persisted data from the failed process will be deleted. Caused by: Sending phase: \ + // \"Attempt failed\". \ + // Signed and hashed txs: \ + // 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + // 0x0000000000000000000000000000000000000000000000000000000000003039" + // )); + // log_handler.exists_log_containing(&format!( + // "WARN: {test_name}: \ + // Deleting sent payable records for \ + // 0x00000000000000000000000000000000000000000000000000000000000015b3, \ + // 0x0000000000000000000000000000000000000000000000000000000000003039", + // )); + // // we haven't supplied any result for mark_pending_payable() and so it's proved uncalled + // } #[test] fn finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found_in_retry_mode( @@ -1327,77 +1352,6 @@ mod tests { )); } - #[test] - fn payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails() { - // Two fatal failures at once, missing sent tx records and another record deletion error - // are both legitimate reasons for panic - init_test_logging(); - let test_name = "payable_scanner_for_failed_rpcs_one_sent_tx_record_missing_and_deletion_of_another_fails"; - let existent_record_hash = make_tx_hash(0xb26e); - let nonexistent_record_hash = make_tx_hash(0x4d2); - let sent_payable_dao = SentPayableDaoMock::default() - .get_tx_identifiers_result(hashmap!(existent_record_hash => 45)) - .delete_records_result(Err(SentPayableDaoError::SqlExecutionFailed( - "Another failure. Really???".to_string(), - ))); - let mut subject = PayableScannerBuilder::new() - .sent_payable_dao(sent_payable_dao) - .build(); - let failed_payment_1 = RpcPayableFailure { - rpc_error: Error::Unreachable, - recipient_wallet: make_wallet("abc"), - hash: existent_record_hash, - }; - let failed_payment_2 = RpcPayableFailure { - rpc_error: Error::Internal, - recipient_wallet: make_wallet("def"), - hash: nonexistent_record_hash, - }; - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ - ProcessedPayableFallible::Failed(failed_payment_1), - ProcessedPayableFallible::Failed(failed_payment_2), - ]), - response_skeleton_opt: None, - }; - - let caught_panic_in_err = catch_unwind(AssertUnwindSafe(|| { - subject.finish_scan(sent_payable, &Logger::new(test_name)) - })); - - let caught_panic = caught_panic_in_err.unwrap_err(); - let panic_msg = caught_panic.downcast_ref::().unwrap(); - assert_eq!( - panic_msg, - "Database corrupt: sent payable record deletion for txs \ - 0x000000000000000000000000000000000000000000000000000000000000b26e failed due to \ - SqlExecutionFailed(\"Another failure. Really???\")" - ); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!( - "WARN: {test_name}: Remote sent payable \ - failure 'Server is unreachable' for wallet 0x0000000000000000000000000000000000616263 \ - and tx hash 0x000000000000000000000000000000000000000000000000000000000000b26e" - )); - log_handler.exists_log_containing(&format!( - "WARN: {test_name}: Remote sent payable \ - failure 'Internal Web3 error' for wallet 0x0000000000000000000000000000000000646566 \ - and tx hash 0x00000000000000000000000000000000000000000000000000000000000004d2" - )); - log_handler.exists_log_containing(&format!( - "WARN: {test_name}: \ - Please check your blockchain service URL configuration due to detected remote failures" - )); - log_handler.exists_log_containing(&format!( - "DEBUG: {test_name}: Got 0 properly sent payables of 2 attempts" - )); - log_handler.exists_log_containing(&format!( - "ERROR: {test_name}: Ran into failed \ - payables 0x00000000000000000000000000000000000000000000000000000000000004d2 with missing \ - records. The system has become unreliable" - )); - } - #[test] fn payable_is_found_innocent_by_age_and_returns() { let is_innocent_age_params_arc = Arc::new(Mutex::new(vec![])); @@ -1629,7 +1583,7 @@ mod tests { let failed_tx = make_failed_tx(789); let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![sent_tx.clone()]); let failed_payable_dao = - FailedPayableDaoMock::new().retrieve_txs_result(vec![failed_tx.clone()]); + FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([failed_tx.clone()])); let mut subject = make_dull_subject(); let pending_payable_scanner = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) diff --git a/node/src/accountant/scanners/payable_scanner/finish_scan.rs b/node/src/accountant/scanners/payable_scanner/finish_scan.rs index 9dd1e27e3..be37e4997 100644 --- a/node/src/accountant/scanners/payable_scanner/finish_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/finish_scan.rs @@ -27,7 +27,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 +37,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}; diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index 22bae8e87..0da5ae5eb 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -12,11 +12,10 @@ 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::payment_adjuster::PaymentAdjuster; use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::SolvencySensitivePaymentInstructor; @@ -31,6 +30,7 @@ use crate::accountant::{ ScanForRetryPayables, SentPayables, }; use crate::blockchain::blockchain_interface::data_structures::BatchResults; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::sub_lib::accountant::PaymentThresholds; use itertools::Itertools; use masq_lib::logger::Logger; @@ -207,7 +207,7 @@ impl PayableScanner { } } - fn update_statuses_of_prev_txs(&self, sent_txs: &Vec) { + fn update_statuses_of_prev_txs(&self, sent_txs: &Vec) { // 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); @@ -225,7 +225,10 @@ impl PayableScanner { } } - fn retrieve_failed_txs_by_receiver_addresses(&self, sent_txs: &Vec) -> BTreeSet { + fn retrieve_failed_txs_by_receiver_addresses( + &self, + sent_txs: &Vec, + ) -> BTreeSet { let receiver_addresses = filter_receiver_addresses_from_txs(sent_txs.iter()); self.failed_payable_dao .retrieve_txs(Some(FailureRetrieveCondition::ByReceiverAddresses( @@ -257,7 +260,7 @@ impl PayableScanner { ) } - fn insert_records_in_sent_payables(&self, sent_txs: &Vec) { + fn insert_records_in_sent_payables(&self, sent_txs: &Vec) { self.sent_payable_dao .insert_new_records(&sent_txs.iter().cloned().collect()) .unwrap_or_else(|e| { 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 3d40e2dfd..78a91867b 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 @@ -30,9 +30,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 +110,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/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index 4b1bac799..5a7a21c65 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -1,15 +1,11 @@ // 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::{comma_joined_stringifiable, PendingPayable}; use crate::blockchain::blockchain_interface::data_structures::BatchResults; use crate::sub_lib::accountant::PaymentThresholds; use crate::sub_lib::wallet::Wallet; diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 70c043909..671540b54 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -11,7 +11,7 @@ use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoEr use crate::accountant::db_access_objects::sent_payable_dao::{ RetrieveCondition, SentPayableDao, SentPayableDaoError, SentTx, TxStatus, }; -use crate::accountant::db_access_objects::utils::{TxHash, TxRecordWithHash}; +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; use crate::accountant::scanners::pending_payable_scanner::utils::{ CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, @@ -181,18 +181,19 @@ impl PendingPayableScanner { Some(failure_hashes) } - fn get_wrapped_hashes( - records: &[Record], - wrap_the_hash: fn(TxHash) -> TxHashByTable, - ) -> Vec - where - Record: TxRecordWithHash, - { - records - .iter() - .map(|record| wrap_the_hash(record.hash())) - .collect_vec() - } + // TODO: GH-605: Another issue with this fn + // fn get_wrapped_hashes( + // records: &[Record], + // wrap_the_hash: fn(TxHash) -> TxHashByTable, + // ) -> Vec + // where + // Record: TxRecordWithHash, + // { + // records + // .iter() + // .map(|record| wrap_the_hash(record.hash())) + // .collect_vec() + // } fn emptiness_check(&self, msg: &TxReceiptsMessage) { if msg.results.is_empty() { @@ -805,6 +806,7 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::{ Detection, SentPayableDaoError, TxStatus, }; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::accountant::scanners::pending_payable_scanner::utils::{ CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, @@ -815,8 +817,8 @@ mod tests { use crate::accountant::scanners::test_utils::PendingPayableCacheMock; use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner}; use crate::accountant::test_utils::{ - make_failed_tx, make_sent_tx, make_transaction_block, FailedPayableDaoMock, PayableDaoMock, - PendingPayableScannerBuilder, SentPayableDaoMock, + make_transaction_block, FailedPayableDaoMock, PayableDaoMock, PendingPayableScannerBuilder, + SentPayableDaoMock, }; use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; use crate::blockchain::blockchain_interface::data_structures::{ diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs index e01425d69..2ff8854ea 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -228,15 +228,14 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::{ Detection, RetrieveCondition, SentTx, TxStatus, }; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; use crate::accountant::scanners::pending_payable_scanner::utils::{ DetectedConfirmations, DetectedFailures, FailedValidation, FailedValidationByTable, PresortedTxFailure, ReceiptScanReport, TxByTable, }; - use crate::accountant::test_utils::{ - make_failed_tx, make_sent_tx, make_transaction_block, SentPayableDaoMock, - }; + use crate::accountant::test_utils::{make_transaction_block, SentPayableDaoMock}; use crate::blockchain::errors::internal_errors::InternalErrorKind; use crate::blockchain::errors::rpc_errors::{ AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index d08808d75..f7026cabc 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -366,13 +366,13 @@ impl From<&TxByTable> for TxHashByTable { mod tests { use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; use crate::accountant::db_access_objects::sent_payable_dao::{Detection, TxStatus}; + use crate::accountant::db_access_objects::test_utils::{make_failed_tx, make_sent_tx}; use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::accountant::scanners::pending_payable_scanner::utils::{ CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, FailedValidationByTable, PendingPayableCache, PresortedTxFailure, ReceiptScanReport, RecheckRequiringFailures, Retry, TxByTable, TxHashByTable, }; - use crate::accountant::test_utils::{make_failed_tx, make_sent_tx}; use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ PreviousAttempts, ValidationFailureClockReal, ValidationStatus, diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index ed8b8144e..366352095 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, FailureReason, 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, ValidationStatus}; +use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::bootstrapper::BootstrapperConfig; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; @@ -47,12 +57,7 @@ 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; +use web3::types::Address; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_unix_timestamp(SystemTime::now()); @@ -90,35 +95,36 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( } } -pub fn make_sent_tx(num: u64) -> SentTx { - if num == 0 { - panic!("num for generating must be greater than 0"); - } - let params = TxRecordCommonParts::new(num); - SentTx { - hash: params.hash, - receiver_address: params.receiver_address, - amount_minor: params.amount_minor, - timestamp: params.timestamp, - gas_price_minor: params.gas_price_minor, - nonce: params.nonce, - status: TxStatus::Pending(ValidationStatus::Waiting), - } -} - -pub fn make_failed_tx(num: u64) -> FailedTx { - let params = TxRecordCommonParts::new(num); - FailedTx { - hash: params.hash, - receiver_address: params.receiver_address, - amount_minor: params.amount_minor, - timestamp: params.timestamp, - gas_price_minor: params.gas_price_minor, - nonce: params.nonce, - reason: FailureReason::PendingTooLong, - status: FailureStatus::RetryRequired, - } -} +// TODO: GH-605: Remove them +// pub fn make_sent_tx(num: u64) -> SentTx { +// if num == 0 { +// panic!("num for generating must be greater than 0"); +// } +// let params = TxRecordCommonParts::new(num); +// SentTx { +// hash: params.hash, +// receiver_address: params.receiver_address, +// amount_minor: params.amount_minor, +// timestamp: params.timestamp, +// gas_price_minor: params.gas_price_minor, +// nonce: params.nonce, +// status: TxStatus::Pending(ValidationStatus::Waiting), +// } +// } +// +// pub fn make_failed_tx(num: u64) -> FailedTx { +// let params = TxRecordCommonParts::new(num); +// FailedTx { +// hash: params.hash, +// receiver_address: params.receiver_address, +// amount_minor: params.amount_minor, +// timestamp: params.timestamp, +// gas_price_minor: params.gas_price_minor, +// nonce: params.nonce, +// reason: FailureReason::PendingTooLong, +// status: FailureStatus::RetryRequired, +// } +// } pub fn make_transaction_block(num: u64) -> TxBlock { TxBlock { @@ -157,8 +163,6 @@ pub struct AccountantBuilder { receivable_dao_factory_opt: Option, sent_payable_dao_factory_opt: Option, failed_payable_dao_factory_opt: Option, - sent_payable_dao_factory_opt: Option, - failed_payable_dao_factory_opt: Option, banned_dao_factory_opt: Option, config_dao_factory_opt: Option, } @@ -1025,7 +1029,7 @@ pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> Boot pub struct SentPayableDaoMock { get_tx_identifiers_params: Arc>>>, get_tx_identifiers_results: RefCell>, - insert_new_records_params: Arc>>>, + insert_new_records_params: Arc>>>, insert_new_records_results: RefCell>>, retrieve_txs_params: Arc>>>, retrieve_txs_results: RefCell>>, @@ -1108,7 +1112,7 @@ impl SentPayableDaoMock { self } - pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { + pub fn insert_new_records_params(mut self, params: &Arc>>>) -> Self { self.insert_new_records_params = params.clone(); self } @@ -1175,42 +1179,6 @@ impl SentPayableDaoMock { } } -pub struct SentPayableDaoFactoryMock { - make_params: Arc>>, - make_results: RefCell>>, -} - -impl SentPayableDaoFactory for SentPayableDaoFactoryMock { - fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!( - "SentPayableDao Missing. This problem mostly occurs when SentPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." - ) - }; - self.make_params.lock().unwrap().push(()); - self.make_results.borrow_mut().remove(0) - } -} - -impl SentPayableDaoFactoryMock { - pub fn new() -> Self { - Self { - make_params: Arc::new(Mutex::new(vec![])), - make_results: RefCell::new(vec![]), - } - } - - pub fn make_params(mut self, params: &Arc>>) -> Self { - self.make_params = params.clone(); - self - } - - pub fn make_result(self, result: SentPayableDaoMock) -> Self { - self.make_results.borrow_mut().push(Box::new(result)); - self - } -} - #[derive(Default)] pub struct FailedPayableDaoMock { get_tx_identifiers_params: Arc>>>, @@ -1363,147 +1331,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>>, -} - -pub struct PayableScannerBuilder { - payable_dao: PayableDaoMock, - sent_payable_dao: SentPayableDaoMock, - payment_thresholds: PaymentThresholds, - payment_adjuster: PaymentAdjusterMock, -} - -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>>, diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 8063216a9..ca2d23df9 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -489,8 +489,6 @@ impl ActorFactory for ActorFactoryReal { payable_dao_factory, sent_payable_dao_factory, failed_payable_dao_factory, - sent_payable_dao_factory, - failed_payable_dao_factory, receivable_dao_factory, banned_dao_factory, config_dao_factory, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 3edec16d1..f017ee6b1 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::{ + PayableScanType, ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, +}; +use crate::accountant::{RequestTransactionReceipts, TxReceiptResult, TxReceiptsMessage}; use crate::actor_system_factory::SubsFactory; use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainInterfaceError, 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}; @@ -30,6 +41,7 @@ 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::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; @@ -37,14 +49,6 @@ use std::string::ToString; use std::sync::{Arc, Mutex}; use std::time::SystemTime; 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"; @@ -557,9 +561,21 @@ 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::LocalPayableError::TransactionID; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainAgentBuildError, LocalPayableError, @@ -567,14 +583,20 @@ mod tests { use crate::blockchain::blockchain_interface::data_structures::{ BlockchainTransaction, RetrievedBlockchainTransactions, TxBlock, }; - use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; + use crate::blockchain::errors::rpc_errors::AppRpcError::Local; + use crate::blockchain::errors::rpc_errors::LocalError::Transport; + 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_blockchain_bridge_subs_from_recorder, @@ -604,22 +626,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 = (); @@ -940,15 +946,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), }], @@ -1037,11 +1043,11 @@ 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]); @@ -1056,7 +1062,6 @@ mod tests { // amount: account.balance_wei // }] // ); - assert_eq!(scan_error_msg.scan_type, ScanType::Payables); assert_eq!( *scan_error_msg, ScanError { @@ -1131,27 +1136,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), }, 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 52f37f2b2..090cff608 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 crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; +use std::collections::HashMap; +use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, LocalPayableError}; +use crate::blockchain::blockchain_interface::data_structures::{BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainInterface}; @@ -21,9 +22,12 @@ use ethereum_types::U64; use itertools::Either; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, PricedQualifiedPayables}; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::accountant::db_access_objects::utils::TxHash; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::PricedNewTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplates; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplates; use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; use crate::accountant::TxReceiptResult; use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, RegisterNewPendingPayables}; @@ -297,11 +301,11 @@ pub struct HashAndAmount { 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, } } } @@ -464,10 +468,6 @@ impl BlockchainInterfaceWeb3 { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::payable_scanner_extension::msgs::{ - QualifiedPayableWithGasPrice, QualifiedPayablesBeforeGasPriceSelection, - UnpricedQualifiedPayables, - }; use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; use crate::accountant::test_utils::make_payable_account; use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; @@ -502,10 +502,10 @@ mod tests { use itertools::Either; use web3::transports::Http; use web3::types::{H256, U256}; - use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayableWithGasPrice}; - use crate::accountant::test_utils::make_payable_account; - use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::initial::retry::RetryTxTemplates; + use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::PricedRetryTxTemplate; + use crate::accountant::scanners::payable_scanner::tx_templates::test_utils::RetryTxTemplateBuilder; #[test] fn constants_are_correct() { 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 47919b738..9d4aeafeb 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -1,13 +1,14 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; -use crate::accountant::db_access_objects::utils::{to_unix_timestamp, TxHash}; -use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; -use crate::accountant::PendingPayable; +use crate::accountant::db_access_objects::utils::to_unix_timestamp; +use crate::accountant::scanners::payable_scanner::tx_templates::signable::{ + SignableTxTemplate, SignableTxTemplates, +}; use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::RegisterNewPendingPayables; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, TRANSFER_METHOD_ID, }; @@ -16,7 +17,6 @@ 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; @@ -40,7 +40,7 @@ pub struct BlockchainAgentFutureResult { pub masq_token_balance: U256, } -fn return_sending_error(sent_txs: &[Tx], error: &Web3Error) -> LocalPayableError { +fn return_sending_error(sent_txs: &[SentTx], error: &Web3Error) -> LocalPayableError { LocalPayableError::Sending( sent_txs .iter() @@ -50,7 +50,7 @@ fn return_sending_error(sent_txs: &[Tx], error: &Web3Error) -> LocalPayableError } pub fn return_batch_results( - txs: Vec, + txs: Vec, responses: Vec>, ) -> BatchResults { txs.into_iter().zip(responses).fold( @@ -192,7 +192,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, @@ -215,12 +215,12 @@ pub fn sign_and_append_payment( 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 +238,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| { @@ -318,6 +318,7 @@ pub fn create_blockchain_agent_web3( #[cfg(test)] mod tests { use super::*; + use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; use crate::accountant::db_access_objects::test_utils::{ assert_on_failed_txs, assert_on_sent_txs, FailedTxBuilder, TxBuilder, }; diff --git a/node/src/blockchain/blockchain_interface/data_structures/mod.rs b/node/src/blockchain/blockchain_interface/data_structures/mod.rs index 5ac413123..724a49f9a 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -2,14 +2,16 @@ pub mod errors; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; +use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; +use crate::accountant::db_access_objects::sent_payable_dao::SentTx; +use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; use crate::blockchain::blockchain_bridge::BlockMarker; use crate::sub_lib::wallet::Wallet; use ethereum_types::U64; use serde_derive::{Deserialize, Serialize}; use std::fmt; -use std::fmt::Formatter; -use web3::types::H256; +use std::fmt::{Display, Formatter}; +use web3::types::{TransactionReceipt, H256}; use web3::Error; #[derive(Clone, Debug, Eq, PartialEq)] @@ -37,7 +39,7 @@ pub struct RetrievedBlockchainTransactions { #[derive(Default, Debug, PartialEq, Clone)] pub struct BatchResults { - pub sent_txs: Vec, + pub sent_txs: Vec, pub failed_txs: Vec, } diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index 36985b40b..9ab2c5a62 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -4,21 +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, PayableTransactionError}; -use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, 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 actix::Recipient; use futures::Future; use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; -use crate::blockchain::blockchain_agent::BlockchainAgent; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; +use std::collections::HashMap; +use web3::types::Address; pub trait BlockchainInterface { fn contract_address(&self) -> Address; diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index c07c09adf..41d9d3863 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -59,7 +59,7 @@ pub enum AppRpcErrorKind { Remote(RemoteErrorKind), } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum LocalErrorKind { Decoder, Internal, @@ -68,7 +68,7 @@ pub enum LocalErrorKind { Transport, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub enum RemoteErrorKind { InvalidResponse, Unreachable, @@ -81,7 +81,7 @@ impl From<&AppRpcError> for AppRpcErrorKind { AppRpcError::Local(local) => match local { LocalError::Decoder(_) => Self::Local(LocalErrorKind::Decoder), LocalError::Internal => Self::Local(LocalErrorKind::Internal), - LocalError::IO(_) => Self::Local(LocalErrorKind::IO), + LocalError::IO(_) => Self::Local(LocalErrorKind::Io), LocalError::Signing(_) => Self::Local(LocalErrorKind::Signing), LocalError::Transport(_) => Self::Local(LocalErrorKind::Transport), }, @@ -162,7 +162,7 @@ mod tests { ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::IO("IO error".to_string()))), - AppRpcErrorKind::Local(LocalErrorKind::IO) + AppRpcErrorKind::Local(LocalErrorKind::Io) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Signing( @@ -200,7 +200,7 @@ mod tests { let errors = vec![ AppRpcErrorKind::Local(LocalErrorKind::Decoder), AppRpcErrorKind::Local(LocalErrorKind::Internal), - AppRpcErrorKind::Local(LocalErrorKind::IO), + AppRpcErrorKind::Local(LocalErrorKind::Io), AppRpcErrorKind::Local(LocalErrorKind::Signing), AppRpcErrorKind::Local(LocalErrorKind::Transport), AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse), diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index be5840840..77e5870ff 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -9,8 +9,8 @@ use serde::{ use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; -use std::hash::{Hash, Hasher}; use std::fmt::Formatter; +use std::hash::{Hash, Hasher}; use std::time::SystemTime; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -167,15 +167,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 std::time::Duration; use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; - use crate::blockchain::test_utils::ValidationFailureClockMock; use crate::test_utils::serde_serializer_mock::{SerdeSerializerMock, SerializeSeqMock}; use serde::ser::Error as SerdeError; - use std::time::{Duration, UNIX_EPOCH}; + use std::collections::hash_map::DefaultHasher; + use std::time::Duration; + use std::time::UNIX_EPOCH; #[test] fn previous_attempts_and_validation_failure_clock_work_together_fine() { @@ -194,7 +193,7 @@ mod tests { ); let timestamp_c = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &validation_failure_clock, ); let timestamp_d = SystemTime::now(); @@ -203,7 +202,7 @@ mod tests { &validation_failure_clock, ); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &validation_failure_clock, ); @@ -240,7 +239,7 @@ mod tests { let io_error_stats = subject .inner .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( - LocalErrorKind::IO, + LocalErrorKind::Io, ))) .unwrap(); assert!( @@ -268,15 +267,17 @@ mod tests { .now_result(now) .now_result(now + Duration::from_secs(2)); let attempts1 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &clock, ); let attempts2 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &clock, + ); + let attempts3 = PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &clock, ); - let attempts3 = - PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Io), &clock); let hash1 = { let mut hasher = DefaultHasher::new(); attempts1.hash(&mut hasher); @@ -306,17 +307,19 @@ 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, ); attempts1 = attempts1.add_attempt( BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), &clock, ); - let mut attempts2 = - PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Io), &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, ); diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index b2e95fa50..e6eceaf2a 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -287,21 +287,3 @@ impl TransactionReceiptBuilder { } } } - -#[derive(Default)] -pub struct ValidationFailureClockMock { - now_results: RefCell>, -} - -impl ValidationFailureClock for ValidationFailureClockMock { - fn now(&self) -> SystemTime { - self.now_results.borrow_mut().remove(0) - } -} - -impl ValidationFailureClockMock { - pub fn now_result(self, result: SystemTime) -> Self { - self.now_results.borrow_mut().push(result); - self - } -} diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 2711594df..039b1fe4f 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -68,8 +68,6 @@ impl PaymentThresholds { pub struct DaoFactories { pub payable_dao_factory: Box, pub sent_payable_dao_factory: Box, - pub sent_payable_dao_factory: Box, - pub failed_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, From 655830d1ed682943319272b8046a36df6c0b530c Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Wed, 24 Sep 2025 19:42:16 +0530 Subject: [PATCH 27/48] GH-605: only 37 errors remaining --- .../db_access_objects/failed_payable_dao.rs | 16 ++- .../db_access_objects/payable_dao.rs | 5 +- .../db_access_objects/sent_payable_dao.rs | 24 +++- node/src/accountant/mod.rs | 4 +- node/src/accountant/scanners/mod.rs | 131 +++++++++--------- .../scanners/payable_scanner/mod.rs | 2 +- .../scanners/pending_payable_scanner/mod.rs | 28 ++-- .../tx_receipt_interpreter.rs | 3 +- node/src/accountant/test_utils.rs | 18 ++- .../blockchain_interface_web3/utils.rs | 71 +++------- 10 files changed, 145 insertions(+), 157 deletions(-) diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index d8ba7369e..2e0b24206 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -98,7 +98,7 @@ impl Transaction for FailedTx { } fn amount(&self) -> u128 { - self.amount + self.amount_minor } fn timestamp(&self) -> i64 { @@ -106,7 +106,7 @@ impl Transaction for FailedTx { } fn gas_price_wei(&self) -> u128 { - self.gas_price_wei + self.gas_price_minor } fn nonce(&self) -> u64 { @@ -132,12 +132,14 @@ impl Ord for FailedTx { .timestamp .cmp(&self.timestamp) .then_with(|| other.nonce.cmp(&self.nonce)) - .then_with(|| other.amount.cmp(&self.amount)) + .then_with(|| other.amount_minor.cmp(&self.amount_minor)) } } 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, @@ -145,7 +147,7 @@ impl From<(&SentTx, &Web3Error)> for FailedTx { timestamp: sent_tx.timestamp, 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, } } @@ -357,7 +359,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { } let case_statements = join_with_separator( - &status_updates, + status_updates, |(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status), " ", ); @@ -1223,9 +1225,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/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 2951b73a8..5582646f7 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -50,7 +50,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, } @@ -585,8 +585,7 @@ mod tests { use rusqlite::ToSql; use rusqlite::{Connection, OpenFlags}; use std::path::Path; - use std::str::FromStr; - use time::Duration; + use std::time::Duration; #[test] fn more_money_payable_works_for_new_address() { 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 3d964f41a..b5eb5eb80 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -86,7 +86,7 @@ impl Ord for SentTx { } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TxStatus { Pending(ValidationStatus), Confirmed { @@ -96,6 +96,18 @@ pub enum TxStatus { }, } +impl PartialOrd for TxStatus { + fn partial_cmp(&self, other: &Self) -> Option { + todo!() + } +} + +impl Ord for TxStatus { + fn cmp(&self, other: &Self) -> Ordering { + todo!() + } +} + impl FromStr for TxStatus { type Err = String; @@ -166,7 +178,7 @@ pub trait SentPayableDao { 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 confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError>; fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError>; fn update_statuses( &self, @@ -553,7 +565,7 @@ mod tests { use ethereum_types::{H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; - use std::collections::{BTreeSet, HashMap, HashSet}; + use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::ops::{Add, Sub}; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -919,12 +931,12 @@ mod tests { .nonce(35) .build(); subject - .insert_new_records(&vec![tx1.clone(), tx2, tx3.clone()]) + .insert_new_records(&BTreeSet::from([tx1.clone(), tx2, tx3.clone()])) .unwrap(); let result = subject.retrieve_txs(Some(ByNonce(vec![33, 35]))); - assert_eq!(result, vec![tx1, tx3]); + assert_eq!(result, BTreeSet::from([tx1, tx3])); } #[test] @@ -1248,7 +1260,7 @@ mod tests { let result = subject.update_statuses(&hashmap); - let updated_txs = subject.retrieve_txs(None); + let updated_txs: Vec<_> = subject.retrieve_txs(None).into_iter().collect(); assert_eq!(result, Ok(())); assert_eq!( updated_txs[0].status, diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 5cac1b70a..6a7c30a3a 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -427,7 +427,7 @@ impl Handler for Accountant { DetailedScanType::RetryPayables => self .scan_schedulers .payable - .schedule_retry_payable_scan(ctx, None, &self.logger), + .schedule_retry_payable_scan(ctx, &self.logger), DetailedScanType::PendingPayables => self .scan_schedulers .pending_payable @@ -5604,7 +5604,7 @@ mod tests { ) -> (TxHashByTable, TxReceiptResult, TxByTable) { match tx_hash { TxHashByTable::SentPayable(hash) => { - let mut sent_tx = make_sent_tx(1 + idx); + let mut sent_tx = make_sent_tx((1 + idx) as u32); sent_tx.hash = hash; if let StatusReadFromReceiptCheck::Succeeded(block) = &status { diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 41aa35802..794507d02 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1144,37 +1144,38 @@ 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 - ); + todo!("pending payable issue"); + // 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] @@ -1191,23 +1192,24 @@ mod tests { 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); + todo!("another pending payable issue"); + // 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] @@ -1216,22 +1218,23 @@ mod tests { to wallet: 0x00000000000000000000000000626c6168323232) \ were not found. The system has become unreliable")] fn payable_scanner_found_out_nonexistent_sent_tx_records() { - init_test_logging(); - let test_name = "payable_scanner_found_out_nonexistent_sent_tx_records"; - let hash_1 = make_tx_hash(0xff); - let hash_2 = make_tx_hash(0xf8); - let sent_payable_dao = - SentPayableDaoMock::default().get_tx_identifiers_result(hashmap!(hash_1 => 7881)); - let payable_1 = PendingPayable::new(make_wallet("blah111"), hash_1); - let payable_2 = PendingPayable::new(make_wallet("blah222"), hash_2); - let payable_dao = PayableDaoMock::new().mark_pending_payables_rowids_result(Err( - PayableDaoError::SignConversion(9999999999999), - )); - let mut subject = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .sent_payable_dao(sent_payable_dao) - .build(); todo!("GH-605: Work on it") + // init_test_logging(); + // let test_name = "payable_scanner_found_out_nonexistent_sent_tx_records"; + // let hash_1 = make_tx_hash(0xff); + // let hash_2 = make_tx_hash(0xf8); + // let sent_payable_dao = + // SentPayableDaoMock::default().get_tx_identifiers_result(hashmap!(hash_1 => 7881)); + // let payable_1 = PendingPayable::new(make_wallet("blah111"), hash_1); + // let payable_2 = PendingPayable::new(make_wallet("blah222"), hash_2); + // let payable_dao = PayableDaoMock::new().mark_pending_payables_rowids_result(Err( + // PayableDaoError::SignConversion(9999999999999), + // )); + // let mut subject = PayableScannerBuilder::new() + // .payable_dao(payable_dao) + // .sent_payable_dao(sent_payable_dao) + // .build(); + // let sent_payables = SentPayables { // payment_procedure_result: Ok(vec![ // ProcessedPayableFallible::Correct(payable_1), @@ -1631,7 +1634,7 @@ mod tests { let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![make_sent_tx(123)]); let failed_payable_dao = - FailedPayableDaoMock::new().retrieve_txs_result(vec![make_failed_tx(456)]); + FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([make_failed_tx(456)])); let pending_payable_scanner = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) .failed_payable_dao(failed_payable_dao) diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index 0da5ae5eb..e57270a3f 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -239,7 +239,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)); } diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 671540b54..35de06667 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -163,7 +163,7 @@ impl PendingPayableScanner { } let pending_tx_hashes = Self::get_wrapped_hashes(&pending_txs, TxHashByTable::SentPayable); - self.current_sent_payables.load_cache(pending_txs); + self.current_sent_payables.load_cache(pending_txs.into()); Some(pending_tx_hashes) } @@ -177,7 +177,8 @@ impl PendingPayableScanner { } let failure_hashes = Self::get_wrapped_hashes(&failures, TxHashByTable::FailedPayable); - self.yet_unproven_failed_payables.load_cache(failures); + self.yet_unproven_failed_payables + .load_cache(failures.into()); Some(failure_hashes) } @@ -409,7 +410,10 @@ impl PendingPayableScanner { hashes_and_blocks: &[(TxHash, TxBlock)], logger: &Logger, ) { - match self.sent_payable_dao.replace_records(sent_txs_to_reclaim) { + match self + .sent_payable_dao + .replace_records(sent_txs_to_reclaim.into()) + { Ok(_) => { debug!(logger, "Replaced records for txs being reclaimed") } @@ -428,7 +432,7 @@ impl PendingPayableScanner { 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) { + match self.failed_payable_dao.delete_records(&hashes.into()) { Ok(_) => { info!( logger, @@ -837,7 +841,7 @@ mod tests { use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use regex::Regex; - use std::collections::HashMap; + use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; @@ -1429,7 +1433,10 @@ mod tests { subject.handle_failed_transactions(detected_failures, &Logger::new("test")); let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); - assert_eq!(*insert_new_records_params, vec![vec![failed_tx_1]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([failed_tx_1])] + ); let delete_records_params = delete_records_params_arc.lock().unwrap(); assert_eq!(*delete_records_params, vec![hashset![tx_hash_1]]); let update_statuses_params = update_status_params_arc.lock().unwrap(); @@ -1621,7 +1628,11 @@ mod tests { let replace_records_params = replace_records_params_arc.lock().unwrap(); assert_eq!(*replace_records_params, vec![vec![sent_tx_1, sent_tx_2]]); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); + // assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); + assert_eq!( + *delete_records_params, + vec![BTreeSet::from([tx_hash_1, tx_hash_2])] + ); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "INFO: {test_name}: Reclaimed txs 0x0000000000000000000000000000000000000000000000000000000000000123 \ @@ -1879,7 +1890,8 @@ mod tests { let replace_records_params = replace_records_params_arc.lock().unwrap(); assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); + // 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 \ diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs index 2ff8854ea..10e6c93a5 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -93,7 +93,8 @@ impl TxReceiptInterpreter { let replacement_tx = sent_payable_dao .retrieve_txs(Some(RetrieveCondition::ByNonce(vec![failed_tx.nonce]))); let replacement_tx_hash = replacement_tx - .get(0) + .iter() + .next() .unwrap_or_else(|| { panic!( "Attempted to display a replacement tx for {:?} but couldn't find \ diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 366352095..fb2ef50ab 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -177,8 +177,6 @@ impl Default for AccountantBuilder { receivable_dao_factory_opt: None, sent_payable_dao_factory_opt: None, failed_payable_dao_factory_opt: None, - sent_payable_dao_factory_opt: None, - failed_payable_dao_factory_opt: None, banned_dao_factory_opt: None, config_dao_factory_opt: None, } @@ -1027,38 +1025,38 @@ pub fn bc_from_wallets(consuming_wallet: Wallet, earning_wallet: Wallet) -> Boot #[derive(Default)] pub struct SentPayableDaoMock { - get_tx_identifiers_params: Arc>>>, + get_tx_identifiers_params: Arc>>>, get_tx_identifiers_results: RefCell>, insert_new_records_params: Arc>>>, insert_new_records_results: RefCell>>, retrieve_txs_params: Arc>>>, - retrieve_txs_results: RefCell>>, + retrieve_txs_results: RefCell>>, confirm_tx_params: Arc>>>, confirm_tx_results: RefCell>>, update_statuses_params: Arc>>>, update_statuses_results: RefCell>>, replace_records_params: Arc>>>, replace_records_results: RefCell>>, - delete_records_params: Arc>>>, + delete_records_params: Arc>>>, delete_records_results: RefCell>>, } impl SentPayableDao for SentPayableDaoMock { - fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers { + fn get_tx_identifiers(&self, hashes: &BTreeSet) -> TxIdentifiers { self.get_tx_identifiers_params .lock() .unwrap() .push(hashes.clone()); self.get_tx_identifiers_results.borrow_mut().remove(0) } - fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + fn insert_new_records(&self, txs: &BTreeSet) -> Result<(), SentPayableDaoError> { self.insert_new_records_params .lock() .unwrap() .push(txs.to_vec()); self.insert_new_records_results.borrow_mut().remove(0) } - fn retrieve_txs(&self, condition: Option) -> Vec { + fn retrieve_txs(&self, condition: Option) -> BTreeSet { self.retrieve_txs_params.lock().unwrap().push(condition); self.retrieve_txs_results.borrow_mut().remove(0) } @@ -1069,7 +1067,7 @@ impl SentPayableDao for SentPayableDaoMock { .push(hash_map.clone()); self.confirm_tx_results.borrow_mut().remove(0) } - fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError> { + fn replace_records(&self, new_txs: &BTreeSet) -> Result<(), SentPayableDaoError> { self.replace_records_params .lock() .unwrap() @@ -1088,7 +1086,7 @@ impl SentPayableDao for SentPayableDaoMock { self.update_statuses_results.borrow_mut().remove(0) } - fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError> { + fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError> { self.delete_records_params .lock() .unwrap() 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 9d4aeafeb..ba3fd3d61 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -31,6 +31,7 @@ use std::time::SystemTime; use thousands::Separable; use web3::transports::{Batch, Http}; use web3::types::{Bytes, SignedTransaction, TransactionParameters, U256}; +use web3::Error as Web3Error; use web3::Web3; #[derive(Debug)] @@ -232,7 +233,6 @@ pub fn append_signed_transaction_to_batch(web3_batch: &Web3>, raw_tr } pub fn sign_and_append_multiple_payments( - now: SystemTime, logger: &Logger, chain: Chain, web3_batch: &Web3>, @@ -318,7 +318,7 @@ pub fn create_blockchain_agent_web3( #[cfg(test)] mod tests { use super::*; - use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus; + use crate::accountant::db_access_objects::failed_payable_dao::{FailureReason, FailureStatus}; use crate::accountant::db_access_objects::test_utils::{ assert_on_failed_txs, assert_on_sent_txs, FailedTxBuilder, TxBuilder, }; @@ -334,9 +334,11 @@ 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::{ + AppRpcError, AppRpcErrorKind, LocalErrorKind, RemoteErrorKind, + }; use crate::blockchain::test_utils::{ make_address, make_blockchain_interface_web3, make_tx_hash, transport_error_code, transport_error_message, @@ -445,7 +447,6 @@ mod tests { ]); let mut result = sign_and_append_multiple_payments( - now, &logger, DEFAULT_CHAIN, &web3_batch, @@ -464,12 +465,12 @@ mod tests { index ); assert_eq!( - sent_tx.amount, template.amount_in_wei, + sent_tx.amount_minor, template.amount_in_wei, "Transaction {} amount mismatch", index ); assert_eq!( - sent_tx.gas_price_wei, template.gas_price_wei, + sent_tx.gas_price_minor, template.gas_price_wei, "Transaction {} gas_price_wei mismatch", index ); @@ -485,47 +486,6 @@ mod tests { index ) }); - let first_actual_sent_tx = result.remove(0); - let second_actual_sent_tx = result.remove(0); - assert_prepared_sent_tx_record( - first_actual_sent_tx, - now, - account_1, - "0x6b85347ff8edf8b126dffb85e7517ac7af1b23eace4ed5ad099d783fd039b1ee", - 1, - 111_234_111, - ); - assert_prepared_sent_tx_record( - second_actual_sent_tx, - now, - account_2, - "0x3dac025697b994920c9cd72ab0d2df82a7caaa24d44e78b7c04e223299819d54", - 2, - 222_432_222, - ); - } - - fn assert_prepared_sent_tx_record( - actual_sent_tx: SentTx, - now: SystemTime, - account_1: PayableAccount, - expected_tx_hash_including_prefix: &str, - expected_nonce: u64, - expected_gas_price_minor: u128, - ) { - assert_eq!(actual_sent_tx.receiver_address, account_1.wallet.address()); - assert_eq!( - actual_sent_tx.hash, - H256::from_str(&expected_tx_hash_including_prefix[2..]).unwrap() - ); - assert_eq!(actual_sent_tx.amount_minor, account_1.balance_wei); - assert_eq!(actual_sent_tx.gas_price_minor, expected_gas_price_minor); - assert_eq!(actual_sent_tx.nonce, expected_nonce); - assert_eq!( - actual_sent_tx.status, - TxStatus::Pending(ValidationStatus::Waiting) - ); - assert_eq!(actual_sent_tx.timestamp, to_unix_timestamp(now)); } #[test] @@ -812,9 +772,9 @@ 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() }) @@ -881,7 +841,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() }) @@ -948,10 +910,9 @@ mod tests { .hash(signed_tx_2.transaction_hash) .template(template_2) .timestamp(to_unix_timestamp(SystemTime::now())) - .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(); From 06106f7e155315d243bd116207e9d2af5c368c0e Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 25 Sep 2025 14:04:09 +0530 Subject: [PATCH 28/48] GH-605: only 5 errors remaining --- masq_lib/src/utils.rs | 60 +++++++++++++++++++ node/src/accountant/mod.rs | 57 +++++++++++------- node/src/accountant/scanners/mod.rs | 13 ++-- .../scanners/pending_payable_scanner/mod.rs | 52 +++++++++------- .../tx_receipt_interpreter.rs | 7 ++- node/src/accountant/test_utils.rs | 16 ++--- 6 files changed, 145 insertions(+), 60 deletions(-) diff --git a/masq_lib/src/utils.rs b/masq_lib/src/utils.rs index 9355d0624..b7691e317 100644 --- a/masq_lib/src/utils.rs +++ b/masq_lib/src/utils.rs @@ -520,6 +520,25 @@ macro_rules! hashset { }; } +#[macro_export(local_inner_macros)] +macro_rules! btreeset { + () => { + ::std::collections::BTreeSet::new() + }; + ($($val:expr,)+) => { + btreeset!($($val),+) + }; + ($($value:expr),+) => { + { + let mut _bts = ::std::collections::BTreeSet::new(); + $( + let _ = _bts.insert($value); + )* + _bts + } + }; +} + #[cfg(test)] mod tests { use super::*; @@ -956,4 +975,45 @@ mod tests { assert_eq!(hashset_of_string, expected_hashset_of_string); assert_eq!(hashset_with_duplicate, expected_hashset_with_duplicate); } + + #[test] + fn btreeset_macro_works() { + let empty_btreeset: BTreeSet = btreeset!(); + let btreeset_with_one_element = btreeset!(2); + let btreeset_with_multiple_elements = btreeset!(2, 20, 42); + let btreeset_with_trailing_comma = btreeset!(2, 20,); + let btreeset_of_string = btreeset!("val_a", "val_b"); + let btreeset_with_duplicate = btreeset!(2, 2); + + let expected_empty_btreeset: BTreeSet = BTreeSet::new(); + let mut expected_btreeset_with_one_element = BTreeSet::new(); + expected_btreeset_with_one_element.insert(2); + let mut expected_btreeset_with_multiple_elements = BTreeSet::new(); + expected_btreeset_with_multiple_elements.insert(2); + expected_btreeset_with_multiple_elements.insert(20); + expected_btreeset_with_multiple_elements.insert(42); + let mut expected_btreeset_with_trailing_comma = BTreeSet::new(); + expected_btreeset_with_trailing_comma.insert(2); + expected_btreeset_with_trailing_comma.insert(20); + let mut expected_btreeset_of_string = BTreeSet::new(); + expected_btreeset_of_string.insert("val_a"); + expected_btreeset_of_string.insert("val_b"); + let mut expected_btreeset_with_duplicate = BTreeSet::new(); + expected_btreeset_with_duplicate.insert(2); + assert_eq!(empty_btreeset, expected_empty_btreeset); + assert_eq!( + btreeset_with_one_element, + expected_btreeset_with_one_element + ); + assert_eq!( + btreeset_with_multiple_elements, + expected_btreeset_with_multiple_elements + ); + assert_eq!( + btreeset_with_trailing_comma, + expected_btreeset_with_trailing_comma + ); + assert_eq!(btreeset_of_string, expected_btreeset_of_string); + assert_eq!(btreeset_with_duplicate, expected_btreeset_with_duplicate); + } } diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 6a7c30a3a..84b90f706 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1177,10 +1177,9 @@ impl Accountant { comma_joined_stringifiable(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } - match self - .sent_payable_dao - .insert_new_records(&BTreeSet::from(msg.new_sent_txs)) - { + let sent_txs: BTreeSet = msg.new_sent_txs.iter().cloned().collect(); + + match self.sent_payable_dao.insert_new_records(&sent_txs) { Ok(_) => debug!( self.logger, "Registered new pending payables for: {}", @@ -1404,8 +1403,6 @@ mod tests { fn new_calls_factories_properly() { let config = make_bc_with_defaults(DEFAULT_CHAIN); let payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); - let sent_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); - let failed_payable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let 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![])); @@ -1428,7 +1425,7 @@ mod tests { .make_result(SentPayableDaoMock::new()); // For Payable Scanner let failed_payable_dao_factory = FailedPayableDaoFactoryMock::new() .make_params(&failed_payable_dao_factory_params_arc) - .make_result(FailedPayableDaoMock::new().retrieve_txs_result(vec![])); // For PendingPayableScanner; + .make_result(FailedPayableDaoMock::new().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 @@ -1446,8 +1443,6 @@ mod tests { payable_dao_factory: Box::new(payable_dao_factory), sent_payable_dao_factory: Box::new(sent_payable_dao_factory), failed_payable_dao_factory: Box::new(failed_payable_dao_factory), - failed_payable_dao_factory: Box::new(failed_payable_dao_factory), - sent_payable_dao_factory: Box::new(sent_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), @@ -1953,8 +1948,10 @@ mod tests { }); let sent_tx = make_sent_tx(555); let tx_hash = sent_tx.hash; - let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![sent_tx]); - let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); + let sent_payable_dao = + SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::from([sent_tx])); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) @@ -2244,7 +2241,7 @@ mod tests { let sent_tx = make_sent_tx(123); let tx_hash = sent_tx.hash; let sent_payable_dao = SentPayableDaoMock::default() - .retrieve_txs_result(vec![sent_tx.clone()]) + .retrieve_txs_result(BTreeSet::from([sent_tx.clone()])) .delete_records_params(&delete_records_params_arc) .delete_records_result(Ok(())); let failed_payable_dao = FailedPayableDaoMock::default() @@ -2311,9 +2308,12 @@ mod tests { system.run(); let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); let expected_failed_tx = FailedTx::from((sent_tx, FailureReason::Reverted)); - assert_eq!(*insert_new_records_params, vec![vec![expected_failed_tx]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([expected_failed_tx])] + ); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![tx_hash]]); + assert_eq!(*delete_records_params, vec![BTreeSet::from([tx_hash])]); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), @@ -3419,8 +3419,9 @@ mod tests { #[test] fn initial_pending_payable_scan_if_some_payables_found() { let sent_payable_dao = - SentPayableDaoMock::default().retrieve_txs_result(vec![make_sent_tx(789)]); - let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); + SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::from([make_sent_tx(789)])); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) @@ -3446,8 +3447,9 @@ mod tests { #[test] fn initial_pending_payable_scan_if_no_payables_found() { - let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(vec![]); - let failed_payable_dao = FailedPayableDaoMock::default().retrieve_txs_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); + let failed_payable_dao = + FailedPayableDaoMock::default().retrieve_txs_result(BTreeSet::new()); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) @@ -4949,7 +4951,7 @@ mod tests { #[test] fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { - let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); + // let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let inserted_new_records_params_arc = Arc::new(Mutex::new(vec![])); let expected_wallet = make_wallet("paying_you"); @@ -5066,7 +5068,10 @@ mod tests { BTreeSet::from([expected_tx]) ); let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); - assert_eq!(*get_tx_identifiers_params, vec![hashset!(expected_hash)]); + assert_eq!( + *get_tx_identifiers_params, + vec![BTreeSet::from([expected_hash])] + ); let pending_payable_notify_later_params = pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -5620,7 +5625,7 @@ mod tests { (tx_hash, result, record_by_table) } TxHashByTable::FailedPayable(hash) => { - let mut failed_tx = make_failed_tx(1 + idx); + let mut failed_tx = make_failed_tx(1 + idx as u32); failed_tx.hash = hash; let result = Ok(status); @@ -5662,7 +5667,10 @@ mod tests { System::current().stop(); assert_eq!(system.run(), 0); let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); - assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([sent_tx_1, sent_tx_2])] + ); TestLogHandler::new().exists_log_containing(&format!( "DEBUG: {test_name}: Registered new pending payables for: \ 0x000000000000000000000000000000000000000000000000000000000006c81c, \ @@ -5701,7 +5709,10 @@ mod tests { let _ = subject.register_new_pending_sent_tx(msg); let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); - assert_eq!(*insert_new_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + assert_eq!( + *insert_new_records_params, + vec![BTreeSet::from([sent_tx_1, sent_tx_2])] + ); TestLogHandler::new().exists_log_containing(&format!( "ERROR: {test_name}: Failed to save new pending payable records for \ 0x00000000000000000000000000000000000000000000000000000000000001c8, \ diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 794507d02..33b5157f4 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -665,6 +665,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}; @@ -1584,7 +1585,8 @@ mod tests { let sent_tx = make_sent_tx(456); let sent_tx_hash = sent_tx.hash; let failed_tx = make_failed_tx(789); - let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![sent_tx.clone()]); + let sent_payable_dao = + SentPayableDaoMock::new().retrieve_txs_result(btreeset![sent_tx.clone()]); let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([failed_tx.clone()])); let mut subject = make_dull_subject(); @@ -1632,7 +1634,7 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = make_dull_subject(); let sent_payable_dao = - SentPayableDaoMock::new().retrieve_txs_result(vec![make_sent_tx(123)]); + SentPayableDaoMock::new().retrieve_txs_result(btreeset![make_sent_tx(123)]); let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::from([make_failed_tx(456)])); let pending_payable_scanner = PendingPayableScannerBuilder::new() @@ -1922,13 +1924,16 @@ mod tests { assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); let sent_tx_2 = SentTx::from((failed_tx_2, tx_block_2)); let replace_records_params = replace_records_params_arc.lock().unwrap(); - assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + assert_eq!(*replace_records_params, vec![btreeset![sent_tx_2]]); let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); let expected_failure_for_tx_3 = FailedTx::from((sent_tx_3, FailureReason::PendingTooLong)); let expected_failure_for_tx_6 = FailedTx::from((sent_tx_6, FailureReason::Reverted)); assert_eq!( *insert_new_records_params, - vec![vec![expected_failure_for_tx_3, expected_failure_for_tx_6]] + vec![btreeset![ + expected_failure_for_tx_3, + expected_failure_for_tx_6 + ]] ); let update_statuses_pending_payable_params = update_statuses_pending_payable_params_arc.lock().unwrap(); diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 35de06667..f7c0ad981 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -38,7 +38,7 @@ use masq_lib::logger::Logger; use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::fmt::Display; use std::rc::Rc; use std::str::FromStr; @@ -163,7 +163,8 @@ impl PendingPayableScanner { } let pending_tx_hashes = Self::get_wrapped_hashes(&pending_txs, TxHashByTable::SentPayable); - self.current_sent_payables.load_cache(pending_txs.into()); + self.current_sent_payables + .load_cache(pending_txs.into_iter().collect()); Some(pending_tx_hashes) } @@ -178,7 +179,7 @@ impl PendingPayableScanner { let failure_hashes = Self::get_wrapped_hashes(&failures, TxHashByTable::FailedPayable); self.yet_unproven_failed_payables - .load_cache(failures.into()); + .load_cache(failures.into_iter().collect()); Some(failure_hashes) } @@ -371,7 +372,7 @@ impl PendingPayableScanner { self.add_to_the_total_of_paid_payable(&reclaimed, logger) } - fn isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> HashSet { + fn isolate_hashes(reclaimed: &[(TxHash, TxBlock)]) -> BTreeSet { reclaimed.iter().map(|(tx_hash, _)| *tx_hash).collect() } @@ -410,10 +411,9 @@ impl PendingPayableScanner { hashes_and_blocks: &[(TxHash, TxBlock)], logger: &Logger, ) { - match self - .sent_payable_dao - .replace_records(sent_txs_to_reclaim.into()) - { + 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") } @@ -579,7 +579,7 @@ impl PendingPayableScanner { } fn add_new_failures(&self, new_failures: Vec, logger: &Logger) { - fn prepare_hashset(failures: &[FailedTx]) -> HashSet { + fn prepare_btreeset(failures: &[FailedTx]) -> BTreeSet { failures.iter().map(|failure| failure.hash).collect() } fn log_procedure_finished(logger: &Logger, new_failures: &[FailedTx]) { @@ -594,7 +594,12 @@ impl PendingPayableScanner { return; } - if let Err(e) = self.failed_payable_dao.insert_new_records(&new_failures) { + let new_failures_btree_set: BTreeSet = new_failures.iter().cloned().collect(); + + if let Err(e) = self + .failed_payable_dao + .insert_new_records(&new_failures_btree_set) + { panic!( "Unable to persist failed txs {} due to: {:?}", comma_joined_stringifiable(&new_failures, |failure| format!("{:?}", failure.hash)), @@ -604,7 +609,7 @@ impl PendingPayableScanner { match self .sent_payable_dao - .delete_records(&prepare_hashset(&new_failures)) + .delete_records(&prepare_btreeset(&new_failures)) { Ok(_) => { log_procedure_finished(logger, &new_failures); @@ -858,9 +863,9 @@ mod tests { let failed_tx_2 = make_failed_tx(890); let failed_tx_hash_2 = failed_tx_2.hash; let sent_payable_dao = SentPayableDaoMock::new() - .retrieve_txs_result(vec![sent_tx_1.clone(), sent_tx_2.clone()]); + .retrieve_txs_result(btreeset![sent_tx_1.clone(), sent_tx_2.clone()]); let failed_payable_dao = FailedPayableDaoMock::new() - .retrieve_txs_result(vec![failed_tx_1.clone(), failed_tx_2.clone()]); + .retrieve_txs_result(btreeset![failed_tx_1.clone(), failed_tx_2.clone()]); let mut subject = PendingPayableScannerBuilder::new() .sent_payable_dao(sent_payable_dao) .failed_payable_dao(failed_payable_dao) @@ -1093,8 +1098,8 @@ mod tests { fn throws_an_error_when_no_records_to_process_were_found() { let now = SystemTime::now(); let consuming_wallet = make_paying_wallet(b"consuming_wallet"); - let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); - let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(btreeset![]); + let failed_payable_dao = FailedPayableDaoMock::new().retrieve_txs_result(btreeset![]); let mut subject = PendingPayableScannerBuilder::new() .failed_payable_dao(failed_payable_dao) .sent_payable_dao(sent_payable_dao) @@ -1156,10 +1161,10 @@ mod tests { let insert_new_records_params = insert_new_records_params_arc.lock().unwrap(); assert_eq!( *insert_new_records_params, - vec![vec![failed_tx_1, failed_tx_2]] + vec![btreeset![failed_tx_1, failed_tx_2]] ); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![hash_1, hash_2]]); + assert_eq!(*delete_records_params, vec![btreeset![hash_1, hash_2]]); TestLogHandler::new().exists_log_containing(&format!( "INFO: {test_name}: Failed txs 0x0000000000000000000000000000000000000000000000000000000000000321, \ 0x0000000000000000000000000000000000000000000000000000000000000654 were processed in the db" @@ -1193,7 +1198,7 @@ mod tests { ))); let failed_payable_dao = FailedPayableDaoMock::default() .retrieve_txs_params(&retrieve_failed_txs_params_arc) - .retrieve_txs_result(vec![failed_tx_1, failed_tx_2]) + .retrieve_txs_result(btreeset![failed_tx_1, failed_tx_2]) .update_statuses_params(&update_statuses_failed_tx_params_arc) .update_statuses_result(Ok(())); let mut sent_tx = make_sent_tx(789); @@ -1201,7 +1206,7 @@ mod tests { sent_tx.status = TxStatus::Pending(ValidationStatus::Waiting); let sent_payable_dao = SentPayableDaoMock::default() .retrieve_txs_params(&retrieve_sent_txs_params_arc) - .retrieve_txs_result(vec![sent_tx.clone()]) + .retrieve_txs_result(btreeset![sent_tx.clone()]) .update_statuses_params(&update_statuses_sent_tx_params_arc) .update_statuses_result(Ok(())); let validation_failure_clock = ValidationFailureClockMock::default() @@ -1438,7 +1443,7 @@ mod tests { vec![BTreeSet::from([failed_tx_1])] ); let delete_records_params = delete_records_params_arc.lock().unwrap(); - assert_eq!(*delete_records_params, vec![hashset![tx_hash_1]]); + assert_eq!(*delete_records_params, vec![btreeset![tx_hash_1]]); let update_statuses_params = update_status_params_arc.lock().unwrap(); assert_eq!( *update_statuses_params, @@ -1626,7 +1631,10 @@ mod tests { ); let replace_records_params = replace_records_params_arc.lock().unwrap(); - assert_eq!(*replace_records_params, vec![vec![sent_tx_1, sent_tx_2]]); + assert_eq!( + *replace_records_params, + vec![btreeset![sent_tx_1, sent_tx_2]] + ); let delete_records_params = delete_records_params_arc.lock().unwrap(); // assert_eq!(*delete_records_params, vec![hashset![tx_hash_1, tx_hash_2]]); assert_eq!( @@ -1888,7 +1896,7 @@ mod tests { let confirm_tx_params = confirm_tx_params_arc.lock().unwrap(); assert_eq!(*confirm_tx_params, vec![hashmap![tx_hash_1 => tx_block_1]]); let replace_records_params = replace_records_params_arc.lock().unwrap(); - assert_eq!(*replace_records_params, vec![vec![sent_tx_2]]); + assert_eq!(*replace_records_params, vec![btreeset![sent_tx_2]]); let delete_records_params = delete_records_params_arc.lock().unwrap(); // assert_eq!(*delete_records_params, vec![hashset![tx_hash_2]]); assert_eq!(*delete_records_params, vec![BTreeSet::from([tx_hash_2])]); diff --git a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs index 10e6c93a5..6039cd711 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/tx_receipt_interpreter.rs @@ -249,6 +249,7 @@ mod tests { use crate::test_utils::unshared_test_utils::capture_digits_with_separators_from_str; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::collections::BTreeSet; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; @@ -417,7 +418,7 @@ mod tests { let newer_sent_tx_for_older_failed_tx = make_sent_tx(2244); let sent_payable_dao = SentPayableDaoMock::new() .retrieve_txs_params(&retrieve_txs_params_arc) - .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + .retrieve_txs_result(BTreeSet::from([newer_sent_tx_for_older_failed_tx])); let hash = make_tx_hash(0x913); let sent_tx_timestamp = to_unix_timestamp( SystemTime::now() @@ -484,7 +485,7 @@ mod tests { newer_sent_tx_for_older_failed_tx.hash = make_tx_hash(0x7c6); let sent_payable_dao = SentPayableDaoMock::new() .retrieve_txs_params(&retrieve_txs_params_arc) - .retrieve_txs_result(vec![newer_sent_tx_for_older_failed_tx]); + .retrieve_txs_result(BTreeSet::from([newer_sent_tx_for_older_failed_tx])); let tx_hash = make_tx_hash(0x913); let mut failed_tx = make_failed_tx(789); let failed_tx_nonce = failed_tx.nonce; @@ -564,7 +565,7 @@ mod tests { ) { let scan_report = ReceiptScanReport::default(); let still_pending_tx = make_failed_tx(456); - let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(vec![]); + let sent_payable_dao = SentPayableDaoMock::new().retrieve_txs_result(BTreeSet::new()); let _ = TxReceiptInterpreter::handle_still_pending_tx( scan_report, diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index fb2ef50ab..0952c4e64 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -379,7 +379,7 @@ impl AccountantBuilder { ) -> Self { specially_configured_daos.iter_mut().for_each(|dao| { if let DaoWithDestination::ForPendingPayableScanner(dao) = dao { - let mut extended_queue = vec![vec![]]; + let mut extended_queue = vec![BTreeSet::new()]; extended_queue.append(&mut dao.retrieve_txs_results.borrow_mut()); dao.retrieve_txs_results.replace(extended_queue); } @@ -1035,7 +1035,7 @@ pub struct SentPayableDaoMock { confirm_tx_results: RefCell>>, update_statuses_params: Arc>>>, update_statuses_results: RefCell>>, - replace_records_params: Arc>>>, + replace_records_params: Arc>>>, replace_records_results: RefCell>>, delete_records_params: Arc>>>, delete_records_results: RefCell>>, @@ -1053,7 +1053,7 @@ impl SentPayableDao for SentPayableDaoMock { self.insert_new_records_params .lock() .unwrap() - .push(txs.to_vec()); + .push(txs.clone()); self.insert_new_records_results.borrow_mut().remove(0) } fn retrieve_txs(&self, condition: Option) -> BTreeSet { @@ -1071,7 +1071,7 @@ impl SentPayableDao for SentPayableDaoMock { self.replace_records_params .lock() .unwrap() - .push(new_txs.to_vec()); + .push(new_txs.clone()); self.replace_records_results.borrow_mut().remove(0) } @@ -1100,7 +1100,7 @@ impl SentPayableDaoMock { SentPayableDaoMock::default() } - pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { + pub fn get_tx_identifiers_params(mut self, params: &Arc>>>) -> Self { self.get_tx_identifiers_params = params.clone(); self } @@ -1128,7 +1128,7 @@ impl SentPayableDaoMock { self } - pub fn retrieve_txs_result(self, result: Vec) -> Self { + pub fn retrieve_txs_result(self, result: BTreeSet) -> Self { self.retrieve_txs_results.borrow_mut().push(result); self } @@ -1143,7 +1143,7 @@ impl SentPayableDaoMock { self } - pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { + pub fn replace_records_params(mut self, params: &Arc>>>) -> Self { self.replace_records_params = params.clone(); self } @@ -1166,7 +1166,7 @@ impl SentPayableDaoMock { self } - pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { + pub fn delete_records_params(mut self, params: &Arc>>>) -> Self { self.delete_records_params = params.clone(); self } From 89471368e779cd32fbb771f71ba02aa1b4284a51 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 25 Sep 2025 14:19:35 +0530 Subject: [PATCH 29/48] GH-605: only 2 errors remaining --- node/src/blockchain/errors/validation_status.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 77e5870ff..b2b1e93a0 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -13,12 +13,24 @@ use std::fmt::Formatter; use std::hash::{Hash, Hasher}; use std::time::SystemTime; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ValidationStatus { Waiting, Reattempting(PreviousAttempts), } +impl PartialOrd for ValidationStatus { + fn partial_cmp(&self, other: &Self) -> Option { + todo!() + } +} + +impl Ord for ValidationStatus { + fn cmp(&self, other: &Self) -> Ordering { + todo!() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreviousAttempts { inner: HashMap, From b2df75966d84f21809b403d658460f42151009ce Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 25 Sep 2025 14:29:53 +0530 Subject: [PATCH 30/48] GH-605: all errors gone --- node/src/accountant/mod.rs | 1 - .../scanners/pending_payable_scanner/mod.rs | 41 ++++++++++--------- node/src/accountant/test_utils.rs | 6 --- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 84b90f706..9a88acd5f 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1673,7 +1673,6 @@ mod tests { 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() - .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) .sent_payable_dao(sent_payable_dao) .bootstrapper_config(config) diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index f7c0ad981..f6029f244 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -12,6 +12,7 @@ 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, @@ -162,9 +163,11 @@ impl PendingPayableScanner { return None; } - let pending_tx_hashes = Self::get_wrapped_hashes(&pending_txs, TxHashByTable::SentPayable); - self.current_sent_payables - .load_cache(pending_txs.into_iter().collect()); + let pending_txs_vec: Vec = pending_txs.into_iter().collect(); + + let pending_tx_hashes = + Self::get_wrapped_hashes(&pending_txs_vec, TxHashByTable::SentPayable); + self.current_sent_payables.load_cache(pending_txs_vec); Some(pending_tx_hashes) } @@ -177,25 +180,25 @@ impl PendingPayableScanner { return None; } - let failure_hashes = Self::get_wrapped_hashes(&failures, TxHashByTable::FailedPayable); - self.yet_unproven_failed_payables - .load_cache(failures.into_iter().collect()); + let failures_vec: Vec = failures.into_iter().collect(); + + let failure_hashes = Self::get_wrapped_hashes(&failures_vec, TxHashByTable::FailedPayable); + self.yet_unproven_failed_payables.load_cache(failures_vec); Some(failure_hashes) } - // TODO: GH-605: Another issue with this fn - // fn get_wrapped_hashes( - // records: &[Record], - // wrap_the_hash: fn(TxHash) -> TxHashByTable, - // ) -> Vec - // where - // Record: TxRecordWithHash, - // { - // records - // .iter() - // .map(|record| wrap_the_hash(record.hash())) - // .collect_vec() - // } + fn get_wrapped_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() { diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 0952c4e64..b42b77222 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -475,12 +475,6 @@ impl AccountantBuilder { .make_result(ReceivableDaoMock::new()) .make_result(ReceivableDaoMock::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 sent_payable_dao_factory = self .sent_payable_dao_factory_opt .unwrap_or(SentPayableDaoFactoryMock::new().make_result(SentPayableDaoMock::new())); From 2828f80fdda3269fa92066ba148b3b5a00a88303 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Thu, 25 Sep 2025 18:57:51 +0530 Subject: [PATCH 31/48] GH-605: tests in sent_payable_dao are passing --- .../db_access_objects/sent_payable_dao.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 b5eb5eb80..1045e8c91 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -656,17 +656,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: 1749204020, gas_price_minor: 0, \ nonce: 0, status: Confirmed { block_hash: \ \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ block_number: 7890123, detection: Reclaim } }, \ - Tx { \ + SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ - amount: 0, timestamp: 1749204017, gas_price_wei: 0, \ + amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ nonce: 0, status: Pending(Waiting) }\ }" .to_string() @@ -1264,13 +1264,6 @@ mod tests { assert_eq!(result, Ok(())); assert_eq!( updated_txs[0].status, - TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)), - &ValidationFailureClockMock::default().now_result(timestamp_a) - ))) - ); - assert_eq!( - updated_txs[1].status, TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( @@ -1286,6 +1279,13 @@ mod tests { ) )) ); + 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::Confirmed { From 9a51af420a39030fbe79ad7825ae5ead7da77703 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 09:17:39 +0530 Subject: [PATCH 32/48] GH-605: all tests pass in payable scanner --- .../scanners/payable_scanner/utils.rs | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/node/src/accountant/scanners/payable_scanner/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index 5a7a21c65..c74a4c2ed 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -498,64 +498,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) - } } From 663c30e2e4995e1725ef3d1649a773481ddd787d Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 09:46:27 +0530 Subject: [PATCH 33/48] GH-605: fix most of the clippy warnings --- masq_lib/src/utils.rs | 1 + .../db_access_objects/failed_payable_dao.rs | 4 +-- .../db_access_objects/payable_dao.rs | 4 +-- .../db_access_objects/sent_payable_dao.rs | 2 +- node/src/accountant/mod.rs | 4 --- node/src/accountant/scanners/mod.rs | 14 +++----- .../scanners/payable_scanner/start_scan.rs | 2 -- .../tx_templates/signable/mod.rs | 1 - .../scanners/payable_scanner/utils.rs | 1 - .../scanners/pending_payable_scanner/mod.rs | 2 +- .../accountant/scanners/scan_schedulers.rs | 2 +- .../blockchain/blockchain_agent/agent_web3.rs | 4 +-- node/src/blockchain/blockchain_bridge.rs | 33 ++++++++++--------- .../blockchain_interface_web3/mod.rs | 3 +- .../blockchain_interface_web3/utils.rs | 3 -- .../data_structures/errors.rs | 6 ++-- .../data_structures/mod.rs | 1 - 17 files changed, 32 insertions(+), 55 deletions(-) diff --git a/masq_lib/src/utils.rs b/masq_lib/src/utils.rs index b7691e317..ad4197aad 100644 --- a/masq_lib/src/utils.rs +++ b/masq_lib/src/utils.rs @@ -543,6 +543,7 @@ macro_rules! btreeset { mod tests { use super::*; use itertools::Itertools; + use std::collections::BTreeSet; use std::collections::{BTreeMap, HashMap, HashSet}; use std::env::current_dir; use std::fmt::Write; 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 2e0b24206..adac977da 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -5,9 +5,9 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_access_objects::Transaction; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, comma_joined_stringifiable, join_with_separator}; +use crate::accountant::{comma_joined_stringifiable, join_with_separator}; use crate::blockchain::errors::rpc_errors::{AppRpcError, AppRpcErrorKind}; -use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationStatus}; +use crate::blockchain::errors::validation_status::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 5582646f7..4f7b1db47 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -5,8 +5,7 @@ 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, TxHash, - VigilantRusqliteFlatten, + CustomQuery, DaoFactoryReal, RangeStmConfig, RowId, TopStmConfig, VigilantRusqliteFlatten, }; use crate::accountant::db_big_integer::big_int_db_processor::KeyVariants::WalletAddress; use crate::accountant::db_big_integer::big_int_db_processor::{ @@ -30,7 +29,6 @@ use rusqlite::{Error, Row}; use std::collections::BTreeSet; use std::fmt::{Debug, Display, Formatter}; use std::time::SystemTime; -use web3::types::H256; #[derive(Debug, PartialEq, Eq)] pub enum PayableDaoError { 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 1045e8c91..81fca1f57 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -14,7 +14,7 @@ use itertools::Itertools; use masq_lib::utils::ExpectValue; use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; use std::fmt::{Display, Formatter}; use std::str::FromStr; use web3::types::Address; diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 9a88acd5f..64575cb3f 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -36,8 +36,6 @@ use crate::accountant::scanners::{Scanners, StartScanError}; use crate::blockchain::blockchain_bridge::{ BlockMarker, RegisterNewPendingPayables, 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, StatusReadFromReceiptCheck, }; @@ -81,12 +79,10 @@ use std::collections::{BTreeSet, HashMap}; #[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; pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 33b5157f4..9c24ee6ed 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -6,10 +6,7 @@ pub mod receivable_scanner; pub mod scan_schedulers; pub mod test_utils; -use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; -use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDao; -use crate::accountant::db_access_objects::utils::TxHash; -use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; +use crate::accountant::payment_adjuster::PaymentAdjusterReal; use crate::accountant::scanners::payable_scanner::msgs::{ InitialTemplatesMessage, PricedTemplatesMessage, }; @@ -25,7 +22,6 @@ use crate::accountant::{ 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, DetailedScanType, FinancialStatistics, PaymentThresholds, @@ -33,14 +29,12 @@ use crate::sub_lib::accountant::{ 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; diff --git a/node/src/accountant/scanners/payable_scanner/start_scan.rs b/node/src/accountant/scanners/payable_scanner/start_scan.rs index cb1d4ae5b..67d71a339 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; 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 7bb488a5d..d1ae97ebe 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 @@ -5,7 +5,6 @@ use crate::accountant::scanners::payable_scanner::tx_templates::priced::new::{ use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::{ PricedRetryTxTemplate, PricedRetryTxTemplates, }; -use bytes::Buf; use itertools::{Either, Itertools}; use std::ops::Deref; use web3::types::Address; diff --git a/node/src/accountant/scanners/payable_scanner/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index c74a4c2ed..6b44a0f46 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -9,7 +9,6 @@ use crate::accountant::{comma_joined_stringifiable, 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; diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index f6029f244..303ee16f2 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -39,7 +39,7 @@ use masq_lib::logger::Logger; use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use std::cell::RefCell; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap}; use std::fmt::Display; use std::rc::Rc; use std::str::FromStr; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index e5a5eeba1..6545f4141 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -2,7 +2,7 @@ use crate::accountant::scanners::StartScanError; use crate::accountant::{ - Accountant, ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, + Accountant, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, ScanForRetryPayables, }; use crate::sub_lib::accountant::ScanIntervals; diff --git a/node/src/blockchain/blockchain_agent/agent_web3.rs b/node/src/blockchain/blockchain_agent/agent_web3.rs index d431e6877..66df08d57 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 { diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index f017ee6b1..4225be167 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -8,7 +8,7 @@ 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::scanners::payable_scanner::utils::initial_templates_msg_stats; use crate::accountant::{ - PayableScanType, ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, + ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, }; use crate::accountant::{RequestTransactionReceipts, TxReceiptResult, TxReceiptsMessage}; use crate::actor_system_factory::SubsFactory; @@ -41,7 +41,6 @@ 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::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; @@ -149,13 +148,14 @@ 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, - todo!( - "This needs to be decided on GH-605. Look what mode you run and set it accordingly" - ), - msg, - ); + todo!("This needs to be decided on GH-605. Look what mode you run and set it accordingly"); + // self.handle_scan_future( + // Self::handle_initial_templates_msg, + // todo!( + // "This needs to be decided on GH-605. Look what mode you run and set it accordingly" + // ), + // msg, + // ); } } @@ -163,13 +163,14 @@ impl Handler for BlockchainBridge { type Result = (); fn handle(&mut self, msg: OutboundPaymentsInstructions, _ctx: &mut Self::Context) { - self.handle_scan_future( - Self::handle_outbound_payments_instructions, - todo!( - "This needs to be decided on GH-605. Look what mode you run and set it accordingly" - ), - msg, - ) + todo!("This needs to be decided on GH-605. Look what mode you run and set it accordingly") + // self.handle_scan_future( + // Self::handle_outbound_payments_instructions, + // todo!( + // "This needs to be decided on GH-605. Look what mode you run and set it accordingly" + // ), + // msg, + // ) } } 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 090cff608..80d557091 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -17,7 +17,6 @@ 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}; @@ -30,7 +29,7 @@ use crate::accountant::scanners::payable_scanner::tx_templates::priced::retry::P use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplates; use crate::accountant::scanners::pending_payable_scanner::utils::TxHashByTable; use crate::accountant::TxReceiptResult; -use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, RegisterNewPendingPayables}; +use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::LowBlockchainIntWeb3; use crate::blockchain::blockchain_interface::blockchain_interface_web3::utils::{create_blockchain_agent_web3, send_payables_within_batch, BlockchainAgentFutureResult}; use crate::blockchain::errors::rpc_errors::{AppRpcError, RemoteError}; 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 ba3fd3d61..54688062e 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -1,7 +1,6 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::failed_payable_dao::FailedTx; -use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::to_unix_timestamp; use crate::accountant::scanners::payable_scanner::tx_templates::signable::{ @@ -19,13 +18,11 @@ use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; 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; use secp256k1secrets::SecretKey; use serde_json::Value; -use std::collections::HashSet; use std::iter::once; use std::time::SystemTime; use thousands::Separable; diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index ffd3c0f50..7c01a9b5c 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -1,10 +1,8 @@ // 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::db_access_objects::utils::TxHash; -use itertools::{Either, Itertools}; -use std::collections::HashSet; +use crate::accountant::join_with_separator; +use itertools::Either; use std::fmt; use std::fmt::{Display, Formatter}; use variant_count::VariantCount; diff --git a/node/src/blockchain/blockchain_interface/data_structures/mod.rs b/node/src/blockchain/blockchain_interface/data_structures/mod.rs index 724a49f9a..d3a961fc0 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -12,7 +12,6 @@ use serde_derive::{Deserialize, Serialize}; use std::fmt; use std::fmt::{Display, Formatter}; use web3::types::{TransactionReceipt, H256}; -use web3::Error; #[derive(Clone, Debug, Eq, PartialEq)] pub struct BlockchainTransaction { From 6c84f821b967374f2cc36764e0364e1685b54e0d Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 09:51:16 +0530 Subject: [PATCH 34/48] GH-605: fix more of these problems --- node/src/accountant/mod.rs | 2 +- node/src/accountant/scanners/mod.rs | 23 ++++++++++--------- .../data_structures/mod.rs | 2 +- .../blockchain/errors/validation_status.rs | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 64575cb3f..9befe62d7 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -147,7 +147,7 @@ pub struct TxReceiptsMessage { pub response_skeleton_opt: Option, } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum PayableScanType { New, Retry, diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 9c24ee6ed..521c787e6 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -278,17 +278,18 @@ impl Scanners { } fn empty_caches(&mut self, logger: &Logger) { - let pending_payable_scanner = self - .pending_payable - .as_any_mut() - .downcast_mut::() - .expect("mismatched types"); - pending_payable_scanner - .current_sent_payables - .ensure_empty_cache(logger); - pending_payable_scanner - .yet_unproven_failed_payables - .ensure_empty_cache(logger); + todo!("GH-605: As any mut is not a type"); + // let pending_payable_scanner = self + // .pending_payable + // .as_any_mut() + // .downcast_mut::() + // .expect("mismatched types"); + // pending_payable_scanner + // .current_sent_payables + // .ensure_empty_cache(logger); + // pending_payable_scanner + // .yet_unproven_failed_payables + // .ensure_empty_cache(logger); } pub fn try_skipping_payable_adjustment( diff --git a/node/src/blockchain/blockchain_interface/data_structures/mod.rs b/node/src/blockchain/blockchain_interface/data_structures/mod.rs index d3a961fc0..f79f12345 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/mod.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/mod.rs @@ -36,7 +36,7 @@ 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 failed_txs: Vec, diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index b2b1e93a0..8ff95c7e5 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -47,7 +47,7 @@ impl Hash for PreviousAttempts { impl PartialOrd for PreviousAttempts { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } From 2f075816387695a212cefbb747cb5cbfbd4fd34e Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 10:04:56 +0530 Subject: [PATCH 35/48] GH-605: write test for the ordering of ValidationStatus --- .../db_access_objects/payable_dao.rs | 3 +- node/src/accountant/mod.rs | 1 + .../scanners/payable_scanner/start_scan.rs | 2 + .../blockchain_interface_web3/utils.rs | 1 + .../blockchain/errors/validation_status.rs | 39 +++++++++++++++++++ 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 4f7b1db47..f8586b59b 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -29,6 +29,7 @@ use rusqlite::{Error, Row}; use std::collections::BTreeSet; use std::fmt::{Debug, Display, Formatter}; use std::time::SystemTime; +use web3::types::H256; #[derive(Debug, PartialEq, Eq)] pub enum PayableDaoError { @@ -563,7 +564,7 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::SentTx; use crate::accountant::db_access_objects::test_utils::make_sent_tx; use crate::accountant::db_access_objects::utils::{ - current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, + current_unix_timestamp, from_unix_timestamp, to_unix_timestamp, TxHash, }; use crate::accountant::gwei_to_wei; use crate::accountant::test_utils::{ diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 9befe62d7..259b2167a 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1376,6 +1376,7 @@ mod tests { use std::sync::Mutex; use std::time::{Duration, UNIX_EPOCH}; use std::vec; + use web3::types::H256; impl Handler> for Accountant { type Result = (); diff --git a/node/src/accountant/scanners/payable_scanner/start_scan.rs b/node/src/accountant/scanners/payable_scanner/start_scan.rs index 67d71a339..2b5c6723d 100644 --- a/node/src/accountant/scanners/payable_scanner/start_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/start_scan.rs @@ -80,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; 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 54688062e..56d3ed990 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -348,6 +348,7 @@ mod tests { use actix::{Actor, System}; use ethabi::Address; use ethereum_types::H256; + use itertools::Either; use jsonrpc_core::ErrorCode::ServerError; use jsonrpc_core::{Error, ErrorCode}; use masq_lib::constants::{DEFAULT_CHAIN, DEFAULT_GAS_PRICE}; diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 8ff95c7e5..e891d01f5 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -431,4 +431,43 @@ mod tests { "invalid type: string \"Yesterday\", expected struct SystemTime at line 1 column 79" ); } + + #[test] + fn validation_status_ordering_works_correctly() { + let now = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(now) + .now_result(now + Duration::from_secs(1)); + + let waiting = ValidationStatus::Waiting; + let reattempting_early = ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &clock, + )); + let reattempting_late = ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), + &clock, + )); + + // Waiting < Reattempting + assert_eq!(waiting.cmp(&reattempting_early), Ordering::Less); + assert_eq!( + waiting.partial_cmp(&reattempting_early), + Some(Ordering::Less) + ); + + // Earlier reattempting < Later reattempting + assert_eq!(reattempting_early.cmp(&reattempting_late), Ordering::Less); + assert_eq!( + reattempting_early.partial_cmp(&reattempting_late), + Some(Ordering::Less) + ); + + // Waiting == Waiting + assert_eq!(waiting.cmp(&ValidationStatus::Waiting), Ordering::Equal); + assert_eq!( + waiting.partial_cmp(&ValidationStatus::Waiting), + Some(Ordering::Equal) + ); + } } From d6b85929efbeff1f3be3ef5cbda474ba2763ebff Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 10:06:46 +0530 Subject: [PATCH 36/48] GH-605: implement ordering for ValidationStatus --- node/src/blockchain/errors/validation_status.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index e891d01f5..0601e2533 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -21,13 +21,21 @@ pub enum ValidationStatus { impl PartialOrd for ValidationStatus { fn partial_cmp(&self, other: &Self) -> Option { - todo!() + Some(self.cmp(other)) } } impl Ord for ValidationStatus { fn cmp(&self, other: &Self) -> Ordering { - todo!() + match (self, other) { + (ValidationStatus::Waiting, ValidationStatus::Waiting) => Ordering::Equal, + (ValidationStatus::Waiting, ValidationStatus::Reattempting(_)) => Ordering::Less, + (ValidationStatus::Reattempting(_), ValidationStatus::Waiting) => Ordering::Greater, + ( + ValidationStatus::Reattempting(attempts1), + ValidationStatus::Reattempting(attempts2), + ) => attempts1.cmp(attempts2), + } } } From 5838a529b999a57c8de5db8b4936d0ec88b6d401 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 10:11:50 +0530 Subject: [PATCH 37/48] GH-605: comment out unused code --- node/src/accountant/test_utils.rs | 43 ++++++++++--------- node/src/blockchain/blockchain_bridge.rs | 15 +++---- .../blockchain_interface_web3/utils.rs | 3 +- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index b42b77222..10a1d9625 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -133,27 +133,28 @@ pub fn make_transaction_block(num: u64) -> TxBlock { } } -struct TxRecordCommonParts { - hash: TxHash, - receiver_address: Address, - amount_minor: u128, - timestamp: i64, - gas_price_minor: u128, - nonce: u64, -} - -impl TxRecordCommonParts { - fn new(num: u64) -> Self { - Self { - hash: make_tx_hash(num as u32), - receiver_address: make_wallet(&format!("wallet{}", num)).address(), - amount_minor: gwei_to_wei(num * num), - timestamp: to_unix_timestamp(SystemTime::now()) - (num as i64 * 60), - gas_price_minor: gwei_to_wei(num), - nonce: num, - } - } -} +// TODO: GH-605: Unused code +// struct TxRecordCommonParts { +// hash: TxHash, +// receiver_address: Address, +// amount_minor: u128, +// timestamp: i64, +// gas_price_minor: u128, +// nonce: u64, +// } +// +// impl TxRecordCommonParts { +// fn new(num: u64) -> Self { +// Self { +// hash: make_tx_hash(num as u32), +// receiver_address: make_wallet(&format!("wallet{}", num)).address(), +// amount_minor: gwei_to_wei(num * num), +// timestamp: to_unix_timestamp(SystemTime::now()) - (num as i64 * 60), +// gas_price_minor: gwei_to_wei(num), +// nonce: num, +// } +// } +// } pub struct AccountantBuilder { config_opt: Option, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 4225be167..d56da965e 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -509,12 +509,13 @@ impl BlockchainBridge { .submit_payables_in_batch(logger, agent, priced_templates) } - fn new_pending_payables_recipient(&self) -> Recipient { - self.pending_payable_confirmation - .register_new_pending_payables_sub_opt - .clone() - .expect("Accountant unbound") - } + // TODO: GH-605: Unused code + // fn new_pending_payables_recipient(&self) -> Recipient { + // self.pending_payable_confirmation + // .register_new_pending_payables_sub_opt + // .clone() + // .expect("Accountant unbound") + // } pub fn extract_max_block_count(error: BlockchainInterfaceError) -> Option { let regex_result = @@ -935,9 +936,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 = 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 56d3ed990..9ae03ce32 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -427,7 +427,6 @@ mod tests { #[test] fn sign_and_append_multiple_payments_works() { - let now = SystemTime::now(); let port = find_free_port(); let logger = Logger::new("sign_and_append_multiple_payments_works"); let (_event_loop_handle, transport) = Http::with_max_parallel( @@ -444,7 +443,7 @@ mod tests { make_signable_tx_template(5), ]); - let mut result = sign_and_append_multiple_payments( + let result = sign_and_append_multiple_payments( &logger, DEFAULT_CHAIN, &web3_batch, From eab920d4506b9f9aeb2f3dd7665fa7a3bc8c99e3 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 12:14:25 +0530 Subject: [PATCH 38/48] GH-605: more changes fixed --- .../db_access_objects/sent_payable_dao.rs | 96 ++++++++++++--- node/src/accountant/mod.rs | 2 +- node/src/accountant/scanners/mod.rs | 109 +----------------- .../tx_templates/test_utils.rs | 1 - node/src/accountant/test_utils.rs | 58 +--------- node/src/blockchain/blockchain_bridge.rs | 16 +-- 6 files changed, 85 insertions(+), 197 deletions(-) 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 81fca1f57..4ea3c3b6d 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -98,13 +98,32 @@ pub enum TxStatus { impl PartialOrd for TxStatus { fn partial_cmp(&self, other: &Self) -> Option { - todo!() + Some(self.cmp(other)) } } impl Ord for TxStatus { fn cmp(&self, other: &Self) -> Ordering { - todo!() + match (self, other) { + (TxStatus::Pending(status1), TxStatus::Pending(status2)) => status1.cmp(status2), + (TxStatus::Pending(_), TxStatus::Confirmed { .. }) => Ordering::Less, + (TxStatus::Confirmed { .. }, TxStatus::Pending(_)) => Ordering::Greater, + ( + TxStatus::Confirmed { + block_number: bn1, + detection: det1, + block_hash: bh1, + }, + TxStatus::Confirmed { + block_number: bn2, + detection: det2, + block_hash: bh2, + }, + ) => bn1 + .cmp(bn2) + .then_with(|| det1.cmp(det2)) + .then_with(|| bh1.cmp(bh2)), + } } } @@ -188,22 +207,6 @@ pub trait SentPayableDao { fn delete_records(&self, hashes: &BTreeSet) -> Result<(), SentPayableDaoError>; } -// TODO: GH-605: Coming from GH-598 -// pub trait SentPayableDao { -// fn get_tx_identifiers(&self, hashes: &HashSet) -> TxIdentifiers; -// fn insert_new_records(&self, txs: &[SentTx]) -> Result<(), SentPayableDaoError>; -// fn retrieve_txs(&self, condition: Option) -> Vec; -// //TODO potentially atomically -// fn confirm_txs(&self, hash_map: &HashMap) -> Result<(), SentPayableDaoError>; -// fn replace_records(&self, new_txs: &[SentTx]) -> Result<(), SentPayableDaoError>; -// fn update_statuses( -// &self, -// hash_map: &HashMap, -// ) -> Result<(), SentPayableDaoError>; -// //TODO potentially atomically -// fn delete_records(&self, hashes: &HashSet) -> Result<(), SentPayableDaoError>; -// } - #[derive(Debug)] pub struct SentPayableDaoReal<'a> { conn: Box, @@ -565,6 +568,7 @@ mod tests { use ethereum_types::{H256, U64}; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; + use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::ops::{Add, Sub}; use std::str::FromStr; @@ -1650,4 +1654,60 @@ mod tests { assert_eq!(tx.nonce(), nonce); assert_eq!(tx.is_failed(), false); } + + #[test] + fn tx_status_ordering_works_correctly() { + let now = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(now) + .now_result(now + Duration::from_secs(1)); + + let pending_waiting = TxStatus::Pending(ValidationStatus::Waiting); + let pending_reattempting = + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &clock, + ))); + let confirmed_early = TxStatus::Confirmed { + block_hash: "0x123".to_string(), + block_number: 100, + detection: Detection::Normal, + }; + let confirmed_late = TxStatus::Confirmed { + block_hash: "0x456".to_string(), + block_number: 200, + detection: Detection::Normal, + }; + + // Pending < Confirmed + assert_eq!(pending_waiting.cmp(&confirmed_early), Ordering::Less); + assert_eq!( + pending_waiting.partial_cmp(&confirmed_early), + Some(Ordering::Less) + ); + + // Within Pending: Waiting < Reattempting + assert_eq!(pending_waiting.cmp(&pending_reattempting), Ordering::Less); + assert_eq!( + pending_waiting.partial_cmp(&pending_reattempting), + Some(Ordering::Less) + ); + + // Within Confirmed: earlier block < later block + assert_eq!(confirmed_early.cmp(&confirmed_late), Ordering::Less); + assert_eq!( + confirmed_early.partial_cmp(&confirmed_late), + Some(Ordering::Less) + ); + + // Equal comparison + assert_eq!( + pending_waiting.cmp(&TxStatus::Pending(ValidationStatus::Waiting)), + Ordering::Equal + ); + assert_eq!( + pending_waiting.partial_cmp(&TxStatus::Pending(ValidationStatus::Waiting)), + Some(Ordering::Equal) + ); + } } diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 259b2167a..4e6921ad6 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -2226,7 +2226,7 @@ mod tests { ) { // TODO when we have more logic in place with the other cards taken in, we'll need to configure these // accordingly - // TODO now only GH-605 logic is missing + // TODO: GH-605: Bert - now only GH-605 logic is missing let response_skeleton_opt = Some(ResponseSkeleton { client_id: 4555, context_id: 5566, diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 521c787e6..7853df511 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -278,7 +278,7 @@ impl Scanners { } fn empty_caches(&mut self, logger: &Logger) { - todo!("GH-605: As any mut is not a type"); + todo!("GH-605: Bert - As any mut is not a type"); // let pending_payable_scanner = self // .pending_payable // .as_any_mut() @@ -1188,7 +1188,7 @@ mod tests { hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }]" )] fn just_baked_pending_payables_contain_duplicates() { - todo!("another pending payable issue"); + todo!("GH-605: Bert"); // let hash_1 = make_tx_hash(123); // let hash_2 = make_tx_hash(456); // let hash_3 = make_tx_hash(789); @@ -1208,111 +1208,6 @@ mod tests { // subject.check_for_missing_records(&pending_payables_ref); } - #[test] - #[should_panic(expected = "Expected sent-payable records for \ - (tx: 0x00000000000000000000000000000000000000000000000000000000000000f8, \ - to wallet: 0x00000000000000000000000000626c6168323232) \ - were not found. The system has become unreliable")] - fn payable_scanner_found_out_nonexistent_sent_tx_records() { - todo!("GH-605: Work on it") - // init_test_logging(); - // let test_name = "payable_scanner_found_out_nonexistent_sent_tx_records"; - // let hash_1 = make_tx_hash(0xff); - // let hash_2 = make_tx_hash(0xf8); - // let sent_payable_dao = - // SentPayableDaoMock::default().get_tx_identifiers_result(hashmap!(hash_1 => 7881)); - // let payable_1 = PendingPayable::new(make_wallet("blah111"), hash_1); - // let payable_2 = PendingPayable::new(make_wallet("blah222"), hash_2); - // let payable_dao = PayableDaoMock::new().mark_pending_payables_rowids_result(Err( - // PayableDaoError::SignConversion(9999999999999), - // )); - // let mut subject = PayableScannerBuilder::new() - // .payable_dao(payable_dao) - // .sent_payable_dao(sent_payable_dao) - // .build(); - - // let sent_payables = SentPayables { - // payment_procedure_result: Ok(vec![ - // ProcessedPayableFallible::Correct(payable_1), - // ProcessedPayableFallible::Correct(payable_2), - // ]), - // response_skeleton_opt: None, - // }; - - // subject.finish_scan(sent_payables, &Logger::new(test_name)); - } - - // TODO: GH-605: Verify and remove - // #[test] - // fn payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist() { - // init_test_logging(); - // let test_name = - // "payable_scanner_is_facing_failed_transactions_and_their_sent_tx_records_exist"; - // let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); - // let delete_records_params_arc = Arc::new(Mutex::new(vec![])); - // let hash_tx_1 = make_tx_hash(0x15b3); - // let hash_tx_2 = make_tx_hash(0x3039); - // let first_sent_tx_rowid = 3; - // let second_sent_tx_rowid = 5; - // let system = System::new(test_name); - // let sent_payable_dao = SentPayableDaoMock::default() - // .get_tx_identifiers_params(&get_tx_identifiers_params_arc) - // .get_tx_identifiers_result( - // hashmap!(hash_tx_1 => first_sent_tx_rowid, hash_tx_2 => second_sent_tx_rowid), - // ) - // .delete_records_params(&delete_records_params_arc) - // .delete_records_result(Ok(())); - // let payable_scanner = PayableScannerBuilder::new() - // .sent_payable_dao(sent_payable_dao) - // .build(); - // let logger = Logger::new(test_name); - // let sent_payable = SentPayables { - // payment_procedure_result: Err(PayableTransactionError::Sending { - // msg: "Attempt failed".to_string(), - // hashes: hashset![hash_tx_1, hash_tx_2], - // }), - // response_skeleton_opt: None, - // }; - // 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, false); - // let sent_tx_rowids_params = get_tx_identifiers_params_arc.lock().unwrap(); - // assert_eq!(*sent_tx_rowids_params, vec![hashset!(hash_tx_1, hash_tx_2)]); - // let delete_records_params = delete_records_params_arc.lock().unwrap(); - // assert_eq!(*delete_records_params, vec![hashset!(hash_tx_1, hash_tx_2)]); - // let log_handler = TestLogHandler::new(); - // log_handler.exists_log_containing(&format!( - // "WARN: {test_name}: \ - // Any persisted data from the failed process will be deleted. Caused by: Sending phase: \ - // \"Attempt failed\". \ - // Signed and hashed txs: \ - // 0x00000000000000000000000000000000000000000000000000000000000015b3, \ - // 0x0000000000000000000000000000000000000000000000000000000000003039" - // )); - // log_handler.exists_log_containing(&format!( - // "WARN: {test_name}: \ - // Deleting sent payable records for \ - // 0x00000000000000000000000000000000000000000000000000000000000015b3, \ - // 0x0000000000000000000000000000000000000000000000000000000000003039", - // )); - // // we haven't supplied any result for mark_pending_payable() and so it's proved uncalled - // } - #[test] fn finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found_in_retry_mode( ) { 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 6a95732cc..b91eaed76 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 @@ -31,7 +31,6 @@ pub fn make_priced_new_tx_template(n: u64) -> PricedNewTxTemplate { } pub fn make_priced_retry_tx_template(prev_nonce: u64) -> PricedRetryTxTemplate { - // TODO: GH-605: During the merge, check against fns used by Bert and keep only one version of the two. PricedRetryTxTemplate { base: BaseTxTemplate::from(&make_payable_account(prev_nonce)), prev_nonce, diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 10a1d9625..996994a5f 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -95,37 +95,6 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( } } -// TODO: GH-605: Remove them -// pub fn make_sent_tx(num: u64) -> SentTx { -// if num == 0 { -// panic!("num for generating must be greater than 0"); -// } -// let params = TxRecordCommonParts::new(num); -// SentTx { -// hash: params.hash, -// receiver_address: params.receiver_address, -// amount_minor: params.amount_minor, -// timestamp: params.timestamp, -// gas_price_minor: params.gas_price_minor, -// nonce: params.nonce, -// status: TxStatus::Pending(ValidationStatus::Waiting), -// } -// } -// -// pub fn make_failed_tx(num: u64) -> FailedTx { -// let params = TxRecordCommonParts::new(num); -// FailedTx { -// hash: params.hash, -// receiver_address: params.receiver_address, -// amount_minor: params.amount_minor, -// timestamp: params.timestamp, -// gas_price_minor: params.gas_price_minor, -// nonce: params.nonce, -// reason: FailureReason::PendingTooLong, -// status: FailureStatus::RetryRequired, -// } -// } - pub fn make_transaction_block(num: u64) -> TxBlock { TxBlock { block_hash: make_block_hash(num as u32), @@ -133,29 +102,6 @@ pub fn make_transaction_block(num: u64) -> TxBlock { } } -// TODO: GH-605: Unused code -// struct TxRecordCommonParts { -// hash: TxHash, -// receiver_address: Address, -// amount_minor: u128, -// timestamp: i64, -// gas_price_minor: u128, -// nonce: u64, -// } -// -// impl TxRecordCommonParts { -// fn new(num: u64) -> Self { -// Self { -// hash: make_tx_hash(num as u32), -// receiver_address: make_wallet(&format!("wallet{}", num)).address(), -// amount_minor: gwei_to_wei(num * num), -// timestamp: to_unix_timestamp(SystemTime::now()) - (num as i64 * 60), -// gas_price_minor: gwei_to_wei(num), -// nonce: num, -// } -// } -// } - pub struct AccountantBuilder { config_opt: Option, consuming_wallet_opt: Option, @@ -410,7 +356,7 @@ impl AccountantBuilder { } pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { - // TODO: GH-605: Merge Cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 + // 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 = @@ -426,7 +372,7 @@ impl AccountantBuilder { } pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { - // TODO: GH-605: Merge cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 + // 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 => { diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index d56da965e..201b67eb9 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -151,9 +151,7 @@ impl Handler for BlockchainBridge { todo!("This needs to be decided on GH-605. Look what mode you run and set it accordingly"); // self.handle_scan_future( // Self::handle_initial_templates_msg, - // todo!( - // "This needs to be decided on GH-605. Look what mode you run and set it accordingly" - // ), + // DetailedScanType:: // msg, // ); } @@ -166,9 +164,7 @@ impl Handler for BlockchainBridge { todo!("This needs to be decided on GH-605. Look what mode you run and set it accordingly") // self.handle_scan_future( // Self::handle_outbound_payments_instructions, - // todo!( - // "This needs to be decided on GH-605. Look what mode you run and set it accordingly" - // ), + // DetailedScanType:: // msg, // ) } @@ -509,14 +505,6 @@ impl BlockchainBridge { .submit_payables_in_batch(logger, agent, priced_templates) } - // TODO: GH-605: Unused code - // fn new_pending_payables_recipient(&self) -> Recipient { - // self.pending_payable_confirmation - // .register_new_pending_payables_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+).*") From 2cbdffed2c1ebab3d59f8848f565bf5c26354f34 Mon Sep 17 00:00:00 2001 From: utkarshg6 Date: Fri, 26 Sep 2025 13:25:15 +0530 Subject: [PATCH 39/48] GH-605: more fixing --- .../db_access_objects/failed_payable_dao.rs | 5 +--- .../db_access_objects/payable_dao.rs | 1 - .../db_access_objects/sent_payable_dao.rs | 2 +- node/src/accountant/mod.rs | 23 ++++------------ node/src/accountant/scanners/mod.rs | 8 +++--- .../scanners/payable_scanner/finish_scan.rs | 18 ++++++++----- .../scanners/pending_payable_scanner/mod.rs | 2 +- node/src/accountant/test_utils.rs | 27 +++++++++++-------- node/src/blockchain/blockchain_bridge.rs | 6 +---- .../blockchain_interface_web3/utils.rs | 14 +++------- .../data_structures/errors.rs | 1 - node/src/blockchain/test_utils.rs | 3 --- 12 files changed, 43 insertions(+), 67 deletions(-) diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index adac977da..5d263bdbb 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -448,10 +448,7 @@ mod tests { use crate::accountant::db_access_objects::utils::current_unix_timestamp; use crate::accountant::db_access_objects::Transaction; use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; - use crate::blockchain::errors::rpc_errors::LocalError::Decoder; - use crate::blockchain::errors::rpc_errors::{ - AppRpcError, AppRpcErrorKind, LocalErrorKind, RemoteErrorKind, - }; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ PreviousAttempts, ValidationFailureClockReal, ValidationStatus, }; diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index f8586b59b..598932226 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -576,7 +576,6 @@ mod tests { DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; use crate::database::rusqlite_wrappers::ConnectionWrapperReal; - use crate::database::test_utils::ConnectionWrapperMock; use crate::test_utils::make_wallet; use itertools::Itertools; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; 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 4ea3c3b6d..d541f8793 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -569,7 +569,7 @@ mod tests { use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; use std::cmp::Ordering; - use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; + use std::collections::{BTreeSet, HashMap}; use std::ops::{Add, Sub}; use std::str::FromStr; use std::sync::{Arc, Mutex}; diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 4e6921ad6..436d76c5b 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -324,7 +324,6 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: TxReceiptsMessage, ctx: &mut Self::Context) -> Self::Result { - let response_skeleton_opt = msg.response_skeleton_opt; match self.scanners.finish_pending_payable_scan(msg, &self.logger) { PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) => { if let Some(node_to_ui_msg) = ui_msg_opt { @@ -1413,15 +1412,11 @@ mod tests { let sent_payable_dao_factory = SentPayableDaoFactoryMock::new() .make_params(&sent_payable_dao_factory_params_arc) .make_result(SentPayableDaoMock::new()) // For Accountant + .make_result(SentPayableDaoMock::new()) // For Payable Scanner .make_result(SentPayableDaoMock::new()); // For PendingPayable Scanner let failed_payable_dao_factory = FailedPayableDaoFactoryMock::new() .make_params(&failed_payable_dao_factory_params_arc) - .make_result(FailedPayableDaoMock::new()); // 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 - 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) @@ -1452,19 +1447,11 @@ mod tests { ); assert_eq!( *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!( *failed_payable_dao_factory_params_arc.lock().unwrap(), - vec![()] + vec![(), ()] ); assert_eq!( *receivable_dao_factory_params_arc.lock().unwrap(), @@ -6858,7 +6845,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 7853df511..6338d4341 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -277,7 +277,7 @@ impl Scanners { }; } - fn empty_caches(&mut self, logger: &Logger) { + fn empty_caches(&mut self, _logger: &Logger) { todo!("GH-605: Bert - As any mut is not a type"); // let pending_payable_scanner = self // .pending_payable @@ -588,7 +588,7 @@ mod tests { use crate::accountant::db_access_objects::failed_payable_dao::{ FailedTx, FailureReason, FailureStatus, }; - use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; + use crate::accountant::db_access_objects::payable_dao::PayableAccount; 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}; @@ -607,7 +607,6 @@ mod tests { }; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; - use crate::accountant::scanners::test_utils::ScannerReplacement::PendingPayable; use crate::accountant::scanners::test_utils::{ assert_timestamps_from_str, parse_system_time_from_str, trim_expected_timestamp_to_three_digits_nanos, MarkScanner, NullScanner, @@ -649,7 +648,7 @@ mod tests { use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::{make_paying_wallet, make_wallet}; - use actix::{Message, System}; + use actix::Message; use ethereum_types::U64; use itertools::Either; use masq_lib::logger::Logger; @@ -665,7 +664,6 @@ mod tests { use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use web3::Error; impl Scanners { pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { diff --git a/node/src/accountant/scanners/payable_scanner/finish_scan.rs b/node/src/accountant/scanners/payable_scanner/finish_scan.rs index be37e4997..7a9ee0b77 100644 --- a/node/src/accountant/scanners/payable_scanner/finish_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/finish_scan.rs @@ -202,13 +202,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 +220,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 +234,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/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 303ee16f2..f9d876074 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -849,7 +849,7 @@ mod tests { use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use regex::Regex; - use std::collections::{BTreeMap, BTreeSet, HashMap}; + use std::collections::{BTreeSet, HashMap}; use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 996994a5f..d4fa1a489 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -4,7 +4,7 @@ use crate::accountant::db_access_objects::banned_dao::{BannedDao, BannedDaoFactory}; use crate::accountant::db_access_objects::failed_payable_dao::{ - FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureReason, + FailedPayableDao, FailedPayableDaoError, FailedPayableDaoFactory, FailedTx, FailureRetrieveCondition, FailureStatus, }; use crate::accountant::db_access_objects::payable_dao::{ @@ -32,8 +32,8 @@ 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, ValidationStatus}; -use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; +use crate::blockchain::errors::validation_status::ValidationFailureClock; +use crate::blockchain::test_utils::make_block_hash; use crate::bootstrapper::BootstrapperConfig; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::db_config::config_dao::{ConfigDao, ConfigDaoFactory}; @@ -51,13 +51,12 @@ use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; -use std::collections::{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 web3::types::Address; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_unix_timestamp(SystemTime::now()); @@ -422,12 +421,18 @@ impl AccountantBuilder { .make_result(ReceivableDaoMock::new()) .make_result(ReceivableDaoMock::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 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 banned_dao_factory = self .banned_dao_factory_opt .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 201b67eb9..d5cc44032 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -566,15 +566,11 @@ mod tests { 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, TxBlock, }; - use crate::blockchain::errors::rpc_errors::AppRpcError::Local; - use crate::blockchain::errors::rpc_errors::LocalError::Transport; use crate::blockchain::errors::rpc_errors::{ AppRpcError, AppRpcErrorKind, LocalErrorKind, RemoteError, }; 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 9ae03ce32..4d6dd61c0 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -331,26 +331,18 @@ mod tests { BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; use crate::blockchain::blockchain_interface::data_structures::errors::LocalPayableError::Sending; - use crate::blockchain::errors::rpc_errors::LocalError::Transport; - use crate::blockchain::errors::rpc_errors::RemoteError::Web3RpcError; - use crate::blockchain::errors::rpc_errors::{ - AppRpcError, AppRpcErrorKind, LocalErrorKind, RemoteErrorKind, - }; + 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 itertools::Either; - use jsonrpc_core::ErrorCode::ServerError; - use jsonrpc_core::{Error, ErrorCode}; 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; diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index 7c01a9b5c..d0a014eeb 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -120,7 +120,6 @@ mod tests { 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}; diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index e6eceaf2a..1ca3890bb 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}; From 62f35421f3b1a5eceecf9244a0406c302264b11c Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 26 Sep 2025 15:57:23 +0200 Subject: [PATCH 40/48] GH-605: interim commit --- .../db_access_objects/test_utils.rs | 14 +++- node/src/accountant/mod.rs | 19 ++--- node/src/accountant/scanners/mod.rs | 61 ++++++++-------- .../scanners/pending_payable_scanner/utils.rs | 9 ++- node/src/accountant/test_utils.rs | 72 ++++++++++--------- node/src/blockchain/test_utils.rs | 2 +- 6 files changed, 89 insertions(+), 88 deletions(-) diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index 02801b2c8..4c3e8da69 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -8,7 +8,7 @@ use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::{current_unix_timestamp, TxHash}; use crate::accountant::scanners::payable_scanner::tx_templates::signable::SignableTxTemplate; use crate::blockchain::errors::validation_status::ValidationStatus; -use crate::blockchain::test_utils::make_tx_hash; +use crate::blockchain::test_utils::{make_address, make_tx_hash}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, }; @@ -168,6 +168,10 @@ pub fn make_failed_tx(n: u32) -> FailedTx { let n = (n * 2) + 1; // Always Odd FailedTxBuilder::default() .hash(make_tx_hash(n)) + .timestamp(((3*n) as i64).pow(3)) + .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() } @@ -176,7 +180,13 @@ pub fn make_sent_tx(n: u32) -> SentTx { let n = n * 2; // Always Even TxBuilder::default() .hash(make_tx_hash(n)) - .nonce(n as u64) + .timestamp(((3*n) as i64).pow(3)) + .template(SignableTxTemplate{ + receiver_address: make_address(n.pow(2)), + amount_in_wei: (n as u128).pow(4), + gas_price_wei: (n as u128).pow(3), + nonce: n as u64, + }) .build() } diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 436d76c5b..c0c0be968 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1306,14 +1306,7 @@ mod tests { use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_payable_account, - make_qualified_and_unqualified_payables, make_transaction_block, BannedDaoFactoryMock, - ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, FailedPayableDaoMock, - MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PaymentAdjusterMock, - PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, - SentPayableDaoFactoryMock, SentPayableDaoMock, - }; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_transaction_block, BannedDaoFactoryMock, ConfigDaoFactoryMock, DaoWithDestination, FailedPayableDaoFactoryMock, FailedPayableDaoMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PaymentAdjusterMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, SentPayableDaoFactoryMock, SentPayableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; @@ -1658,7 +1651,7 @@ mod tests { let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); let subject = AccountantBuilder::default() .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(); @@ -4947,14 +4940,12 @@ mod tests { // let sent_payable_dao = SentPayableDaoMock::default() // .get_tx_identifiers_params(&get_tx_identifiers_params_arc) // .get_tx_identifiers_result(hashmap! (expected_hash => expected_rowid)); - let system = System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .payable_daos(vec![ForPayableScanner(payable_dao)]) - // .sent_payable_daos(vec![ForPayableScanner(sent_payable_dao)]) - .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; @@ -5015,8 +5006,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; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 6338d4341..40768d370 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -277,19 +277,18 @@ impl Scanners { }; } - fn empty_caches(&mut self, _logger: &Logger) { - todo!("GH-605: Bert - As any mut is not a type"); - // let pending_payable_scanner = self - // .pending_payable - // .as_any_mut() - // .downcast_mut::() - // .expect("mismatched types"); - // pending_payable_scanner - // .current_sent_payables - // .ensure_empty_cache(logger); - // pending_payable_scanner - // .yet_unproven_failed_payables - // .ensure_empty_cache(logger); + fn empty_caches(&mut self, logger: &Logger) { + let pending_payable_scanner = self + .pending_payable + .as_any_mut() + .downcast_mut::() + .expect("mismatched types"); + pending_payable_scanner + .current_sent_payables + .ensure_empty_cache(logger); + pending_payable_scanner + .yet_unproven_failed_payables + .ensure_empty_cache(logger); } pub fn try_skipping_payable_adjustment( @@ -1186,24 +1185,24 @@ mod tests { hash: 0x0000000000000000000000000000000000000000000000000000000000000315 }]" )] fn just_baked_pending_payables_contain_duplicates() { - todo!("GH-605: Bert"); - // 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); + // todo!("GH-605: Bert"); + // 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] diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index f7026cabc..0c406f798 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -865,7 +865,6 @@ mod tests { let test_name = "failure_cache_ensure_empty_sad_path"; let mut subject = RecheckRequiringFailures::new(); let failed_tx = make_failed_tx(567); - let tx_timestamp = failed_tx.timestamp; let records = vec![failed_tx.clone()]; let logger = Logger::new(test_name); subject.load_cache(records); @@ -880,10 +879,10 @@ mod tests { TestLogHandler::default().exists_log_containing(&format!( "DEBUG: {test_name}: \ Cache misuse - some tx failures left unprocessed: \ - {{0x0000000000000000000000000000000000000000000000000000000000000237: FailedTx {{ hash: \ - 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ - 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ - {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: \ + {{0x000000000000000000000000000000000000000000000000000000000000046f: FailedTx {{ hash: \ + 0x000000000000000000000000000000000000000000000000000000000000046f, receiver_address: \ + 0x000000000000000000000000000000000013a821, amount_minor: 1659523650625, timestamp: \ + 39477655125, gas_price_minor: 1462135375, nonce: 1135, reason: PendingTooLong, status: \ RetryRequired }}}}. Dumping." )); } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index d4fa1a489..2d3866bf2 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -260,9 +260,11 @@ const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::PendingPayableScanner, ]; -//TODO Utkarsh should also update this -const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 1] = - [DestinationMarker::PendingPayableScanner]; +const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 2] = + [ + DestinationMarker::PayableScanner, + DestinationMarker::PendingPayableScanner + ]; const SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, @@ -354,38 +356,38 @@ impl AccountantBuilder { ) } - pub fn sent_payable_dao(mut self, sent_payable_dao: SentPayableDaoMock) -> Self { - // TODO: GH-605: Bert Merge Cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 - match self.sent_payable_dao_factory_opt { - None => { - self.sent_payable_dao_factory_opt = - Some(SentPayableDaoFactoryMock::new().make_result(sent_payable_dao)) - } - Some(sent_payable_dao_factory) => { - self.sent_payable_dao_factory_opt = - Some(sent_payable_dao_factory.make_result(sent_payable_dao)) - } - } - - self - } - - pub fn failed_payable_dao(mut self, failed_payable_dao: FailedPayableDaoMock) -> Self { - // TODO: GH-605: Bert Merge cleanup - Prefer the standard create_or_update_factory! style - as in GH-598 - - match self.failed_payable_dao_factory_opt { - None => { - self.failed_payable_dao_factory_opt = - Some(FailedPayableDaoFactoryMock::new().make_result(failed_payable_dao)) - } - Some(failed_payable_dao_factory) => { - self.failed_payable_dao_factory_opt = - Some(failed_payable_dao_factory.make_result(failed_payable_dao)) - } - } - - self - } + // 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 { diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 1ca3890bb..6c76f19e2 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -185,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)) } From 97f54c8987e915847246b9080fda8ec3c857ede6 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 28 Sep 2025 00:39:28 +0200 Subject: [PATCH 41/48] GH-605: all tests in Node passing --- .../db_access_objects/failed_payable_dao.rs | 2 +- .../db_access_objects/test_utils.rs | 40 +- node/src/accountant/mod.rs | 182 +++++---- node/src/accountant/scanners/mod.rs | 295 +------------- .../scanners/payable_scanner/finish_scan.rs | 7 + .../scanners/payable_scanner/mod.rs | 372 +++++++++++++++++- .../scanners/payable_scanner/msgs.rs | 38 ++ .../scanners/payable_scanner/utils.rs | 22 +- .../scanners/pending_payable_scanner/mod.rs | 141 ++++--- .../scanners/pending_payable_scanner/utils.rs | 26 +- .../accountant/scanners/scan_schedulers.rs | 11 +- node/src/accountant/test_utils.rs | 12 +- node/src/blockchain/blockchain_bridge.rs | 54 ++- .../blockchain_interface_web3/utils.rs | 23 +- .../data_structures/errors.rs | 42 +- node/src/blockchain/test_utils.rs | 9 +- node/src/sub_lib/blockchain_bridge.rs | 47 ++- 17 files changed, 771 insertions(+), 552 deletions(-) diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 5d263bdbb..863d0b72e 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -769,7 +769,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')" ) } diff --git a/node/src/accountant/db_access_objects/test_utils.rs b/node/src/accountant/db_access_objects/test_utils.rs index 4c3e8da69..fca96ed7f 100644 --- a/node/src/accountant/db_access_objects/test_utils.rs +++ b/node/src/accountant/db_access_objects/test_utils.rs @@ -165,10 +165,10 @@ 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(((3*n) as i64).pow(3)) + .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)) @@ -177,12 +177,12 @@ pub fn make_failed_tx(n: u32) -> FailedTx { } pub fn make_sent_tx(n: u32) -> SentTx { - let n = n * 2; // Always Even + let n = n % 0xfff; TxBuilder::default() .hash(make_tx_hash(n)) - .timestamp(((3*n) as i64).pow(3)) - .template(SignableTxTemplate{ - receiver_address: make_address(n.pow(2)), + .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, @@ -190,24 +190,24 @@ pub fn make_sent_tx(n: u32) -> SentTx { .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_minor, t2.amount_minor); - assert_eq!(t1.gas_price_minor, t2.gas_price_minor); - 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_minor, f2.amount_minor); diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index c0c0be968..1b3023910 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -340,22 +340,24 @@ impl Handler for Accountant { .schedule_new_payable_scan(ctx, &self.logger) } } - PendingPayableScanResult::PaymentRetryRequired(retry_either) => match retry_either { - Either::Left(Retry::RetryPayments) => self - .scan_schedulers - .payable - .schedule_retry_payable_scan(ctx, &self.logger), - Either::Left(Retry::RetryTxStatusCheckOnly) => self - .scan_schedulers - .pending_payable - .schedule(ctx, &self.logger), - Either::Right(node_to_ui_msg) => self - .ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"), - }, + PendingPayableScanResult::PaymentRetryRequired(response_skeleton_opt) => self + .scan_schedulers + .payable + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), + PendingPayableScanResult::ProcedureShouldBeRepeated(ui_msg_opt) => { + if let Some(node_to_ui_msg) = ui_msg_opt { + todo!() + // self.ui_message_sub_opt + // .as_ref() + // .expect("UIGateway is not bound") + // .try_send(node_to_ui_msg) + // .expect("UIGateway is dead"); + } else { + self.scan_schedulers + .pending_payable + .schedule(ctx, &self.logger) + } + } }; } } @@ -375,7 +377,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() @@ -384,8 +386,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. } } } @@ -422,7 +424,7 @@ impl Handler for Accountant { DetailedScanType::RetryPayables => self .scan_schedulers .payable - .schedule_retry_payable_scan(ctx, &self.logger), + .schedule_retry_payable_scan(ctx, None, &self.logger), DetailedScanType::PendingPayables => self .scan_schedulers .pending_payable @@ -1150,7 +1152,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 @@ -1163,7 +1169,7 @@ impl Accountant { NextScanToRun::RetryPayableScan => self .scan_schedulers .payable - .schedule_retry_payable_scan(ctx, &self.logger), + .schedule_retry_payable_scan(ctx, None, &self.logger), } } @@ -1306,7 +1312,14 @@ mod tests { use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_transaction_block, BannedDaoFactoryMock, ConfigDaoFactoryMock, DaoWithDestination, FailedPayableDaoFactoryMock, FailedPayableDaoMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PaymentAdjusterMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, SentPayableDaoFactoryMock, SentPayableDaoMock}; + use crate::accountant::test_utils::{ + bc_from_earning_wallet, bc_from_wallets, make_payable_account, + make_qualified_and_unqualified_payables, make_transaction_block, BannedDaoFactoryMock, + ConfigDaoFactoryMock, DaoWithDestination, FailedPayableDaoFactoryMock, + FailedPayableDaoMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, + PaymentAdjusterMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, + ReceivableDaoMock, SentPayableDaoFactoryMock, SentPayableDaoMock, + }; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; use crate::blockchain::blockchain_agent::test_utils::BlockchainAgentMock; @@ -1464,10 +1477,17 @@ mod tests { .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()), // For PendingPayable Scanner ); - let failed_payable_dao_factory = - Box::new(FailedPayableDaoFactoryMock::new().make_result(FailedPayableDaoMock::new())); // For Payable Scanner - 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 @@ -1651,7 +1671,9 @@ mod tests { let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); let subject = AccountantBuilder::default() .payable_daos(vec![ForPayableScanner(payable_dao)]) - .sent_payable_daos(vec![DaoWithDestination::ForPayableScanner(sent_payable_dao)]) + .sent_payable_daos(vec![DaoWithDestination::ForPayableScanner( + sent_payable_dao, + )]) .bootstrapper_config(config) .build(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); @@ -2204,30 +2226,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 - // TODO: GH-605: Bert - now only GH-605 logic is missing - let response_skeleton_opt = Some(ResponseSkeleton { - client_id: 4555, - context_id: 5566, - }); let insert_new_records_params_arc = Arc::new(Mutex::new(vec![])); let delete_records_params_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let payable_dao_for_payable_scanner = + PayableDaoMock::default().retrieve_payables_result(vec![]); + let payable_dao_for_pending_payable_scanner = + PayableDaoMock::default().transactions_confirmed_result(Ok(())); let sent_tx = make_sent_tx(123); let tx_hash = sent_tx.hash; - let sent_payable_dao = SentPayableDaoMock::default() + let sent_payable_dao_for_payable_scanner = SentPayableDaoMock::default() + // TODO should be removed with GH-701 + .insert_new_records_result(Ok(())); + let sent_payable_dao_for_pending_payable_scanner = SentPayableDaoMock::default() .retrieve_txs_result(BTreeSet::from([sent_tx.clone()])) .delete_records_params(&delete_records_params_arc) .delete_records_result(Ok(())); - let failed_payable_dao = FailedPayableDaoMock::default() + let failed_tx = make_failed_tx(123); + let failed_payable_dao_for_payable_scanner = + FailedPayableDaoMock::default().retrieve_txs_result(btreeset!(failed_tx)); + let failed_payable_dao_for_pending_payable_scanner = FailedPayableDaoMock::default() .insert_new_records_params(&insert_new_records_params_arc) .insert_new_records_result(Ok(())); let mut subject = AccountantBuilder::default() .consuming_wallet(make_wallet("consuming")) - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .sent_payable_daos(vec![ForPendingPayableScanner(sent_payable_dao)]) - .failed_payable_daos(vec![ForPendingPayableScanner(failed_payable_dao)]) + .payable_daos(vec![ + ForPayableScanner(payable_dao_for_payable_scanner), + ForPendingPayableScanner(payable_dao_for_pending_payable_scanner), + ]) + .sent_payable_daos(vec![ + ForPayableScanner(sent_payable_dao_for_payable_scanner), + ForPendingPayableScanner(sent_payable_dao_for_pending_payable_scanner), + ]) + .failed_payable_daos(vec![ + ForPayableScanner(failed_payable_dao_for_payable_scanner), + ForPendingPayableScanner(failed_payable_dao_for_pending_payable_scanner), + ]) .build(); subject.scan_schedulers.automatic_scans_enabled = false; let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); @@ -2853,9 +2886,7 @@ mod tests { response_skeleton_opt: None, })) .finish_scan_params(&scan_params.pending_payable_finish_scan) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( - Either::Left(Retry::RetryPayments), - )); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(None)); let receivable_scanner = ScannerMock::new() .scan_started_at_result(None) .start_scan_params(&scan_params.receivable_start_scan) @@ -4989,14 +5020,11 @@ mod tests { #[test] fn accountant_finishes_processing_of_retry_payables_and_schedules_pending_payable_scanner() { - let get_tx_identifiers_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let 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(); let sent_payable_dao = SentPayableDaoMock::new() - .get_tx_identifiers_params(&get_tx_identifiers_params_arc) .insert_new_records_params(&inserted_new_records_params_arc) .insert_new_records_result(Ok(())); let failed_payble_dao = FailedPayableDaoMock::new().retrieve_txs_result(BTreeSet::new()); @@ -5041,11 +5069,6 @@ mod tests { inserted_new_records_params[0], BTreeSet::from([expected_tx]) ); - let get_tx_identifiers_params = get_tx_identifiers_params_arc.lock().unwrap(); - assert_eq!( - *get_tx_identifiers_params, - vec![BTreeSet::from([expected_hash])] - ); let pending_payable_notify_later_params = pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -5114,10 +5137,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() @@ -5125,9 +5149,7 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( - Either::Left(Retry::RetryPayments), - )); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(None)); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( @@ -5152,11 +5174,7 @@ mod tests { status: StatusReadFromReceiptCheck::Reverted, }, ]); - let response_skeleton_opt = Some(ResponseSkeleton { - client_id: 45, - context_id: 7, - }); - msg.response_skeleton_opt = response_skeleton_opt; + msg.response_skeleton_opt = None; let subject_addr = subject.start(); subject_addr.try_send(msg.clone()).unwrap(); @@ -5170,7 +5188,7 @@ 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) @@ -5188,9 +5206,7 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( - Either::Left(Retry::RetryTxStatusCheckOnly), - )); + .finish_scan_result(PendingPayableScanResult::ProcedureShouldBeRepeated(None)); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( @@ -5237,39 +5253,31 @@ mod tests { } #[test] - fn accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected() - { + fn accountant_in_manual_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed() { init_test_logging(); - let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); - let ui_gateway = - ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); let test_name = - "accountant_sends_ui_msg_for_an_external_scan_trigger_despite_the_need_of_retry_was_detected"; + "accountant_in_manual_mode_schedules_tx_retry_as_some_pending_payables_have_not_completed"; + let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); - subject.ui_message_sub_opt = Some(ui_gateway.start().recipient()); let response_skeleton = ResponseSkeleton { client_id: 123, context_id: 333, }; - let node_to_ui_msg = NodeToUiMessage { - target: MessageTarget::ClientId(123), - body: UiScanResponse {}.tmb(333), - }; let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired( - Either::Right(node_to_ui_msg.clone()), - )); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired(Some( + response_skeleton, + ))); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); subject.scan_schedulers.payable.retry_payable_notify = - Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); subject.scan_schedulers.payable.new_payable_notify_later = @@ -5277,7 +5285,6 @@ mod tests { subject.scan_schedulers.pending_payable.handle = Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let system = System::new(test_name); - let msg = TxReceiptsMessage { results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), response_skeleton_opt: Some(response_skeleton), @@ -5286,13 +5293,18 @@ mod tests { subject_addr.try_send(msg.clone()).unwrap(); + System::current().stop(); system.run(); let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); let (msg_actual, logger) = finish_scan_params.remove(0); assert_eq!(msg_actual, msg); - let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); - let captured_msg = ui_gateway_recording.get_record::(0); - assert_eq!(captured_msg, &node_to_ui_msg); + let retry_payable_notify_params = retry_payable_notify_params_arc.lock().unwrap(); + assert_eq!( + *retry_payable_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt: Some(response_skeleton) + }] + ); assert_using_the_same_logger(&logger, test_name, None) } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 40768d370..a937649f2 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1135,76 +1135,6 @@ mod tests { )); } - #[test] - fn no_missing_records() { - todo!("pending payable issue"); - // 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() { - // todo!("GH-605: Bert"); - // 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 finish_payable_scan_changes_the_aware_of_unresolved_pending_payable_flag_as_true_when_pending_txs_found_in_retry_mode( ) { @@ -1243,226 +1173,6 @@ 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:\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 pending_payable_scanner_can_initiate_a_scan() { init_test_logging(); @@ -1796,10 +1506,7 @@ mod tests { let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - assert_eq!( - result, - PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) - ); + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); sent_tx_1.status = TxStatus::Confirmed { block_hash: format!("{:?}", tx_block_1.block_hash), diff --git a/node/src/accountant/scanners/payable_scanner/finish_scan.rs b/node/src/accountant/scanners/payable_scanner/finish_scan.rs index 7a9ee0b77..900bf9b56 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); diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index e57270a3f..e0948c4d4 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -16,26 +16,29 @@ use crate::accountant::db_access_objects::failed_payable_dao::{ use crate::accountant::db_access_objects::payable_dao::PayableRetrieveCondition::ByAddresses; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; use crate::accountant::db_access_objects::sent_payable_dao::{SentPayableDao, SentTx}; +use crate::accountant::db_access_objects::utils::TxHash; use crate::accountant::payment_adjuster::PaymentAdjuster; use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; use crate::accountant::scanners::payable_scanner::payment_adjuster_integration::SolvencySensitivePaymentInstructor; use crate::accountant::scanners::payable_scanner::utils::{ batch_stats, calculate_occurences, filter_receiver_addresses_from_txs, generate_status_updates, payables_debug_summary, NextScanToRun, PayableScanResult, PayableThresholdsGauge, - PayableThresholdsGaugeReal, + PayableThresholdsGaugeReal, PendingPayableMissingInDb, }; use crate::accountant::scanners::{Scanner, ScannerCommon, StartableScanner}; use crate::accountant::{ - gwei_to_wei, join_with_separator, PayableScanType, ResponseSkeleton, ScanForNewPayables, - ScanForRetryPayables, SentPayables, + comma_joined_stringifiable, gwei_to_wei, 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; @@ -106,7 +109,7 @@ impl PayableScanner { } } - pub fn payable_exceeded_threshold( + fn payable_exceeded_threshold( &self, payable: &PayableAccount, now: SystemTime, @@ -140,6 +143,71 @@ 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: Vec = hashset_with_hashes_to_eliminate_duplicates + .difference(&hashes_from_db) + .copied() + .collect(); + + let mut sent_payables_hashmap = just_baked_sent_payables + .iter() + .map(|payable| (payable.hash, &payable.recipient_wallet)) + .collect::>(); + missing_sent_payables_hashes + .into_iter() + .map(|hash| { + let wallet_address = sent_payables_hashmap + .remove(&hash) + .expectv("wallet") + .address(); + PendingPayableMissingInDb::new(wallet_address, hash) + }) + .collect() + } + + // 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", + comma_joined_stringifiable(nonexistent, |missing_sent_tx_ids| format!( + "(tx: {:?}, to wallet: {:?})", + missing_sent_tx_ids.hash, missing_sent_tx_ids.recipient + )) + ) + } + + let missing_sent_tx_records = self.check_for_missing_records(sent_payments); + if !missing_sent_tx_records.is_empty() { + panic!("{}", missing_record_msg(&missing_sent_tx_records)) + } + } + fn determine_next_scan_to_run(msg: &SentPayables) -> NextScanToRun { match &msg.payment_procedure_result { Ok(batch_results) => { @@ -317,12 +385,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() { @@ -496,6 +570,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![])); diff --git a/node/src/accountant/scanners/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner/msgs.rs index b905bd133..51ecec9b9 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,30 @@ impl SkeletonOptHolder for InitialTemplatesMessage { self.response_skeleton_opt } } + +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/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index 6b44a0f46..b3ddc1cc0 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -146,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(), @@ -157,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, @@ -378,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] diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index f9d876074..e436cc483 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -79,30 +79,16 @@ impl StartableScanner logger: &Logger, ) -> Result { self.mark_as_started(timestamp); - info!(logger, "Scanning for pending payable"); - let pending_tx_hashes_opt = self.handle_pending_payables(); - let failure_hashes_opt = self.handle_unproven_failures(); + info!(logger, "Scanning for pending payable"); - if pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() { + let tx_hashes = self.harvest_tables(logger).map_err(|e| { self.mark_as_ended(logger); - return Err(StartScanError::NothingToProcess); - } - - Self::log_records_found_for_receipt_check( - pending_tx_hashes_opt.as_ref(), - failure_hashes_opt.as_ref(), - logger, - ); - - let all_hashes = pending_tx_hashes_opt - .unwrap_or_default() - .into_iter() - .chain(failure_hashes_opt.unwrap_or_default()) - .collect_vec(); + e + })?; Ok(RequestTransactionReceipts { - tx_hashes: all_hashes, + tx_hashes, response_skeleton_opt, }) } @@ -154,40 +140,83 @@ impl PendingPayableScanner { } } - fn handle_pending_payables(&mut self) -> Option> { + fn harvest_tables(&mut self, logger: &Logger) -> Result, StartScanError> { + let pending_tx_hashes_opt = self.harvest_pending_payables(); + let failure_hashes_opt = self.harvest_unproven_failures(); + eprintln!("ph: {:?}", pending_tx_hashes_opt); + eprintln!("fail: {:?}", failure_hashes_opt); + + if Self::is_there_nothing_to_process( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + ) { + return Err(StartScanError::NothingToProcess); + } + + Self::log_records_for_receipt_check( + pending_tx_hashes_opt.as_ref(), + failure_hashes_opt.as_ref(), + logger, + ); + + Ok(Self::merge_hashes( + pending_tx_hashes_opt, + failure_hashes_opt, + )) + } + + fn harvest_pending_payables(&mut self) -> Option> { let pending_txs = self .sent_payable_dao - .retrieve_txs(Some(RetrieveCondition::IsPending)); + .retrieve_txs(Some(RetrieveCondition::IsPending)) + .into_iter() + .collect_vec(); if pending_txs.is_empty() { return None; } - let pending_txs_vec: Vec = pending_txs.into_iter().collect(); - - let pending_tx_hashes = - Self::get_wrapped_hashes(&pending_txs_vec, TxHashByTable::SentPayable); - self.current_sent_payables.load_cache(pending_txs_vec); + let pending_tx_hashes = Self::wrap_hashes(&pending_txs, TxHashByTable::SentPayable); + self.current_sent_payables.load_cache(pending_txs); Some(pending_tx_hashes) } - fn handle_unproven_failures(&mut self) -> Option> { + fn harvest_unproven_failures(&mut self) -> Option> { let failures = self .failed_payable_dao - .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)); + .retrieve_txs(Some(FailureRetrieveCondition::EveryRecheckRequiredRecord)) + .into_iter() + .collect_vec(); if failures.is_empty() { return None; } - let failures_vec: Vec = failures.into_iter().collect(); - - let failure_hashes = Self::get_wrapped_hashes(&failures_vec, TxHashByTable::FailedPayable); - self.yet_unproven_failed_payables.load_cache(failures_vec); + let failure_hashes = Self::wrap_hashes(&failures, TxHashByTable::FailedPayable); + self.yet_unproven_failed_payables.load_cache(failures); Some(failure_hashes) } - fn get_wrapped_hashes( + fn is_there_nothing_to_process( + pending_tx_hashes_opt: Option<&Vec>, + failure_hashes_opt: Option<&Vec>, + ) -> bool { + pending_tx_hashes_opt.is_none() && failure_hashes_opt.is_none() + } + + fn merge_hashes( + pending_tx_hashes_opt: Option>, + failure_hashes_opt: Option>, + ) -> Vec { + let failures = failure_hashes_opt.unwrap_or_default(); + pending_tx_hashes_opt + .unwrap_or_default() + .into_iter() + .chain(failures) + .collect() + } + + fn wrap_hashes( records: &[Record], wrap_the_hash: fn(TxHash) -> TxHashByTable, ) -> Vec @@ -214,14 +243,23 @@ impl PendingPayableScanner { response_skeleton_opt: Option, ) -> PendingPayableScanResult { if let Some(retry) = retry_opt { - if let Some(response_skeleton) = response_skeleton_opt { - let ui_msg = NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }; - PendingPayableScanResult::PaymentRetryRequired(Either::Right(ui_msg)) - } else { - PendingPayableScanResult::PaymentRetryRequired(Either::Left(retry)) + match retry { + Retry::RetryPayments => { + PendingPayableScanResult::PaymentRetryRequired(response_skeleton_opt) + } + Retry::RetryTxStatusCheckOnly => { + if let Some(response_skeleton) = response_skeleton_opt { + todo!() + // let ui_msg = NodeToUiMessage { + // target: MessageTarget::ClientId(response_skeleton.client_id), + // body: UiScanResponse {}.tmb(response_skeleton.context_id), + // }; + //PendingPayableScanResult::ProcedureShouldBeRepeated(Some(ui_msg)) + } else { + todo!() + //PendingPayableScanResult::ProcedureShouldBeRepeated() + } + } } } else { let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { @@ -791,7 +829,7 @@ impl PendingPayableScanner { ) } - fn log_records_found_for_receipt_check( + fn log_records_for_receipt_check( pending_tx_hashes_opt: Option<&Vec>, failure_hashes_opt: Option<&Vec>, logger: &Logger, @@ -885,10 +923,10 @@ mod tests { 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) + TxHashByTable::SentPayable(sent_tx_hash_1), + TxHashByTable::FailedPayable(failed_tx_hash_2), + TxHashByTable::FailedPayable(failed_tx_hash_1) ], response_skeleton_opt: None }) @@ -969,10 +1007,7 @@ mod tests { let result = subject.finish_scan(msg, &logger); - assert_eq!( - result, - PendingPayableScanResult::PaymentRetryRequired(Either::Left(Retry::RetryPayments)) - ); + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)); let get_record_by_hash_failed_payable_cache_params = get_record_by_hash_failed_payable_cache_params_arc .lock() @@ -1037,8 +1072,8 @@ mod tests { r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, r#" Unproven failures: \[FailedTx \{ hash:"#, r#" 0x0000000000000000000000000000000000000000000000000000000000000987, receiver_address:"#, - r#" 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \d*,"#, - r#" gas_price_minor: 567000000000, nonce: 567, reason: PendingTooLong, status: RetryRequired \}\]."#, + r#" 0x000000000000000000185d00000000185d000000, amount_minor: 103355177121, timestamp: \d*,"#, + r#" gas_price_minor: 182284263, nonce: 567, reason: PendingTooLong, status: RetryRequired \}\]."#, r#" Hashes yet not looked up: \[FailedPayable\(0x000000000000000000000000000000000000000"#, r#"0000000000000000000000987\)\]"#, ]; @@ -1962,11 +1997,11 @@ mod tests { #[test] #[should_panic( expected = "Unable to complete the tx confirmation by the adjustment of the payable accounts \ - 0x000000000000000000000077616c6c6574343536 due to: \ + 0x0000000000000000000558000000000558000000 due to: \ RusqliteError(\"record change not successful\")" )] fn handle_confirmed_transactions_panics_on_unchecking_payable_table() { - let hash = make_tx_hash(0x315); + let hash = make_tx_hash(315); let payable_dao = PayableDaoMock::new().transactions_confirmed_result(Err( PayableDaoError::RusqliteError("record change not successful".to_string()), )); diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index 0c406f798..35183c563 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -3,7 +3,7 @@ use crate::accountant::db_access_objects::failed_payable_dao::{FailedTx, FailureStatus}; use crate::accountant::db_access_objects::sent_payable_dao::{SentTx, TxStatus}; use crate::accountant::db_access_objects::utils::TxHash; -use crate::accountant::TxReceiptResult; +use crate::accountant::{ResponseSkeleton, TxReceiptResult}; use crate::blockchain::errors::rpc_errors::AppRpcError; use crate::blockchain::errors::validation_status::{ PreviousAttempts, ValidationFailureClock, ValidationStatus, @@ -300,7 +300,8 @@ impl RecheckRequiringFailures { #[derive(Debug, PartialEq, Eq)] pub enum PendingPayableScanResult { NoPendingPayablesLeft(Option), - PaymentRetryRequired(Either), + PaymentRetryRequired(Option), + ProcedureShouldBeRepeated(Option), } #[derive(Debug, PartialEq, Eq)] @@ -720,8 +721,7 @@ mod tests { init_test_logging(); let test_name = "pending_payable_cache_ensure_empty_sad_path"; let mut subject = CurrentPendingPayables::new(); - let sent_tx = make_sent_tx(567); - let tx_timestamp = sent_tx.timestamp; + let sent_tx = make_sent_tx(0x567); let records = vec![sent_tx.clone()]; let logger = Logger::new(test_name); subject.load_cache(records); @@ -736,10 +736,10 @@ mod tests { TestLogHandler::default().exists_log_containing(&format!( "DEBUG: {test_name}: \ Cache misuse - some pending payables left unprocessed: \ - {{0x0000000000000000000000000000000000000000000000000000000000000237: SentTx {{ hash: \ - 0x0000000000000000000000000000000000000000000000000000000000000237, receiver_address: \ - 0x000000000000000000000077616c6c6574353637, amount_minor: 321489000000000, timestamp: \ - {tx_timestamp}, gas_price_minor: 567000000000, nonce: 567, status: Pending(Waiting) }}}}. \ + {{0x0000000000000000000000000000000000000000000000000000000000000567: SentTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000567, receiver_address: \ + 0x0000000000000000001035000000001035000000, amount_minor: 3658379210721, timestamp: \ + 275427216, gas_price_minor: 2645248887, nonce: 1383, status: Pending(Waiting) }}}}. \ Dumping." )); } @@ -864,7 +864,7 @@ mod tests { init_test_logging(); let test_name = "failure_cache_ensure_empty_sad_path"; let mut subject = RecheckRequiringFailures::new(); - let failed_tx = make_failed_tx(567); + let failed_tx = make_failed_tx(0x567); let records = vec![failed_tx.clone()]; let logger = Logger::new(test_name); subject.load_cache(records); @@ -879,10 +879,10 @@ mod tests { TestLogHandler::default().exists_log_containing(&format!( "DEBUG: {test_name}: \ Cache misuse - some tx failures left unprocessed: \ - {{0x000000000000000000000000000000000000000000000000000000000000046f: FailedTx {{ hash: \ - 0x000000000000000000000000000000000000000000000000000000000000046f, receiver_address: \ - 0x000000000000000000000000000000000013a821, amount_minor: 1659523650625, timestamp: \ - 39477655125, gas_price_minor: 1462135375, nonce: 1135, reason: PendingTooLong, status: \ + {{0x0000000000000000000000000000000000000000000000000000000000000567: FailedTx {{ hash: \ + 0x0000000000000000000000000000000000000000000000000000000000000567, receiver_address: \ + 0x00000000000000000003cc0000000003cc000000, amount_minor: 3658379210721, timestamp: \ + 275427216, gas_price_minor: 2645248887, nonce: 1383, reason: PendingTooLong, status: \ RetryRequired }}}}. Dumping." )); } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 6545f4141..dd23e05bd 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -2,7 +2,7 @@ use crate::accountant::scanners::StartScanError; use crate::accountant::{ - Accountant, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, + Accountant, ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, ScanForRetryPayables, }; use crate::sub_lib::accountant::ScanIntervals; @@ -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, ) diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 2d3866bf2..8d6fb49ed 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -260,11 +260,10 @@ const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::PendingPayableScanner, ]; -const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 2] = - [ - DestinationMarker::PayableScanner, - DestinationMarker::PendingPayableScanner - ]; +const FAILED_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 2] = [ + DestinationMarker::PayableScanner, + DestinationMarker::PendingPayableScanner, +]; const SENT_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, @@ -1284,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) } diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index d5cc44032..13fe3b685 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -144,16 +144,19 @@ impl Handler for BlockchainBridge { } } +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) { - todo!("This needs to be decided on GH-605. Look what mode you run and set it accordingly"); - // self.handle_scan_future( - // Self::handle_initial_templates_msg, - // DetailedScanType:: - // msg, - // ); + self.handle_scan_future( + Self::handle_initial_templates_msg, + msg.detailed_scan_type(), + msg, + ); } } @@ -161,12 +164,11 @@ impl Handler for BlockchainBridge { type Result = (); fn handle(&mut self, msg: OutboundPaymentsInstructions, _ctx: &mut Self::Context) { - todo!("This needs to be decided on GH-605. Look what mode you run and set it accordingly") - // self.handle_scan_future( - // Self::handle_outbound_payments_instructions, - // DetailedScanType:: - // msg, - // ) + self.handle_scan_future( + Self::handle_outbound_payments_instructions, + msg.detailed_scan_type(), + msg, + ) } } @@ -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, }), @@ -597,6 +599,7 @@ mod tests { }; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; + use clap::AppSettings::DontCollapseArgsInUsage; use ethereum_types::U64; use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; use masq_lib::test_utils::logging::init_test_logging; @@ -607,10 +610,11 @@ mod tests { }; use masq_lib::utils::find_free_port; use std::any::TypeId; + use std::ops::Add; use std::path::Path; use std::str::FromStr; use std::sync::{Arc, Mutex}; - use std::time::{Duration, SystemTime}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; use web3::types::{TransactionReceipt, H160}; impl Handler> for BlockchainBridge { @@ -1046,27 +1050,21 @@ mod tests { // amount: account.balance_wei // }] // ); + assert_eq!(scan_error_msg.scan_type, DetailedScanType::NewPayables); assert_eq!( - *scan_error_msg, - ScanError { - scan_type: DetailedScanType::NewPayables, - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321 - }), - msg: format!( - "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". \ - Signed and hashed txs: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" - ) - } + scan_error_msg.response_skeleton_opt, + Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321 + }) ); assert!(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!(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); } 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 4d6dd61c0..9734f7e76 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -39,12 +39,13 @@ pub struct BlockchainAgentFutureResult { } fn return_sending_error(sent_txs: &[SentTx], error: &Web3Error) -> LocalPayableError { - LocalPayableError::Sending( - sent_txs + LocalPayableError::Sending { + error: format!("{}", error), + failed_txs: sent_txs .iter() .map(|sent_tx| FailedTx::from((sent_tx, error))) .collect(), - ) + } } pub fn return_batch_results( @@ -641,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 {}", @@ -768,7 +774,10 @@ mod tests { .build() }) .collect(); - let expected_result = Err(Sending(failed_txs)); + let error = "Transport error: Error(Connect, Os { code: 111, kind: ConnectionRefused, \ + message: \"Connection refused\" })" + .to_string(); + let expected_result = Err(Sending { error, failed_txs }); test_send_payables_within_batch( "send_payables_within_batch_fails_on_submit_batch_call", diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index d0a014eeb..03899343e 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -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,6 +110,7 @@ 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, }; @@ -171,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, ]; @@ -194,12 +188,10 @@ mod tests { "Missing consuming wallet to pay payable from", "Unsuccessful gas price query: \"Blockchain error: Query failed: Gas halves shut, no drop left\"", "Transaction id fetching failed: Blockchain error: Invalid response", - "Unusable wallet for signing payable transactions: \"This is a LEATHER wallet, not \ - LEDGER wallet, stupid.\"", - "Signing phase: \"You cannot sign with just three crosses here, clever boy\"", - "Sending phase: \"Sending to cosmos belongs elsewhere\". Signed and hashed \ - txs: 0x000000000000000000000000000000000000000000000000000000000000006f, \ - 0x00000000000000000000000000000000000000000000000000000000000000de", + "Sending error: \"Terrible error!!\". Signed and hashed transactions: \"FailedTx { hash: 0x00000000000000\ + 000000000000000000000000000000000000000000000001c8, receiver_address: 0x00000000000\ + 00000002556000000002556000000, amount_minor: 43237380096, timestamp: 29942784, gas_\ + price_minor: 94818816, nonce: 456, reason: PendingTooLong, status: RetryRequired }\"", BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED ]) ) diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 6c76f19e2..238703d98 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -198,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) } diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 1b2fca21b..1cfce6798 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -7,7 +7,10 @@ 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; @@ -50,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, @@ -94,12 +106,23 @@ 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, make_priced_retry_tx_template, + }; + 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}; @@ -131,4 +154,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) + } } From d94d5cb4e3924a72fd856901ce5a4a5019d2eef4 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 28 Sep 2025 11:31:04 +0200 Subject: [PATCH 42/48] GH-605: last todo removed --- node/src/accountant/mod.rs | 75 +++++++++++++++++-- .../blockchain_interface_web3/utils.rs | 7 +- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 1b3023910..ba52a64f7 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -346,12 +346,14 @@ impl Handler for Accountant { .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 { - todo!() - // self.ui_message_sub_opt - // .as_ref() - // .expect("UIGateway is not bound") - // .try_send(node_to_ui_msg) - // .expect("UIGateway is dead"); + 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 @@ -5195,10 +5197,10 @@ mod tests { } #[test] - fn accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed() { + fn accountant_reschedules_pending_p_scanner_in_automatic_mode_after_receipt_fetching_failed() { init_test_logging(); let test_name = - "accountant_reschedules_pending_payable_scanner_as_receipt_check_efforts_alone_failed"; + "accountant_reschedules_pending_p_scanner_in_automatic_mode_after_receipt_fetching_failed"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default() @@ -5252,6 +5254,63 @@ mod tests { 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: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + response_skeleton_opt: Some(response_skeleton), + }; + let subject_addr = subject.start(); + + subject_addr.try_send(msg.clone()).unwrap(); + + system.run(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let 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(); 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 9734f7e76..9589712c0 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -752,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 @@ -774,10 +774,7 @@ mod tests { .build() }) .collect(); - let error = "Transport error: Error(Connect, Os { code: 111, kind: ConnectionRefused, \ - message: \"Connection refused\" })" - .to_string(); - let expected_result = Err(Sending { error, 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", From b82e3d2e93aa17ab44ac98cfd6079f6e04ca9d39 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 28 Sep 2025 12:01:54 +0200 Subject: [PATCH 43/48] GH-605: fixed poor test coverage --- node/src/accountant/mod.rs | 39 ++++--- node/src/accountant/scanners/mod.rs | 18 ++- .../scanners/pending_payable_scanner/mod.rs | 108 +++++++++++++++--- node/src/actor_system_factory.rs | 2 - node/src/blockchain/blockchain_bridge.rs | 4 +- .../blockchain_interface_web3/utils.rs | 5 +- node/src/sub_lib/blockchain_bridge.rs | 4 +- 7 files changed, 131 insertions(+), 49 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index ba52a64f7..1f097c8e9 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -27,7 +27,7 @@ use crate::accountant::scanners::payable_scanner::msgs::{ }; use crate::accountant::scanners::payable_scanner::utils::NextScanToRun; use crate::accountant::scanners::pending_payable_scanner::utils::{ - PendingPayableScanResult, Retry, TxHashByTable, + PendingPayableScanResult, TxHashByTable, }; use crate::accountant::scanners::scan_schedulers::{ PayableSequenceScanner, ScanReschedulingAfterEarlyStop, ScanSchedulers, @@ -346,8 +346,11 @@ impl Handler for Accountant { .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."); + 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") @@ -1664,11 +1667,8 @@ mod tests { } #[test] - fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { + fn sent_payables_with_response_skeleton_results_in_scan_response_to_ui_gateway() { let config = bc_from_earning_wallet(make_wallet("earning_wallet")); - let tx_hash = make_tx_hash(123); - let sent_payable_dao = - SentPayableDaoMock::default().get_tx_identifiers_result(hashmap! (tx_hash => 1)); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); let sent_payable_dao = SentPayableDaoMock::default().insert_new_records_result(Ok(())); let subject = AccountantBuilder::default() @@ -4963,7 +4963,6 @@ mod tests { // 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(); let sent_payable_dao = SentPayableDaoMock::new() @@ -5261,14 +5260,20 @@ mod tests { "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() + 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()))); + .finish_scan_result(PendingPayableScanResult::ProcedureShouldBeRepeated(Some( + expected_node_to_ui_msg.clone(), + ))); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( @@ -5282,12 +5287,14 @@ mod tests { 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.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 response_skeleton = ResponseSkeleton { + client_id: 1234, + context_id: 54, + }; let msg = TxReceiptsMessage { results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), response_skeleton_opt: Some(response_skeleton), diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index a937649f2..0debb3acd 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -587,10 +587,9 @@ mod tests { use crate::accountant::db_access_objects::failed_payable_dao::{ FailedTx, FailureReason, FailureStatus, }; - use crate::accountant::db_access_objects::payable_dao::PayableAccount; 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; @@ -601,8 +600,7 @@ mod tests { use crate::accountant::scanners::payable_scanner::PayableScanner; use crate::accountant::scanners::pending_payable_scanner::test_utils::ValidationFailureClockMock; use crate::accountant::scanners::pending_payable_scanner::utils::{ - CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, Retry, - TxHashByTable, + CurrentPendingPayables, PendingPayableScanResult, RecheckRequiringFailures, TxHashByTable, }; use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; @@ -615,16 +613,15 @@ mod tests { ManulTriggerError, Scanner, ScannerCommon, Scanners, StartScanError, StartableScanner, }; use crate::accountant::test_utils::{ - make_custom_payment_thresholds, make_payable_account, - make_qualified_and_unqualified_payables, make_receivable_account, BannedDaoFactoryMock, - BannedDaoMock, ConfigDaoFactoryMock, FailedPayableDaoFactoryMock, FailedPayableDaoMock, - PayableDaoFactoryMock, PayableDaoMock, PayableThresholdsGaugeMock, + 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, RequestTransactionReceipts, - ResponseSkeleton, ScanError, SentPayables, TxReceiptsMessage, + PayableScanType, ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, ScanError, + SentPayables, TxReceiptsMessage, }; use crate::blockchain::blockchain_bridge::{BlockMarker, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::{ @@ -642,7 +639,6 @@ mod tests { use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ DaoFactories, DetailedScanType, FinancialStatistics, PaymentThresholds, - DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index e436cc483..319daec11 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -248,17 +248,12 @@ impl PendingPayableScanner { PendingPayableScanResult::PaymentRetryRequired(response_skeleton_opt) } Retry::RetryTxStatusCheckOnly => { - if let Some(response_skeleton) = response_skeleton_opt { - todo!() - // let ui_msg = NodeToUiMessage { - // target: MessageTarget::ClientId(response_skeleton.client_id), - // body: UiScanResponse {}.tmb(response_skeleton.context_id), - // }; - //PendingPayableScanResult::ProcedureShouldBeRepeated(Some(ui_msg)) - } else { - todo!() - //PendingPayableScanResult::ProcedureShouldBeRepeated() - } + 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 { @@ -870,7 +865,7 @@ mod tests { make_transaction_block, FailedPayableDaoMock, PayableDaoMock, PendingPayableScannerBuilder, SentPayableDaoMock, }; - use crate::accountant::{RequestTransactionReceipts, TxReceiptsMessage}; + use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, TxReceiptsMessage}; use crate::blockchain::blockchain_interface::data_structures::{ StatusReadFromReceiptCheck, TxBlock, }; @@ -883,9 +878,11 @@ mod tests { use crate::blockchain::errors::BlockchainErrorKind; use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; use crate::test_utils::{make_paying_wallet, make_wallet}; - use itertools::{Either, Itertools}; + use itertools::Itertools; use masq_lib::logger::Logger; + use masq_lib::messages::{ToMessageBody, UiScanResponse}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use regex::Regex; use std::collections::{BTreeSet, HashMap}; use std::ops::Sub; @@ -1132,6 +1129,91 @@ mod tests { ); } + #[test] + fn compose_scan_result_all_payments_resolved_in_automatic_mode() { + let result = PendingPayableScanner::compose_scan_result(None, None); + + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ) + } + + #[test] + fn compose_scan_result_all_payments_resolved_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + None, + Some(ResponseSkeleton { + client_id: 2222, + context_id: 22, + }), + ); + + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(Some(NodeToUiMessage { + target: MessageTarget::ClientId(2222), + body: UiScanResponse {}.tmb(22) + })) + ) + } + + #[test] + fn compose_scan_result_payments_retry_required_in_automatic_mode() { + let result = PendingPayableScanner::compose_scan_result(Some(Retry::RetryPayments), None); + + assert_eq!(result, PendingPayableScanResult::PaymentRetryRequired(None)) + } + + #[test] + fn compose_scan_result_payments_retry_required_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + Some(Retry::RetryPayments), + Some(ResponseSkeleton { + client_id: 1234, + context_id: 21, + }), + ); + + assert_eq!( + result, + PendingPayableScanResult::PaymentRetryRequired(Some(ResponseSkeleton { + client_id: 1234, + context_id: 21 + })) + ) + } + + #[test] + fn compose_scan_result_only_scan_procedure_should_be_repeated_in_automatic_mode() { + let result = + PendingPayableScanner::compose_scan_result(Some(Retry::RetryTxStatusCheckOnly), None); + + assert_eq!( + result, + PendingPayableScanResult::ProcedureShouldBeRepeated(None) + ) + } + + #[test] + fn compose_scan_result_only_scan_procedure_should_be_repeated_in_manual_mode() { + let result = PendingPayableScanner::compose_scan_result( + Some(Retry::RetryTxStatusCheckOnly), + Some(ResponseSkeleton { + client_id: 4455, + context_id: 12, + }), + ); + + assert_eq!( + result, + PendingPayableScanResult::ProcedureShouldBeRepeated(Some(NodeToUiMessage { + target: MessageTarget::ClientId(4455), + body: UiScanResponse {}.tmb(12) + })) + ) + } + #[test] fn throws_an_error_when_no_records_to_process_were_found() { let now = SystemTime::now(); diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index ca2d23df9..79cfa03f8 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -473,8 +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 sent_payable_dao_factory = Box::new(Accountant::dao_factory(data_directory)); - let failed_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)); diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 13fe3b685..9d2ac4c18 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -599,7 +599,6 @@ mod tests { }; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; - use clap::AppSettings::DontCollapseArgsInUsage; use ethereum_types::U64; use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; use masq_lib::test_utils::logging::init_test_logging; @@ -610,11 +609,10 @@ mod tests { }; use masq_lib::utils::find_free_port; use std::any::TypeId; - use std::ops::Add; use std::path::Path; use std::str::FromStr; use std::sync::{Arc, Mutex}; - use std::time::{Duration, SystemTime, UNIX_EPOCH}; + use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160}; impl Handler> for BlockchainBridge { 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 9589712c0..a4c771fb1 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -774,7 +774,10 @@ mod tests { .build() }) .collect(); - let expected_result = Err(Sending { error: err_msg, 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", diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 1cfce6798..25834d5f6 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -107,9 +107,7 @@ 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, make_priced_retry_tx_template, - }; + 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_agent::test_utils::BlockchainAgentMock; From 76a857cd318b1b507e07b81a92afda0b21022c5c Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 28 Sep 2025 20:36:49 +0200 Subject: [PATCH 44/48] GH-605: free of unnecessary boilerplate code - erased manual impls for PartialOrd and Ord --- .../db_access_objects/failed_payable_dao.rs | 123 +++++----- .../db_access_objects/payable_dao.rs | 2 +- .../db_access_objects/sent_payable_dao.rs | 223 +++++++++--------- node/src/accountant/mod.rs | 2 +- node/src/accountant/scanners/mod.rs | 36 +-- .../scanners/payable_scanner/mod.rs | 39 ++- .../scanners/payable_scanner/msgs.rs | 1 + .../scanners/payable_scanner/start_scan.rs | 2 +- .../tx_templates/initial/new.rs | 7 +- .../tx_templates/initial/retry.rs | 3 +- .../tx_templates/priced/retry.rs | 6 +- .../scanners/payable_scanner/utils.rs | 2 +- .../scanners/pending_payable_scanner/mod.rs | 39 ++- .../scanners/pending_payable_scanner/utils.rs | 2 +- node/src/accountant/scanners/test_utils.rs | 24 ++ node/src/blockchain/errors/mod.rs | 2 +- .../blockchain/errors/validation_status.rs | 162 ++++++------- 17 files changed, 337 insertions(+), 338 deletions(-) diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 863d0b72e..49070f94a 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -11,7 +11,6 @@ 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,7 +26,7 @@ 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(AppRpcErrorKind), Reverted, @@ -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,7 +75,7 @@ impl FromStr for FailureStatus { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct FailedTx { pub hash: TxHash, pub receiver_address: Address, @@ -118,23 +117,24 @@ 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_minor.cmp(&self.amount_minor)) - } -} +//TODO find me +// // 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_minor.cmp(&self.amount_minor)) +// } +// } impl From<(&SentTx, &Web3Error)> for FailedTx { fn from((sent_tx, error): (&SentTx, &Web3Error)) -> Self { @@ -541,12 +541,12 @@ mod tests { hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount_minor: 0, timestamp: 1719990000, gas_price_minor: 0, \ - nonce: 2, reason: PendingTooLong, status: RecheckRequired(Waiting) }, \ + nonce: 1, reason: PendingTooLong, status: RetryRequired }, \ FailedTx { \ hash: 0x000000000000000000000000000000000000000000000000000000000000007b, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount_minor: 0, timestamp: 1719990000, gas_price_minor: 0, \ - nonce: 1, reason: PendingTooLong, status: RetryRequired }}" + nonce: 2, reason: PendingTooLong, status: RecheckRequired(Waiting) }}" .to_string() )) ); @@ -656,9 +656,9 @@ 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] @@ -1171,42 +1171,43 @@ 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); - } + //TODO find me + // #[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() { diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 598932226..13b5104af 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -19,6 +19,7 @@ use crate::accountant::{ use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; use ethabi::Address; +use ethereum_types::H256; #[cfg(test)] use ethereum_types::{BigEndianHash, U256}; use itertools::Either; @@ -29,7 +30,6 @@ use rusqlite::{Error, Row}; use std::collections::BTreeSet; use std::fmt::{Debug, Display, Formatter}; use std::time::SystemTime; -use web3::types::H256; #[derive(Debug, PartialEq, Eq)] pub enum PayableDaoError { 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 d541f8793..774f0ec83 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -13,7 +13,6 @@ 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; @@ -28,7 +27,7 @@ pub enum SentPayableDaoError { SqlExecutionFailed(String), } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct SentTx { pub hash: TxHash, pub receiver_address: Address, @@ -69,24 +68,25 @@ impl Transaction for SentTx { } } -impl PartialOrd for SentTx { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for SentTx { - fn cmp(&self, other: &Self) -> Ordering { - // Descending Order - other - .timestamp - .cmp(&self.timestamp) - .then_with(|| other.nonce.cmp(&self.nonce)) - .then_with(|| other.amount_minor.cmp(&self.amount_minor)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +//TODO find me +// impl PartialOrd for SentTx { +// fn partial_cmp(&self, other: &Self) -> Option { +// Some(self.cmp(other)) +// } +// } +// +// impl Ord for SentTx { +// fn cmp(&self, other: &Self) -> Ordering { +// // Descending Order +// other +// .timestamp +// .cmp(&self.timestamp) +// .then_with(|| other.nonce.cmp(&self.nonce)) +// .then_with(|| other.amount_minor.cmp(&self.amount_minor)) +// } +// } + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum TxStatus { Pending(ValidationStatus), Confirmed { @@ -96,36 +96,37 @@ pub enum TxStatus { }, } -impl PartialOrd for TxStatus { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -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::Less, - (TxStatus::Confirmed { .. }, TxStatus::Pending(_)) => Ordering::Greater, - ( - TxStatus::Confirmed { - block_number: bn1, - detection: det1, - block_hash: bh1, - }, - TxStatus::Confirmed { - block_number: bn2, - detection: det2, - block_hash: bh2, - }, - ) => bn1 - .cmp(bn2) - .then_with(|| det1.cmp(det2)) - .then_with(|| bh1.cmp(bh2)), - } - } -} +//TODO find me +// impl PartialOrd for TxStatus { +// fn partial_cmp(&self, other: &Self) -> Option { +// Some(self.cmp(other)) +// } +// } +// +// 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::Less, +// (TxStatus::Confirmed { .. }, TxStatus::Pending(_)) => Ordering::Greater, +// ( +// TxStatus::Confirmed { +// block_number: bn1, +// detection: det1, +// block_hash: bh1, +// }, +// TxStatus::Confirmed { +// block_number: bn2, +// detection: det2, +// block_hash: bh2, +// }, +// ) => bn1 +// .cmp(bn2) +// .then_with(|| det1.cmp(det2)) +// .then_with(|| bh1.cmp(bh2)), +// } +// } +// } impl FromStr for TxStatus { type Err = String; @@ -660,18 +661,17 @@ mod tests { Err(SentPayableDaoError::InvalidInput( "Duplicate hashes found in the input. Input Transactions: \ {\ + SentTx { hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ + receiver_address: 0x0000000000000000000000000000000000000000, \ + amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ + nonce: 0, status: Pending(Waiting) }, \ SentTx { \ hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ receiver_address: 0x0000000000000000000000000000000000000000, \ amount_minor: 0, timestamp: 1749204020, gas_price_minor: 0, \ nonce: 0, status: Confirmed { block_hash: \ \"0x000000000000000000000000000000000000000000000000000000003b9acbc8\", \ - block_number: 7890123, detection: Reclaim } }, \ - SentTx { \ - hash: 0x00000000000000000000000000000000000000000000000000000000000004d2, \ - receiver_address: 0x0000000000000000000000000000000000000000, \ - amount_minor: 0, timestamp: 1749204017, gas_price_minor: 0, \ - nonce: 0, status: Pending(Waiting) }\ + block_number: 7890123, detection: Reclaim } }\ }" .to_string() )) @@ -1268,6 +1268,22 @@ mod tests { 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( @@ -1283,22 +1299,6 @@ mod tests { ) )) ); - 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::Confirmed { - block_hash: "0x0000000000000000000000000000000000000000000000000000000000000002" - .to_string(), - block_number: 123, - detection: Detection::Normal, - } - ); assert_eq!(updated_txs.len(), 3) } @@ -1414,7 +1414,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'")); @@ -1587,44 +1587,45 @@ mod tests { ) } - #[test] - fn tx_ordering_works() { - let tx1 = SentTx { - hash: make_tx_hash(1), - receiver_address: make_address(1), - amount_minor: 100, - timestamp: 1000, - gas_price_minor: 10, - nonce: 1, - status: TxStatus::Pending(ValidationStatus::Waiting), - }; - let tx2 = SentTx { - hash: make_tx_hash(2), - receiver_address: make_address(2), - amount_minor: 200, - timestamp: 1000, - gas_price_minor: 20, - nonce: 1, - status: TxStatus::Pending(ValidationStatus::Waiting), - }; - let tx3 = SentTx { - hash: make_tx_hash(3), - receiver_address: make_address(3), - amount_minor: 100, - timestamp: 2000, - gas_price_minor: 30, - nonce: 2, - status: TxStatus::Pending(ValidationStatus::Waiting), - }; - - let mut set = BTreeSet::new(); - set.insert(tx1.clone()); - set.insert(tx2.clone()); - set.insert(tx3.clone()); - - let expected_order = vec![tx3, tx2, tx1]; - assert_eq!(set.into_iter().collect::>(), expected_order); - } + // TODO find me + // #[test] + // fn tx_ordering_works() { + // let tx1 = SentTx { + // hash: make_tx_hash(1), + // receiver_address: make_address(1), + // amount_minor: 100, + // timestamp: 1000, + // gas_price_minor: 10, + // nonce: 1, + // status: TxStatus::Pending(ValidationStatus::Waiting), + // }; + // let tx2 = SentTx { + // hash: make_tx_hash(2), + // receiver_address: make_address(2), + // amount_minor: 200, + // timestamp: 1000, + // gas_price_minor: 20, + // nonce: 1, + // status: TxStatus::Pending(ValidationStatus::Waiting), + // }; + // let tx3 = SentTx { + // hash: make_tx_hash(3), + // receiver_address: make_address(3), + // amount_minor: 100, + // timestamp: 2000, + // gas_price_minor: 30, + // nonce: 2, + // status: TxStatus::Pending(ValidationStatus::Waiting), + // }; + // + // let mut set = BTreeSet::new(); + // set.insert(tx1.clone()); + // set.insert(tx2.clone()); + // set.insert(tx3.clone()); + // + // let expected_order = vec![tx3, tx2, tx1]; + // assert_eq!(set.into_iter().collect::>(), expected_order); + // } #[test] fn transaction_trait_methods_for_tx() { diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 1f097c8e9..e4224a0f1 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -153,7 +153,7 @@ pub enum PayableScanType { 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, diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 0debb3acd..2b19ba61a 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -14,12 +14,13 @@ 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::pending_payable_scanner::{ + ExtendedPendingPayablePrivateScanner, PendingPayableScanner, +}; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ResponseSkeleton, ScanError, ScanForNewPayables, - ScanForPendingPayables, ScanForReceivables, ScanForRetryPayables, SentPayables, - TxReceiptsMessage, + ScanForReceivables, ScanForRetryPayables, SentPayables, TxReceiptsMessage, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::db_config::persistent_configuration::PersistentConfigurationReal; @@ -47,14 +48,7 @@ pub struct Scanners { payable: Box, aware_of_unresolved_pending_payable: bool, initial_pending_payable_scan: bool, - pending_payable: Box< - dyn PrivateScanner< - ScanForPendingPayables, - RequestTransactionReceipts, - TxReceiptsMessage, - PendingPayableScanResult, - >, - >, + pending_payable: Box, receivable: Box< dyn PrivateScanner< ScanForReceivables, @@ -239,10 +233,9 @@ impl Scanners { pub fn finish_payable_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { let scan_result = self.payable.finish_scan(msg, logger); - match scan_result.result { - NextScanToRun::PendingPayableScan => self.aware_of_unresolved_pending_payable = true, - _ => (), - }; + if scan_result.result == NextScanToRun::PendingPayableScan { + self.aware_of_unresolved_pending_payable = true + } scan_result } @@ -278,17 +271,7 @@ impl Scanners { } fn empty_caches(&mut self, logger: &Logger) { - let pending_payable_scanner = self - .pending_payable - .as_any_mut() - .downcast_mut::() - .expect("mismatched types"); - pending_payable_scanner - .current_sent_payables - .ensure_empty_cache(logger); - pending_payable_scanner - .yet_unproven_failed_payables - .ensure_empty_cache(logger); + self.pending_payable.empty_caches(logger) } pub fn try_skipping_payable_adjustment( @@ -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!(); } diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index e0948c4d4..e0a7c8162 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -44,6 +44,14 @@ 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, @@ -53,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 { @@ -168,17 +168,15 @@ impl PayableScanner { .copied() .collect::>(); - let missing_sent_payables_hashes: Vec = hashset_with_hashes_to_eliminate_duplicates + let missing_sent_payables_hashes = hashset_with_hashes_to_eliminate_duplicates .difference(&hashes_from_db) - .copied() - .collect(); + .copied(); let mut sent_payables_hashmap = just_baked_sent_payables .iter() .map(|payable| (payable.hash, &payable.recipient_wallet)) .collect::>(); missing_sent_payables_hashes - .into_iter() .map(|hash| { let wallet_address = sent_payables_hashmap .remove(&hash) @@ -243,7 +241,7 @@ impl PayableScanner { } fn handle_batch_results_for_new_scan(&self, batch_results: &BatchResults, logger: &Logger) { - let (sent, failed) = calculate_occurences(&batch_results); + let (sent, failed) = calculate_occurences(batch_results); debug!( logger, "Processed new txs while sending to RPC: {}", @@ -258,7 +256,7 @@ impl PayableScanner { } fn handle_batch_results_for_retry_scan(&self, batch_results: &BatchResults, logger: &Logger) { - let (sent, failed) = calculate_occurences(&batch_results); + let (sent, failed) = calculate_occurences(batch_results); debug!( logger, "Processed retried txs while sending to RPC: {}", @@ -275,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)); @@ -293,10 +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( @@ -328,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| { @@ -339,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| { diff --git a/node/src/accountant/scanners/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner/msgs.rs index 51ecec9b9..5379d26f5 100644 --- a/node/src/accountant/scanners/payable_scanner/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner/msgs.rs @@ -40,6 +40,7 @@ impl SkeletonOptHolder for InitialTemplatesMessage { } } +#[cfg(test)] mod tests { use crate::accountant::scanners::payable_scanner::msgs::InitialTemplatesMessage; use crate::accountant::scanners::payable_scanner::tx_templates::initial::new::NewTxTemplates; diff --git a/node/src/accountant/scanners/payable_scanner/start_scan.rs b/node/src/accountant/scanners/payable_scanner/start_scan.rs index 2b5c6723d..35cbd3ab2 100644 --- a/node/src/accountant/scanners/payable_scanner/start_scan.rs +++ b/node/src/accountant/scanners/payable_scanner/start_scan.rs @@ -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 21a8fd31e..aceb532b0 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 78a91867b..9990635cd 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 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 d97a1c5ca..48e41f4b9 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 } diff --git a/node/src/accountant/scanners/payable_scanner/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index b3ddc1cc0..429ae7320 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -19,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, diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 319daec11..ecaea9a5f 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -47,6 +47,20 @@ use std::time::SystemTime; use thousands::Separable; use web3::types::H256; +pub(in crate::accountant::scanners) trait ExtendedPendingPayablePrivateScanner: + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + TxReceiptsMessage, + PendingPayableScanResult, + > + CachesEmptiableScanner +{ +} + +pub trait CachesEmptiableScanner { + fn empty_caches(&mut self, logger: &Logger); +} + pub struct PendingPayableScanner { pub common: ScannerCommon, pub payable_dao: Box, @@ -58,6 +72,8 @@ pub struct PendingPayableScanner { pub clock: Box, } +impl ExtendedPendingPayablePrivateScanner for PendingPayableScanner {} + impl PrivateScanner< ScanForPendingPayables, @@ -120,6 +136,13 @@ impl Scanner for PendingPayableScan as_any_mut_in_trait_impl!(); } +impl CachesEmptiableScanner for PendingPayableScanner { + fn empty_caches(&mut self, logger: &Logger) { + self.current_sent_payables.ensure_empty_cache(logger); + self.yet_unproven_failed_payables.ensure_empty_cache(logger); + } +} + impl PendingPayableScanner { pub fn new( payable_dao: Box, @@ -143,8 +166,6 @@ impl PendingPayableScanner { 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(); - eprintln!("ph: {:?}", pending_tx_hashes_opt); - eprintln!("fail: {:?}", failure_hashes_opt); if Self::is_there_nothing_to_process( pending_tx_hashes_opt.as_ref(), @@ -277,7 +298,7 @@ impl PendingPayableScanner { let interpretable_data = self.prepare_cases_to_interpret(msg, logger); TxReceiptInterpreter::default().compose_receipt_scan_report( interpretable_data, - &self, + self, logger, ) } @@ -291,7 +312,7 @@ impl PendingPayableScanner { let either = msg .results .into_iter() - // This must be in for predictability in tests + // // This must be in for predictability in tests .sorted_by_key(|(hash_by_table, _)| hash_by_table.hash()) .fold( init, @@ -468,7 +489,7 @@ impl PendingPayableScanner { 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.into()) { + match self.failed_payable_dao.delete_records(&hashes) { Ok(_) => { info!( logger, @@ -667,7 +688,7 @@ impl PendingPayableScanner { fn prepare_hashmap(rechecks_completed: &[TxHash]) -> HashMap { rechecks_completed .iter() - .map(|tx_hash| (tx_hash.clone(), FailureStatus::Concluded)) + .map(|tx_hash| (*tx_hash, FailureStatus::Concluded)) .collect() } @@ -920,10 +941,10 @@ mod tests { result, Ok(RequestTransactionReceipts { tx_hashes: vec![ - TxHashByTable::SentPayable(sent_tx_hash_2), TxHashByTable::SentPayable(sent_tx_hash_1), - TxHashByTable::FailedPayable(failed_tx_hash_2), - TxHashByTable::FailedPayable(failed_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 }) diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index 35183c563..d43086c88 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -203,7 +203,7 @@ impl UpdatableValidationStatus for FailureStatus { FailureStatus::RecheckRequired(ValidationStatus::Reattempting(previous_attempts)) => { Some(FailureStatus::RecheckRequired( ValidationStatus::Reattempting( - previous_attempts.clone().add_attempt(error.into(), clock), + previous_attempts.clone().add_attempt(error, clock), ), )) } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 214e46c5b..731fb508d 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -14,6 +14,9 @@ use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, use crate::accountant::scanners::pending_payable_scanner::utils::{ PendingPayableCache, PendingPayableScanResult, }; +use crate::accountant::scanners::pending_payable_scanner::{ + CachesEmptiableScanner, ExtendedPendingPayablePrivateScanner, +}; use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, ScanReschedulingAfterEarlyStop, @@ -110,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() @@ -306,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 {} diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index 905f33193..b6d1af111 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -14,7 +14,7 @@ pub enum BlockchainError { Internal(InternalError), } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum BlockchainErrorKind { AppRpc(AppRpcErrorKind), Internal(InternalErrorKind), diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 0601e2533..bb8afd333 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -8,65 +8,46 @@ use serde::{ }; use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; -use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::Formatter; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; use std::time::SystemTime; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum ValidationStatus { Waiting, Reattempting(PreviousAttempts), } -impl PartialOrd for ValidationStatus { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ValidationStatus { - fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (ValidationStatus::Waiting, ValidationStatus::Waiting) => Ordering::Equal, - (ValidationStatus::Waiting, ValidationStatus::Reattempting(_)) => Ordering::Less, - (ValidationStatus::Reattempting(_), ValidationStatus::Waiting) => Ordering::Greater, - ( - ValidationStatus::Reattempting(attempts1), - ValidationStatus::Reattempting(attempts2), - ) => attempts1.cmp(attempts2), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PreviousAttempts { - inner: HashMap, -} - -impl Hash for PreviousAttempts { - fn hash(&self, state: &mut H) { - for (key, value) in &self.inner { - key.hash(state); - value.hash(state); - } - } -} - -impl PartialOrd for PreviousAttempts { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } + inner: BTreeMap, } -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(); - - self_first_seen.cmp(&other_first_seen) - } -} +// TODO find me +// impl Hash for PreviousAttempts { +// fn hash(&self, state: &mut H) { +// for (key, value) in &self.inner { +// key.hash(state); +// value.hash(state); +// } +// } +// } + +// impl PartialOrd for PreviousAttempts { +// fn partial_cmp(&self, other: &Self) -> Option { +// Some(self.cmp(other)) +// } +// } +// +// 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(); +// +// self_first_seen.cmp(&other_first_seen) +// } +// } // 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 @@ -121,7 +102,7 @@ impl<'de> Visitor<'de> for PreviousAttemptsVisitor { stats: ErrorStats, } - let mut error_stats_map: HashMap = hashmap!(); + let mut error_stats_map: BTreeMap = btreemap!(); while let Some(entry) = seq.next_element::()? { error_stats_map.insert(entry.error_kind, entry.stats); } @@ -134,7 +115,7 @@ impl<'de> Visitor<'de> for PreviousAttemptsVisitor { impl PreviousAttempts { pub fn new(error: BlockchainErrorKind, clock: &dyn ValidationFailureClock) -> Self { Self { - inner: hashmap!(error => ErrorStats::now(clock)), + inner: btreemap!(error => ErrorStats::now(clock)), } } @@ -151,7 +132,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, @@ -192,7 +173,6 @@ mod tests { 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::hash_map::DefaultHasher; use std::time::Duration; use std::time::UNIX_EPOCH; @@ -279,44 +259,44 @@ mod tests { assert_eq!(other_error_stats, None); } - #[test] - fn previous_attempts_hash_works_correctly() { - let now = SystemTime::now(); - let clock = ValidationFailureClockMock::default() - .now_result(now) - .now_result(now) - .now_result(now + Duration::from_secs(2)); - let attempts1 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), - &clock, - ); - let attempts2 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), - &clock, - ); - let attempts3 = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), - &clock, - ); - let hash1 = { - let mut hasher = DefaultHasher::new(); - attempts1.hash(&mut hasher); - hasher.finish() - }; - let hash2 = { - let mut hasher = DefaultHasher::new(); - attempts2.hash(&mut hasher); - hasher.finish() - }; - let hash3 = { - let mut hasher = DefaultHasher::new(); - attempts3.hash(&mut hasher); - hasher.finish() - }; - - assert_eq!(hash1, hash2); - assert_ne!(hash1, hash3); - } + // #[test] + // fn previous_attempts_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() { @@ -423,7 +403,7 @@ mod tests { let clock = ValidationFailureClockMock::default().now_result(timestamp); assert_eq!( result.unwrap().inner, - hashmap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) + btreemap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) ); } From b9ad9359b8c1eebde13541871bfd832464bd6207 Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 29 Sep 2025 12:57:18 +0200 Subject: [PATCH 45/48] GH-605: continuous impovement -- Ord and little refactoring --- .../db_access_objects/failed_payable_dao.rs | 57 ---- .../db_access_objects/sent_payable_dao.rs | 243 +++++++----------- node/src/accountant/mod.rs | 20 +- node/src/accountant/scanners/mod.rs | 4 +- .../scanners/pending_payable_scanner/mod.rs | 121 ++++----- .../scanners/pending_payable_scanner/utils.rs | 57 +++- node/src/blockchain/blockchain_bridge.rs | 4 +- .../blockchain_interface_web3/mod.rs | 6 +- .../blockchain/blockchain_interface/mod.rs | 4 +- .../blockchain/errors/validation_status.rs | 96 +++---- 10 files changed, 264 insertions(+), 348 deletions(-) diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 49070f94a..202b1f08e 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -117,25 +117,6 @@ impl Transaction for FailedTx { } } -//TODO find me -// // 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_minor.cmp(&self.amount_minor)) -// } -// } - impl From<(&SentTx, &Web3Error)> for FailedTx { fn from((sent_tx, error): (&SentTx, &Web3Error)) -> Self { let app_rpc_error = AppRpcError::from(error.clone()); @@ -1171,44 +1152,6 @@ mod tests { ) } - //TODO find me - // #[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); 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 774f0ec83..7ac38fbd7 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -13,6 +13,7 @@ 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; @@ -68,25 +69,7 @@ impl Transaction for SentTx { } } -//TODO find me -// impl PartialOrd for SentTx { -// fn partial_cmp(&self, other: &Self) -> Option { -// Some(self.cmp(other)) -// } -// } -// -// impl Ord for SentTx { -// fn cmp(&self, other: &Self) -> Ordering { -// // Descending Order -// other -// .timestamp -// .cmp(&self.timestamp) -// .then_with(|| other.nonce.cmp(&self.nonce)) -// .then_with(|| other.amount_minor.cmp(&self.amount_minor)) -// } -// } - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TxStatus { Pending(ValidationStatus), Confirmed { @@ -96,37 +79,40 @@ pub enum TxStatus { }, } -//TODO find me -// impl PartialOrd for TxStatus { -// fn partial_cmp(&self, other: &Self) -> Option { -// Some(self.cmp(other)) -// } -// } -// -// 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::Less, -// (TxStatus::Confirmed { .. }, TxStatus::Pending(_)) => Ordering::Greater, -// ( -// TxStatus::Confirmed { -// block_number: bn1, -// detection: det1, -// block_hash: bh1, -// }, -// TxStatus::Confirmed { -// block_number: bn2, -// detection: det2, -// block_hash: bh2, -// }, -// ) => bn1 -// .cmp(bn2) -// .then_with(|| det1.cmp(det2)) -// .then_with(|| bh1.cmp(bh2)), -// } -// } -// } +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; @@ -556,6 +542,7 @@ mod tests { 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, @@ -1587,45 +1574,67 @@ mod tests { ) } - // TODO find me - // #[test] - // fn tx_ordering_works() { - // let tx1 = SentTx { - // hash: make_tx_hash(1), - // receiver_address: make_address(1), - // amount_minor: 100, - // timestamp: 1000, - // gas_price_minor: 10, - // nonce: 1, - // status: TxStatus::Pending(ValidationStatus::Waiting), - // }; - // let tx2 = SentTx { - // hash: make_tx_hash(2), - // receiver_address: make_address(2), - // amount_minor: 200, - // timestamp: 1000, - // gas_price_minor: 20, - // nonce: 1, - // status: TxStatus::Pending(ValidationStatus::Waiting), - // }; - // let tx3 = SentTx { - // hash: make_tx_hash(3), - // receiver_address: make_address(3), - // amount_minor: 100, - // timestamp: 2000, - // gas_price_minor: 30, - // nonce: 2, - // status: TxStatus::Pending(ValidationStatus::Waiting), - // }; - // - // let mut set = BTreeSet::new(); - // set.insert(tx1.clone()); - // set.insert(tx2.clone()); - // set.insert(tx3.clone()); - // - // let expected_order = vec![tx3, tx2, tx1]; - // assert_eq!(set.into_iter().collect::>(), expected_order); - // } + #[test] + fn tx_status_ordering_works() { + let tx_status_1 = TxStatus::Pending(ValidationStatus::Waiting); + let tx_status_2 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), + &ValidationFailureClockReal::default(), + ))); + let tx_status_3 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + &ValidationFailureClockReal::default(), + ))); + let tx_status_4 = TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( + BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), + &ValidationFailureClockReal::default(), + ))); + let tx_status_5 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(1)), + block_number: 123456, + detection: Detection::Normal, + }; + let tx_status_6 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(2)), + block_number: 6543, + detection: Detection::Normal, + }; + let tx_status_7 = TxStatus::Confirmed { + block_hash: format!("{:?}", make_tx_hash(1)), + block_number: 123456, + detection: Detection::Reclaim, + }; + let tx_status_1_identical = tx_status_1.clone(); + let tx_status_6_identical = tx_status_6.clone(); + + let mut set = BTreeSet::new(); + vec![ + tx_status_1.clone(), + tx_status_2.clone(), + tx_status_3.clone(), + tx_status_4.clone(), + tx_status_5.clone(), + tx_status_6.clone(), + tx_status_7.clone(), + ] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); + + let 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() { @@ -1655,60 +1664,4 @@ mod tests { assert_eq!(tx.nonce(), nonce); assert_eq!(tx.is_failed(), false); } - - #[test] - fn tx_status_ordering_works_correctly() { - let now = SystemTime::now(); - let clock = ValidationFailureClockMock::default() - .now_result(now) - .now_result(now + Duration::from_secs(1)); - - let pending_waiting = TxStatus::Pending(ValidationStatus::Waiting); - let pending_reattempting = - TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), - &clock, - ))); - let confirmed_early = TxStatus::Confirmed { - block_hash: "0x123".to_string(), - block_number: 100, - detection: Detection::Normal, - }; - let confirmed_late = TxStatus::Confirmed { - block_hash: "0x456".to_string(), - block_number: 200, - detection: Detection::Normal, - }; - - // Pending < Confirmed - assert_eq!(pending_waiting.cmp(&confirmed_early), Ordering::Less); - assert_eq!( - pending_waiting.partial_cmp(&confirmed_early), - Some(Ordering::Less) - ); - - // Within Pending: Waiting < Reattempting - assert_eq!(pending_waiting.cmp(&pending_reattempting), Ordering::Less); - assert_eq!( - pending_waiting.partial_cmp(&pending_reattempting), - Some(Ordering::Less) - ); - - // Within Confirmed: earlier block < later block - assert_eq!(confirmed_early.cmp(&confirmed_late), Ordering::Less); - assert_eq!( - confirmed_early.partial_cmp(&confirmed_late), - Some(Ordering::Less) - ); - - // Equal comparison - assert_eq!( - pending_waiting.cmp(&TxStatus::Pending(ValidationStatus::Waiting)), - Ordering::Equal - ); - assert_eq!( - pending_waiting.partial_cmp(&TxStatus::Pending(ValidationStatus::Waiting)), - Some(Ordering::Equal) - ); - } } diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index e4224a0f1..5e56678f8 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -75,7 +75,7 @@ use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet}; #[cfg(test)] use std::default::Default; use std::fmt::Display; @@ -143,7 +143,7 @@ pub type TxReceiptResult = Result; #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct TxReceiptsMessage { - pub results: HashMap, + pub results: BTreeMap, pub response_skeleton_opt: Option, } @@ -2041,7 +2041,7 @@ mod tests { block_number: 78901234.into(), }; let tx_receipts_msg = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + results: btreemap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( StatusReadFromReceiptCheck::Succeeded(tx_block), )], response_skeleton_opt, @@ -2282,7 +2282,7 @@ mod tests { let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( RequestTransactionReceipts, TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( + results: btreemap![TxHashByTable::SentPayable(sent_tx.hash) => Ok( StatusReadFromReceiptCheck::Reverted ),], response_skeleton_opt @@ -2906,7 +2906,7 @@ mod tests { let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); let expected_tx_receipts_msg = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok( + results: btreemap![TxHashByTable::SentPayable(tx_hash) => Ok( StatusReadFromReceiptCheck::Reverted, )], response_skeleton_opt: None, @@ -3656,7 +3656,7 @@ mod tests { block_number: 4444444444u64.into(), }); let counter_msg_3 = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], + results: btreemap![TxHashByTable::SentPayable(tx_hash) => Ok(tx_status)], response_skeleton_opt: None, }; let request_transaction_receipts_msg = RequestTransactionReceipts { @@ -5227,7 +5227,7 @@ mod tests { ); let system = System::new(test_name); let msg = TxReceiptsMessage { - results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + results: btreemap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), response_skeleton_opt: None, }; let subject_addr = subject.start(); @@ -5296,7 +5296,7 @@ mod tests { context_id: 54, }; let msg = TxReceiptsMessage { - results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + results: btreemap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), response_skeleton_opt: Some(response_skeleton), }; let subject_addr = subject.start(); @@ -5352,7 +5352,7 @@ mod tests { Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let system = System::new(test_name); let msg = TxReceiptsMessage { - results: hashmap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), + results: btreemap!(TxHashByTable::SentPayable(make_tx_hash(123)) => Err(AppRpcError::Remote(RemoteError::Unreachable))), response_skeleton_opt: Some(response_skeleton), }; let subject_addr = subject.start(); @@ -5634,7 +5634,7 @@ mod tests { seeds: Vec, ) -> (TxReceiptsMessage, Vec) { let (tx_receipt_results, tx_record_vec) = seeds.into_iter().enumerate().fold( - (hashmap![], vec![]), + (btreemap![], vec![]), |(mut tx_receipt_results, mut record_by_table_vec), (idx, seed_params)| { let tx_hash = seed_params.tx_hash; let status = seed_params.status; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 2b19ba61a..a11813615 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1468,7 +1468,7 @@ mod tests { .validation_failure_clock(Box::new(validation_failure_clock)) .build(); let msg = TxReceiptsMessage { - results: hashmap![ + results: btreemap![ TxHashByTable::SentPayable(tx_hash_1) => Ok(tx_status_1), TxHashByTable::FailedPayable(tx_hash_2) => Ok(tx_status_2), TxHashByTable::SentPayable(tx_hash_3) => Ok(tx_status_3), @@ -1545,7 +1545,7 @@ mod tests { fn pending_payable_scanner_handles_empty_report_transaction_receipts_message() { let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); let msg = TxReceiptsMessage { - results: hashmap![], + results: btreemap![], response_skeleton_opt: None, }; pending_payable_scanner.mark_as_started(SystemTime::now()); diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index ecaea9a5f..04048568a 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -16,9 +16,9 @@ use crate::accountant::db_access_objects::Transaction; use crate::accountant::scanners::pending_payable_scanner::tx_receipt_interpreter::TxReceiptInterpreter; use crate::accountant::scanners::pending_payable_scanner::utils::{ CurrentPendingPayables, DetectedConfirmations, DetectedFailures, FailedValidation, - FailedValidationByTable, MismatchReport, PendingPayableCache, PendingPayableScanResult, - PresortedTxFailure, ReceiptScanReport, RecheckRequiringFailures, Retry, TxByTable, - TxCaseToBeInterpreted, TxHashByTable, UpdatableValidationStatus, + FailedValidationByTable, PendingPayableCache, PendingPayableScanResult, PresortedTxFailure, + ReceiptScanReport, RecheckRequiringFailures, Retry, TxByTable, TxCaseToBeInterpreted, + TxHashByTable, UpdatableValidationStatus, }; use crate::accountant::scanners::{ PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, @@ -308,28 +308,23 @@ impl PendingPayableScanner { msg: TxReceiptsMessage, logger: &Logger, ) -> Vec { - let init: Either, MismatchReport> = Either::Left(vec![]); - let either = msg - .results - .into_iter() - // // This must be in for predictability in tests - .sorted_by_key(|(hash_by_table, _)| hash_by_table.hash()) - .fold( - init, - |acc, (tx_hash_by_table, tx_receipt_result)| match acc { - Either::Left(cases) => { - self.resolve_real_query(cases, tx_receipt_result, tx_hash_by_table) - } - Either::Right(mut mismatch_report) => { - mismatch_report.remaining_hashes.push(tx_hash_by_table); - Either::Right(mismatch_report) - } - }, - ); + let init: Either, TxHashByTable> = Either::Left(vec![]); + let either = + msg.results + .into_iter() + .fold( + init, + |acc, (tx_hash_by_table, tx_receipt_result)| match acc { + Either::Left(cases) => { + self.resolve_real_query(cases, tx_receipt_result, tx_hash_by_table) + } + Either::Right(missing_entry) => Either::Right(missing_entry), + }, + ); let cases = match either { Either::Left(cases) => cases, - Either::Right(mismatch_report) => self.panic_dump(mismatch_report), + Either::Right(missing_entry) => self.panic_dump(missing_entry), }; self.current_sent_payables.ensure_empty_cache(logger); @@ -343,7 +338,7 @@ impl PendingPayableScanner { mut cases: Vec, receipt_result: TxReceiptResult, looked_up_hash: TxHashByTable, - ) -> Either, MismatchReport> { + ) -> Either, TxHashByTable> { match looked_up_hash { TxHashByTable::SentPayable(tx_hash) => { match self.current_sent_payables.get_record_by_hash(tx_hash) { @@ -354,10 +349,7 @@ impl PendingPayableScanner { )); Either::Left(cases) } - None => Either::Right(MismatchReport { - noticed_with: looked_up_hash, - remaining_hashes: vec![], - }), + None => Either::Right(looked_up_hash), } } TxHashByTable::FailedPayable(tx_hash) => { @@ -372,16 +364,13 @@ impl PendingPayableScanner { )); Either::Left(cases) } - None => Either::Right(MismatchReport { - noticed_with: looked_up_hash, - remaining_hashes: vec![], - }), + None => Either::Right(looked_up_hash), } } } } - fn panic_dump(&mut self, mismatch_report: MismatchReport) -> ! { + fn panic_dump(&mut self, missing_entry: TxHashByTable) -> ! { fn rearrange(hashmap: HashMap) -> Vec { hashmap .into_iter() @@ -392,12 +381,10 @@ impl PendingPayableScanner { panic!( "Looking up '{:?}' in the cache, the record could not be found. Dumping \ - the remaining values. Pending payables: {:?}. Unproven failures: {:?}. \ - Hashes yet not looked up: {:?}.", - mismatch_report.noticed_with, + the remaining values. Pending payables: {:?}. Unproven failures: {:?}.", + missing_entry, rearrange(self.current_sent_payables.dump_cache()), rearrange(self.yet_unproven_failed_payables.dump_cache()), - mismatch_report.remaining_hashes ) } @@ -904,7 +891,6 @@ mod tests { use masq_lib::messages::{ToMessageBody, UiScanResponse}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; - use regex::Regex; use std::collections::{BTreeSet, HashMap}; use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; @@ -1014,7 +1000,7 @@ mod tests { let confirmed_tx_block_sent_tx = make_transaction_block(901); let confirmed_tx_block_failed_tx = make_transaction_block(902); let msg = TxReceiptsMessage { - results: hashmap![ + results: btreemap![ TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(confirmed_tx_block_sent_tx)), TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), @@ -1052,11 +1038,10 @@ mod tests { #[test] fn finish_scan_with_missing_records_inside_caches_noticed_on_missing_sent_tx() { - // Note: the ordering of the hashes matters in this test - let sent_tx_hash_1 = make_tx_hash(0x123); + let sent_tx_hash_1 = make_tx_hash(0x890); let mut sent_tx_1 = make_sent_tx(456); sent_tx_1.hash = sent_tx_hash_1; - let sent_tx_hash_2 = make_tx_hash(0x876); + let sent_tx_hash_2 = make_tx_hash(0x123); let failed_tx_hash_1 = make_tx_hash(0x987); let mut failed_tx_1 = make_failed_tx(567); failed_tx_1.hash = failed_tx_hash_1; @@ -1072,7 +1057,7 @@ mod tests { subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); let logger = Logger::new("test"); let msg = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok( + results: btreemap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok( StatusReadFromReceiptCheck::Pending), TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), @@ -1085,24 +1070,13 @@ mod tests { catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); let panic_msg = panic.downcast_ref::().unwrap(); - let regex_str_in_pieces = vec![ - r#"Looking up 'SentPayable\(0x0000000000000000000000000000000000000000000000000000000000000876\)'"#, - r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, - r#" Unproven failures: \[FailedTx \{ hash:"#, - r#" 0x0000000000000000000000000000000000000000000000000000000000000987, receiver_address:"#, - r#" 0x000000000000000000185d00000000185d000000, amount_minor: 103355177121, timestamp: \d*,"#, - r#" gas_price_minor: 182284263, nonce: 567, reason: PendingTooLong, status: RetryRequired \}\]."#, - r#" Hashes yet not looked up: \[FailedPayable\(0x000000000000000000000000000000000000000"#, - r#"0000000000000000000000987\)\]"#, - ]; - let regex_str = regex_str_in_pieces.join(""); - let expected_msg_regex = Regex::new(®ex_str).unwrap(); - assert!( - expected_msg_regex.is_match(panic_msg), - "Expected string that matches this regex '{}' but it couldn't with '{}'", - regex_str, - panic_msg - ); + let expected = "Looking up 'SentPayable(0x00000000000000000000000000000000000000000000\ + 00000000000000000123)' in the cache, the record could not be found. Dumping the remaining \ + values. Pending payables: [SentTx { hash: 0x0000000000000000000000000000000000000000000000\ + 000000000000000890, receiver_address: 0x0000000000000000000558000000000558000000, \ + amount_minor: 43237380096, timestamp: 29942784, gas_price_minor: 94818816, nonce: 456, \ + status: Pending(Waiting) }]. Unproven failures: []."; + assert_eq!(panic_msg, expected); } #[test] @@ -1113,7 +1087,7 @@ mod tests { let sent_tx_hash_2 = sent_tx_2.hash; let failed_tx_1 = make_failed_tx(567); let failed_tx_hash_1 = failed_tx_1.hash; - let failed_tx_hash_2 = make_tx_hash(901); + let failed_tx_hash_2 = make_tx_hash(987); let mut pending_payable_cache = CurrentPendingPayables::default(); pending_payable_cache.load_cache(vec![sent_tx_1, sent_tx_2]); let mut failed_payable_cache = RecheckRequiringFailures::default(); @@ -1123,7 +1097,7 @@ mod tests { subject.yet_unproven_failed_payables = Box::new(failed_payable_cache); let logger = Logger::new("test"); let msg = TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + results: btreemap![TxHashByTable::SentPayable(sent_tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), TxHashByTable::SentPayable(sent_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(444))), TxHashByTable::FailedPayable(failed_tx_hash_1) => Err(AppRpcError::Local(LocalError::Internal)), TxHashByTable::FailedPayable(failed_tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(make_transaction_block(555))), @@ -1135,19 +1109,16 @@ mod tests { catch_unwind(AssertUnwindSafe(|| subject.finish_scan(msg, &logger))).unwrap_err(); let panic_msg = panic.downcast_ref::().unwrap(); - let regex_str_in_pieces = vec![ - r#"Looking up 'FailedPayable\(0x0000000000000000000000000000000000000000000000000000000000000385\)'"#, - r#" in the cache, the record could not be found. Dumping the remaining values. Pending payables: \[\]."#, - r#" Unproven failures: \[\]. Hashes yet not looked up: \[\]."#, - ]; - let regex_str = regex_str_in_pieces.join(""); - let expected_msg_regex = Regex::new(®ex_str).unwrap(); - assert!( - expected_msg_regex.is_match(panic_msg), - "Expected string that matches this regex '{}' but it couldn't with '{}'", - regex_str, - panic_msg - ); + let expected = "Looking up 'FailedPayable(0x000000000000000000000000000000000000000000\ + 00000000000000000003db)' in the cache, the record could not be found. Dumping the remaining \ + values. Pending payables: [SentTx { hash: 0x000000000000000000000000000000000000000000000000\ + 00000000000001c8, receiver_address: 0x0000000000000000000558000000000558000000, amount_minor: \ + 43237380096, timestamp: 29942784, gas_price_minor: 94818816, nonce: 456, status: \ + Pending(Waiting) }, SentTx { hash: 0x0000000000000000000000000000000000000000000000000000000\ + 000000315, receiver_address: 0x000000000000000000093f00000000093f000000, amount_minor: \ + 387532395441, timestamp: 89643024, gas_price_minor: 491169069, nonce: 789, status: \ + Pending(Waiting) }]. Unproven failures: []."; + assert_eq!(panic_msg, expected); } #[test] diff --git a/node/src/accountant/scanners/pending_payable_scanner/utils.rs b/node/src/accountant/scanners/pending_payable_scanner/utils.rs index d43086c88..f86984df0 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/utils.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/utils.rs @@ -12,6 +12,7 @@ use crate::blockchain::errors::BlockchainErrorKind; use itertools::Either; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; +use std::cmp::Ordering; use std::collections::HashMap; #[derive(Debug, Default, PartialEq, Eq, Clone)] @@ -212,11 +213,6 @@ impl UpdatableValidationStatus for FailureStatus { } } -pub struct MismatchReport { - pub noticed_with: TxHashByTable, - pub remaining_hashes: Vec, -} - pub trait PendingPayableCache { fn load_cache(&mut self, records: Vec); fn get_record_by_hash(&mut self, hash: TxHash) -> Option; @@ -339,12 +335,37 @@ impl TxByTable { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum TxHashByTable { SentPayable(TxHash), FailedPayable(TxHash), } +impl PartialOrd for TxHashByTable { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for TxHashByTable { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (TxHashByTable::FailedPayable(..), TxHashByTable::SentPayable(..)) => Ordering::Less, + (TxHashByTable::SentPayable(..), TxHashByTable::FailedPayable(..)) => Ordering::Greater, + (TxHashByTable::SentPayable(hash_1), TxHashByTable::SentPayable(hash_2)) => { + hash_1.cmp(hash_2) + } + (TxHashByTable::FailedPayable(hash_1), TxHashByTable::FailedPayable(hash_2)) => { + hash_1.cmp(hash_2) + } + } + } +} + impl TxHashByTable { pub fn hash(&self) -> TxHash { match self { @@ -382,6 +403,8 @@ mod tests { use crate::blockchain::test_utils::make_tx_hash; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use std::cmp::Ordering; + use std::collections::BTreeSet; use std::ops::Sub; use std::time::{Duration, SystemTime}; use std::vec; @@ -1156,4 +1179,26 @@ mod tests { assert_eq!(result_a, TxHashByTable::SentPayable(expected_hash_a)); assert_eq!(result_b, TxHashByTable::FailedPayable(expected_hash_b)); } + + #[test] + fn tx_hash_by_table_ordering_works_correctly() { + let tx_1 = TxHashByTable::SentPayable(make_tx_hash(123)); + let tx_2 = TxHashByTable::FailedPayable(make_tx_hash(333)); + let tx_3 = TxHashByTable::SentPayable(make_tx_hash(654)); + let tx_4 = TxHashByTable::FailedPayable(make_tx_hash(222)); + let tx_1_identical = tx_1; + let tx_2_identical = tx_2; + + let mut set = BTreeSet::new(); + vec![tx_1.clone(), tx_2.clone(), tx_3.clone(), tx_4.clone()] + .into_iter() + .for_each(|tx| { + set.insert(tx); + }); + + let expected_order = vec![tx_4, tx_2, tx_1, tx_3]; + assert_eq!(set.into_iter().collect::>(), expected_order); + assert_eq!(tx_1.cmp(&tx_1_identical), Ordering::Equal); + assert_eq!(tx_2.cmp(&tx_2_identical), Ordering::Equal); + } } diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 9d2ac4c18..119acaee9 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1249,7 +1249,7 @@ mod tests { assert_eq!( tx_receipts_message, &TxReceiptsMessage { - results: hashmap![ + results: btreemap![ TxHashByTable::SentPayable(tx_hash_1) => Ok( expected_receipt.into() ), @@ -1386,7 +1386,7 @@ mod tests { assert_eq!( *report_receipts_msg, TxReceiptsMessage { - results: hashmap![TxHashByTable::SentPayable(tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), + results: btreemap![TxHashByTable::SentPayable(tx_hash_1) => Ok(StatusReadFromReceiptCheck::Pending), TxHashByTable::SentPayable(tx_hash_2) => Ok(StatusReadFromReceiptCheck::Succeeded(TxBlock { block_hash: Default::default(), block_number, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 80d557091..9249c6ee0 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,7 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use std::collections::HashMap; +use std::collections::{BTreeMap}; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, LocalPayableError}; use crate::blockchain::blockchain_interface::data_structures::{BatchResults, BlockchainTransaction, StatusReadFromReceiptCheck}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -223,7 +223,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { tx_hashes: Vec, ) -> Box< dyn Future< - Item = HashMap, + Item = BTreeMap, Error = BlockchainInterfaceError, >, > { @@ -257,7 +257,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { } Err(e) => (tx_hash, Err(AppRpcError::from(e))), }) - .collect::>()) + .collect::>()) }), ) } diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index 9ab2c5a62..3db1bbeab 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -22,7 +22,7 @@ use futures::Future; use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use std::collections::HashMap; +use std::collections::BTreeMap; use web3::types::Address; pub trait BlockchainInterface { @@ -49,7 +49,7 @@ pub trait BlockchainInterface { tx_hashes: Vec, ) -> Box< dyn Future< - Item = HashMap, + Item = BTreeMap, Error = BlockchainInterfaceError, >, >; diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index bb8afd333..dfde1b4e8 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -8,47 +8,45 @@ use serde::{ }; use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Formatter; use std::hash::Hash; use std::time::SystemTime; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ValidationStatus { Waiting, Reattempting(PreviousAttempts), } +impl PartialOrd for ValidationStatus { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Manual impl of Ord for enums makes sense because the derive macro determines the ordering +// by the order of the enum variants in its declaration, not only alphabetically. Swiping +// the position of the variants makes a difference, which is counter-intuitive. Structs are not +// implemented the same way and are safe to be used with derive. +impl Ord for ValidationStatus { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (ValidationStatus::Reattempting(..), ValidationStatus::Waiting) => Ordering::Less, + (ValidationStatus::Waiting, ValidationStatus::Reattempting(..)) => Ordering::Greater, + (ValidationStatus::Waiting, ValidationStatus::Waiting) => Ordering::Equal, + (ValidationStatus::Reattempting(prev1), ValidationStatus::Reattempting(prev2)) => { + prev1.cmp(prev2) + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PreviousAttempts { inner: BTreeMap, } -// TODO find me -// impl Hash for PreviousAttempts { -// fn hash(&self, state: &mut H) { -// for (key, value) in &self.inner { -// key.hash(state); -// value.hash(state); -// } -// } -// } - -// impl PartialOrd for PreviousAttempts { -// fn partial_cmp(&self, other: &Self) -> Option { -// Some(self.cmp(other)) -// } -// } -// -// 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(); -// -// self_first_seen.cmp(&other_first_seen) -// } -// } - // 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) @@ -436,26 +434,32 @@ mod tests { BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Io)), &clock, )); - - // Waiting < Reattempting - assert_eq!(waiting.cmp(&reattempting_early), Ordering::Less); - assert_eq!( - waiting.partial_cmp(&reattempting_early), - Some(Ordering::Less) - ); - - // Earlier reattempting < Later reattempting - assert_eq!(reattempting_early.cmp(&reattempting_late), Ordering::Less); - assert_eq!( - reattempting_early.partial_cmp(&reattempting_late), - Some(Ordering::Less) - ); - - // Waiting == Waiting - assert_eq!(waiting.cmp(&ValidationStatus::Waiting), Ordering::Equal); + 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!( - waiting.partial_cmp(&ValidationStatus::Waiting), - Some(Ordering::Equal) + reattempting_early.cmp(&reattempting_early_identical), + Ordering::Equal ); } } From 00f0223e350b253f7a795d85b8ad9888bfefd96e Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 29 Sep 2025 13:23:05 +0200 Subject: [PATCH 46/48] GH-605: CI except MNT just fine --- node/src/accountant/db_access_objects/payable_dao.rs | 3 +-- node/src/blockchain/errors/validation_status.rs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 13b5104af..811f5ce02 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -19,9 +19,8 @@ use crate::accountant::{ use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; use ethabi::Address; -use ethereum_types::H256; #[cfg(test)] -use ethereum_types::{BigEndianHash, U256}; +use ethereum_types::{BigEndianHash, H256, U256}; use itertools::Either; use masq_lib::utils::ExpectValue; #[cfg(test)] diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index dfde1b4e8..a3e8ada27 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -8,7 +8,7 @@ use serde::{ }; use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use std::fmt::Formatter; use std::hash::Hash; use std::time::SystemTime; @@ -171,6 +171,7 @@ mod tests { 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; From 15a4de1ffcdde8e308c525cbdcdffb9d354bb15e Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 29 Sep 2025 13:47:32 +0200 Subject: [PATCH 47/48] GH-605: MNT fixed --- multinode_integration_tests/tests/bookkeeping_test.rs | 2 +- multinode_integration_tests/tests/verify_bill_payment.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/multinode_integration_tests/tests/bookkeeping_test.rs b/multinode_integration_tests/tests/bookkeeping_test.rs index dd5e2bca4..ea5c8ae90 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 0202609a0..9b369e192 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -225,7 +225,7 @@ fn verify_bill_payment() { } let now = Instant::now(); - while !consuming_payable_dao.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", From 02d98fb03109c568c592d5777cb6a73faf8038b0 Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 29 Sep 2025 14:08:58 +0200 Subject: [PATCH 48/48] GH-605: small correction --- .../db_access_objects/failed_payable_dao.rs | 15 ++++--- .../db_access_objects/payable_dao.rs | 10 ++--- .../db_access_objects/sent_payable_dao.rs | 16 +++---- node/src/accountant/mod.rs | 18 ++++---- .../scanners/payable_scanner/mod.rs | 4 +- .../scanners/payable_scanner/utils.rs | 4 +- .../scanners/pending_payable_scanner/mod.rs | 42 +++++++------------ 7 files changed, 48 insertions(+), 61 deletions(-) diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 202b1f08e..7d4644ffa 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -5,7 +5,7 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_access_objects::Transaction; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{comma_joined_stringifiable, 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::ValidationStatus; use crate::database::rusqlite_wrappers::ConnectionWrapper; @@ -149,7 +149,7 @@ impl Display for FailureRetrieveCondition { write!( f, "WHERE tx_hash IN ({})", - comma_joined_stringifiable(hashes, |hash| format!("'{:?}'", hash)) + join_with_commas(hashes, |hash| format!("'{:?}'", hash)) ) } FailureRetrieveCondition::ByStatus(status) => { @@ -159,7 +159,7 @@ 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 => { @@ -197,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 @@ -251,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([]) { @@ -344,8 +344,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> { |(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 \ @@ -379,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([]) { diff --git a/node/src/accountant/db_access_objects/payable_dao.rs b/node/src/accountant/db_access_objects/payable_dao.rs index 811f5ce02..f9a723f54 100644 --- a/node/src/accountant/db_access_objects/payable_dao.rs +++ b/node/src/accountant/db_access_objects/payable_dao.rs @@ -13,9 +13,7 @@ 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_separator, sign_conversion, PendingPayableId, -}; +use crate::accountant::{checked_conversion, join_with_commas, sign_conversion, PendingPayableId}; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::sub_lib::wallet::Wallet; use ethabi::Address; @@ -66,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)) ), } } @@ -408,7 +406,7 @@ impl TableNameDAO for PayableDaoReal { // TODO Will be an object of removal in GH-662 // mod mark_pending_payable_associated_functions { -// use crate::accountant::comma_joined_stringifiable; +// use crate::accountant::join_with_commas; // use crate::accountant::db_access_objects::payable_dao::{MarkPendingPayableID, PayableDaoError}; // use crate::accountant::db_access_objects::utils::{ // update_rows_and_return_valid_count, VigilantRusqliteFlatten, @@ -546,7 +544,7 @@ impl TableNameDAO for PayableDaoReal { // pairs: &[(W, R)], // rowid_pretty_writer: fn(&R) -> Box, // ) -> String { -// comma_joined_stringifiable(pairs, |(wallet, rowid)| { +// join_with_commas(pairs, |(wallet, rowid)| { // format!( // "( Wallet: {}, Rowid: {} )", // wallet, 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 7ac38fbd7..d0edbfa34 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -5,7 +5,7 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_access_objects::Transaction; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; -use crate::accountant::{checked_conversion, comma_joined_stringifiable, join_with_separator}; +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; @@ -165,14 +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 ({})", - comma_joined_stringifiable(nonces, |nonce| nonce.to_string()) + join_with_commas(nonces, |nonce| nonce.to_string()) ) } } @@ -209,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 @@ -262,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([]) { @@ -398,7 +398,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { }); 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 \ @@ -456,7 +456,7 @@ impl SentPayableDao for SentPayableDaoReal<'_> { .iter() .map(|(hash, status)| format!("WHEN tx_hash = '{:?}' THEN '{}'", hash, status)) .join(" "); - let tx_hashes = comma_joined_stringifiable(&status_updates.keys().collect_vec(), |hash| { + let tx_hashes = join_with_commas(&status_updates.keys().collect_vec(), |hash| { format!("'{:?}'", hash) }); @@ -492,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([]) { diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 5e56678f8..20512a135 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1180,7 +1180,7 @@ impl Accountant { fn register_new_pending_sent_tx(&self, msg: RegisterNewPendingPayables) { fn serialize_hashes(tx_hashes: &[SentTx]) -> String { - comma_joined_stringifiable(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) + join_with_commas(tx_hashes, |sent_tx| format!("{:?}", sent_tx.hash)) } let sent_txs: BTreeSet = msg.new_sent_txs.iter().cloned().collect(); @@ -1233,14 +1233,6 @@ impl PendingPayableId { } } -// 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 where F: Fn(&T) -> String, @@ -1252,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) } diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index e0a7c8162..b82d2c46e 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -27,7 +27,7 @@ use crate::accountant::scanners::payable_scanner::utils::{ }; use crate::accountant::scanners::{Scanner, ScannerCommon, StartableScanner}; use crate::accountant::{ - comma_joined_stringifiable, gwei_to_wei, join_with_separator, PayableScanType, PendingPayable, + gwei_to_wei, join_with_commas, join_with_separator, PayableScanType, PendingPayable, ResponseSkeleton, ScanForNewPayables, ScanForRetryPayables, SentPayables, }; use crate::blockchain::blockchain_interface::data_structures::BatchResults; @@ -193,7 +193,7 @@ impl PayableScanner { fn missing_record_msg(nonexistent: &[PendingPayableMissingInDb]) -> String { format!( "Expected sent-payable records for {} were not found. The system has become unreliable", - comma_joined_stringifiable(nonexistent, |missing_sent_tx_ids| format!( + join_with_commas(nonexistent, |missing_sent_tx_ids| format!( "(tx: {:?}, to wallet: {:?})", missing_sent_tx_ids.hash, missing_sent_tx_ids.recipient )) diff --git a/node/src/accountant/scanners/payable_scanner/utils.rs b/node/src/accountant/scanners/payable_scanner/utils.rs index 429ae7320..3ace3b8b6 100644 --- a/node/src/accountant/scanners/payable_scanner/utils.rs +++ b/node/src/accountant/scanners/payable_scanner/utils.rs @@ -5,7 +5,7 @@ use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableD 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::{comma_joined_stringifiable, PendingPayable}; +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; @@ -202,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 diff --git a/node/src/accountant/scanners/pending_payable_scanner/mod.rs b/node/src/accountant/scanners/pending_payable_scanner/mod.rs index 04048568a..7e179ac9d 100644 --- a/node/src/accountant/scanners/pending_payable_scanner/mod.rs +++ b/node/src/accountant/scanners/pending_payable_scanner/mod.rs @@ -24,8 +24,8 @@ use crate::accountant::scanners::{ PrivateScanner, Scanner, ScannerCommon, StartScanError, StartableScanner, }; use crate::accountant::{ - comma_joined_stringifiable, RequestTransactionReceipts, ResponseSkeleton, - ScanForPendingPayables, TxReceiptResult, TxReceiptsMessage, + join_with_commas, RequestTransactionReceipts, ResponseSkeleton, ScanForPendingPayables, + TxReceiptResult, TxReceiptsMessage, }; use crate::blockchain::blockchain_interface::data_structures::TxBlock; use crate::blockchain::errors::validation_status::{ @@ -465,7 +465,7 @@ impl PendingPayableScanner { panic!( "Unable to proceed in a reclaim as the replacement of sent tx records \ {} failed due to: {:?}", - comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + join_with_commas(hashes_and_blocks, |(tx_hash, _)| { format!("{:?}", tx_hash) }), e @@ -481,7 +481,7 @@ impl PendingPayableScanner { info!( logger, "Reclaimed txs {} as confirmed on-chain", - comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, tx_block)| { + join_with_commas(hashes_and_blocks, |(tx_hash, tx_block)| { format!("{:?} (block {})", tx_hash, tx_block.block_number) }) ) @@ -489,7 +489,7 @@ impl PendingPayableScanner { Err(e) => { panic!( "Unable to delete failed tx records {} to finish the reclaims due to: {:?}", - comma_joined_stringifiable(hashes_and_blocks, |(tx_hash, _)| { + join_with_commas(hashes_and_blocks, |(tx_hash, _)| { format!("{:?}", tx_hash) }), e @@ -546,7 +546,7 @@ impl PendingPayableScanner { panic!( "Unable to complete the tx confirmation by the adjustment of the payable accounts \ {} due to: {:?}", - comma_joined_stringifiable( + join_with_commas( &confirmed_txs .iter() .map(|tx| tx.receiver_address) @@ -562,7 +562,7 @@ impl PendingPayableScanner { ) -> ! { panic!( "Unable to update sent payable records {} by their tx blocks due to: {:?}", - comma_joined_stringifiable( + join_with_commas( &tx_hashes_and_tx_blocks.keys().sorted().collect_vec(), |tx_hash| format!("{:?}", tx_hash) ), @@ -630,7 +630,7 @@ impl PendingPayableScanner { info!( logger, "Failed txs {} were processed in the db", - comma_joined_stringifiable(new_failures, |failure| format!("{:?}", failure.hash)) + join_with_commas(new_failures, |failure| format!("{:?}", failure.hash)) ) } @@ -646,7 +646,7 @@ impl PendingPayableScanner { { panic!( "Unable to persist failed txs {} due to: {:?}", - comma_joined_stringifiable(&new_failures, |failure| format!("{:?}", failure.hash)), + join_with_commas(&new_failures, |failure| format!("{:?}", failure.hash)), e ) } @@ -661,10 +661,7 @@ impl PendingPayableScanner { Err(e) => { panic!( "Unable to purge sent payable records for failed txs {} due to: {:?}", - comma_joined_stringifiable(&new_failures, |failure| format!( - "{:?}", - failure.hash - )), + join_with_commas(&new_failures, |failure| format!("{:?}", failure.hash)), e ) } @@ -691,19 +688,13 @@ impl PendingPayableScanner { debug!( logger, "Concluded failures that had required rechecks: {}.", - comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( - "{:?}", - tx_hash - )) + join_with_commas(&rechecks_completed, |tx_hash| format!("{:?}", tx_hash)) ); } Err(e) => { panic!( "Unable to conclude rechecks for failed txs {} due to: {:?}", - comma_joined_stringifiable(&rechecks_completed, |tx_hash| format!( - "{:?}", - tx_hash - )), + join_with_commas(&rechecks_completed, |tx_hash| format!("{:?}", tx_hash)), e ) } @@ -747,7 +738,7 @@ impl PendingPayableScanner { logger, "Pending-tx statuses were processed in the db for validation failure \ of txs {}", - comma_joined_stringifiable(&sent_payable_failures, |failure| { + join_with_commas(&sent_payable_failures, |failure| { format!("{:?}", failure.tx_hash) }) ) @@ -782,10 +773,9 @@ impl PendingPayableScanner { logger, "Failed-tx statuses were processed in the db for validation failure \ of txs {}", - comma_joined_stringifiable( - &failed_txs_validation_failures, - |failure| { format!("{:?}", failure.tx_hash) } - ) + join_with_commas(&failed_txs_validation_failures, |failure| { + format!("{:?}", failure.tx_hash) + }) ) } Err(e) => {