diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 7d9f2ae47..d6b0a35b2 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -4,7 +4,7 @@ use crate::accountant::db_access_objects::utils::{ }; use crate::accountant::db_big_integer::big_int_divider::BigIntDivider; use crate::accountant::{checked_conversion, comma_joined_stringifiable}; -use crate::blockchain::errors::AppRpcError; +use crate::blockchain::errors::validation_status::PreviousAttempts; use crate::database::rusqlite_wrappers::ConnectionWrapper; use itertools::Itertools; use masq_lib::utils::ExpectValue; @@ -25,7 +25,7 @@ pub enum FailedPayableDaoError { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FailureReason { - Submission(AppRpcError), + Submission(PreviousAttempts), Reverted, PendingTooLong, } @@ -75,7 +75,7 @@ impl FromStr for FailureStatus { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ValidationStatus { Waiting, - Reattempting { attempt: usize, error: AppRpcError }, + Reattempting(PreviousAttempts), } #[derive(Clone, Debug, PartialEq, Eq)] @@ -381,8 +381,11 @@ mod tests { make_read_only_db_connection, FailedTxBuilder, }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; - use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError}; - use crate::blockchain::test_utils::make_tx_hash; + use crate::blockchain::errors::blockchain_db_error::app_rpc_web3_error_kind::AppRpcWeb3ErrorKind; + use crate::blockchain::errors::validation_status::{ + PreviousAttempts, ValidationFailureClockReal, + }; + use crate::blockchain::test_utils::{make_tx_hash, ValidationFailureClockMock}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, }; @@ -390,7 +393,9 @@ mod tests { use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use rusqlite::Connection; use std::collections::{HashMap, HashSet}; + use std::ops::Add; use std::str::FromStr; + use std::time::{Duration, SystemTime}; #[test] fn insert_new_records_works() { @@ -582,13 +587,14 @@ mod tests { #[test] fn failure_reason_from_str_works() { + let validation_failure_clock = ValidationFailureClockMock::default(); // Submission error assert_eq!( - FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder":"Test decoder error"}}}"#) - .unwrap(), - FailureReason::Submission(AppRpcError::Local(LocalError::Decoder( - "Test decoder error".to_string() - ))) + FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder"}}}"#).unwrap(), + FailureReason::Submission(PreviousAttempts::new( + Box::new(AppRpcWeb3ErrorKind::Decoder), + &validation_failure_clock + )) ); // Reverted @@ -620,6 +626,11 @@ mod tests { #[test] fn failure_status_from_str_works() { + let validation_failure_clock = ValidationFailureClockMock::default().now_result( + SystemTime::UNIX_EPOCH + .add(Duration::from_secs(1755080031)) + .add(Duration::from_nanos(612180914)), + ); assert_eq!( FailureStatus::from_str("\"RetryRequired\"").unwrap(), FailureStatus::RetryRequired @@ -631,8 +642,8 @@ mod tests { ); assert_eq!( - FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"attempt":2,"error":{"Remote":"Unreachable"}}}}"#).unwrap(), - FailureStatus::RecheckRequired(ValidationStatus::Reattempting { attempt: 2, error: AppRpcError::Remote(RemoteError::Unreachable) }) + FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"ServerUnreachable":{"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}}}}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), &validation_failure_clock))) ); assert_eq!( @@ -713,10 +724,12 @@ mod tests { let tx3 = FailedTxBuilder::default() .hash(make_tx_hash(3)) .reason(PendingTooLong) - .status(RecheckRequired(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable), - })) + .status(RecheckRequired(ValidationStatus::Reattempting( + PreviousAttempts::new( + Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ), + ))) .build(); let tx4 = FailedTxBuilder::default() .hash(make_tx_hash(4)) @@ -768,10 +781,10 @@ mod tests { (tx1.hash, Concluded), ( tx2.hash, - RecheckRequired(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable), - }), + RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ))), ), (tx3.hash, Concluded), ]); @@ -785,10 +798,10 @@ mod tests { assert_eq!(tx2.status, RecheckRequired(ValidationStatus::Waiting)); assert_eq!( updated_txs[1].status, - RecheckRequired(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable) - }) + RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( + Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default() + ))) ); assert_eq!(tx3.status, RetryRequired); assert_eq!(updated_txs[2].status, Concluded); diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs index ac3fbec86..c17061ee6 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -424,8 +424,10 @@ impl SentPayableDao for SentPayableDaoReal<'_> { #[cfg(test)] mod tests { use std::collections::{HashMap, HashSet}; + use std::ops::Add; use std::str::FromStr; use std::sync::{Arc, Mutex}; + use std::time::{Duration, UNIX_EPOCH}; use crate::accountant::db_access_objects::sent_payable_dao::{Detection, RetrieveCondition, SentPayableDao, SentPayableDaoError, SentPayableDaoReal, TxConfirmation, TxStatus}; use crate::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, @@ -439,8 +441,9 @@ mod tests { use crate::accountant::db_access_objects::sent_payable_dao::SentPayableDaoError::{EmptyInput, PartialExecution}; use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; - use crate::blockchain::errors::{AppRpcError, RemoteError}; - use crate::blockchain::test_utils::{make_block_hash, make_tx_hash}; + use crate::blockchain::errors::blockchain_db_error::app_rpc_web3_error_kind::AppRpcWeb3ErrorKind; + use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; + use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; #[test] fn insert_new_records_works() { @@ -452,10 +455,16 @@ mod tests { let tx1 = TxBuilder::default().hash(make_tx_hash(1)).build(); let tx2 = TxBuilder::default() .hash(make_tx_hash(2)) - .status(TxStatus::Pending(ValidationStatus::Reattempting { - attempt: 2, - error: AppRpcError::Remote(RemoteError::Unreachable), - })) + .status(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ) + .add_attempt( + Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ), + ))) .build(); let subject = SentPayableDaoReal::new(wrapped_conn); let txs = vec![tx1, tx2]; @@ -682,10 +691,12 @@ mod tests { .build(); let tx2 = TxBuilder::default() .hash(make_tx_hash(2)) - .status(TxStatus::Pending(ValidationStatus::Reattempting { - attempt: 1, - error: AppRpcError::Remote(RemoteError::Unreachable), - })) + .status(TxStatus::Pending(ValidationStatus::Reattempting( + PreviousAttempts::new( + Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), + &ValidationFailureClockReal::default(), + ), + ))) .build(); let tx3 = TxBuilder::default() .hash(make_tx_hash(3)) @@ -1169,14 +1180,16 @@ mod tests { #[test] fn tx_status_from_str_works() { + let validation_failure_clock = ValidationFailureClockMock::default() + .now_result(UNIX_EPOCH.add(Duration::from_secs(12456))); assert_eq!( TxStatus::from_str(r#"{"Pending":"Waiting"}"#).unwrap(), TxStatus::Pending(ValidationStatus::Waiting) ); assert_eq!( - TxStatus::from_str(r#"{"Pending":{"Reattempting":{"attempt":3,"error":{"Remote":{"InvalidResponse":"bluh"}}}}}"#).unwrap(), - TxStatus::Pending(ValidationStatus::Reattempting { attempt: 3, error: AppRpcError::Remote(RemoteError::InvalidResponse("bluh".to_string())) }) + TxStatus::from_str(r#"{"Pending":{"Reattempting":{"InvalidResponse":{"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}}}}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(Box::new(AppRpcWeb3ErrorKind::InvalidResponse), &validation_failure_clock))) ); assert_eq!( diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 39ead2d76..845b43272 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -2387,7 +2387,7 @@ mod tests { payable_scanner_mock, ))); subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); - // It must be populated because no errors are tolerated at the RetryPayableScanner + // It must be populated because no app_rpc_web3_error_kind are tolerated at the RetryPayableScanner // if automatic scans are on let response_skeleton_opt = Some(ResponseSkeleton { client_id: 789, @@ -4020,7 +4020,7 @@ mod tests { // the first message. Now we reset the state by ending the first scan by a failure and see // that the third scan request is going to be accepted willingly again. addr.try_send(SentPayables { - payment_procedure_result: Err(PayableTransactionError::Signing("bluh".to_string())), + payment_procedure_result: Err(PayableTransactionError::Signing("blah".to_string())), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1122, context_id: 7788, diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 02aa19459..d49cf3efc 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -9,19 +9,18 @@ pub mod test_utils; use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDao}; use crate::accountant::db_access_objects::pending_payable_dao::{PendingPayable, PendingPayableDao}; -use crate::accountant::db_access_objects::receivable_dao::ReceivableDao; use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; -use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; +use crate::accountant::{ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; -use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; +use crate::blockchain::blockchain_bridge::{RetrieveTransactions}; use crate::sub_lib::accountant::{ DaoFactories, FinancialStatistics, PaymentThresholds, }; @@ -48,7 +47,6 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAge use crate::accountant::scanners::pending_payable_scanner::PendingPayableScanner; use crate::accountant::scanners::pending_payable_scanner::utils::PendingPayableScanResult; use crate::accountant::scanners::receivable_scanner::ReceivableScanner; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfigurationReal}; @@ -976,10 +974,10 @@ mod tests { }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, ManulTriggerError}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::{gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1004,13 +1002,11 @@ mod tests { use regex::{Regex}; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; - use std::collections::HashSet; - use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use web3::types::{TransactionReceipt, H256}; + use web3::types::{H256}; use web3::Error; use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; @@ -3099,7 +3095,7 @@ mod tests { ScanError { scan_type, response_skeleton_opt: None, - msg: "bluh".to_string(), + msg: "blah".to_string(), } } diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 971030e14..3747728ab 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -342,7 +342,7 @@ mod tests { use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::time::SystemTime; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; - use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; + use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; #[test] @@ -645,11 +645,11 @@ mod tests { #[test] fn count_total_errors_says_unknown_number_for_early_local_errors() { let early_local_errors = [ - PayableTransactionError::TransactionID(BlockchainError::QueryFailed( + PayableTransactionError::TransactionID(BlockchainInterfaceError::QueryFailed( "blah".to_string(), )), PayableTransactionError::MissingConsumingWallet, - PayableTransactionError::GasPriceQueryFailed(BlockchainError::QueryFailed( + PayableTransactionError::GasPriceQueryFailed(BlockchainInterfaceError::QueryFailed( "ouch".to_string(), )), PayableTransactionError::UnusableWallet("fooo".to_string()), diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index e427ab934..83fb4b2e1 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -9,7 +9,7 @@ use crate::accountant::{ReportTransactionReceipts, RequestTransactionReceipts}; use crate::actor_system_factory::SubsFactory; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::{ - BlockchainError, PayableTransactionError, + BlockchainInterfaceError, PayableTransactionError, }; use crate::blockchain::blockchain_interface::data_structures::ProcessedPayableFallible; use crate::blockchain::blockchain_interface::BlockchainInterface; @@ -505,12 +505,13 @@ impl BlockchainBridge { .expect("Accountant unbound") } - pub fn extract_max_block_count(error: BlockchainError) -> Option { + pub fn extract_max_block_count(error: BlockchainInterfaceError) -> Option { let regex_result = Regex::new(r".* (max: |allowed for your plan: |is limited to |block range limit \(|exceeds max block range )(?P\d+).*") .expect("Invalid regex"); let max_block_count = match error { - BlockchainError::QueryFailed(msg) => match regex_result.captures(msg.as_str()) { + BlockchainInterfaceError::QueryFailed(msg) => match regex_result.captures(msg.as_str()) + { Some(captures) => match captures.name("max_block_count") { Some(m) => match m.as_str().parse::() { Ok(value) => Some(value), @@ -821,7 +822,7 @@ mod tests { assert_eq!(accountant_recording.len(), 0); let service_fee_balance_error = BlockchainAgentBuildError::ServiceFeeBalance( consuming_wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Api error: Transport error: Error(IncompleteMessage)".to_string(), ), ); @@ -1129,7 +1130,7 @@ mod tests { let error_result = result.unwrap_err(); assert_eq!( error_result, - TransactionID(BlockchainError::QueryFailed( + TransactionID(BlockchainInterfaceError::QueryFailed( "Decoder error: Error(\"0x prefix is missing\", line: 0, column: 0) for wallet 0x2581…7849".to_string() )) ); @@ -2127,7 +2128,7 @@ mod tests { #[test] fn extract_max_block_range_from_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs block range too large, range: 33636, max: 3500\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs block range too large, range: 33636, max: 3500\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2136,7 +2137,7 @@ mod tests { #[test] fn extract_max_block_range_from_pokt_error_response() { - let result = BlockchainError::QueryFailed("Rpc(Error { code: ServerError(-32001), message: \"Relay request failed validation: invalid relay request: eth_getLogs block range limit (100000 blocks) exceeded\", data: None })".to_string()); + let result = BlockchainInterfaceError::QueryFailed("Rpc(Error { code: ServerError(-32001), message: \"Relay request failed validation: invalid relay request: eth_getLogs block range limit (100000 blocks) exceeded\", data: None })".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2152,7 +2153,7 @@ mod tests { */ #[test] fn extract_max_block_range_for_ankr_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32600), message: \"block range is too wide\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32600), message: \"block range is too wide\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2165,7 +2166,7 @@ mod tests { */ #[test] fn extract_max_block_range_for_matic_vigil_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"Blockheight too far in the past. Check params passed to eth_getLogs or eth_call requests.Range of blocks allowed for your plan: 1000\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"Blockheight too far in the past. Check params passed to eth_getLogs or eth_call requests.Range of blocks allowed for your plan: 1000\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2178,7 +2179,7 @@ mod tests { */ #[test] fn extract_max_block_range_for_blockpi_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs is limited to 1024 block range. Please check the parameter requirements at https://docs.blockpi.io/documentations/api-reference\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32005), message: \"eth_getLogs is limited to 1024 block range. Please check the parameter requirements at https://docs.blockpi.io/documentations/api-reference\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2187,13 +2188,13 @@ mod tests { /* blastapi - completely rejected call on Public endpoint as won't handle eth_getLogs method on public API - [{"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"Method not found","data":{"method":""}}},{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request","data":{"message":"Cancelled due to validation errors in batch request"}}}] (edited) + [{"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"Method not found","data":{"method":""}}},{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request","data":{"message":"Cancelled due to validation app_rpc_web3_error_kind in batch request"}}}] (edited) [8:50 AM] */ #[test] fn extract_max_block_range_for_blastapi_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: ServerError(-32601), message: \"Method not found\", data: \"'eth_getLogs' is not available on our public API. Head over to https://docs.blastapi.io/blast-documentation/tutorials-and-guides/using-blast-to-get-a-blockchain-endpoint for more information\" }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: ServerError(-32601), message: \"Method not found\", data: \"'eth_getLogs' is not available on our public API. Head over to https://docs.blastapi.io/blast-documentation/tutorials-and-guides/using-blast-to-get-a-blockchain-endpoint for more information\" }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2202,7 +2203,7 @@ mod tests { #[test] fn extract_max_block_range_for_nodies_error_response() { - let result = BlockchainError::QueryFailed("RPC error: Error { code: InvalidParams, message: \"query exceeds max block range 100000\", data: None }".to_string()); + let result = BlockchainInterfaceError::QueryFailed("RPC error: Error { code: InvalidParams, message: \"query exceeds max block range 100000\", data: None }".to_string()); let max_block_count = BlockchainBridge::extract_max_block_count(result); @@ -2211,7 +2212,7 @@ mod tests { #[test] fn extract_max_block_range_for_expected_batch_got_single_error_response() { - let result = BlockchainError::QueryFailed( + let result = BlockchainInterfaceError::QueryFailed( "Got invalid response: Expected batch, got single.".to_string(), ); diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index b91e2c924..c93c07b53 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::blockchain::blockchain_interface::blockchain_interface_web3::CONTRACT_ABI; -use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError; -use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError::QueryFailed; +use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError; +use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use ethereum_types::{H256, U256, U64}; use futures::Future; @@ -115,7 +115,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_fee_balance( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.web3 .eth() @@ -127,7 +127,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_service_fee_balance( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.contract .query("balanceOf", address, None, Options::default(), None) @@ -135,7 +135,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { ) } - fn get_gas_price(&self) -> Box> { + fn get_gas_price(&self) -> Box> { Box::new( self.web3 .eth() @@ -144,7 +144,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { ) } - fn get_block_number(&self) -> Box> { + fn get_block_number(&self) -> Box> { Box::new( self.web3 .eth() @@ -156,7 +156,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_id( &self, address: Address, - ) -> Box> { + ) -> Box> { Box::new( self.web3 .eth() @@ -168,7 +168,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_receipt_in_batch( &self, hash_vec: Vec, - ) -> Box>, Error = BlockchainError>> { + ) -> Box>, Error = BlockchainInterfaceError>> { hash_vec.into_iter().for_each(|hash| { self.web3_batch.eth().transaction_receipt(hash); }); @@ -188,7 +188,7 @@ impl LowBlockchainInt for LowBlockchainIntWeb3 { fn get_transaction_logs( &self, filter: Filter, - ) -> Box, Error = BlockchainError>> { + ) -> Box, Error = BlockchainInterfaceError>> { Box::new( self.web3 .eth() @@ -220,8 +220,8 @@ impl LowBlockchainIntWeb3 { #[cfg(test)] mod tests { use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_LITERAL; - use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError::QueryFailed; - use crate::blockchain::blockchain_interface::{BlockchainError, BlockchainInterface}; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; + use crate::blockchain::blockchain_interface::{BlockchainInterfaceError, BlockchainInterface}; use crate::blockchain::test_utils::make_blockchain_interface_web3; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; @@ -269,7 +269,9 @@ mod tests { .wait(); match result { - Err(BlockchainError::QueryFailed(msg)) if msg.contains("invalid hex character: Q") => { + Err(BlockchainInterfaceError::QueryFailed(msg)) + if msg.contains("invalid hex character: Q") => + { () } x => panic!("Expected complaint about hex character, but got {:?}", x), @@ -377,7 +379,9 @@ mod tests { .wait(); match result { - Err(BlockchainError::QueryFailed(msg)) if msg.contains("invalid hex character: Q") => { + Err(BlockchainInterfaceError::QueryFailed(msg)) + if msg.contains("invalid hex character: Q") => + { () } x => panic!("Expected complaint about hex character, but got {:?}", x), @@ -430,8 +434,11 @@ mod tests { .wait(); let err_msg = match result { - Err(BlockchainError::QueryFailed(msg)) => msg, - x => panic!("Expected BlockchainError::QueryFailed, but got {:?}", x), + Err(BlockchainInterfaceError::QueryFailed(msg)) => msg, + x => panic!( + "Expected BlockchainInterfaceError::QueryFailed, but got {:?}", + x + ), }; assert!( err_msg.contains(expected_err_msg), diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 81c7fe62d..bb9cde491 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,7 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; +use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::blockchain::blockchain_interface::RetrievedBlockchainTransactions; @@ -104,7 +104,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { start_block_marker: BlockMarker, scan_range: BlockScanRange, recipient: Address, - ) -> Box> { + ) -> Box> + { let lower_level_interface = self.lower_interface(); let logger = self.logger.clone(); let contract_address = lower_level_interface.get_contract_address(); @@ -213,7 +214,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { fn process_transaction_receipts( &self, transaction_hashes: Vec, - ) -> Box, Error = BlockchainError>> { + ) -> Box, Error = BlockchainInterfaceError>> + { Box::new( self.lower_interface() .get_transaction_receipt_in_batch(transaction_hashes.clone()) @@ -366,7 +368,7 @@ impl BlockchainInterfaceWeb3 { fn calculate_end_block_marker( start_block_marker: BlockMarker, scan_range: BlockScanRange, - rpc_block_number_result: Result, + rpc_block_number_result: Result, logger: &Logger, ) -> BlockMarker { let locally_determined_end_block_marker = match (start_block_marker, scan_range) { @@ -398,9 +400,9 @@ impl BlockchainInterfaceWeb3 { } fn handle_transaction_logs( - logs_result: Result, BlockchainError>, + logs_result: Result, BlockchainInterfaceError>, logger: &Logger, - ) -> Result, BlockchainError> { + ) -> Result, BlockchainInterfaceError> { let logs = logs_result?; let logs_len = logs.len(); if logs @@ -412,7 +414,7 @@ impl BlockchainInterfaceWeb3 { "Invalid response from blockchain server: {:?}", logs ); - Err(BlockchainError::InvalidResponse) + Err(BlockchainInterfaceError::InvalidResponse) } else { let transactions: Vec = Self::extract_transactions_from_logs(logs); @@ -438,10 +440,10 @@ mod tests { BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, }; - use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError::QueryFailed; + use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError::QueryFailed; use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; use crate::blockchain::blockchain_interface::{ - BlockchainAgentBuildError, BlockchainError, BlockchainInterface, + BlockchainAgentBuildError, BlockchainInterfaceError, BlockchainInterface, RetrievedBlockchainTransactions, }; use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; @@ -733,7 +735,7 @@ mod tests { assert_eq!( result.expect_err("Expected an Err, got Ok"), - BlockchainError::InvalidResponse + BlockchainInterfaceError::InvalidResponse ); } @@ -757,7 +759,7 @@ mod tests { ) .wait(); - assert_eq!(result, Err(BlockchainError::InvalidResponse)); + assert_eq!(result, Err(BlockchainInterfaceError::InvalidResponse)); } #[test] @@ -1007,7 +1009,7 @@ mod tests { let expected_err_factory = |wallet: &Wallet| { BlockchainAgentBuildError::TransactionFeeBalance( wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Transport error: Error(IncompleteMessage)".to_string(), ), ) @@ -1029,7 +1031,7 @@ mod tests { let expected_err_factory = |wallet: &Wallet| { BlockchainAgentBuildError::ServiceFeeBalance( wallet.address(), - BlockchainError::QueryFailed( + BlockchainInterfaceError::QueryFailed( "Api error: Transport error: Error(IncompleteMessage)".to_string(), ), ) @@ -1207,7 +1209,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Uninitialized, BlockScanRange::NoLimit, - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1225,7 +1227,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Uninitialized, BlockScanRange::Range(100), - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1243,7 +1245,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Value(50), BlockScanRange::NoLimit, - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Uninitialized @@ -1261,7 +1263,7 @@ mod tests { Subject::calculate_end_block_marker( BlockMarker::Value(50), BlockScanRange::Range(100), - Err(BlockchainError::InvalidResponse), + Err(BlockchainInterfaceError::InvalidResponse), &logger ), BlockMarker::Value(150) diff --git a/node/src/blockchain/blockchain_interface/data_structures/errors.rs b/node/src/blockchain/blockchain_interface/data_structures/errors.rs index 3084accfb..e168f7d73 100644 --- a/node/src/blockchain/blockchain_interface/data_structures/errors.rs +++ b/node/src/blockchain/blockchain_interface/data_structures/errors.rs @@ -11,7 +11,7 @@ const BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED: &str = "Uninitialized blockchain int being delinquency-banned, you should restart the Node with a value for blockchain-service-url"; #[derive(Clone, Debug, PartialEq, Eq, VariantCount)] -pub enum BlockchainError { +pub enum BlockchainInterfaceError { InvalidUrl, InvalidAddress, InvalidResponse, @@ -19,13 +19,13 @@ pub enum BlockchainError { UninitializedBlockchainInterface, } -impl Display for BlockchainError { +impl Display for BlockchainInterfaceError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let err_spec = match self { Self::InvalidUrl => Either::Left("Invalid url"), Self::InvalidAddress => Either::Left("Invalid address"), Self::InvalidResponse => Either::Left("Invalid response"), - Self::QueryFailed(msg) => Either::Right(format!("Query failed: {}", msg)), + Self::QueryFailed(msg) => Either::Right(format!("Query failed: {}", msg)), //TODO this should also incorporate AppRpcWeb3Error Self::UninitializedBlockchainInterface => { Either::Left(BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED) } @@ -37,8 +37,8 @@ impl Display for BlockchainError { #[derive(Clone, Debug, PartialEq, Eq, VariantCount)] pub enum PayableTransactionError { MissingConsumingWallet, - GasPriceQueryFailed(BlockchainError), - TransactionID(BlockchainError), + GasPriceQueryFailed(BlockchainInterfaceError), + TransactionID(BlockchainInterfaceError), UnusableWallet(String), Signing(String), Sending { msg: String, hashes: Vec }, @@ -78,9 +78,9 @@ impl Display for PayableTransactionError { #[derive(Clone, Debug, PartialEq, Eq, VariantCount)] pub enum BlockchainAgentBuildError { - GasPrice(BlockchainError), - TransactionFeeBalance(Address, BlockchainError), - ServiceFeeBalance(Address, BlockchainError), + GasPrice(BlockchainInterfaceError), + TransactionFeeBalance(Address, BlockchainInterfaceError), + ServiceFeeBalance(Address, BlockchainInterfaceError), UninitializedBlockchainInterface, } @@ -119,7 +119,9 @@ mod tests { use crate::blockchain::blockchain_interface::data_structures::errors::{ PayableTransactionError, BLOCKCHAIN_SERVICE_URL_NOT_SPECIFIED, }; - use crate::blockchain::blockchain_interface::{BlockchainAgentBuildError, BlockchainError}; + use crate::blockchain::blockchain_interface::{ + BlockchainAgentBuildError, BlockchainInterfaceError, + }; use crate::blockchain::test_utils::make_tx_hash; use crate::test_utils::make_wallet; use masq_lib::utils::{slice_of_strs_to_vec_of_strings, to_string}; @@ -136,20 +138,20 @@ mod tests { #[test] fn blockchain_error_implements_display() { let original_errors = [ - BlockchainError::InvalidUrl, - BlockchainError::InvalidAddress, - BlockchainError::InvalidResponse, - BlockchainError::QueryFailed( + BlockchainInterfaceError::InvalidUrl, + BlockchainInterfaceError::InvalidAddress, + BlockchainInterfaceError::InvalidResponse, + BlockchainInterfaceError::QueryFailed( "Don't query so often, it gives me a headache".to_string(), ), - BlockchainError::UninitializedBlockchainInterface, + BlockchainInterfaceError::UninitializedBlockchainInterface, ]; let actual_error_msgs = original_errors.iter().map(to_string).collect::>(); assert_eq!( original_errors.len(), - BlockchainError::VARIANT_COUNT, + BlockchainInterfaceError::VARIANT_COUNT, "you forgot to add all variants in this test" ); assert_eq!( @@ -168,10 +170,10 @@ mod tests { fn payable_payment_error_implements_display() { let original_errors = [ PayableTransactionError::MissingConsumingWallet, - PayableTransactionError::GasPriceQueryFailed(BlockchainError::QueryFailed( + PayableTransactionError::GasPriceQueryFailed(BlockchainInterfaceError::QueryFailed( "Gas halves shut, no drop left".to_string(), )), - PayableTransactionError::TransactionID(BlockchainError::InvalidResponse), + PayableTransactionError::TransactionID(BlockchainInterfaceError::InvalidResponse), PayableTransactionError::UnusableWallet( "This is a LEATHER wallet, not LEDGER wallet, stupid.".to_string(), ), @@ -213,14 +215,14 @@ mod tests { fn blockchain_agent_build_error_implements_display() { let wallet = make_wallet("abc"); let original_errors = [ - BlockchainAgentBuildError::GasPrice(BlockchainError::InvalidResponse), + BlockchainAgentBuildError::GasPrice(BlockchainInterfaceError::InvalidResponse), BlockchainAgentBuildError::TransactionFeeBalance( wallet.address(), - BlockchainError::InvalidResponse, + BlockchainInterfaceError::InvalidResponse, ), BlockchainAgentBuildError::ServiceFeeBalance( wallet.address(), - BlockchainError::InvalidAddress, + BlockchainInterfaceError::InvalidAddress, ), BlockchainAgentBuildError::UninitializedBlockchainInterface, ]; diff --git a/node/src/blockchain/blockchain_interface/lower_level_interface.rs b/node/src/blockchain/blockchain_interface/lower_level_interface.rs index c8653f985..6ae07dca2 100644 --- a/node/src/blockchain/blockchain_interface/lower_level_interface.rs +++ b/node/src/blockchain/blockchain_interface/lower_level_interface.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainError; +use crate::blockchain::blockchain_interface::data_structures::errors::BlockchainInterfaceError; use ethereum_types::{H256, U64}; use futures::Future; use serde_json::Value; @@ -15,33 +15,33 @@ pub trait LowBlockchainInt { fn get_transaction_fee_balance( &self, address: Address, - ) -> Box>; + ) -> Box>; fn get_service_fee_balance( &self, address: Address, - ) -> Box>; + ) -> Box>; - fn get_gas_price(&self) -> Box>; + fn get_gas_price(&self) -> Box>; - fn get_block_number(&self) -> Box>; + fn get_block_number(&self) -> Box>; fn get_transaction_id( &self, address: Address, - ) -> Box>; + ) -> Box>; fn get_transaction_receipt_in_batch( &self, hash_vec: Vec, - ) -> Box>, Error = BlockchainError>>; + ) -> Box>, Error = BlockchainInterfaceError>>; fn get_contract_address(&self) -> Address; fn get_transaction_logs( &self, filter: Filter, - ) -> Box, Error = BlockchainError>>; + ) -> Box, Error = BlockchainInterfaceError>>; fn get_web3_batch(&self) -> Web3>; } diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index 242bf433f..eb736b2a3 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,7 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; +use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainInterfaceError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use crate::sub_lib::wallet::Wallet; @@ -31,7 +31,7 @@ pub trait BlockchainInterface { start_block: BlockMarker, scan_range: BlockScanRange, recipient: Address, - ) -> Box>; + ) -> Box>; fn introduce_blockchain_agent( &self, @@ -41,7 +41,7 @@ pub trait BlockchainInterface { fn process_transaction_receipts( &self, transaction_hashes: Vec, - ) -> Box, Error = BlockchainError>>; + ) -> Box, Error = BlockchainInterfaceError>>; fn submit_payables_in_batch( &self, diff --git a/node/src/blockchain/errors.rs b/node/src/blockchain/errors.rs deleted file mode 100644 index 865bea29c..000000000 --- a/node/src/blockchain/errors.rs +++ /dev/null @@ -1,127 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; -use web3::error::Error as Web3Error; - -// Prefixed with App to clearly distinguish app-specific errors from library errors. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum AppRpcError { - Local(LocalError), - Remote(RemoteError), -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum LocalError { - Decoder(String), - Internal, - Io(String), - Signing(String), - Transport(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RemoteError { - InvalidResponse(String), - Unreachable, - Web3RpcError { code: i64, message: String }, -} - -// EVM based errors -impl From for AppRpcError { - fn from(error: Web3Error) -> Self { - match error { - // Local Errors - Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), - Web3Error::Internal => AppRpcError::Local(LocalError::Internal), - Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), - Web3Error::Signing(error) => { - // This variant cannot be tested due to import limitations. - AppRpcError::Local(LocalError::Signing(error.to_string())) - } - Web3Error::Transport(error) => AppRpcError::Local(LocalError::Transport(error)), - - // Api Errors - Web3Error::InvalidResponse(response) => { - AppRpcError::Remote(RemoteError::InvalidResponse(response)) - } - Web3Error::Rpc(web3_rpc_error) => AppRpcError::Remote(RemoteError::Web3RpcError { - code: web3_rpc_error.code.code(), - message: web3_rpc_error.message, - }), - Web3Error::Unreachable => AppRpcError::Remote(RemoteError::Unreachable), - } - } -} - -mod tests { - use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError}; - use web3::error::Error as Web3Error; - - #[test] - fn web3_error_to_failure_reason_conversion_works() { - // Local Errors - assert_eq!( - AppRpcError::from(Web3Error::Decoder("Decoder error".to_string())), - AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())) - ); - assert_eq!( - AppRpcError::from(Web3Error::Internal), - AppRpcError::Local(LocalError::Internal) - ); - assert_eq!( - AppRpcError::from(Web3Error::Io(std::io::Error::new( - std::io::ErrorKind::Other, - "IO error" - ))), - AppRpcError::Local(LocalError::Io("IO error".to_string())) - ); - assert_eq!( - AppRpcError::from(Web3Error::Transport("Transport error".to_string())), - AppRpcError::Local(LocalError::Transport("Transport error".to_string())) - ); - - // Api Errors - assert_eq!( - AppRpcError::from(Web3Error::InvalidResponse("Invalid response".to_string())), - AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())) - ); - assert_eq!( - AppRpcError::from(Web3Error::Rpc(jsonrpc_core::types::error::Error { - code: jsonrpc_core::types::error::ErrorCode::ServerError(42), - message: "RPC error".to_string(), - data: None, - })), - AppRpcError::Remote(RemoteError::Web3RpcError { - code: 42, - message: "RPC error".to_string(), - }) - ); - assert_eq!( - AppRpcError::from(Web3Error::Unreachable), - AppRpcError::Remote(RemoteError::Unreachable) - ); - } - - #[test] - fn app_rpc_error_serialization_deserialization() { - let errors = vec![ - // Local Errors - AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())), - AppRpcError::Local(LocalError::Internal), - AppRpcError::Local(LocalError::Io("IO error".to_string())), - AppRpcError::Local(LocalError::Signing("Signing error".to_string())), - AppRpcError::Local(LocalError::Transport("Transport error".to_string())), - // Remote Errors - AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())), - AppRpcError::Remote(RemoteError::Unreachable), - AppRpcError::Remote(RemoteError::Web3RpcError { - code: 42, - message: "RPC error".to_string(), - }), - ]; - - errors.into_iter().for_each(|error| { - let serialized = serde_json::to_string(&error).unwrap(); - let deserialized: AppRpcError = serde_json::from_str(&serialized).unwrap(); - assert_eq!(error, deserialized, "Error: {:?}", error); - }); - } -} diff --git a/node/src/blockchain/errors/blockchain_db_error/app_rpc_web3_error_kind.rs b/node/src/blockchain/errors/blockchain_db_error/app_rpc_web3_error_kind.rs new file mode 100644 index 000000000..d53d0145a --- /dev/null +++ b/node/src/blockchain/errors/blockchain_db_error/app_rpc_web3_error_kind.rs @@ -0,0 +1,268 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::blockchain_db_error::{BlockchainDbError, CustomHash, CustomSeDe}; +use crate::blockchain::errors::blockchain_loggable_error::app_rpc_web3_error::{ + AppRpcWeb3Error, LocalError, RemoteError, +}; +use crate::blockchain::errors::common_methods::CommonMethods; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; + +impl BlockchainDbError for AppRpcWeb3ErrorKind { + fn as_common_methods(&self) -> &dyn CommonMethods> { + self + } +} + +impl CustomSeDe for AppRpcWeb3ErrorKind { + fn custom_serialize(&self) -> Result { + serde_json::to_value(self) + } + + fn custom_deserialize(str: &str) -> Result, serde_json::Error> + where + Self: Sized, + { + let res: Result = serde_json::from_str(str); + res.map(|kind| Box::new(kind) as Box) + } +} + +impl CommonMethods> for AppRpcWeb3ErrorKind { + fn partial_eq(&self, other: &Box) -> bool { + other + .as_common_methods() + .as_any() + .downcast_ref::() + .map_or(false, |other| self == other) + } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + + as_any_ref_in_trait_impl!(); +} + +// Hash discriminants for each error variant +const HASH_DECODER: u8 = 0; +const HASH_INTERNAL: u8 = 1; +const HASH_IO: u8 = 2; +const HASH_SIGNING: u8 = 3; +const HASH_TRANSPORT: u8 = 4; +const HASH_INVALID_RESPONSE: u8 = 5; +const HASH_SERVER_UNREACHABLE: u8 = 6; +const HASH_WEB3_RPC_ERROR: u8 = 7; + +impl CustomHash for AppRpcWeb3ErrorKind { + fn custom_hash(&self, hasher: &mut dyn Hasher) { + match self { + AppRpcWeb3ErrorKind::Decoder => hasher.write_u8(HASH_DECODER), + AppRpcWeb3ErrorKind::Internal => hasher.write_u8(HASH_INTERNAL), + AppRpcWeb3ErrorKind::IO => hasher.write_u8(HASH_IO), + AppRpcWeb3ErrorKind::Signing => hasher.write_u8(HASH_SIGNING), + AppRpcWeb3ErrorKind::Transport => hasher.write_u8(HASH_TRANSPORT), + AppRpcWeb3ErrorKind::InvalidResponse => hasher.write_u8(HASH_INVALID_RESPONSE), + AppRpcWeb3ErrorKind::ServerUnreachable => hasher.write_u8(HASH_SERVER_UNREACHABLE), + AppRpcWeb3ErrorKind::Web3RpcError(code) => { + hasher.write_u8(HASH_WEB3_RPC_ERROR); + hasher.write_i64(*code); + } + } + } +} + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum AppRpcWeb3ErrorKind { + // Local + Decoder, + Internal, + IO, + Signing, + Transport, + + // Remote + InvalidResponse, + ServerUnreachable, + Web3RpcError(i64), // Keep only the stable error code +} + +impl From<&AppRpcWeb3Error> for AppRpcWeb3ErrorKind { + fn from(err: &AppRpcWeb3Error) -> Self { + match err { + AppRpcWeb3Error::Local(local) => match local { + LocalError::Decoder(_) => Self::Decoder, + LocalError::Internal => Self::Internal, + LocalError::Io(_) => Self::IO, + LocalError::Signing(_) => Self::Signing, + LocalError::Transport(_) => Self::Transport, + }, + AppRpcWeb3Error::Remote(remote) => match remote { + RemoteError::InvalidResponse(_) => Self::InvalidResponse, + RemoteError::Unreachable => Self::ServerUnreachable, + RemoteError::Web3RpcError { code, .. } => Self::Web3RpcError(*code), + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::blockchain::errors::blockchain_db_error::app_rpc_web3_error_kind::AppRpcWeb3ErrorKind; + use crate::blockchain::errors::blockchain_db_error::BlockchainDbError; + use crate::blockchain::errors::blockchain_loggable_error::app_rpc_web3_error::{ + AppRpcWeb3Error, LocalError, RemoteError, + }; + use crate::blockchain::errors::test_utils::test_clone_impl_for_blockchain_db_error; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + #[test] + fn conversion_between_app_rpc_error_and_app_rpc_error_kind_works() { + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Local(LocalError::Decoder( + "Decoder error".to_string() + ))), + AppRpcWeb3ErrorKind::Decoder + ); + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Local(LocalError::Internal)), + AppRpcWeb3ErrorKind::Internal + ); + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Local(LocalError::Io( + "IO error".to_string() + ))), + AppRpcWeb3ErrorKind::IO + ); + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Local(LocalError::Signing( + "Signing error".to_string() + ))), + AppRpcWeb3ErrorKind::Signing + ); + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Local(LocalError::Transport( + "Transport error".to_string() + ))), + AppRpcWeb3ErrorKind::Transport + ); + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Remote(RemoteError::InvalidResponse( + "Invalid response".to_string() + ))), + AppRpcWeb3ErrorKind::InvalidResponse + ); + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Remote(RemoteError::Unreachable)), + AppRpcWeb3ErrorKind::ServerUnreachable + ); + assert_eq!( + AppRpcWeb3ErrorKind::from(&AppRpcWeb3Error::Remote(RemoteError::Web3RpcError { + code: 55, + message: "Booga".to_string() + })), + AppRpcWeb3ErrorKind::Web3RpcError(55) + ); + } + + #[test] + fn clone_works_for_blockchain_db_error_wrapping_app_rpc_error_kind() { + let subject: Box = Box::new(AppRpcWeb3ErrorKind::Web3RpcError(123)); + + test_clone_impl_for_blockchain_db_error::(subject); + } + + #[test] + fn hashing_for_app_rpc_error_kind_works() { + use std::collections::HashSet; + + let errors = vec![ + Box::new(AppRpcWeb3ErrorKind::Decoder) as Box, + Box::new(AppRpcWeb3ErrorKind::Internal), + Box::new(AppRpcWeb3ErrorKind::IO), + Box::new(AppRpcWeb3ErrorKind::Signing), + Box::new(AppRpcWeb3ErrorKind::Transport), + Box::new(AppRpcWeb3ErrorKind::InvalidResponse), + Box::new(AppRpcWeb3ErrorKind::ServerUnreachable), + Box::new(AppRpcWeb3ErrorKind::Web3RpcError(123)), + Box::new(AppRpcWeb3ErrorKind::Web3RpcError(124)), + Box::new(AppRpcWeb3ErrorKind::Web3RpcError(555555)), + ]; + + let hashes: HashSet = errors + .into_iter() + .map(|blockchain_error| { + let mut hasher = DefaultHasher::default(); + blockchain_error.hash(&mut hasher); + hasher.finish() + }) + .collect(); + + // If all hashes are unique, the set size should equal the number of errors + assert_eq!(hashes.len(), 10, "Some error kinds produced duplicate hashes"); + } + + #[test] + fn partial_eq_for_app_rpc_error_kind_works() { + let subject: Box = Box::new(AppRpcWeb3ErrorKind::Web3RpcError(123)); + let other_1: Box = Box::new(AppRpcWeb3ErrorKind::Web3RpcError(124)); + let other_2: Box = Box::new(AppRpcWeb3ErrorKind::Web3RpcError(123)); + let other_3: Box = Box::new(AppRpcWeb3ErrorKind::Internal); + + assert_ne!(&subject, &other_1); + assert_eq!(&subject, &other_2); + assert_ne!(&subject, &other_3); + } + + #[test] + fn app_rpc_error_kind_serialization_deserialization() { + let errors = vec![ + // Local Errors + AppRpcWeb3ErrorKind::Decoder, + AppRpcWeb3ErrorKind::Internal, + AppRpcWeb3ErrorKind::IO, + AppRpcWeb3ErrorKind::Signing, + AppRpcWeb3ErrorKind::Transport, + // Remote Errors + AppRpcWeb3ErrorKind::InvalidResponse, + AppRpcWeb3ErrorKind::ServerUnreachable, + AppRpcWeb3ErrorKind::Web3RpcError(42), + ]; + + errors.into_iter().for_each(|error| { + let serialized = serde_json::to_string(&error).unwrap(); + let deserialized: AppRpcWeb3ErrorKind = serde_json::from_str(&serialized).unwrap(); + assert_eq!( + error, deserialized, + "Failed serde attempt for {:?} that should look \ + like {:?}", + deserialized, error + ); + }); + } + + #[test] + fn serialization_and_deserialization_for_blockchain_db_error_works() { + vec![ + ( + Box::new(AppRpcWeb3ErrorKind::Internal) as Box, + "\"Internal\"", + ), + ( + Box::new(AppRpcWeb3ErrorKind::Web3RpcError(123)), + "{\"Web3RpcError\":123}", + ), + ] + .into_iter() + .for_each(|(blockchain_error, expected_result)| { + let json_result = serde_json::to_string(&blockchain_error).unwrap(); + assert_eq!(json_result, expected_result); + let trait_object_result = + serde_json::from_str::>(&json_result).unwrap(); + assert_eq!(&trait_object_result, &blockchain_error); + }) + } +} diff --git a/node/src/blockchain/errors/blockchain_db_error/masq_error_kind.rs b/node/src/blockchain/errors/blockchain_db_error/masq_error_kind.rs new file mode 100644 index 000000000..d8d174cc7 --- /dev/null +++ b/node/src/blockchain/errors/blockchain_db_error/masq_error_kind.rs @@ -0,0 +1,161 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::blockchain_db_error::{BlockchainDbError, CustomHash, CustomSeDe}; +use crate::blockchain::errors::blockchain_loggable_error::masq_error::MASQError; +use crate::blockchain::errors::common_methods::CommonMethods; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use std::hash::Hasher; +use variant_count::VariantCount; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, VariantCount)] +pub enum MASQErrorKind { + PendingTooLongNotReplaced, +} + +impl BlockchainDbError for MASQErrorKind { + fn as_common_methods(&self) -> &dyn CommonMethods> { + self + } +} + +impl CustomSeDe for MASQErrorKind { + fn custom_serialize(&self) -> Result { + serde_json::to_value(self) + } + + fn custom_deserialize(str: &str) -> Result, serde_json::Error> + where + Self: Sized, + { + let res: Result = serde_json::from_str(str); + res.map(|kind| Box::new(kind) as Box) + } +} + +impl CommonMethods> for MASQErrorKind { + fn partial_eq(&self, other: &Box) -> bool { + other + .as_common_methods() + .as_any() + .downcast_ref::() + .map_or(false, |other| self == other) + } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + + as_any_ref_in_trait_impl!(); +} + +impl CustomHash for MASQErrorKind { + fn custom_hash(&self, hasher: &mut dyn Hasher) { + match self { + MASQErrorKind::PendingTooLongNotReplaced => hasher.write_u8(0), + } + } +} + +impl From<&MASQError> for MASQErrorKind { + fn from(masq_error: &MASQError) -> Self { + match masq_error { + MASQError::PendingTooLongNotReplaced => MASQErrorKind::PendingTooLongNotReplaced, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blockchain::errors::blockchain_loggable_error::masq_error::MASQError; + use crate::blockchain::errors::blockchain_loggable_error::BlockchainLoggableError; + use crate::blockchain::errors::test_utils::test_clone_impl_for_blockchain_db_error; + use std::collections::hash_map::DefaultHasher; + use std::hash::Hash; + + #[test] + fn conversion_between_masq_error_and_masq_error_kind_works() { + assert_eq!( + MASQErrorKind::from(&MASQError::PendingTooLongNotReplaced), + MASQErrorKind::PendingTooLongNotReplaced + ); + } + + #[test] + fn clone_works_for_blockchain_db_error_wrapping_masq_error_kind() { + let subject: Box = + Box::new(MASQErrorKind::PendingTooLongNotReplaced); + + test_clone_impl_for_blockchain_db_error::(subject); + } + + #[test] + fn hashing_for_masq_error_kind_works() { + let mut hasher = DefaultHasher::default(); + let mut hashes = vec![ + Box::new(MASQErrorKind::PendingTooLongNotReplaced) as Box, + // Add more types here as there are more types of MASQ app_rpc_web3_error_kind. + ] + .into_iter() + .map(|blockchain_error| { + blockchain_error.hash(&mut hasher); + + hasher.finish() + }) + .collect::>(); + + hashes.clone().iter().for_each(|picked_hash| { + hashes.remove(0); + hashes.iter().for_each(|other_hash| { + assert_ne!(picked_hash, other_hash); + }); + }); + // Expand this test as there are more variants of MASQErrorKind. + assert_eq!(MASQErrorKind::VARIANT_COUNT, 1); + } + + #[test] + fn partial_eq_for_masq_error_kind_works() { + let subject: Box = + Box::new(MASQErrorKind::PendingTooLongNotReplaced); + let other: Box = Box::new(MASQErrorKind::PendingTooLongNotReplaced); + + assert_eq!(&subject, &other); + // Expand this test as there are more variants of MASQErrorKind. + assert_eq!(MASQErrorKind::VARIANT_COUNT, 1); + } + + #[test] + fn serialization_and_deserialization_for_blockchain_db_error_works() { + vec![( + Box::new(MASQErrorKind::PendingTooLongNotReplaced) as Box, + "\"PendingTooLongNotReplaced\"", + )] + .into_iter() + .for_each(|(blockchain_error, expected_result)| { + let json_result = serde_json::to_string(&blockchain_error).unwrap(); + assert_eq!(json_result, expected_result); + let trait_object_result = + serde_json::from_str::>(&json_result).unwrap(); + assert_eq!(&trait_object_result, &blockchain_error); + }); + // Expand this test as there are more variants of MASQErrorKind. + assert_eq!(MASQErrorKind::VARIANT_COUNT, 1); + } + + #[test] + fn blockchain_loggable_error_can_be_converted_to_blockchain_db_error_for_masq_errors() { + let error: Box = + Box::new(MASQError::PendingTooLongNotReplaced); + + let result = >::from(error); + + assert_eq!( + &result, + &(Box::new(MASQErrorKind::PendingTooLongNotReplaced) as Box) + ); + // Expand this test as there are more variants of MASQErrorKind. + assert_eq!(MASQErrorKind::VARIANT_COUNT, 1); + } +} diff --git a/node/src/blockchain/errors/blockchain_db_error/mod.rs b/node/src/blockchain/errors/blockchain_db_error/mod.rs new file mode 100644 index 000000000..44ae27c02 --- /dev/null +++ b/node/src/blockchain/errors/blockchain_db_error/mod.rs @@ -0,0 +1,122 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod app_rpc_web3_error_kind; +pub mod masq_error_kind; + +use crate::blockchain::errors::blockchain_db_error::app_rpc_web3_error_kind::AppRpcWeb3ErrorKind; +use crate::blockchain::errors::blockchain_db_error::masq_error_kind::MASQErrorKind; +use crate::blockchain::errors::common_methods::CommonMethods; +use serde::{Deserialize as DeserializeTrait, Serialize as SerializeTrait}; +use serde_json::Value; +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; + +pub trait BlockchainDbError: CustomSeDe + CustomHash + Debug { + fn as_common_methods(&self) -> &dyn CommonMethods>; +} + +pub trait CustomSeDe { + fn custom_serialize(&self) -> Result; + fn custom_deserialize(str: &str) -> Result, serde_json::Error> + where + Self: Sized; +} + +pub trait CustomHash { + fn custom_hash(&self, hasher: &mut dyn Hasher); +} + +impl SerializeTrait for Box { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.custom_serialize() + .map_err(|e| serde::ser::Error::custom(e))? + .serialize(serializer) + } +} + +impl<'de> DeserializeTrait<'de> for Box { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let json_value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; + let json_str = + serde_json::to_string(&json_value).map_err(|e| serde::de::Error::custom(e))?; // Untested error + + if let Ok(error) = AppRpcWeb3ErrorKind::custom_deserialize(&json_str) { + return Ok(error); + } + + if let Ok(error) = MASQErrorKind::custom_deserialize(&json_str) { + return Ok(error); + } + + Err(serde::de::Error::custom(format!( + "Unable to deserialize BlockchainDbError from: {}", + json_str + ))) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.as_common_methods().clone_boxed() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.as_common_methods().partial_eq(other) + } +} + +impl Hash for Box { + fn hash(&self, state: &mut H) { + self.custom_hash(state) + } +} + +impl Eq for Box {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blockchain::errors::test_utils::BlockchainDbErrorMock; + + #[test] + fn deserialization_fails() { + let str = "\"bluh\""; + + let err = serde_json::from_str::>(str).unwrap_err(); + + assert_eq!( + err.to_string(), + "Unable to deserialize BlockchainDbError from: \"bluh\"" + ) + } + + #[test] + fn pre_serialization_custom_error_is_well_arranged() { + let mock = BlockchainDbErrorMock::default(); + let subject: Box = Box::new(mock); + + let res = serde_json::to_string(&subject).unwrap_err(); + + assert_eq!( + res.to_string(), + "invalid type: character `a`, expected null" + ); + } + + #[test] + fn deserialization_other_error() { + let result = + serde_json::from_str::>(r#"{"key":invalid_json_value}"#) + .unwrap_err(); + + assert_eq!(result.to_string(), "expected value at line 1 column 8"); + } +} diff --git a/node/src/blockchain/errors/blockchain_loggable_error/app_rpc_web3_error.rs b/node/src/blockchain/errors/blockchain_loggable_error/app_rpc_web3_error.rs new file mode 100644 index 000000000..18b747a50 --- /dev/null +++ b/node/src/blockchain/errors/blockchain_loggable_error/app_rpc_web3_error.rs @@ -0,0 +1,223 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::blockchain_db_error::app_rpc_web3_error_kind::AppRpcWeb3ErrorKind; +use crate::blockchain::errors::blockchain_db_error::BlockchainDbError; +use crate::blockchain::errors::blockchain_loggable_error::BlockchainLoggableError; +use crate::blockchain::errors::common_methods::CommonMethods; +use std::fmt::{Display, Formatter}; +use web3::error::Error as Web3Error; + +// Prefixed with App to clearly distinguish app-specific errors from library errors. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AppRpcWeb3Error { + Local(LocalError), + Remote(RemoteError), +} + +impl BlockchainLoggableError for AppRpcWeb3Error { + fn as_common_methods(&self) -> &dyn CommonMethods> { + self + } + + fn downgrade(&self) -> Box { + Box::new(AppRpcWeb3ErrorKind::from(self)) + } +} + +impl CommonMethods> for AppRpcWeb3Error { + fn partial_eq(&self, other: &Box) -> bool { + other + .as_common_methods() + .as_any() + .downcast_ref::() + .map_or(false, |other| self == other) + } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + + as_any_ref_in_trait_impl!(); +} + +impl Display for AppRpcWeb3Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LocalError { + Decoder(String), + Internal, + Io(String), + Signing(String), + Transport(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RemoteError { + InvalidResponse(String), + Unreachable, + Web3RpcError { code: i64, message: String }, +} + +// EVM based errors +impl From for AppRpcWeb3Error { + fn from(error: Web3Error) -> Self { + match error { + // Local Errors + Web3Error::Decoder(error) => AppRpcWeb3Error::Local(LocalError::Decoder(error)), + Web3Error::Internal => AppRpcWeb3Error::Local(LocalError::Internal), + Web3Error::Io(error) => AppRpcWeb3Error::Local(LocalError::Io(error.to_string())), + Web3Error::Signing(error) => { + // This variant cannot be tested due to import limitations. + AppRpcWeb3Error::Local(LocalError::Signing(error.to_string())) + } + Web3Error::Transport(error) => AppRpcWeb3Error::Local(LocalError::Transport(error)), + + // Api Errors + Web3Error::InvalidResponse(response) => { + AppRpcWeb3Error::Remote(RemoteError::InvalidResponse(response)) + } + Web3Error::Rpc(web3_rpc_error) => AppRpcWeb3Error::Remote(RemoteError::Web3RpcError { + code: web3_rpc_error.code.code(), + message: web3_rpc_error.message, + }), + Web3Error::Unreachable => AppRpcWeb3Error::Remote(RemoteError::Unreachable), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blockchain::errors::blockchain_db_error::app_rpc_web3_error_kind::AppRpcWeb3ErrorKind; + use crate::blockchain::errors::test_utils::test_clone_impl_for_blockchain_error; + use std::vec; + + #[test] + fn web3_error_to_failure_reason_conversion_works() { + // Local Errors + assert_eq!( + AppRpcWeb3Error::from(Web3Error::Decoder("Decoder error".to_string())), + AppRpcWeb3Error::Local(LocalError::Decoder("Decoder error".to_string())) + ); + assert_eq!( + AppRpcWeb3Error::from(Web3Error::Internal), + AppRpcWeb3Error::Local(LocalError::Internal) + ); + assert_eq!( + AppRpcWeb3Error::from(Web3Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "IO error" + ))), + AppRpcWeb3Error::Local(LocalError::Io("IO error".to_string())) + ); + assert_eq!( + AppRpcWeb3Error::from(Web3Error::Transport("Transport error".to_string())), + AppRpcWeb3Error::Local(LocalError::Transport("Transport error".to_string())) + ); + + // Api Errors + assert_eq!( + AppRpcWeb3Error::from(Web3Error::InvalidResponse("Invalid response".to_string())), + AppRpcWeb3Error::Remote(RemoteError::InvalidResponse("Invalid response".to_string())) + ); + assert_eq!( + AppRpcWeb3Error::from(Web3Error::Rpc(jsonrpc_core::types::error::Error { + code: jsonrpc_core::types::error::ErrorCode::ServerError(42), + message: "RPC error".to_string(), + data: None, + })), + AppRpcWeb3Error::Remote(RemoteError::Web3RpcError { + code: 42, + message: "RPC error".to_string(), + }) + ); + assert_eq!( + AppRpcWeb3Error::from(Web3Error::Unreachable), + AppRpcWeb3Error::Remote(RemoteError::Unreachable) + ); + } + + #[test] + fn clone_works_for_blockchain_error_wrapping_app_rpc_web3_error() { + let subject: Box = + Box::new(AppRpcWeb3Error::Local(LocalError::Internal)); + + test_clone_impl_for_blockchain_error::(subject); + } + + #[test] + fn partial_eq_for_app_rpc_error_works() { + let subject: Box = + Box::new(AppRpcWeb3Error::Remote(RemoteError::Web3RpcError { + code: 222, + message: "Some message".to_string(), + })); + let other_1: Box = + Box::new(AppRpcWeb3Error::Remote(RemoteError::Unreachable)); + let other_2: Box = + Box::new(AppRpcWeb3Error::Remote(RemoteError::Web3RpcError { + code: 123, + message: "Some message".to_string(), + })); + let other_3: Box = + Box::new(AppRpcWeb3Error::Remote(RemoteError::Web3RpcError { + code: 222, + message: "Some other message".to_string(), + })); + let other_4: Box = + Box::new(AppRpcWeb3Error::Local(LocalError::Internal)); + let other_5: Box = + Box::new(AppRpcWeb3Error::Remote(RemoteError::Web3RpcError { + code: 222, + message: "Some message".to_string(), + })); + + assert_ne!(&subject, &other_1); + assert_ne!(&subject, &other_2); + assert_ne!(&subject, &other_3); + assert_ne!(&subject, &other_4); + assert_eq!(&subject, &other_5); + } + + #[test] + fn display_for_blockchain_error_object_works() { + vec![ + AppRpcWeb3Error::Local(LocalError::Decoder("Serious decoder error".to_string())), + AppRpcWeb3Error::Remote(RemoteError::InvalidResponse( + "The most invalid response of all invalid responses".to_string(), + )), + AppRpcWeb3Error::Local(LocalError::Internal), + AppRpcWeb3Error::Remote(RemoteError::Unreachable), + ] + .into_iter() + .for_each(|error| { + let wrapped_as_trait_object: Box = Box::new(error.clone()); + assert_eq!(wrapped_as_trait_object.to_string(), format!("{:?}", error)); + }) + } + + #[test] + fn blockchain_loggable_error_can_be_converted_to_blockchain_db_error_for_app_rpc_web3_errors() { + let error_1: Box = Box::new(AppRpcWeb3Error::Local( + LocalError::Decoder("This is a decoder error".to_string()), + )); + let error_2: Box = + Box::new(AppRpcWeb3Error::Remote(RemoteError::Unreachable)); + + let result_1 = >::from(error_1); + let result_2 = >::from(error_2); + + assert_eq!( + &result_1, + &(Box::new(AppRpcWeb3ErrorKind::Decoder) as Box) + ); + assert_eq!( + &result_2, + &(Box::new(AppRpcWeb3ErrorKind::ServerUnreachable) as Box) + ); + } +} diff --git a/node/src/blockchain/errors/blockchain_loggable_error/masq_error.rs b/node/src/blockchain/errors/blockchain_loggable_error/masq_error.rs new file mode 100644 index 000000000..266377f3c --- /dev/null +++ b/node/src/blockchain/errors/blockchain_loggable_error/masq_error.rs @@ -0,0 +1,83 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::blockchain_db_error::masq_error_kind::MASQErrorKind; +use crate::blockchain::errors::blockchain_db_error::BlockchainDbError; +use crate::blockchain::errors::blockchain_loggable_error::BlockchainLoggableError; +use crate::blockchain::errors::common_methods::CommonMethods; +use std::fmt::{Debug, Display, Formatter}; +use variant_count::VariantCount; + +#[derive(Debug, PartialEq, Clone, VariantCount)] +pub enum MASQError { + PendingTooLongNotReplaced, +} + +impl BlockchainLoggableError for MASQError { + fn as_common_methods(&self) -> &dyn CommonMethods> { + self + } + + fn downgrade(&self) -> Box { + Box::new(MASQErrorKind::from(self)) + } +} + +impl CommonMethods> for MASQError { + fn partial_eq(&self, other: &Box) -> bool { + other + .as_common_methods() + .as_any() + .downcast_ref::() + .map_or(false, |other| self == other) + } + + fn clone_boxed(&self) -> Box { + Box::new(self.clone()) + } + + as_any_ref_in_trait_impl!(); +} + +impl Display for MASQError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blockchain::errors::blockchain_loggable_error::BlockchainLoggableError; + use crate::blockchain::errors::test_utils::test_clone_impl_for_blockchain_error; + + #[test] + fn clone_works_for_blockchain_error_wrapping_masq_error() { + let subject: Box = + Box::new(MASQError::PendingTooLongNotReplaced); + + test_clone_impl_for_blockchain_error::(subject); + } + + #[test] + fn partial_eq_for_masq_error_works() { + let subject: Box = + Box::new(MASQError::PendingTooLongNotReplaced); + let other: Box = + Box::new(MASQError::PendingTooLongNotReplaced); + + assert_eq!(&subject, &other); + // Expand this test as there are more variants of MASQError. + assert_eq!(MASQError::VARIANT_COUNT, 1); + } + + #[test] + fn display_for_blockchain_error_object_works() { + vec![MASQError::PendingTooLongNotReplaced] + .into_iter() + .for_each(|error| { + let wrapped_as_trait_object: Box = + Box::new(error.clone()); + assert_eq!(wrapped_as_trait_object.to_string(), format!("{:?}", error)); + }) + } +} diff --git a/node/src/blockchain/errors/blockchain_loggable_error/mod.rs b/node/src/blockchain/errors/blockchain_loggable_error/mod.rs new file mode 100644 index 000000000..7cf948b53 --- /dev/null +++ b/node/src/blockchain/errors/blockchain_loggable_error/mod.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::blockchain_db_error::BlockchainDbError; +use crate::blockchain::errors::common_methods::CommonMethods; +use std::fmt::{Debug, Display}; + +pub mod app_rpc_web3_error; +pub mod masq_error; + +// The Display impl is meant to be used for logging purposes. +pub trait BlockchainLoggableError: Display + Debug { + fn as_common_methods(&self) -> &dyn CommonMethods>; + fn downgrade(&self) -> Box; +} + +impl From> for Box { + fn from(more_verbose_error: Box) -> Self { + more_verbose_error.downgrade() + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.as_common_methods().clone_boxed() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.as_common_methods().partial_eq(other) + } +} + +impl Eq for Box {} diff --git a/node/src/blockchain/errors/common_methods.rs b/node/src/blockchain/errors/common_methods.rs new file mode 100644 index 000000000..946f86aae --- /dev/null +++ b/node/src/blockchain/errors/common_methods.rs @@ -0,0 +1,7 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub trait CommonMethods { + fn partial_eq(&self, other: &Other) -> bool; + fn clone_boxed(&self) -> Other; + as_any_ref_in_trait!(); +} diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs new file mode 100644 index 000000000..91fdd107c --- /dev/null +++ b/node/src/blockchain/errors/mod.rs @@ -0,0 +1,7 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod blockchain_db_error; +pub mod blockchain_loggable_error; +mod common_methods; +mod test_utils; +pub mod validation_status; diff --git a/node/src/blockchain/errors/test_utils.rs b/node/src/blockchain/errors/test_utils.rs new file mode 100644 index 000000000..2e7094b99 --- /dev/null +++ b/node/src/blockchain/errors/test_utils.rs @@ -0,0 +1,81 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::blockchain_db_error::{BlockchainDbError, CustomHash, CustomSeDe}; +use crate::blockchain::errors::blockchain_loggable_error::BlockchainLoggableError; +use crate::blockchain::errors::common_methods::CommonMethods; +use serde::de::{Error, Unexpected}; +use serde_json::Value; +use std::fmt::Debug; +use std::hash::Hasher; + +macro_rules! test_clone_impl { + ($test_fn_name: ident, $boxed_trait: ident) => { + pub fn $test_fn_name(subject: Box) + where + ErrorType: PartialEq + Debug + 'static, + { + let result = subject.clone(); + + let specified_subject = subject + .as_common_methods() + .as_any() + .downcast_ref::() + .unwrap(); + let specified_result = result + .as_common_methods() + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(specified_result, specified_subject) + } + }; +} + +test_clone_impl!(test_clone_impl_for_blockchain_db_error, BlockchainDbError); +test_clone_impl!( + test_clone_impl_for_blockchain_error, + BlockchainLoggableError +); + +#[derive(Debug, Default)] +pub struct BlockchainDbErrorMock {} + +impl BlockchainDbError for BlockchainDbErrorMock { + fn as_common_methods(&self) -> &dyn CommonMethods> { + unimplemented!("not needed for testing") + } +} + +impl CustomSeDe for BlockchainDbErrorMock { + fn custom_serialize(&self) -> Result { + Err(serde_json::Error::invalid_type( + Unexpected::Char('a'), + &"null", + )) + } + + fn custom_deserialize( + _str: &str, + ) -> Result, serde_json::error::Error> + where + Self: Sized, + { + unimplemented!("not needed for testing") + } +} + +impl CustomHash for BlockchainDbErrorMock { + fn custom_hash(&self, _hasher: &mut dyn Hasher) { + unimplemented!("not needed for testing") + } +} + +impl CommonMethods> for BlockchainDbErrorMock { + fn partial_eq(&self, _other: &Box) -> bool { + unimplemented!("not needed for testing") + } + + fn clone_boxed(&self) -> Box { + unimplemented!("not needed for testing") + } +} diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs new file mode 100644 index 000000000..d0b47ebb0 --- /dev/null +++ b/node/src/blockchain/errors/validation_status.rs @@ -0,0 +1,147 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::errors::blockchain_db_error::BlockchainDbError; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::SystemTime; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ValidationStatus { + Waiting, + Reattempting(PreviousAttempts), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PreviousAttempts { + #[serde(flatten)] + inner: HashMap, ErrorStats>, +} + +impl PreviousAttempts { + pub fn new(error: Box, clock: &dyn ValidationFailureClock) -> Self { + Self { + inner: hashmap!(error => ErrorStats::now(clock)), + } + } + + pub fn add_attempt( + mut self, + error: Box, + clock: &dyn ValidationFailureClock, + ) -> Self { + self.inner + .entry(error) + .and_modify(|stats| stats.increment()) + .or_insert_with(|| ErrorStats::now(clock)); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ErrorStats { + #[serde(rename = "firstSeen")] + pub first_seen: SystemTime, + pub attempts: u16, +} + +impl ErrorStats { + pub fn now(clock: &dyn ValidationFailureClock) -> Self { + Self { + first_seen: clock.now(), + attempts: 1, + } + } + + pub fn increment(&mut self) { + self.attempts = self.attempts.saturating_add(1); + } +} + +pub trait ValidationFailureClock { + fn now(&self) -> SystemTime; +} + +#[derive(Default)] +pub struct ValidationFailureClockReal; + +impl ValidationFailureClock for ValidationFailureClockReal { + fn now(&self) -> SystemTime { + SystemTime::now() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blockchain::errors::blockchain_db_error::app_rpc_web3_error_kind::AppRpcWeb3ErrorKind; + + #[test] + fn previous_attempts_and_validation_failure_clock_work_together_fine() { + let validation_failure_clock = ValidationFailureClockReal::default(); + // new() + let timestamp_a = SystemTime::now(); + let subject = PreviousAttempts::new( + Box::new(AppRpcWeb3ErrorKind::Decoder), + &validation_failure_clock, + ); + // add_attempt() + let timestamp_b = SystemTime::now(); + let subject = subject.add_attempt( + Box::new(AppRpcWeb3ErrorKind::Internal), + &validation_failure_clock, + ); + let timestamp_c = SystemTime::now(); + let subject = + subject.add_attempt(Box::new(AppRpcWeb3ErrorKind::IO), &validation_failure_clock); + let timestamp_d = SystemTime::now(); + let subject = subject.add_attempt( + Box::new(AppRpcWeb3ErrorKind::Decoder), + &validation_failure_clock, + ); + let subject = + subject.add_attempt(Box::new(AppRpcWeb3ErrorKind::IO), &validation_failure_clock); + + let decoder_error_stats = subject + .inner + .get(&(Box::new(AppRpcWeb3ErrorKind::Decoder) as Box)) + .expect("Failed to get decoder error stats"); + assert!( + timestamp_a <= decoder_error_stats.first_seen + && decoder_error_stats.first_seen <= timestamp_b, + "Was expected from {:?} to {:?} but was {:?}", + timestamp_a, + timestamp_b, + decoder_error_stats.first_seen + ); + assert_eq!(decoder_error_stats.attempts, 2); + let internal_error_stats = subject + .inner + .get(&(Box::new(AppRpcWeb3ErrorKind::Internal) as Box)) + .expect("Failed to get internal error stats"); + assert!( + timestamp_b <= internal_error_stats.first_seen + && internal_error_stats.first_seen <= timestamp_c, + "Was expected from {:?} to {:?} but was {:?}", + timestamp_b, + timestamp_c, + internal_error_stats.first_seen + ); + assert_eq!(internal_error_stats.attempts, 1); + let io_error_stats = subject + .inner + .get(&(Box::new(AppRpcWeb3ErrorKind::IO) as Box)) + .expect("Failed to get IO error stats"); + assert!( + timestamp_c <= io_error_stats.first_seen && io_error_stats.first_seen <= timestamp_d, + "Was expected from {:?} to {:?} but was {:?}", + timestamp_c, + timestamp_d, + io_error_stats.first_seen + ); + assert_eq!(io_error_stats.attempts, 2); + let other_error_stats = subject + .inner + .get(&(Box::new(AppRpcWeb3ErrorKind::Signing) as Box)); + assert_eq!(other_error_stats, None); + } +} diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 6259e8739..f3b354931 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -5,6 +5,7 @@ use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; +use crate::blockchain::errors::validation_status::ValidationFailureClock; use bip39::{Language, Mnemonic, Seed}; use ethabi::Hash; use ethereum_types::{BigEndianHash, H160, H256, U64}; @@ -13,8 +14,10 @@ use masq_lib::blockchains::chains::Chain; use masq_lib::utils::to_string; use serde::Serialize; use serde_derive::Deserialize; +use std::cell::RefCell; use std::fmt::Debug; use std::net::Ipv4Addr; +use std::time::SystemTime; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Index, Log, SignedTransaction, TransactionReceipt, H2048, U256}; @@ -225,3 +228,21 @@ pub fn transport_error_message() -> String { "Connection refused".to_string() } } + +#[derive(Default)] +pub struct ValidationFailureClockMock { + now_results: RefCell>, +} + +impl ValidationFailureClock for ValidationFailureClockMock { + fn now(&self) -> SystemTime { + self.now_results.borrow_mut().remove(0) + } +} + +impl ValidationFailureClockMock { + pub fn now_result(self, result: SystemTime) -> Self { + self.now_results.borrow_mut().push(result); + self + } +} diff --git a/node/src/listener_handler.rs b/node/src/listener_handler.rs index 1a63b9083..f595e3a2c 100644 --- a/node/src/listener_handler.rs +++ b/node/src/listener_handler.rs @@ -96,7 +96,7 @@ impl Future for ListenerHandlerReal { } Err(e) => { // TODO FIXME we should kill the entire Node if there is a fatal error in a listener_handler - // TODO this could be exploitable and inefficient: if we keep getting errors, we go into a tight loop and do not return + // TODO this could be exploitable and inefficient: if we keep getting app_rpc_web3_error_kind, we go into a tight loop and do not return error!(self.logger, "Could not accept connection: {}", e); } Ok(Async::NotReady) => return Ok(Async::NotReady), diff --git a/node/src/proxy_client/stream_reader.rs b/node/src/proxy_client/stream_reader.rs index 992b58dbf..5abfe8641 100644 --- a/node/src/proxy_client/stream_reader.rs +++ b/node/src/proxy_client/stream_reader.rs @@ -68,7 +68,7 @@ impl Future for StreamReader { self.shutdown(); return Err(()); } else { - // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream errors, we go into a tight loop and do not return + // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream app_rpc_web3_error_kind, we go into a tight loop and do not return warning!( self.logger, "Continuing after read error on stream from {}: {}", diff --git a/node/src/proxy_client/stream_writer.rs b/node/src/proxy_client/stream_writer.rs index c9842741a..02be017f2 100644 --- a/node/src/proxy_client/stream_writer.rs +++ b/node/src/proxy_client/stream_writer.rs @@ -104,7 +104,7 @@ impl StreamWriter { ); return Err(()); } else { - // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream errors, we go into a tight loop and do not return + // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream app_rpc_web3_error_kind, we go into a tight loop and do not return warning!(self.logger, "Continuing after write error: {}", e); self.sequence_buffer.repush(packet); } diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index d18a7ceba..e465eebd3 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -353,7 +353,7 @@ impl ProxyServer { .try_send(TransmitDataMsg { endpoint: Endpoint::Socket(client_addr), last_data: true, - sequence_number: Some(0), // DNS resolution errors always happen on the first request + sequence_number: Some(0), // DNS resolution app_rpc_web3_error_kind always happen on the first request data: from_protocol(proxy_protocol) .server_impersonator() .dns_resolution_failure_response(hostname_opt), diff --git a/node/src/stream_reader.rs b/node/src/stream_reader.rs index 34a7b62bd..a7f816ddd 100644 --- a/node/src/stream_reader.rs +++ b/node/src/stream_reader.rs @@ -68,7 +68,7 @@ impl Future for StreamReaderReal { self.shutdown(); return Err(()); } else { - // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream errors, we go into a tight loop and do not return + // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream app_rpc_web3_error_kind, we go into a tight loop and do not return warning!( self.logger, "Continuing after read error on stream {}: {}", diff --git a/node/src/stream_writer_sorted.rs b/node/src/stream_writer_sorted.rs index cd41dd5dd..26cde1ee9 100644 --- a/node/src/stream_writer_sorted.rs +++ b/node/src/stream_writer_sorted.rs @@ -100,7 +100,7 @@ impl StreamWriterSorted { ); return WriteBufferStatus::StreamInError; } else { - // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream errors, we go into a tight loop and do not return + // TODO this could be exploitable and inefficient: if we keep getting non-dead-stream app_rpc_web3_error_kind, we go into a tight loop and do not return warning!(self.logger, "Continuing after write error: {}", e); self.sequence_buffer.repush(packet); } diff --git a/node/src/stream_writer_unsorted.rs b/node/src/stream_writer_unsorted.rs index 3a6c73925..172b5b997 100644 --- a/node/src/stream_writer_unsorted.rs +++ b/node/src/stream_writer_unsorted.rs @@ -50,7 +50,7 @@ impl Future for StreamWriterUnsorted { return Err(()); } else { self.buf = Some(packet); - // TODO this could be... inefficient, if we keep getting non-dead-stream errors. (we do not return) + // TODO this could be... inefficient, if we keep getting non-dead-stream app_rpc_web3_error_kind. (we do not return) warning!(self.logger, "Continuing after write error: {}", e); } }