1use lazy_static::lazy_static;
29use regex::Regex;
30use std::fmt;
31use thiserror::Error;
32
33fn get_class_hash_regex() -> Result<&'static Regex, ClassHashError> {
34 lazy_static! {
35 static ref CLASS_HASH_REGEX: Result<Regex, regex::Error> = Regex::new(r"^0x[a-fA-F0-9]+$");
36 }
37
38 match CLASS_HASH_REGEX.as_ref() {
39 Ok(regex) => Ok(regex),
40 Err(_) => Err(ClassHashError::RegexError),
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
75pub struct ClassHash(String);
76
77#[derive(Error, Debug, Clone, PartialEq, Eq)]
82pub enum ClassHashError {
83 #[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")]
84 Match(String),
85 #[error("[E011] Internal regex compilation error\n\nThis is an internal error. Please report this issue.")]
86 RegexError,
87}
88
89impl ClassHashError {
90 pub const fn error_code(&self) -> &'static str {
91 match self {
92 Self::Match(_) => "E010",
93 Self::RegexError => "E011",
94 }
95 }
96}
97
98impl ClassHash {
99 const NORMALIZED_LENGTH: usize = 66;
100
101 pub fn new(raw: &str) -> Result<Self, ClassHashError> {
137 let regex = get_class_hash_regex()?;
138 if raw.len() <= Self::NORMALIZED_LENGTH && regex.is_match(raw) {
139 Ok(Self(raw.into()))
140 } else {
141 Err(ClassHashError::Match(raw.to_string()))
142 }
143 }
144}
145
146impl fmt::Display for ClassHash {
147 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148 write!(f, "{}", self.0)
149 }
150}
151
152impl AsRef<str> for ClassHash {
153 fn as_ref(&self) -> &str {
154 self.0.as_str()
155 }
156}
157
158impl AsRef<String> for ClassHash {
159 fn as_ref(&self) -> &String {
160 &self.0
161 }
162}
163
164#[cfg(test)]
165#[allow(clippy::unwrap_used)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_valid_class_hash_normalized() {
171 let valid_hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
172 assert!(ClassHash::new(valid_hash).is_ok());
173 }
174
175 #[test]
176 fn test_valid_class_hash_without_leading_zeros() {
177 let valid_hash = "0x44dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
178 assert!(ClassHash::new(valid_hash).is_ok());
179 }
180
181 #[test]
182 fn test_invalid_class_hash_pattern() {
183 let invalid_hash = "0xGHIJKLMNOPQRSTUVWXYZ";
184 assert!(ClassHash::new(invalid_hash).is_err());
185 }
186
187 #[test]
188 fn test_invalid_class_hash_no_prefix() {
189 let invalid_hash = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
190 assert!(ClassHash::new(invalid_hash).is_err());
191 }
192
193 #[test]
194 fn test_invalid_class_hash_too_long() {
195 let invalid_hash =
196 "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da1812345";
197 assert!(ClassHash::new(invalid_hash).is_err());
198 }
199
200 #[test]
201 fn test_empty_class_hash() {
202 assert!(ClassHash::new("").is_err());
203 }
204
205 #[test]
206 fn test_class_hash_display() {
207 let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
208 let class_hash = ClassHash::new(hash).unwrap();
209 assert_eq!(format!("{class_hash}"), hash);
210 }
211
212 #[test]
213 fn test_class_hash_as_ref_str() {
214 let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
215 let class_hash = ClassHash::new(hash).unwrap();
216 let as_str: &str = class_hash.as_ref();
217 assert_eq!(as_str, hash);
218 }
219
220 #[test]
221 fn test_class_hash_as_ref_string() {
222 let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
223 let class_hash = ClassHash::new(hash).unwrap();
224 let expected_string = hash.to_string();
225 let as_string: &String = class_hash.as_ref();
226 assert_eq!(as_string, &expected_string);
227 }
228
229 #[test]
230 fn test_class_hash_clone() {
231 let hash = "0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18";
232 let class_hash = ClassHash::new(hash).unwrap();
233 let cloned = class_hash.clone();
234 assert_eq!(class_hash, cloned);
235 }
236
237 #[test]
238 fn test_class_hash_error_display() {
239 let error = ClassHashError::Match("invalid_hash".to_string());
240 let error_message = format!("{error}");
241 assert!(error_message.contains("[E010]"));
242 assert!(error_message.contains("Invalid class hash format"));
243 assert!(error_message.contains("invalid_hash"));
244 assert!(error_message.contains("Expected format: 0x followed by"));
245 }
246}