Skip to main content

dash_platform_queries/
dpns_usernames.rs

1//! Transport-free DPNS username helpers.
2//!
3//! The Sdk-bound DPNS surface (registration, availability checks, name
4//! resolution) lives in `dash-sdk`; these free functions are pure string
5//! validation/normalization shared with embedders.
6
7/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l'
8/// with '0', '1', and '1' respectively to prevent homograph attacks
9pub fn convert_to_homograph_safe_chars(input: &str) -> String {
10    input
11        .chars()
12        .map(|c| match c {
13            'o' | 'O' => '0',
14            'i' | 'I' => '1',
15            'l' | 'L' => '1',
16            _ => c.to_ascii_lowercase(),
17        })
18        .collect()
19}
20
21/// Check if a username is valid according to DPNS rules
22///
23/// A username is valid if:
24/// - It's between 3 and 63 characters long
25/// - It starts and ends with alphanumeric characters (a-zA-Z0-9)
26/// - It contains only alphanumeric characters and hyphens
27/// - It doesn't have consecutive hyphens (enforced by the pattern)
28///
29/// Pattern: `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`
30///
31/// # Arguments
32///
33/// * `label` - The username label to check (e.g., "alice")
34///
35/// # Returns
36///
37/// Returns `true` if the username is valid, `false` otherwise
38pub fn is_valid_username(label: &str) -> bool {
39    // Check length
40    if label.len() < 3 || label.len() > 63 {
41        return false;
42    }
43
44    let chars: Vec<char> = label.chars().collect();
45
46    // Check first character (must be alphanumeric)
47    if !chars[0].is_ascii_alphanumeric() {
48        return false;
49    }
50
51    // Check last character (must be alphanumeric)
52    if !chars[chars.len() - 1].is_ascii_alphanumeric() {
53        return false;
54    }
55
56    // Check middle characters (can be alphanumeric or hyphen)
57    for &ch in &chars[1..chars.len() - 1] {
58        if !ch.is_ascii_alphanumeric() && ch != '-' {
59            return false;
60        }
61    }
62
63    // Additional check: no consecutive hyphens (good practice)
64    for i in 0..chars.len() - 1 {
65        if chars[i] == '-' && chars[i + 1] == '-' {
66            return false;
67        }
68    }
69
70    true
71}
72
73/// Check if a username is contested (requires masternode voting)
74///
75/// A username is contested if its normalized label:
76/// - Is between 3 and 19 characters long (inclusive)
77/// - Contains only lowercase letters a-z, digits 0-1, and hyphens
78///
79/// # Arguments
80///
81/// * `label` - The username label to check (e.g., "alice")
82///
83/// # Returns
84///
85/// Returns `true` if the username would be contested, `false` otherwise
86pub fn is_contested_username(label: &str) -> bool {
87    let normalized = convert_to_homograph_safe_chars(label);
88
89    // Check length
90    if normalized.len() < 3 || normalized.len() > 19 {
91        return false;
92    }
93
94    // Check if all characters match the pattern [a-z01-]
95    normalized
96        .chars()
97        .all(|c| matches!(c, 'a'..='z' | '0' | '1' | '-'))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_convert_to_homograph_safe_chars() {
106        assert_eq!(convert_to_homograph_safe_chars("alice"), "a11ce");
107        assert_eq!(convert_to_homograph_safe_chars("bob"), "b0b");
108        assert_eq!(convert_to_homograph_safe_chars("COOL"), "c001");
109        assert_eq!(convert_to_homograph_safe_chars("test123"), "test123");
110    }
111
112    #[test]
113    fn test_is_valid_username() {
114        // Valid usernames
115        assert!(is_valid_username("abc"));
116        assert!(is_valid_username("alice"));
117        assert!(is_valid_username("Alice123"));
118        assert!(is_valid_username("dash-p2p"));
119        assert!(is_valid_username("test-name-123"));
120        assert!(is_valid_username("a-b-c"));
121        assert!(is_valid_username("user2024"));
122        assert!(is_valid_username("CryptoKing"));
123        assert!(is_valid_username("web3-developer"));
124        assert!(is_valid_username("a".repeat(63).as_str())); // Max length
125
126        // Invalid - too short
127        assert!(!is_valid_username("ab"));
128        assert!(!is_valid_username("a"));
129        assert!(!is_valid_username(""));
130
131        // Invalid - too long
132        assert!(!is_valid_username("a".repeat(64).as_str()));
133
134        // Invalid - starts with hyphen
135        assert!(!is_valid_username("-alice"));
136        assert!(!is_valid_username("-test"));
137
138        // Invalid - ends with hyphen
139        assert!(!is_valid_username("alice-"));
140        assert!(!is_valid_username("test-"));
141
142        // Invalid - starts and ends with hyphen
143        assert!(!is_valid_username("-alice-"));
144
145        // Invalid - contains invalid characters
146        assert!(!is_valid_username("alice_bob")); // underscore
147        assert!(!is_valid_username("alice.bob")); // dot
148        assert!(!is_valid_username("alice@dash")); // at sign
149        assert!(!is_valid_username("alice!")); // exclamation
150        assert!(!is_valid_username("alice bob")); // space
151        assert!(!is_valid_username("alice#1")); // hash
152        assert!(!is_valid_username("alice$")); // dollar
153        assert!(!is_valid_username("alice%20")); // percent
154
155        // Invalid - consecutive hyphens
156        assert!(!is_valid_username("alice--bob"));
157        assert!(!is_valid_username("test---name"));
158    }
159
160    #[test]
161    fn test_is_contested_username() {
162        // Contested usernames (3-19 chars, only [a-z01-])
163        assert!(is_contested_username("abc"));
164        assert!(is_contested_username("alice")); // becomes "a11ce"
165        assert!(is_contested_username("b0b"));
166        assert!(is_contested_username("cool")); // becomes "c001"
167        assert!(is_contested_username("a-b-c"));
168        assert!(is_contested_username("hello")); // becomes "he110"
169        assert!(is_contested_username("world")); // becomes "w0r1d"
170        assert!(is_contested_username("dash"));
171        assert!(is_contested_username("a11ce")); // already normalized
172        assert!(is_contested_username("dash-dao")); // becomes "dash-da0"
173
174        // Not contested - too short
175        assert!(!is_contested_username("ab"));
176        assert!(!is_contested_username("io")); // becomes "10" which is 2 chars
177        assert!(!is_contested_username("a"));
178
179        // Not contested - too long (20+ chars)
180        assert!(!is_contested_username("twenty-characters-ab")); // 20 chars
181        assert!(!is_contested_username(
182            "this-is-a-very-long-username-that-exceeds-limit"
183        ));
184
185        // Not contested - contains invalid characters after normalization
186        assert!(!is_contested_username("alice2")); // contains '2'
187        assert!(!is_contested_username("alice_bob")); // contains '_'
188        assert!(!is_contested_username("alice.bob")); // contains '.'
189        assert!(!is_contested_username("alice@dash")); // contains '@'
190        assert!(!is_contested_username("alice!")); // contains '!'
191        assert!(!is_contested_username("test123")); // contains '2' and '3'
192        assert!(!is_contested_username("dash-p2p")); // contains 'p' and '2'
193        assert!(!is_contested_username("user5")); // contains '5'
194        assert!(!is_contested_username("name_with_underscore")); // contains '_'
195    }
196}