verifier/
class_hash.rs

1//! # Class Hash Validation
2//!
3//! This module provides type-safe handling and validation of Starknet class hashes.
4//! Class hashes are 256-bit values represented as hexadecimal strings with "0x" prefix.
5//!
6//! ## Features
7//!
8//! - **Type Safety**: Strong typing prevents invalid class hashes from being used
9//! - **Validation**: Automatic validation of format and length
10//! - **Performance**: Compiled regex patterns for efficient validation
11//! - **Error Handling**: Detailed error messages with actionable suggestions
12//!
13//! ## Example Usage
14//!
15//! ```rust
16//! use verifier::class_hash::ClassHash;
17//!
18//! // Valid class hash
19//! let hash = ClassHash::new("0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18")?;
20//! println!("Class hash: {}", hash);
21//!
22//! // Invalid class hash will return an error
23//! let invalid = ClassHash::new("invalid_hash");
24//! assert!(invalid.is_err());
25//! # Ok::<(), Box<dyn std::error::Error>>(())
26//! ```
27
28use 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/// A type-safe wrapper for Starknet class hashes.
45///
46/// Class hashes are 256-bit values represented as hexadecimal strings prefixed with "0x".
47/// This type ensures that only valid class hashes can be constructed and used throughout
48/// the application.
49///
50/// ## Format Requirements
51///
52/// - Must start with "0x"
53/// - Must contain only hexadecimal characters (0-9, a-f, A-F)
54/// - Must be at most 66 characters long (including "0x" prefix)
55///
56/// ## Examples
57///
58/// ```rust
59/// use verifier::class_hash::ClassHash;
60///
61/// // Valid class hash
62/// let hash = ClassHash::new("0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18")?;
63/// println!("Hash: {}", hash);
64///
65/// // Shorter hashes are also valid
66/// let short_hash = ClassHash::new("0x123")?;
67/// println!("Short hash: {}", short_hash);
68///
69/// // Invalid hashes will return an error
70/// assert!(ClassHash::new("invalid").is_err());
71/// assert!(ClassHash::new("0xGGG").is_err());
72/// # Ok::<(), Box<dyn std::error::Error>>(())
73/// ```
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
75pub struct ClassHash(String);
76
77/// Errors that can occur when validating or creating class hashes.
78///
79/// Each error variant provides detailed information about what went wrong
80/// and includes actionable suggestions for fixing the issue.
81#[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    /// Creates a new `ClassHash` from a string.
102    ///
103    /// Validates that the input string follows the correct format for a Starknet class hash:
104    /// - Must start with "0x"
105    /// - Must contain only hexadecimal characters (0-9, a-f, A-F)
106    /// - Must be at most 66 characters long (including "0x" prefix)
107    ///
108    /// # Arguments
109    ///
110    /// * `raw` - The string representation of the class hash
111    ///
112    /// # Returns
113    ///
114    /// Returns `Ok(ClassHash)` if the input is valid, or `Err(ClassHashError)` with
115    /// detailed error information if the input is invalid.
116    ///
117    /// # Examples
118    ///
119    /// ```rust
120    /// use verifier::class_hash::ClassHash;
121    ///
122    /// // Valid class hash
123    /// let hash = ClassHash::new("0x044dc2b3239382230d8b1e943df23b96f52eebcac93efe6e8bde92f9a2f1da18")?;
124    /// println!("Created hash: {}", hash);
125    ///
126    /// // Invalid format will return an error
127    /// let result = ClassHash::new("invalid_hash");
128    /// assert!(result.is_err());
129    /// # Ok::<(), Box<dyn std::error::Error>>(())
130    /// ```
131    ///
132    /// # Errors
133    ///
134    /// Returns `ClassHashError::Match` if the input doesn't match the expected format,
135    /// or `ClassHashError::RegexError` if there's an internal regex compilation error.
136    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}