Skip to content

Commit 3a11c6e

Browse files
authored
GH-672: Improve FailureReason (#673)
* GH-672: add more errors * GH-672: improve errors; compiling * GH-672: introduce the conversion of FailureReason * GH-672: all tests are passing :) * GH-672: From conversion is properly tested * GH-672: final touches * GH-672: return String as Error instead of an error from serde * GH-672: better error classification
1 parent 04515c5 commit 3a11c6e

File tree

3 files changed

+197
-27
lines changed

3 files changed

+197
-27
lines changed

node/src/accountant/db_access_objects/failed_payable_dao.rs

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use crate::accountant::db_access_objects::utils::{
44
};
55
use crate::accountant::db_big_integer::big_int_divider::BigIntDivider;
66
use crate::accountant::{checked_conversion, comma_joined_stringifiable};
7+
use crate::blockchain::errors::AppRpcError;
78
use crate::database::rusqlite_wrappers::ConnectionWrapper;
89
use itertools::Itertools;
910
use masq_lib::utils::ExpectValue;
11+
use serde_derive::{Deserialize, Serialize};
1012
use std::collections::{HashMap, HashSet};
1113
use std::fmt::{Display, Formatter};
1214
use std::str::FromStr;
@@ -21,11 +23,29 @@ pub enum FailedPayableDaoError {
2123
SqlExecutionFailed(String),
2224
}
2325

24-
#[derive(Clone, Debug, PartialEq, Eq)]
26+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2527
pub enum FailureReason {
28+
Submission(AppRpcError),
29+
Validation(AppRpcError),
30+
Reverted,
2631
PendingTooLong,
27-
NonceIssue,
28-
General,
32+
}
33+
34+
impl Display for FailureReason {
35+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
36+
match serde_json::to_string(self) {
37+
Ok(json) => write!(f, "{}", json),
38+
Err(_) => write!(f, "<invalid FailureReason>"),
39+
}
40+
}
41+
}
42+
43+
impl FromStr for FailureReason {
44+
type Err = String;
45+
46+
fn from_str(s: &str) -> Result<Self, Self::Err> {
47+
serde_json::from_str(s).map_err(|e| e.to_string())
48+
}
2949
}
3050

3151
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -47,19 +67,6 @@ impl FromStr for FailureStatus {
4767
}
4868
}
4969

50-
impl FromStr for FailureReason {
51-
type Err = String;
52-
53-
fn from_str(s: &str) -> Result<Self, Self::Err> {
54-
match s {
55-
"PendingTooLong" => Ok(FailureReason::PendingTooLong),
56-
"NonceIssue" => Ok(FailureReason::NonceIssue),
57-
"General" => Ok(FailureReason::General),
58-
_ => Err(format!("Invalid FailureReason: {}", s)),
59-
}
60-
}
61-
}
62-
6370
#[derive(Clone, Debug, PartialEq, Eq)]
6471
pub struct FailedTx {
6572
pub hash: TxHash,
@@ -174,7 +181,7 @@ impl FailedPayableDao for FailedPayableDaoReal<'_> {
174181
let (gas_price_wei_high_b, gas_price_wei_low_b) =
175182
BigIntDivider::deconstruct(gas_price_wei_checked);
176183
format!(
177-
"('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{:?}', '{:?}')",
184+
"('{:?}', '{:?}', {}, {}, {}, {}, {}, {}, '{}', '{:?}')",
178185
tx.hash,
179186
tx.receiver_address,
180187
amount_high_b,
@@ -350,7 +357,7 @@ impl FailedPayableDaoFactory for DaoFactoryReal {
350357
#[cfg(test)]
351358
mod tests {
352359
use crate::accountant::db_access_objects::failed_payable_dao::FailureReason::{
353-
General, NonceIssue, PendingTooLong,
360+
PendingTooLong, Reverted,
354361
};
355362
use crate::accountant::db_access_objects::failed_payable_dao::FailureStatus::{
356363
Concluded, RecheckRequired, RetryRequired,
@@ -363,6 +370,7 @@ mod tests {
363370
make_read_only_db_connection, FailedTxBuilder,
364371
};
365372
use crate::accountant::db_access_objects::utils::current_unix_timestamp;
373+
use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError};
366374
use crate::blockchain::test_utils::make_tx_hash;
367375
use crate::database::db_initializer::{
368376
DbInitializationConfig, DbInitializer, DbInitializerReal,
@@ -382,7 +390,7 @@ mod tests {
382390
.unwrap();
383391
let tx1 = FailedTxBuilder::default()
384392
.hash(make_tx_hash(1))
385-
.reason(NonceIssue)
393+
.reason(Reverted)
386394
.build();
387395
let tx2 = FailedTxBuilder::default()
388396
.hash(make_tx_hash(2))
@@ -563,15 +571,49 @@ mod tests {
563571

564572
#[test]
565573
fn failure_reason_from_str_works() {
574+
// Submission error
566575
assert_eq!(
567-
FailureReason::from_str("PendingTooLong"),
568-
Ok(PendingTooLong)
576+
FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder":"Test decoder error"}}}"#)
577+
.unwrap(),
578+
FailureReason::Submission(AppRpcError::Local(LocalError::Decoder(
579+
"Test decoder error".to_string()
580+
)))
569581
);
570-
assert_eq!(FailureReason::from_str("NonceIssue"), Ok(NonceIssue));
571-
assert_eq!(FailureReason::from_str("General"), Ok(General));
582+
583+
// Validation error
584+
assert_eq!(
585+
FailureReason::from_str(r#"{"Validation":{"Remote":{"Web3RpcError":{"code":42,"message":"Test RPC error"}}}}"#).unwrap(),
586+
FailureReason::Validation(AppRpcError::Remote(RemoteError::Web3RpcError {
587+
code: 42,
588+
message: "Test RPC error".to_string()
589+
}))
590+
);
591+
592+
// Reverted
593+
assert_eq!(
594+
FailureReason::from_str(r#"{"Reverted":null}"#).unwrap(),
595+
FailureReason::Reverted
596+
);
597+
598+
// PendingTooLong
599+
assert_eq!(
600+
FailureReason::from_str(r#"{"PendingTooLong":null}"#).unwrap(),
601+
FailureReason::PendingTooLong
602+
);
603+
604+
// Invalid Variant
605+
assert_eq!(
606+
FailureReason::from_str(r#"{"UnknownReason":null}"#).unwrap_err(),
607+
"unknown variant `UnknownReason`, \
608+
expected one of `Submission`, `Validation`, `Reverted`, `PendingTooLong` \
609+
at line 1 column 16"
610+
.to_string()
611+
);
612+
613+
// Invalid Input
572614
assert_eq!(
573-
FailureReason::from_str("InvalidReason"),
574-
Err("Invalid FailureReason: InvalidReason".to_string())
615+
FailureReason::from_str("random string").unwrap_err(),
616+
"expected value at line 1 column 1".to_string()
575617
);
576618
}
577619

@@ -640,7 +682,7 @@ mod tests {
640682
.build();
641683
let tx2 = FailedTxBuilder::default()
642684
.hash(make_tx_hash(2))
643-
.reason(NonceIssue)
685+
.reason(Reverted)
644686
.timestamp(now - 3600)
645687
.status(RetryRequired)
646688
.build();
@@ -674,7 +716,7 @@ mod tests {
674716
let subject = FailedPayableDaoReal::new(wrapped_conn);
675717
let tx1 = FailedTxBuilder::default()
676718
.hash(make_tx_hash(1))
677-
.reason(NonceIssue)
719+
.reason(Reverted)
678720
.status(RetryRequired)
679721
.build();
680722
let tx2 = FailedTxBuilder::default()

node/src/blockchain/errors.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use serde_derive::{Deserialize, Serialize};
2+
use web3::error::Error as Web3Error;
3+
4+
// Prefixed with App to clearly distinguish app-specific errors from library errors.
5+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6+
pub enum AppRpcError {
7+
Local(LocalError),
8+
Remote(RemoteError),
9+
}
10+
11+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12+
pub enum LocalError {
13+
Decoder(String),
14+
Internal,
15+
Io(String),
16+
Signing(String),
17+
Transport(String),
18+
}
19+
20+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21+
pub enum RemoteError {
22+
InvalidResponse(String),
23+
Unreachable,
24+
Web3RpcError { code: i64, message: String },
25+
}
26+
27+
// EVM based errors
28+
impl From<Web3Error> for AppRpcError {
29+
fn from(error: Web3Error) -> Self {
30+
match error {
31+
// Local Errors
32+
Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)),
33+
Web3Error::Internal => AppRpcError::Local(LocalError::Internal),
34+
Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())),
35+
Web3Error::Signing(error) => {
36+
// This variant cannot be tested due to import limitations.
37+
AppRpcError::Local(LocalError::Signing(error.to_string()))
38+
}
39+
Web3Error::Transport(error) => AppRpcError::Local(LocalError::Transport(error)),
40+
41+
// Api Errors
42+
Web3Error::InvalidResponse(response) => {
43+
AppRpcError::Remote(RemoteError::InvalidResponse(response))
44+
}
45+
Web3Error::Rpc(web3_rpc_error) => AppRpcError::Remote(RemoteError::Web3RpcError {
46+
code: web3_rpc_error.code.code(),
47+
message: web3_rpc_error.message,
48+
}),
49+
Web3Error::Unreachable => AppRpcError::Remote(RemoteError::Unreachable),
50+
}
51+
}
52+
}
53+
54+
mod tests {
55+
use crate::blockchain::errors::{AppRpcError, LocalError, RemoteError};
56+
use web3::error::Error as Web3Error;
57+
58+
#[test]
59+
fn web3_error_to_failure_reason_conversion_works() {
60+
// Local Errors
61+
assert_eq!(
62+
AppRpcError::from(Web3Error::Decoder("Decoder error".to_string())),
63+
AppRpcError::Local(LocalError::Decoder("Decoder error".to_string()))
64+
);
65+
assert_eq!(
66+
AppRpcError::from(Web3Error::Internal),
67+
AppRpcError::Local(LocalError::Internal)
68+
);
69+
assert_eq!(
70+
AppRpcError::from(Web3Error::Io(std::io::Error::new(
71+
std::io::ErrorKind::Other,
72+
"IO error"
73+
))),
74+
AppRpcError::Local(LocalError::Io("IO error".to_string()))
75+
);
76+
assert_eq!(
77+
AppRpcError::from(Web3Error::Transport("Transport error".to_string())),
78+
AppRpcError::Local(LocalError::Transport("Transport error".to_string()))
79+
);
80+
81+
// Api Errors
82+
assert_eq!(
83+
AppRpcError::from(Web3Error::InvalidResponse("Invalid response".to_string())),
84+
AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string()))
85+
);
86+
assert_eq!(
87+
AppRpcError::from(Web3Error::Rpc(jsonrpc_core::types::error::Error {
88+
code: jsonrpc_core::types::error::ErrorCode::ServerError(42),
89+
message: "RPC error".to_string(),
90+
data: None,
91+
})),
92+
AppRpcError::Remote(RemoteError::Web3RpcError {
93+
code: 42,
94+
message: "RPC error".to_string(),
95+
})
96+
);
97+
assert_eq!(
98+
AppRpcError::from(Web3Error::Unreachable),
99+
AppRpcError::Remote(RemoteError::Unreachable)
100+
);
101+
}
102+
103+
#[test]
104+
fn app_rpc_error_serialization_deserialization() {
105+
let errors = vec![
106+
// Local Errors
107+
AppRpcError::Local(LocalError::Decoder("Decoder error".to_string())),
108+
AppRpcError::Local(LocalError::Internal),
109+
AppRpcError::Local(LocalError::Io("IO error".to_string())),
110+
AppRpcError::Local(LocalError::Signing("Signing error".to_string())),
111+
AppRpcError::Local(LocalError::Transport("Transport error".to_string())),
112+
// Remote Errors
113+
AppRpcError::Remote(RemoteError::InvalidResponse("Invalid response".to_string())),
114+
AppRpcError::Remote(RemoteError::Unreachable),
115+
AppRpcError::Remote(RemoteError::Web3RpcError {
116+
code: 42,
117+
message: "RPC error".to_string(),
118+
}),
119+
];
120+
121+
errors.into_iter().for_each(|error| {
122+
let serialized = serde_json::to_string(&error).unwrap();
123+
let deserialized: AppRpcError = serde_json::from_str(&serialized).unwrap();
124+
assert_eq!(error, deserialized, "Error: {:?}", error);
125+
});
126+
}
127+
}

node/src/blockchain/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod blockchain_agent;
55
pub mod blockchain_bridge;
66
pub mod blockchain_interface;
77
pub mod blockchain_interface_initializer;
8+
pub mod errors;
89
pub mod payer;
910
pub mod signature;
1011
#[cfg(test)]

0 commit comments

Comments
 (0)