tycho_ethereum/rpc/
errors.rs1use std::fmt::Display;
2
3use alloy::transports::{RpcError as AlloyRpcError, TransportErrorKind};
4use thiserror::Error;
5
6fn redact_url_paths(s: &str) -> String {
11 let mut result = String::with_capacity(s.len());
12 let mut pos = 0;
13
14 while pos < s.len() {
15 let Some(idx) = s[pos..].find("://") else {
16 result.push_str(&s[pos..]);
17 break;
18 };
19
20 let abs = pos + idx;
21 result.push_str(&s[pos..abs + 3]); pos = abs + 3;
23
24 let token_end = s[pos..]
26 .find(|c: char| c.is_whitespace() || matches!(c, '(' | ')' | '"' | '\''))
27 .map(|i| pos + i)
28 .unwrap_or(s.len());
29
30 if let Some(slash) = s[pos..token_end].find('/') {
32 result.push_str(&s[pos..pos + slash]); result.push_str("/***");
34 } else {
35 result.push_str(&s[pos..token_end]); }
37 pos = token_end;
38 }
39
40 result
41}
42
43#[derive(Error, Debug)]
44pub struct ReqwestError {
45 pub msg: String,
46 #[source]
47 pub source: AlloyRpcError<TransportErrorKind>,
48}
49
50impl Display for ReqwestError {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{}: {}", self.msg, redact_url_paths(&self.source.to_string()))
53 }
54}
55
56#[derive(Error, Debug)]
57pub enum RequestError {
58 Reqwest(ReqwestError),
59 Other(String),
60}
61
62impl Display for RequestError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 RequestError::Reqwest(e) => write!(f, "{e}"),
66 RequestError::Other(e) => write!(f, "{e}"),
67 }
68 }
69}
70
71#[derive(Error, Debug)]
72pub enum RPCError {
73 #[error("RPC setup error: {0}")]
74 SetupError(String),
75 #[error("Request error: {0}")]
76 RequestError(RequestError),
77 #[error("Tracing failure: {0}")]
78 TracingFailure(String),
79 #[error("Unknown error: {0}")]
80 UnknownError(String),
81}
82
83impl RPCError {
84 pub(super) fn from_alloy<S: ToString>(
85 msg: S,
86 error: AlloyRpcError<TransportErrorKind>,
87 ) -> Self {
88 RPCError::RequestError(RequestError::Reqwest(ReqwestError {
89 msg: msg.to_string(),
90 source: error,
91 }))
92 }
93
94 pub fn is_execution_reverted(&self) -> bool {
98 let RPCError::RequestError(RequestError::Reqwest(e)) = self else {
99 return false;
100 };
101 let AlloyRpcError::ErrorResp(payload) = &e.source else {
102 return false;
103 };
104 payload.code == 3 ||
107 payload
108 .message
109 .to_lowercase()
110 .contains("execution reverted")
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use alloy::rpc::json_rpc::ErrorPayload;
117 use rstest::rstest;
118
119 use super::{redact_url_paths, AlloyRpcError, RPCError, TransportErrorKind};
120
121 #[rstest]
122 #[case::eip_1474_revert_code(3, "execution reverted", true)]
123 #[case::revert_under_generic_code(-32000, "execution reverted: something", true)]
124 #[case::header_not_found(-32000, "header not found", false)]
125 #[case::invalid_request(-32600, "invalid request", false)]
126 fn is_execution_reverted_classification(
127 #[case] code: i64,
128 #[case] message: &str,
129 #[case] expected: bool,
130 ) {
131 let payload = ErrorPayload { code, message: message.to_string().into(), data: None };
132 let err = RPCError::from_alloy(
133 "eth_call failed",
134 AlloyRpcError::<TransportErrorKind>::ErrorResp(payload),
135 );
136 assert_eq!(err.is_execution_reverted(), expected);
137 }
138
139 #[test]
140 fn is_execution_reverted_false_for_non_request_errors() {
141 assert!(!RPCError::SetupError("bad url".to_string()).is_execution_reverted());
142 assert!(!RPCError::UnknownError("boom".to_string()).is_execution_reverted());
143 }
144
145 #[test]
146 fn redacts_path_component() {
147 let input =
148 "error sending request for url (https://arbitrum.chainstack.com/supersecretkey)";
149 let output = redact_url_paths(input);
150 assert_eq!(output, "error sending request for url (https://arbitrum.chainstack.com/***)");
151 }
152
153 #[test]
154 fn preserves_url_without_path() {
155 let input = "error sending request for url (https://arbitrum.chainstack.com)";
156 let output = redact_url_paths(input);
157 assert_eq!(output, input);
158 }
159
160 #[test]
161 fn passthrough_when_no_url() {
162 let input = "connection refused";
163 assert_eq!(redact_url_paths(input), input);
164 }
165
166 #[test]
167 fn redacts_multiple_urls() {
168 let input = "primary https://a.example.com/key1 fallback https://b.example.com/key2";
169 let output = redact_url_paths(input);
170 assert_eq!(output, "primary https://a.example.com/*** fallback https://b.example.com/***");
171 }
172
173 #[test]
174 fn redacts_url_at_end_of_string() {
175 let input = "failed: https://node.example.com/apikey";
176 let output = redact_url_paths(input);
177 assert_eq!(output, "failed: https://node.example.com/***");
178 }
179}