1use std::backtrace::Backtrace;
2use std::error::Error as StdError;
3use std::fmt;
4
5use alloy::primitives::ChainId;
6use alloy::primitives::ruint::ParseError;
7use hmac::digest::InvalidLength;
8pub use reqwest::Method;
10pub use reqwest::StatusCode;
12use reqwest::header;
13
14#[non_exhaustive]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Kind {
17 Status,
19 Validation,
21 Synchronization,
23 Internal,
25 WebSocket,
27 Geoblock,
29}
30
31#[derive(Debug)]
32pub struct Error {
33 kind: Kind,
34 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
35 backtrace: Backtrace,
36}
37
38impl Error {
39 pub fn with_source<S: StdError + Send + Sync + 'static>(kind: Kind, source: S) -> Self {
40 Self {
41 kind,
42 source: Some(Box::new(source)),
43 backtrace: Backtrace::capture(),
44 }
45 }
46
47 pub fn kind(&self) -> Kind {
48 self.kind
49 }
50
51 pub fn backtrace(&self) -> &Backtrace {
52 &self.backtrace
53 }
54
55 pub fn inner(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
56 self.source.as_deref()
57 }
58
59 pub fn downcast_ref<E: StdError + 'static>(&self) -> Option<&E> {
60 let e = self.source.as_deref()?;
61 e.downcast_ref::<E>()
62 }
63
64 pub fn validation<S: Into<String>>(message: S) -> Self {
65 Validation {
66 reason: message.into(),
67 }
68 .into()
69 }
70
71 pub fn status<S: Into<String>>(
72 status_code: StatusCode,
73 method: Method,
74 path: String,
75 message: S,
76 ) -> Self {
77 Status {
78 status_code,
79 method,
80 path,
81 message: message.into(),
82 }
83 .into()
84 }
85
86 #[must_use]
87 pub fn missing_contract_config(chain_id: ChainId, neg_risk: bool) -> Self {
88 MissingContractConfig { chain_id, neg_risk }.into()
89 }
90}
91
92impl fmt::Display for Error {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 match &self.source {
95 Some(src) => write!(f, "{:?}: {}", self.kind, src),
96 None => write!(f, "{:?}", self.kind),
97 }
98 }
99}
100
101impl StdError for Error {
102 fn source(&self) -> Option<&(dyn StdError + 'static)> {
103 self.source
104 .as_deref()
105 .map(|e| e as &(dyn StdError + 'static))
106 }
107}
108
109#[non_exhaustive]
110#[derive(Debug)]
111pub struct Status {
112 pub status_code: StatusCode,
113 pub method: Method,
114 pub path: String,
115 pub message: String,
116}
117
118impl fmt::Display for Status {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(
121 f,
122 "error({}) making {} call to {} with {}",
123 self.status_code, self.method, self.path, self.message
124 )
125 }
126}
127
128impl StdError for Status {}
129
130#[non_exhaustive]
131#[derive(Debug)]
132pub struct Validation {
133 pub reason: String,
134}
135
136impl fmt::Display for Validation {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 write!(f, "invalid: {}", self.reason)
139 }
140}
141
142impl StdError for Validation {}
143
144#[non_exhaustive]
145#[derive(Debug)]
146pub struct Synchronization;
147
148impl fmt::Display for Synchronization {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(
151 f,
152 "synchronization error: multiple threads are attempting to log in or log out"
153 )
154 }
155}
156
157impl StdError for Synchronization {}
158
159#[non_exhaustive]
160#[derive(Debug, Clone, Copy)]
161pub struct MissingContractConfig {
162 pub chain_id: ChainId,
163 pub neg_risk: bool,
164}
165
166impl fmt::Display for MissingContractConfig {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(
169 f,
170 "missing contract config for chain id {} with neg_risk = {}",
171 self.chain_id, self.neg_risk,
172 )
173 }
174}
175
176impl std::error::Error for MissingContractConfig {}
177
178impl From<MissingContractConfig> for Error {
179 fn from(err: MissingContractConfig) -> Self {
180 Error::with_source(Kind::Internal, err)
181 }
182}
183
184#[non_exhaustive]
189#[derive(Debug, Clone)]
190pub struct Geoblock {
191 pub ip: String,
193 pub country: String,
195 pub region: String,
197}
198
199impl fmt::Display for Geoblock {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 write!(
202 f,
203 "access blocked from country: {}, region: {}, ip: {}",
204 self.country, self.region, self.ip
205 )
206 }
207}
208
209impl StdError for Geoblock {}
210
211impl From<Geoblock> for Error {
212 fn from(err: Geoblock) -> Self {
213 Error::with_source(Kind::Geoblock, err)
214 }
215}
216
217impl From<base64::DecodeError> for Error {
218 fn from(e: base64::DecodeError) -> Self {
219 Error::with_source(Kind::Internal, e)
220 }
221}
222
223impl From<reqwest::Error> for Error {
224 fn from(e: reqwest::Error) -> Self {
225 Error::with_source(Kind::Internal, e)
226 }
227}
228
229impl From<header::InvalidHeaderValue> for Error {
230 fn from(e: header::InvalidHeaderValue) -> Self {
231 Error::with_source(Kind::Internal, e)
232 }
233}
234
235impl From<InvalidLength> for Error {
236 fn from(e: InvalidLength) -> Self {
237 Error::with_source(Kind::Internal, e)
238 }
239}
240
241impl From<serde_json::Error> for Error {
242 fn from(e: serde_json::Error) -> Self {
243 Error::with_source(Kind::Internal, e)
244 }
245}
246
247impl From<alloy::signers::Error> for Error {
248 fn from(e: alloy::signers::Error) -> Self {
249 Error::with_source(Kind::Internal, e)
250 }
251}
252
253impl From<url::ParseError> for Error {
254 fn from(e: url::ParseError) -> Self {
255 Error::with_source(Kind::Internal, e)
256 }
257}
258
259impl From<ParseError> for Error {
260 fn from(e: ParseError) -> Self {
261 Error::with_source(Kind::Internal, e)
262 }
263}
264
265impl From<Validation> for Error {
266 fn from(err: Validation) -> Self {
267 Error::with_source(Kind::Validation, err)
268 }
269}
270
271impl From<Status> for Error {
272 fn from(err: Status) -> Self {
273 Error::with_source(Kind::Status, err)
274 }
275}
276
277impl From<Synchronization> for Error {
278 fn from(err: Synchronization) -> Self {
279 Error::with_source(Kind::Synchronization, err)
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn geoblock_display_should_succeed() {
289 let geoblock = Geoblock {
290 ip: "192.168.1.1".to_owned(),
291 country: "US".to_owned(),
292 region: "NY".to_owned(),
293 };
294
295 assert_eq!(
296 geoblock.to_string(),
297 "access blocked from country: US, region: NY, ip: 192.168.1.1"
298 );
299 }
300
301 #[test]
302 fn geoblock_into_error_should_succeed() {
303 let geoblock = Geoblock {
304 ip: "10.0.0.1".to_owned(),
305 country: "CU".to_owned(),
306 region: "HAV".to_owned(),
307 };
308
309 let error: Error = geoblock.into();
310
311 assert_eq!(error.kind(), Kind::Geoblock);
312 assert!(error.to_string().contains("CU"));
313 }
314}