rustywallet_vanity/
regex_pattern.rs

1//! Regex pattern matching for vanity addresses.
2//!
3//! This module provides regex-based pattern matching for more flexible
4//! vanity address generation.
5
6use crate::address_type::AddressType;
7use crate::error::PatternError;
8use regex::Regex;
9
10/// A regex pattern for matching addresses.
11///
12/// Supports full Rust regex syntax for flexible pattern matching.
13///
14/// # Example
15///
16/// ```rust
17/// use rustywallet_vanity::regex_pattern::RegexPattern;
18///
19/// // Match addresses starting with "1" followed by 3-5 letters
20/// let pattern = RegexPattern::new(r"^1[A-Za-z]{3,5}").unwrap();
21/// assert!(pattern.matches("1Love123"));
22/// assert!(pattern.matches("1BTC456"));
23/// assert!(!pattern.matches("1AB789")); // Only 2 letters
24/// ```
25#[derive(Debug, Clone)]
26pub struct RegexPattern {
27    /// The compiled regex
28    regex: Regex,
29    /// Original pattern string
30    pattern_str: String,
31    /// Whether to match case-insensitively
32    case_insensitive: bool,
33}
34
35impl RegexPattern {
36    /// Create a new regex pattern.
37    ///
38    /// # Arguments
39    ///
40    /// * `pattern` - A valid Rust regex pattern
41    ///
42    /// # Errors
43    ///
44    /// Returns `PatternError::InvalidRegex` if the pattern is invalid.
45    pub fn new(pattern: &str) -> Result<Self, PatternError> {
46        if pattern.is_empty() {
47            return Err(PatternError::EmptyPattern);
48        }
49
50        let regex = Regex::new(pattern)
51            .map_err(|e| PatternError::InvalidRegex(e.to_string()))?;
52
53        Ok(Self {
54            regex,
55            pattern_str: pattern.to_string(),
56            case_insensitive: false,
57        })
58    }
59
60    /// Create a case-insensitive regex pattern.
61    pub fn new_case_insensitive(pattern: &str) -> Result<Self, PatternError> {
62        if pattern.is_empty() {
63            return Err(PatternError::EmptyPattern);
64        }
65
66        // Prepend (?i) for case-insensitive matching
67        let ci_pattern = format!("(?i){}", pattern);
68        let regex = Regex::new(&ci_pattern)
69            .map_err(|e| PatternError::InvalidRegex(e.to_string()))?;
70
71        Ok(Self {
72            regex,
73            pattern_str: pattern.to_string(),
74            case_insensitive: true,
75        })
76    }
77
78    /// Check if an address matches this pattern.
79    pub fn matches(&self, address: &str) -> bool {
80        self.regex.is_match(address)
81    }
82
83    /// Get the original pattern string.
84    pub fn as_str(&self) -> &str {
85        &self.pattern_str
86    }
87
88    /// Check if pattern is case-insensitive.
89    pub fn is_case_insensitive(&self) -> bool {
90        self.case_insensitive
91    }
92
93    /// Estimate difficulty of finding a match.
94    ///
95    /// This is a rough estimate based on pattern complexity.
96    pub fn estimate_difficulty(&self, address_type: AddressType) -> f64 {
97        // Base alphabet size
98        let alphabet_size: f64 = match address_type {
99            AddressType::P2PKH => 58.0,
100            AddressType::P2WPKH => 32.0,
101            AddressType::P2TR => 32.0,
102            AddressType::Ethereum => 16.0,
103        };
104
105        // Estimate based on pattern length (rough approximation)
106        // Count non-metacharacters as required matches
107        let effective_len = self.pattern_str
108            .chars()
109            .filter(|c| c.is_alphanumeric())
110            .count();
111
112        if effective_len == 0 {
113            return 1.0;
114        }
115
116        // Case insensitivity factor
117        let case_factor = if self.case_insensitive {
118            let letter_count = self.pattern_str
119                .chars()
120                .filter(|c| c.is_alphabetic())
121                .count();
122            2.0_f64.powi(letter_count as i32)
123        } else {
124            1.0
125        };
126
127        alphabet_size.powi(effective_len as i32) / case_factor
128    }
129}
130
131impl std::fmt::Display for RegexPattern {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(f, "regex:{}", self.pattern_str)
134    }
135}
136
137/// Common regex patterns for vanity addresses.
138pub struct CommonPatterns;
139
140impl CommonPatterns {
141    /// Match addresses starting with specific text.
142    pub fn starts_with(text: &str) -> Result<RegexPattern, PatternError> {
143        RegexPattern::new(&format!("^{}", regex::escape(text)))
144    }
145
146    /// Match addresses ending with specific text.
147    pub fn ends_with(text: &str) -> Result<RegexPattern, PatternError> {
148        RegexPattern::new(&format!("{}$", regex::escape(text)))
149    }
150
151    /// Match addresses containing specific text.
152    pub fn contains(text: &str) -> Result<RegexPattern, PatternError> {
153        RegexPattern::new(&regex::escape(text))
154    }
155
156    /// Match addresses with repeated characters.
157    ///
158    /// # Example
159    ///
160    /// ```rust
161    /// use rustywallet_vanity::regex_pattern::CommonPatterns;
162    ///
163    /// let pattern = CommonPatterns::repeated_char('A', 3).unwrap();
164    /// assert!(pattern.matches("1AAA123"));
165    /// assert!(!pattern.matches("1AA123")); // Only 2 A's
166    /// ```
167    pub fn repeated_char(c: char, count: usize) -> Result<RegexPattern, PatternError> {
168        if count == 0 {
169            return Err(PatternError::EmptyPattern);
170        }
171        RegexPattern::new(&format!("{}{{{}}}", regex::escape(&c.to_string()), count))
172    }
173
174    /// Match addresses with alternating characters.
175    pub fn alternating(chars: &str) -> Result<RegexPattern, PatternError> {
176        if chars.len() < 2 {
177            return Err(PatternError::InvalidPattern("Need at least 2 characters".into()));
178        }
179        let pattern: String = chars.chars()
180            .map(|c| regex::escape(&c.to_string()))
181            .collect::<Vec<_>>()
182            .join("");
183        RegexPattern::new(&pattern)
184    }
185
186    /// Match addresses with a word boundary pattern.
187    pub fn word(word: &str) -> Result<RegexPattern, PatternError> {
188        RegexPattern::new_case_insensitive(&regex::escape(word))
189    }
190
191    /// Match addresses with numeric sequences.
192    ///
193    /// # Example
194    ///
195    /// ```rust
196    /// use rustywallet_vanity::regex_pattern::CommonPatterns;
197    ///
198    /// let pattern = CommonPatterns::numeric_sequence(4).unwrap();
199    /// assert!(pattern.matches("1abc1234xyz"));
200    /// assert!(!pattern.matches("1abc123xyz")); // Only 3 digits
201    /// ```
202    pub fn numeric_sequence(min_length: usize) -> Result<RegexPattern, PatternError> {
203        if min_length == 0 {
204            return Err(PatternError::EmptyPattern);
205        }
206        RegexPattern::new(&format!(r"\d{{{},}}", min_length))
207    }
208
209    /// Match addresses with letter sequences.
210    pub fn letter_sequence(min_length: usize) -> Result<RegexPattern, PatternError> {
211        if min_length == 0 {
212            return Err(PatternError::EmptyPattern);
213        }
214        RegexPattern::new(&format!(r"[A-Za-z]{{{},}}", min_length))
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_regex_pattern_basic() {
224        let pattern = RegexPattern::new(r"^1[A-Z]{3}").unwrap();
225        assert!(pattern.matches("1ABC123"));
226        assert!(pattern.matches("1XYZ456"));
227        assert!(!pattern.matches("1abc123")); // lowercase
228        assert!(!pattern.matches("2ABC123")); // wrong start
229    }
230
231    #[test]
232    fn test_regex_pattern_case_insensitive() {
233        let pattern = RegexPattern::new_case_insensitive(r"^1love").unwrap();
234        assert!(pattern.matches("1Love123"));
235        assert!(pattern.matches("1LOVE123"));
236        assert!(pattern.matches("1love123"));
237    }
238
239    #[test]
240    fn test_common_patterns_starts_with() {
241        let pattern = CommonPatterns::starts_with("1BTC").unwrap();
242        assert!(pattern.matches("1BTC123"));
243        assert!(!pattern.matches("2BTC123"));
244    }
245
246    #[test]
247    fn test_common_patterns_ends_with() {
248        let pattern = CommonPatterns::ends_with("BTC").unwrap();
249        assert!(pattern.matches("1abcBTC"));
250        assert!(!pattern.matches("1BTCabc"));
251    }
252
253    #[test]
254    fn test_common_patterns_repeated() {
255        let pattern = CommonPatterns::repeated_char('A', 3).unwrap();
256        assert!(pattern.matches("1AAA123"));
257        assert!(pattern.matches("1AAAA123")); // 4 A's also matches
258        assert!(!pattern.matches("1AA123"));
259    }
260
261    #[test]
262    fn test_common_patterns_numeric() {
263        let pattern = CommonPatterns::numeric_sequence(4).unwrap();
264        assert!(pattern.matches("1abc1234xyz"));
265        assert!(pattern.matches("1abc12345xyz"));
266        assert!(!pattern.matches("1abc123xyz"));
267    }
268
269    #[test]
270    fn test_difficulty_estimate() {
271        let pattern = RegexPattern::new(r"^1[A-Z]{2}").unwrap();
272        let diff = pattern.estimate_difficulty(AddressType::P2PKH);
273        // Should be roughly 58^2 = 3364 for 2 characters
274        assert!(diff > 1000.0);
275    }
276
277    #[test]
278    fn test_invalid_regex() {
279        let result = RegexPattern::new(r"[invalid");
280        assert!(result.is_err());
281    }
282
283    #[test]
284    fn test_empty_pattern() {
285        assert!(RegexPattern::new("").is_err());
286    }
287}