1use std::fmt;
22use std::str::FromStr;
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct Address(String);
44
45impl Address {
46 #[must_use]
50 pub fn new(address: &str) -> Option<Self> {
51 normalize_address(address).map(Self)
52 }
53
54 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59
60 #[must_use]
62 pub fn into_inner(self) -> String {
63 self.0
64 }
65
66 #[must_use]
68 pub fn zero() -> Self {
69 Self("0x0000000000000000000000000000000000000000".to_string())
70 }
71
72 #[must_use]
74 pub fn is_zero(&self) -> bool {
75 self.0 == "0x0000000000000000000000000000000000000000"
76 }
77}
78
79impl fmt::Display for Address {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}", self.0)
82 }
83}
84
85impl FromStr for Address {
86 type Err = AddressParseError;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 Self::new(s).ok_or(AddressParseError)
90 }
91}
92
93impl AsRef<str> for Address {
94 fn as_ref(&self) -> &str {
95 &self.0
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct AddressParseError;
102
103impl fmt::Display for AddressParseError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "invalid Ethereum address")
106 }
107}
108
109impl std::error::Error for AddressParseError {}
110
111#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub struct TxHash(String);
121
122impl TxHash {
123 #[must_use]
127 pub fn new(hash: &str) -> Option<Self> {
128 if is_valid_tx_hash(hash) {
129 Some(Self(hash.to_lowercase()))
130 } else {
131 None
132 }
133 }
134
135 #[must_use]
137 pub fn as_str(&self) -> &str {
138 &self.0
139 }
140
141 #[must_use]
143 pub fn into_inner(self) -> String {
144 self.0
145 }
146}
147
148impl fmt::Display for TxHash {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{}", self.0)
151 }
152}
153
154impl FromStr for TxHash {
155 type Err = TxHashParseError;
156
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
158 Self::new(s).ok_or(TxHashParseError)
159 }
160}
161
162impl AsRef<str> for TxHash {
163 fn as_ref(&self) -> &str {
164 &self.0
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct TxHashParseError;
171
172impl fmt::Display for TxHashParseError {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 write!(f, "invalid transaction hash")
175 }
176}
177
178impl std::error::Error for TxHashParseError {}
179
180#[must_use]
203pub fn is_valid_address(address: &str) -> bool {
204 if !address.starts_with("0x") && !address.starts_with("0X") {
205 return false;
206 }
207
208 if address.len() != 42 {
209 return false;
210 }
211
212 address[2..].chars().all(|c| c.is_ascii_hexdigit())
213}
214
215#[must_use]
231pub fn normalize_address(address: &str) -> Option<String> {
232 if !is_valid_address(address) {
233 return None;
234 }
235 Some(address.to_lowercase())
236}
237
238#[must_use]
254pub fn is_valid_tx_hash(hash: &str) -> bool {
255 if !hash.starts_with("0x") && !hash.starts_with("0X") {
256 return false;
257 }
258
259 if hash.len() != 66 {
260 return false;
261 }
262
263 hash[2..].chars().all(|c| c.is_ascii_hexdigit())
264}
265
266#[must_use]
271pub fn is_valid_bytes32(hash: &str) -> bool {
272 is_valid_tx_hash(hash)
273}
274
275#[must_use]
289pub fn pad_to_32_bytes(value: &str) -> String {
290 let hex = value
291 .strip_prefix("0x")
292 .or_else(|| value.strip_prefix("0X"))
293 .unwrap_or(value);
294 format!("0x{hex:0>64}")
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum HttpStatusKind {
300 Success,
302 BadRequest,
304 Unauthorized,
306 Forbidden,
308 NotFound,
310 RateLimited,
312 ServerError,
314 Other,
316}
317
318impl HttpStatusKind {
319 #[must_use]
321 pub fn from_status(status: u16) -> Self {
322 match status {
323 200..=299 => Self::Success,
324 400 => Self::BadRequest,
325 401 => Self::Unauthorized,
326 403 => Self::Forbidden,
327 404 => Self::NotFound,
328 429 => Self::RateLimited,
329 500..=599 => Self::ServerError,
330 _ => Self::Other,
331 }
332 }
333
334 #[must_use]
336 pub fn is_retryable(&self) -> bool {
337 matches!(self, Self::RateLimited | Self::ServerError)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_valid_addresses() {
347 assert!(is_valid_address(
348 "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
349 ));
350 assert!(is_valid_address(
351 "0x0000000000000000000000000000000000000000"
352 ));
353 assert!(is_valid_address(
354 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
355 ));
356 assert!(is_valid_address(
357 "0XABCDEF1234567890ABCDEF1234567890ABCDEF12"
358 ));
359 }
360
361 #[test]
362 fn test_invalid_addresses() {
363 assert!(!is_valid_address(""));
364 assert!(!is_valid_address("0x"));
365 assert!(!is_valid_address("0x123"));
366 assert!(!is_valid_address("invalid"));
367 assert!(!is_valid_address(
368 "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
369 )); assert!(!is_valid_address(
371 "0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG"
372 )); }
374
375 #[test]
376 fn test_normalize_address() {
377 assert_eq!(
378 normalize_address("0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045"),
379 Some("0xd8da6bf26964af9d7eed9e03e53415d37aa96045".to_string())
380 );
381 assert_eq!(normalize_address("invalid"), None);
382 }
383
384 #[test]
385 fn test_valid_tx_hashes() {
386 assert!(is_valid_tx_hash(
387 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
388 ));
389 assert!(is_valid_tx_hash(
390 "0xABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"
391 ));
392 }
393
394 #[test]
395 fn test_invalid_tx_hashes() {
396 assert!(!is_valid_tx_hash("0x123"));
397 assert!(!is_valid_tx_hash("invalid"));
398 assert!(!is_valid_tx_hash(""));
399 }
400
401 #[test]
402 fn test_pad_to_32_bytes() {
403 assert_eq!(
404 pad_to_32_bytes("0x1"),
405 "0x0000000000000000000000000000000000000000000000000000000000000001"
406 );
407 assert_eq!(
408 pad_to_32_bytes("1"),
409 "0x0000000000000000000000000000000000000000000000000000000000000001"
410 );
411 assert_eq!(
412 pad_to_32_bytes("0x64"),
413 "0x0000000000000000000000000000000000000000000000000000000000000064"
414 );
415 }
416
417 #[test]
418 fn test_http_status_kind() {
419 assert_eq!(HttpStatusKind::from_status(200), HttpStatusKind::Success);
420 assert_eq!(
421 HttpStatusKind::from_status(401),
422 HttpStatusKind::Unauthorized
423 );
424 assert_eq!(
425 HttpStatusKind::from_status(429),
426 HttpStatusKind::RateLimited
427 );
428 assert_eq!(
429 HttpStatusKind::from_status(500),
430 HttpStatusKind::ServerError
431 );
432 assert_eq!(
433 HttpStatusKind::from_status(503),
434 HttpStatusKind::ServerError
435 );
436
437 assert!(HttpStatusKind::RateLimited.is_retryable());
438 assert!(HttpStatusKind::ServerError.is_retryable());
439 assert!(!HttpStatusKind::Unauthorized.is_retryable());
440 }
441
442 #[test]
443 fn test_address_newtype() {
444 let addr = Address::new("0xD8DA6BF26964AF9D7EED9E03E53415D37AA96045");
446 assert!(addr.is_some());
447 let addr = addr.unwrap();
448 assert_eq!(addr.as_str(), "0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
449 assert!(!addr.is_zero());
450
451 assert!(Address::new("invalid").is_none());
453 assert!(Address::new("0x123").is_none());
454
455 let zero = Address::zero();
457 assert!(zero.is_zero());
458
459 let parsed: Address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
461 .parse()
462 .unwrap();
463 assert_eq!(addr, parsed);
464
465 let err = "invalid".parse::<Address>();
467 assert!(err.is_err());
468 }
469
470 #[test]
471 fn test_tx_hash_newtype() {
472 let hash =
474 TxHash::new("0x1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF");
475 assert!(hash.is_some());
476 let hash = hash.unwrap();
477 assert_eq!(
478 hash.as_str(),
479 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
480 );
481
482 assert!(TxHash::new("invalid").is_none());
484 assert!(TxHash::new("0x123").is_none());
485
486 let parsed: TxHash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
488 .parse()
489 .unwrap();
490 assert_eq!(hash, parsed);
491 }
492}