rustywallet_vanity/
pattern.rs

1//! Pattern matching for vanity addresses.
2
3use crate::address_type::AddressType;
4use crate::error::PatternError;
5
6/// A pattern to match against addresses.
7#[derive(Debug, Clone)]
8pub enum Pattern {
9    /// Match addresses starting with this prefix.
10    Prefix(String),
11    /// Match addresses ending with this suffix.
12    Suffix(String),
13    /// Match addresses containing this substring.
14    Contains(String),
15}
16
17impl Pattern {
18    /// Create a prefix pattern.
19    pub fn prefix(s: &str) -> Result<Self, PatternError> {
20        if s.is_empty() {
21            return Err(PatternError::EmptyPattern);
22        }
23        Ok(Pattern::Prefix(s.to_string()))
24    }
25
26    /// Create a suffix pattern.
27    pub fn suffix(s: &str) -> Result<Self, PatternError> {
28        if s.is_empty() {
29            return Err(PatternError::EmptyPattern);
30        }
31        Ok(Pattern::Suffix(s.to_string()))
32    }
33
34    /// Create a contains pattern.
35    pub fn contains(s: &str) -> Result<Self, PatternError> {
36        if s.is_empty() {
37            return Err(PatternError::EmptyPattern);
38        }
39        Ok(Pattern::Contains(s.to_string()))
40    }
41
42    /// Get the pattern string.
43    pub fn as_str(&self) -> &str {
44        match self {
45            Pattern::Prefix(s) => s,
46            Pattern::Suffix(s) => s,
47            Pattern::Contains(s) => s,
48        }
49    }
50
51    /// Check if an address matches this pattern.
52    pub fn matches(&self, address: &str, case_sensitive: bool) -> bool {
53        if case_sensitive {
54            self.matches_case_sensitive(address)
55        } else {
56            self.matches_case_insensitive(address)
57        }
58    }
59
60    fn matches_case_sensitive(&self, address: &str) -> bool {
61        match self {
62            Pattern::Prefix(p) => address.starts_with(p),
63            Pattern::Suffix(s) => address.ends_with(s),
64            Pattern::Contains(c) => address.contains(c),
65        }
66    }
67
68    fn matches_case_insensitive(&self, address: &str) -> bool {
69        let addr_lower = address.to_lowercase();
70        match self {
71            Pattern::Prefix(p) => addr_lower.starts_with(&p.to_lowercase()),
72            Pattern::Suffix(s) => addr_lower.ends_with(&s.to_lowercase()),
73            Pattern::Contains(c) => addr_lower.contains(&c.to_lowercase()),
74        }
75    }
76
77    /// Validate this pattern for a specific address type.
78    pub fn validate_for_type(
79        &self,
80        address_type: AddressType,
81        testnet: bool,
82    ) -> Result<(), PatternError> {
83        let pattern_str = self.as_str();
84
85        // For prefix patterns, use address type validation
86        if let Pattern::Prefix(_) = self {
87            address_type.validate_pattern(pattern_str, testnet)?;
88        } else {
89            // For suffix/contains, just validate characters
90            let valid_chars = address_type.valid_chars();
91            for c in pattern_str.chars() {
92                let c_lower = c.to_ascii_lowercase();
93                if !valid_chars.contains(c_lower) && !valid_chars.contains(c) {
94                    return Err(PatternError::InvalidCharacter(c));
95                }
96            }
97        }
98
99        Ok(())
100    }
101
102    /// Calculate the difficulty of finding this pattern.
103    /// Returns the expected number of attempts.
104    pub fn difficulty(&self, address_type: AddressType, case_sensitive: bool) -> f64 {
105        let pattern_str = self.as_str();
106        let fixed_prefix = address_type.fixed_prefix(false);
107
108        // Calculate effective pattern length (excluding fixed prefix for prefix patterns)
109        let effective_len = match self {
110            Pattern::Prefix(p) => {
111                if p.len() > fixed_prefix.len() {
112                    p.len() - fixed_prefix.len()
113                } else {
114                    0
115                }
116            }
117            Pattern::Suffix(s) => s.len(),
118            Pattern::Contains(c) => c.len(),
119        };
120
121        if effective_len == 0 {
122            return 1.0;
123        }
124
125        // Base alphabet size
126        let alphabet_size: f64 = match address_type {
127            AddressType::P2PKH => 58.0,       // Base58
128            AddressType::P2WPKH => 32.0,      // Bech32
129            AddressType::P2TR => 32.0,        // Bech32
130            AddressType::Ethereum => 16.0,    // Hex
131        };
132
133        // Case sensitivity factor
134        let case_factor = if case_sensitive {
135            1.0
136        } else {
137            // For case-insensitive, we have more matches
138            // Roughly 2x for each letter that has case variants
139            let letter_count = pattern_str.chars().filter(|c| c.is_alphabetic()).count();
140            2.0_f64.powi(letter_count as i32)
141        };
142
143        // Expected attempts = alphabet_size ^ effective_len / case_factor
144        let base_difficulty = alphabet_size.powi(effective_len as i32);
145
146        // For suffix/contains, multiply by address length factor
147        let position_factor = match self {
148            Pattern::Prefix(_) => 1.0,
149            Pattern::Suffix(_) => 1.0,
150            Pattern::Contains(_) => 0.5, // Can match anywhere, so easier
151        };
152
153        base_difficulty / case_factor * position_factor
154    }
155}
156
157impl std::fmt::Display for Pattern {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            Pattern::Prefix(p) => write!(f, "prefix:{}", p),
161            Pattern::Suffix(s) => write!(f, "suffix:{}", s),
162            Pattern::Contains(c) => write!(f, "contains:{}", c),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_prefix_matching() {
173        let pattern = Pattern::prefix("1Love").unwrap();
174
175        assert!(pattern.matches("1LoveXYZ123", true));
176        assert!(!pattern.matches("1loveXYZ123", true)); // case sensitive
177        assert!(pattern.matches("1loveXYZ123", false)); // case insensitive
178        assert!(!pattern.matches("2LoveXYZ123", true));
179    }
180
181    #[test]
182    fn test_suffix_matching() {
183        let pattern = Pattern::suffix("BTC").unwrap();
184
185        assert!(pattern.matches("1abcdefBTC", true));
186        assert!(!pattern.matches("1abcdefbtc", true));
187        assert!(pattern.matches("1abcdefbtc", false));
188    }
189
190    #[test]
191    fn test_contains_matching() {
192        let pattern = Pattern::contains("Love").unwrap();
193
194        assert!(pattern.matches("1abcLoveXYZ", true));
195        assert!(pattern.matches("1LoveXYZ", true));
196        assert!(pattern.matches("1XYZLove", true));
197    }
198
199    #[test]
200    fn test_difficulty_calculation() {
201        let pattern = Pattern::prefix("1A").unwrap();
202        let diff = pattern.difficulty(AddressType::P2PKH, true);
203
204        // 1 char after prefix, Base58 = 58 expected attempts
205        assert!(diff > 50.0 && diff < 70.0);
206    }
207
208    #[test]
209    fn test_empty_pattern_rejected() {
210        assert!(Pattern::prefix("").is_err());
211        assert!(Pattern::suffix("").is_err());
212        assert!(Pattern::contains("").is_err());
213    }
214}