rialo_api_types/
validation.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Validation for API request types
5
6use std::str::FromStr;
7
8use rialo_s_sdk::pubkey::Pubkey;
9use thiserror::Error;
10use validator::ValidationErrors;
11
12use crate::constants::*;
13
14/// Validation error types for RPC requests
15#[derive(Debug, Error, Clone)]
16pub enum ValidationError {
17    #[error("Invalid format: {0}")]
18    InvalidFormat(String),
19
20    #[error("Value out of range: {0}")]
21    OutOfRange(String),
22
23    #[error("Missing required field: {0}")]
24    MissingField(String),
25
26    #[error("Invalid signature: {0}")]
27    InvalidSignature(String),
28
29    #[error("Invalid encoding: {0}. Supported encodings: base64, base58")]
30    InvalidEncoding(String),
31
32    #[error("Invalid public key: {0}")]
33    InvalidPublicKey(String),
34
35    #[error("Invalid transaction: {0}")]
36    InvalidTransaction(String),
37
38    #[error("Multiple validation errors: {0}")]
39    Multiple(String),
40}
41
42impl From<ValidationErrors> for ValidationError {
43    fn from(errors: ValidationErrors) -> Self {
44        let error_messages: Vec<String> = errors
45            .field_errors()
46            .iter()
47            .flat_map(|(field, errors)| {
48                errors.iter().map(move |error| {
49                    format!(
50                        "{}: {}",
51                        field,
52                        error
53                            .message
54                            .as_ref()
55                            .unwrap_or(&"validation failed".into())
56                    )
57                })
58            })
59            .collect();
60
61        if error_messages.len() == 1 {
62            ValidationError::InvalidFormat(error_messages[0].clone())
63        } else {
64            ValidationError::Multiple(error_messages.join(", "))
65        }
66    }
67}
68
69/// Result type for validation operations
70pub type ValidationResult<T> = Result<T, ValidationError>;
71
72/// Validate Solana public key format
73pub fn validate_pubkey(pubkey: &str) -> Result<(), validator::ValidationError> {
74    Pubkey::from_str(pubkey).map_err(|_| validator::ValidationError::new("invalid_pubkey"))?;
75    Ok(())
76}
77
78/// Validate base64 encoded data
79pub fn validate_base64(data: &str) -> Result<(), validator::ValidationError> {
80    use fastcrypto::encoding::{Base64, Encoding};
81    Base64::decode(data).map_err(|_| validator::ValidationError::new("invalid_base64"))?;
82    Ok(())
83}
84
85/// Validate base58 encoded data
86pub fn validate_base58(data: &str) -> Result<(), validator::ValidationError> {
87    use fastcrypto::encoding::{Base58, Encoding};
88    Base58::decode(data).map_err(|_| validator::ValidationError::new("invalid_base58"))?;
89    Ok(())
90}
91
92/// Validate transaction signature
93pub fn validate_signature(signature: &str) -> Result<(), validator::ValidationError> {
94    // Solana signatures are base58 encoded and should be 87 or 88 characters long
95    // (87 when there are leading zero bytes that get omitted in base58 encoding)
96    if signature.len() < MIN_SIGNATURE_LENGTH || signature.len() > MAX_SIGNATURE_LENGTH {
97        return Err(validator::ValidationError::new("invalid_signature_length"));
98    }
99    validate_base58(signature)
100}
101
102/// Validate nonce format (should be valid UTF-8 and reasonable length)
103pub fn validate_nonce(nonce: &str) -> Result<(), validator::ValidationError> {
104    if nonce.is_empty() {
105        return Err(validator::ValidationError::new("empty_nonce"));
106    }
107    if nonce.len() > MAX_NONCE_LENGTH {
108        return Err(validator::ValidationError::new("nonce_too_long"));
109    }
110    Ok(())
111}
112
113/// Validate lamports amount (should be reasonable)
114pub fn validate_lamports(lamports: u64) -> Result<(), validator::ValidationError> {
115    // Validate against maximum possible lamports (500 million SOL * 1e9 lamports/SOL)
116    if lamports > MAX_LAMPORTS {
117        return Err(validator::ValidationError::new("lamports_too_large"));
118    }
119    Ok(())
120}
121
122/// Validate limit parameter for paginated requests
123pub fn validate_limit(limit: &u64) -> Result<(), validator::ValidationError> {
124    if *limit == 0 {
125        return Err(validator::ValidationError::new("limit_zero"));
126    }
127    if *limit > MAX_PAGINATION_LIMIT {
128        return Err(validator::ValidationError::new("limit_too_large"));
129    }
130    Ok(())
131}
132
133/// Custom validator for array of public keys
134pub fn validate_pubkey_array(pubkeys: &[String]) -> Result<(), validator::ValidationError> {
135    for pubkey in pubkeys {
136        validate_pubkey(pubkey)?;
137    }
138    Ok(())
139}
140
141/// Custom validator for array of signatures
142pub fn validate_signatures_array(signatures: &[String]) -> Result<(), validator::ValidationError> {
143    for signature in signatures {
144        validate_signature(signature)?;
145    }
146    Ok(())
147}
148
149/// Custom validator for airdrop amounts
150pub fn validate_airdrop_amount(lamports: u64) -> Result<(), validator::ValidationError> {
151    validate_lamports(lamports)?;
152
153    // Additional airdrop-specific validation
154    if lamports > MAX_AIRDROP_AMOUNT {
155        return Err(validator::ValidationError::new("airdrop_amount_too_large"));
156    }
157
158    if lamports == 0 {
159        return Err(validator::ValidationError::new("airdrop_amount_zero"));
160    }
161
162    Ok(())
163}
164
165/// Custom validator for airdrop amounts (i64 version)
166pub fn validate_airdrop_amount_i64(lamports: i64) -> Result<(), validator::ValidationError> {
167    // Check for negative values
168    if lamports < 0 {
169        return Err(validator::ValidationError::new("airdrop_amount_negative"));
170    }
171
172    // Check for zero
173    if lamports == 0 {
174        return Err(validator::ValidationError::new("airdrop_amount_zero"));
175    }
176
177    // Convert to u64 for other validations
178    let lamports_u64 = lamports as u64;
179    validate_lamports(lamports_u64)?;
180
181    // Additional airdrop-specific validation
182    if lamports_u64 > MAX_AIRDROP_AMOUNT {
183        return Err(validator::ValidationError::new("airdrop_amount_too_large"));
184    }
185
186    Ok(())
187}
188
189/// Validate signature limit (1-1000)
190pub fn validate_signature_limit(limit: &u16) -> Result<(), validator::ValidationError> {
191    if *limit == 0 {
192        return Err(validator::ValidationError::new("limit_must_be_positive"));
193    }
194    if *limit > MAX_PAGINATION_LIMIT as u16 {
195        return Err(validator::ValidationError::new("limit_exceeds_maximum"));
196    }
197    Ok(())
198}
199
200/// Custom validator for transaction data based on encoding
201pub fn validate_transaction_data(transaction: &str) -> Result<(), validator::ValidationError> {
202    // Empty strings are handled by the length validator, so skip custom validation for them
203    if transaction.is_empty() {
204        return Ok(());
205    }
206
207    // Validate the encoding format - try base64 first (most common)
208    if validate_base64(transaction).is_ok() {
209        // Now validate that the decoded data can be parsed as a transaction
210        return validate_transaction_structure_base64(transaction);
211    }
212
213    // If base64 fails, try base58
214    if validate_base58(transaction).is_ok() {
215        return validate_transaction_structure_base58(transaction);
216    }
217
218    // If neither encoding works, return error
219    Err(validator::ValidationError::new(
220        "invalid_transaction_encoding",
221    ))
222}
223
224/// Validate that base64 encoded data represents a valid transaction structure
225fn validate_transaction_structure_base64(
226    transaction: &str,
227) -> Result<(), validator::ValidationError> {
228    use fastcrypto::encoding::{Base64, Encoding};
229
230    // Decode the base64 data
231    let decoded = Base64::decode(transaction)
232        .map_err(|_| validator::ValidationError::new("invalid_base64_transaction"))?;
233
234    validate_transaction_bytes(&decoded)
235}
236
237/// Validate that base58 encoded data represents a valid transaction structure  
238fn validate_transaction_structure_base58(
239    transaction: &str,
240) -> Result<(), validator::ValidationError> {
241    use fastcrypto::encoding::{Base58, Encoding};
242
243    // Decode the base58 data
244    let decoded = Base58::decode(transaction)
245        .map_err(|_| validator::ValidationError::new("invalid_base58_transaction"))?;
246
247    validate_transaction_bytes(&decoded)
248}
249
250/// Validate the raw transaction bytes represent a valid transaction structure
251fn validate_transaction_bytes(transaction_bytes: &[u8]) -> Result<(), validator::ValidationError> {
252    // Basic size validation - transactions should be at least some minimum size
253    if transaction_bytes.len() < MIN_TRANSACTION_SIZE {
254        return Err(validator::ValidationError::new("transaction_too_small"));
255    }
256
257    // Maximum transaction size check (Solana has a 1232 byte limit)
258    if transaction_bytes.len() > MAX_TRANSACTION_SIZE {
259        return Err(validator::ValidationError::new("transaction_too_large"));
260    }
261
262    // Try to deserialize as a VersionedTransaction to validate structure
263    match bincode::deserialize::<rialo_s_sdk::transaction::VersionedTransaction>(transaction_bytes)
264    {
265        Ok(_) => Ok(()),
266        Err(_) => {
267            // If VersionedTransaction fails, try legacy Transaction format
268            match bincode::deserialize::<rialo_s_sdk::transaction::Transaction>(transaction_bytes) {
269                Ok(_) => Ok(()),
270                Err(_) => Err(validator::ValidationError::new(
271                    "invalid_transaction_structure",
272                )),
273            }
274        }
275    }
276}
277
278/// Custom validator for limit as string (some endpoints use string format)
279pub fn validate_limit_string(limit: &str) -> Result<(), validator::ValidationError> {
280    let limit_val: u64 = limit
281        .parse()
282        .map_err(|_| validator::ValidationError::new("invalid_limit_format"))?;
283    validate_limit(&limit_val)?;
284    Ok(())
285}
286
287/// Validate blockhash format (should be valid base58)
288pub fn validate_blockhash(blockhash: &str) -> Result<(), validator::ValidationError> {
289    // Solana blockhashes are base58 encoded and should be 32 bytes (44 characters in base58)
290    if blockhash.len() < MIN_BLOCKHASH_LENGTH || blockhash.len() > MAX_BLOCKHASH_LENGTH {
291        return Err(validator::ValidationError::new("invalid_blockhash_length"));
292    }
293    validate_base58(blockhash)
294}
295
296/// Validate array of addresses (public keys)
297pub fn validate_addresses(addresses: &[String]) -> Result<(), validator::ValidationError> {
298    for address in addresses {
299        validate_pubkey(address)?;
300    }
301    Ok(())
302}
303
304/// Validate array of signatures
305pub fn validate_signatures(signatures: &[String]) -> Result<(), validator::ValidationError> {
306    for signature in signatures {
307        validate_signature(signature)?;
308    }
309    Ok(())
310}
311
312/// Validate encoding format
313pub fn validate_encoding(encoding: &str) -> Result<(), validator::ValidationError> {
314    match encoding {
315        "json" | "jsonParsed" | "base58" | "base64" => Ok(()),
316        _ => Err(validator::ValidationError::new("invalid_encoding_format")),
317    }
318}
319
320/// Validate max transaction version
321pub fn validate_max_transaction_version(version: &u8) -> Result<(), validator::ValidationError> {
322    if *version <= 1 {
323        Ok(())
324    } else {
325        Err(validator::ValidationError::new(
326            "invalid_max_transaction_version",
327        ))
328    }
329}
330
331/// Validation middleware that validates a request
332pub fn validate_request<T>(request: T) -> ValidationResult<T>
333where
334    T: validator::Validate,
335{
336    request.validate().map_err(ValidationError::from)?;
337    Ok(request)
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_validate_limit() {
346        assert!(validate_limit(&1).is_ok());
347        assert!(validate_limit(&MAX_PAGINATION_LIMIT).is_ok());
348        assert!(validate_limit(&0).is_err());
349        assert!(validate_limit(&(MAX_PAGINATION_LIMIT + 1)).is_err());
350    }
351
352    #[test]
353    fn test_validate_nonce() {
354        assert!(validate_nonce("valid_nonce").is_ok());
355        assert!(validate_nonce("").is_err());
356        let long_nonce = "x".repeat(65);
357        assert!(validate_nonce(&long_nonce).is_err());
358    }
359
360    #[test]
361    fn test_validate_encoding() {
362        assert!(validate_encoding("json").is_ok());
363        assert!(validate_encoding("jsonParsed").is_ok());
364        assert!(validate_encoding("base58").is_ok());
365        assert!(validate_encoding("base64").is_ok());
366        assert!(validate_encoding("invalid").is_err());
367    }
368
369    #[test]
370    fn test_validate_max_transaction_version() {
371        assert!(validate_max_transaction_version(&0).is_ok());
372        assert!(validate_max_transaction_version(&1).is_ok());
373        assert!(validate_max_transaction_version(&2).is_err());
374    }
375}