Skip to main content

goldrush_sdk/
validation.rs

1use crate::{Error, Result};
2use std::collections::HashSet;
3use tracing::{debug, instrument};
4
5/// Validation utilities for blockchain data.
6pub struct Validator;
7
8impl Validator {
9    /// Validate an Ethereum-style address (42 characters, starts with 0x).
10    #[instrument(fields(address = %address))]
11    pub fn validate_address(address: &str) -> Result<()> {
12        let address = address.trim();
13        
14        if address.is_empty() {
15            return Err(Error::Config("Address cannot be empty".to_string()));
16        }
17        
18        if !address.starts_with("0x") {
19            return Err(Error::Config("Address must start with '0x'".to_string()));
20        }
21        
22        if address.len() != 42 {
23            return Err(Error::Config(format!(
24                "Address must be 42 characters long, got {}", address.len()
25            )));
26        }
27        
28        // Check if the rest are valid hex characters
29        for char in address.chars().skip(2) {
30            if !char.is_ascii_hexdigit() {
31                return Err(Error::Config(format!(
32                    "Address contains invalid character: '{}'", char
33                )));
34            }
35        }
36        
37        debug!("Address validation passed");
38        Ok(())
39    }
40    
41    /// Validate a transaction hash (66 characters, starts with 0x).
42    #[instrument(fields(tx_hash = %tx_hash))]
43    pub fn validate_tx_hash(tx_hash: &str) -> Result<()> {
44        let tx_hash = tx_hash.trim();
45        
46        if tx_hash.is_empty() {
47            return Err(Error::Config("Transaction hash cannot be empty".to_string()));
48        }
49        
50        if !tx_hash.starts_with("0x") {
51            return Err(Error::Config("Transaction hash must start with '0x'".to_string()));
52        }
53        
54        if tx_hash.len() != 66 {
55            return Err(Error::Config(format!(
56                "Transaction hash must be 66 characters long, got {}", tx_hash.len()
57            )));
58        }
59        
60        // Check if the rest are valid hex characters
61        for char in tx_hash.chars().skip(2) {
62            if !char.is_ascii_hexdigit() {
63                return Err(Error::Config(format!(
64                    "Transaction hash contains invalid character: '{}'", char
65                )));
66            }
67        }
68        
69        debug!("Transaction hash validation passed");
70        Ok(())
71    }
72    
73    /// Validate a chain name/identifier.
74    #[instrument(fields(chain_name = %chain_name))]
75    pub fn validate_chain_name(chain_name: &str) -> Result<()> {
76        let chain_name = chain_name.trim();
77        
78        if chain_name.is_empty() {
79            return Err(Error::Config("Chain name cannot be empty".to_string()));
80        }
81        
82        // Check for valid characters (alphanumeric, hyphens, underscores)
83        for char in chain_name.chars() {
84            if !char.is_alphanumeric() && char != '-' && char != '_' {
85                return Err(Error::Config(format!(
86                    "Chain name contains invalid character: '{}'. Only alphanumeric, hyphens, and underscores are allowed", char
87                )));
88            }
89        }
90        
91        debug!("Chain name validation passed");
92        Ok(())
93    }
94    
95    /// Validate page size parameter.
96    #[instrument(fields(page_size = %page_size))]
97    pub fn validate_page_size(page_size: u32) -> Result<()> {
98        const MIN_PAGE_SIZE: u32 = 1;
99        const MAX_PAGE_SIZE: u32 = 1000;
100        
101        if page_size < MIN_PAGE_SIZE {
102            return Err(Error::Config(format!(
103                "Page size must be at least {}, got {}", MIN_PAGE_SIZE, page_size
104            )));
105        }
106        
107        if page_size > MAX_PAGE_SIZE {
108            return Err(Error::Config(format!(
109                "Page size cannot exceed {}, got {}", MAX_PAGE_SIZE, page_size
110            )));
111        }
112        
113        debug!("Page size validation passed");
114        Ok(())
115    }
116    
117    /// Validate an API key format.
118    #[instrument(fields(api_key_prefix = %api_key.chars().take(8).collect::<String>()))]
119    pub fn validate_api_key(api_key: &str) -> Result<()> {
120        let api_key = api_key.trim();
121        
122        if api_key.is_empty() {
123            return Err(Error::Config("API key cannot be empty".to_string()));
124        }
125        
126        if api_key.len() < 10 {
127            return Err(Error::Config("API key appears to be too short".to_string()));
128        }
129        
130        // Check for potential placeholder values
131        let invalid_keys = ["your-api-key", "test", "demo", "placeholder", "xxx"];
132        let lower_key = api_key.to_lowercase();
133        
134        for invalid in &invalid_keys {
135            if lower_key.contains(invalid) {
136                return Err(Error::Config("API key appears to be a placeholder value".to_string()));
137            }
138        }
139        
140        debug!("API key validation passed");
141        Ok(())
142    }
143    
144    /// Validate contract address and token ID combination.
145    #[instrument(fields(contract_address = %contract_address, token_id = %token_id))]
146    pub fn validate_nft_identifier(contract_address: &str, token_id: &str) -> Result<()> {
147        Self::validate_address(contract_address)?;
148        
149        let token_id = token_id.trim();
150        if token_id.is_empty() {
151            return Err(Error::Config("Token ID cannot be empty".to_string()));
152        }
153        
154        // Token ID should be a valid number (decimal or hex)
155        if token_id.starts_with("0x") {
156            // Hex token ID
157            for char in token_id.chars().skip(2) {
158                if !char.is_ascii_hexdigit() {
159                    return Err(Error::Config(format!(
160                        "Token ID contains invalid hex character: '{}'", char
161                    )));
162                }
163            }
164        } else {
165            // Decimal token ID
166            if token_id.parse::<u64>().is_err() {
167                return Err(Error::Config("Token ID must be a valid number".to_string()));
168            }
169        }
170        
171        debug!("NFT identifier validation passed");
172        Ok(())
173    }
174    
175    /// Validate URL format.
176    #[instrument(fields(url = %url))]
177    pub fn validate_url(url: &str) -> Result<()> {
178        let url = url.trim();
179        
180        if url.is_empty() {
181            return Err(Error::Config("URL cannot be empty".to_string()));
182        }
183        
184        if !url.starts_with("http://") && !url.starts_with("https://") {
185            return Err(Error::Config("URL must start with http:// or https://".to_string()));
186        }
187        
188        // Basic URL validation - check for valid characters
189        let valid_chars: HashSet<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~:/?#[]@!$&'()*+,;=%"
190            .chars()
191            .collect();
192        
193        for char in url.chars() {
194            if !valid_chars.contains(&char) {
195                return Err(Error::Config(format!(
196                    "URL contains invalid character: '{}'", char
197                )));
198            }
199        }
200        
201        debug!("URL validation passed");
202        Ok(())
203    }
204}
205
206/// Sanitization utilities for user input.
207pub struct Sanitizer;
208
209impl Sanitizer {
210    /// Sanitize an address by trimming and converting to lowercase.
211    pub fn sanitize_address(address: &str) -> String {
212        let trimmed = address.trim();
213        if trimmed.starts_with("0x") || trimmed.starts_with("0X") {
214            format!("0x{}", &trimmed[2..].to_lowercase())
215        } else {
216            trimmed.to_lowercase()
217        }
218    }
219    
220    /// Sanitize a transaction hash by trimming and converting to lowercase.
221    pub fn sanitize_tx_hash(tx_hash: &str) -> String {
222        let trimmed = tx_hash.trim();
223        if trimmed.starts_with("0x") || trimmed.starts_with("0X") {
224            format!("0x{}", &trimmed[2..].to_lowercase())
225        } else {
226            format!("0x{}", trimmed.to_lowercase())
227        }
228    }
229    
230    /// Sanitize chain name by trimming and converting to lowercase.
231    pub fn sanitize_chain_name(chain_name: &str) -> String {
232        chain_name.trim().to_lowercase()
233    }
234    
235    /// Remove potentially dangerous characters from user input.
236    pub fn sanitize_user_input(input: &str) -> String {
237        input
238            .trim()
239            .chars()
240            .filter(|c| c.is_alphanumeric() || "-_. ".contains(*c))
241            .collect()
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    
249    #[test]
250    fn test_address_validation() {
251        // Valid address
252        assert!(Validator::validate_address("0x742d35Cc6634C0532925a3b8D4fc24f3C4aD6a8b").is_ok());
253        
254        // Invalid cases
255        assert!(Validator::validate_address("").is_err());
256        assert!(Validator::validate_address("742d35Cc6634C0532925a3b8D4fc24f3C4aD6a8b").is_err());
257        assert!(Validator::validate_address("0x742d35Cc6634").is_err());
258        assert!(Validator::validate_address("0x742d35Cc6634C0532925a3b8D4fc24f3C4aD6a8bXX").is_err());
259    }
260    
261    #[test]
262    fn test_tx_hash_validation() {
263        // Valid tx hash
264        assert!(Validator::validate_tx_hash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").is_ok());
265        
266        // Invalid cases
267        assert!(Validator::validate_tx_hash("").is_err());
268        assert!(Validator::validate_tx_hash("1234567890abcdef").is_err());
269        assert!(Validator::validate_tx_hash("0x1234").is_err());
270    }
271    
272    #[test]
273    fn test_sanitization() {
274        assert_eq!(
275            Sanitizer::sanitize_address("  0X742d35Cc6634C0532925a3b8D4fc24f3C4aD6a8b  "),
276            "0x742d35cc6634c0532925a3b8d4fc24f3c4ad6a8b"
277        );
278        
279        assert_eq!(
280            Sanitizer::sanitize_chain_name("  ETH-MAINNET  "),
281            "eth-mainnet"
282        );
283    }
284}