verifier/
class_hash.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3use std::fmt;
4use thiserror::Error;
5
6fn get_class_hash_regex() -> Result<&'static Regex, ClassHashError> {
7    lazy_static! {
8        static ref CLASS_HASH_REGEX: Result<Regex, regex::Error> = Regex::new(r"^0x[a-fA-F0-9]+$");
9    }
10
11    match CLASS_HASH_REGEX.as_ref() {
12        Ok(regex) => Ok(regex),
13        Err(_) => Err(ClassHashError::RegexError),
14    }
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub struct ClassHash(String);
19
20#[derive(Error, Debug, Clone, PartialEq, Eq)]
21pub enum ClassHashError {
22    #[error("[E010] Invalid class hash format: '{0}'\n\nExpected format: 0x followed by up to 64 hexadecimal characters\nExample: 0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18\n\nSuggestions:\n  • Check that the hash starts with '0x'\n  • Verify all characters are hexadecimal (0-9, a-f, A-F)\n  • Ensure the hash is not longer than 66 characters total")]
23    Match(String),
24    #[error("[E011] Internal regex compilation error\n\nThis is an internal error. Please report this issue.")]
25    RegexError,
26}
27
28impl ClassHashError {
29    pub const fn error_code(&self) -> &'static str {
30        match self {
31            Self::Match(_) => "E010",
32            Self::RegexError => "E011",
33        }
34    }
35}
36
37impl ClassHash {
38    const NORMALIZED_LENGTH: usize = 66;
39
40    /// # Errors
41    ///
42    /// Will fail if the `raw` dosn't match class hash regex, i.e. it
43    /// has to start with "0x" followed by 64 hexadecimal digits.
44    pub fn new(raw: &str) -> Result<Self, ClassHashError> {
45        let regex = get_class_hash_regex()?;
46        if raw.len() <= Self::NORMALIZED_LENGTH && regex.is_match(raw) {
47            Ok(Self(raw.into()))
48        } else {
49            Err(ClassHashError::Match(raw.to_string()))
50        }
51    }
52}
53
54impl fmt::Display for ClassHash {
55    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56        write!(f, "{}", self.0)
57    }
58}
59
60impl AsRef<str> for ClassHash {
61    fn as_ref(&self) -> &str {
62        self.0.as_str()
63    }
64}
65
66impl AsRef<String> for ClassHash {
67    fn as_ref(&self) -> &String {
68        &self.0
69    }
70}
71
72#[cfg(test)]
73#[allow(clippy::unwrap_used)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_valid_class_hash_normalized() {
79        let valid_hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
80        assert!(ClassHash::new(valid_hash).is_ok());
81    }
82
83    #[test]
84    fn test_valid_class_hash_without_leading_zeros() {
85        let valid_hash = "0x44dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
86        assert!(ClassHash::new(valid_hash).is_ok());
87    }
88
89    #[test]
90    fn test_invalid_class_hash_pattern() {
91        let invalid_hash = "0xGHIJKLMNOPQRSTUVWXYZ";
92        assert!(ClassHash::new(invalid_hash).is_err());
93    }
94
95    #[test]
96    fn test_invalid_class_hash_no_prefix() {
97        let invalid_hash = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
98        assert!(ClassHash::new(invalid_hash).is_err());
99    }
100
101    #[test]
102    fn test_invalid_class_hash_too_long() {
103        let invalid_hash =
104            "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da1812345";
105        assert!(ClassHash::new(invalid_hash).is_err());
106    }
107
108    #[test]
109    fn test_empty_class_hash() {
110        assert!(ClassHash::new("").is_err());
111    }
112
113    #[test]
114    fn test_class_hash_display() {
115        let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
116        let class_hash = ClassHash::new(hash).unwrap();
117        assert_eq!(format!("{class_hash}"), hash);
118    }
119
120    #[test]
121    fn test_class_hash_as_ref_str() {
122        let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
123        let class_hash = ClassHash::new(hash).unwrap();
124        let as_str: &str = class_hash.as_ref();
125        assert_eq!(as_str, hash);
126    }
127
128    #[test]
129    fn test_class_hash_as_ref_string() {
130        let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
131        let class_hash = ClassHash::new(hash).unwrap();
132        let expected_string = hash.to_string();
133        let as_string: &String = class_hash.as_ref();
134        assert_eq!(as_string, &expected_string);
135    }
136
137    #[test]
138    fn test_class_hash_clone() {
139        let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
140        let class_hash = ClassHash::new(hash).unwrap();
141        let cloned = class_hash.clone();
142        assert_eq!(class_hash, cloned);
143    }
144
145    #[test]
146    fn test_class_hash_error_display() {
147        let error = ClassHashError::Match("invalid_hash".to_string());
148        let error_message = format!("{error}");
149        assert!(error_message.contains("[E010]"));
150        assert!(error_message.contains("Invalid class hash format"));
151        assert!(error_message.contains("invalid_hash"));
152        assert!(error_message.contains("Expected format: 0x followed by"));
153    }
154}