goldrush_sdk/
validation.rs1use crate::{Error, Result};
2use std::collections::HashSet;
3use tracing::{debug, instrument};
4
5pub struct Validator;
7
8impl Validator {
9 #[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 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 #[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 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 #[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 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 #[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 #[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 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 #[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 if token_id.starts_with("0x") {
156 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 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 #[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 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
206pub struct Sanitizer;
208
209impl Sanitizer {
210 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 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 pub fn sanitize_chain_name(chain_name: &str) -> String {
232 chain_name.trim().to_lowercase()
233 }
234
235 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 assert!(Validator::validate_address("0x742d35Cc6634C0532925a3b8D4fc24f3C4aD6a8b").is_ok());
253
254 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 assert!(Validator::validate_tx_hash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").is_ok());
265
266 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}