rialo-api-types 0.1.0

API types for Rialo RPC endpoints
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Validation for API request types

use std::str::FromStr;

use rialo_s_sdk::pubkey::Pubkey;
use thiserror::Error;
use validator::ValidationErrors;

use crate::constants::*;

/// Validation error types for RPC requests
#[derive(Debug, Error, Clone)]
pub enum ValidationError {
    #[error("Invalid format: {0}")]
    InvalidFormat(String),

    #[error("Value out of range: {0}")]
    OutOfRange(String),

    #[error("Missing required field: {0}")]
    MissingField(String),

    #[error("Invalid signature: {0}")]
    InvalidSignature(String),

    #[error("Invalid encoding: {0}. Supported encodings: base64, base58")]
    InvalidEncoding(String),

    #[error("Invalid public key: {0}")]
    InvalidPublicKey(String),

    #[error("Invalid transaction: {0}")]
    InvalidTransaction(String),

    #[error("Multiple validation errors: {0}")]
    Multiple(String),
}

impl From<ValidationErrors> for ValidationError {
    fn from(errors: ValidationErrors) -> Self {
        let error_messages: Vec<String> = errors
            .field_errors()
            .iter()
            .flat_map(|(field, errors)| {
                errors.iter().map(move |error| {
                    format!(
                        "{}: {}",
                        field,
                        error
                            .message
                            .as_ref()
                            .unwrap_or(&"validation failed".into())
                    )
                })
            })
            .collect();

        if error_messages.len() == 1 {
            ValidationError::InvalidFormat(error_messages[0].clone())
        } else {
            ValidationError::Multiple(error_messages.join(", "))
        }
    }
}

/// Result type for validation operations
pub type ValidationResult<T> = Result<T, ValidationError>;

/// Validate Solana public key format
pub fn validate_pubkey(pubkey: &str) -> Result<(), validator::ValidationError> {
    Pubkey::from_str(pubkey).map_err(|_| validator::ValidationError::new("invalid_pubkey"))?;
    Ok(())
}

/// Validate base64 encoded data
pub fn validate_base64(data: &str) -> Result<(), validator::ValidationError> {
    use fastcrypto::encoding::{Base64, Encoding};
    Base64::decode(data).map_err(|_| validator::ValidationError::new("invalid_base64"))?;
    Ok(())
}

/// Validate base58 encoded data
pub fn validate_base58(data: &str) -> Result<(), validator::ValidationError> {
    use fastcrypto::encoding::{Base58, Encoding};
    Base58::decode(data).map_err(|_| validator::ValidationError::new("invalid_base58"))?;
    Ok(())
}

/// Validate transaction signature
pub fn validate_signature(signature: &str) -> Result<(), validator::ValidationError> {
    // Solana signatures are base58 encoded and should be 87 or 88 characters long
    // (87 when there are leading zero bytes that get omitted in base58 encoding)
    if signature.len() < MIN_SIGNATURE_LENGTH || signature.len() > MAX_SIGNATURE_LENGTH {
        return Err(validator::ValidationError::new("invalid_signature_length"));
    }
    validate_base58(signature)
}

/// Validate nonce format (should be valid UTF-8 and reasonable length)
pub fn validate_nonce(nonce: &str) -> Result<(), validator::ValidationError> {
    if nonce.is_empty() {
        return Err(validator::ValidationError::new("empty_nonce"));
    }
    if nonce.len() > MAX_NONCE_LENGTH {
        return Err(validator::ValidationError::new("nonce_too_long"));
    }
    Ok(())
}

/// Validate lamports amount (should be reasonable)
pub fn validate_lamports(lamports: u64) -> Result<(), validator::ValidationError> {
    // Validate against maximum possible lamports (500 million SOL * 1e9 lamports/SOL)
    if lamports > MAX_LAMPORTS {
        return Err(validator::ValidationError::new("lamports_too_large"));
    }
    Ok(())
}

/// Validate limit parameter for paginated requests
pub fn validate_limit(limit: &u64) -> Result<(), validator::ValidationError> {
    if *limit == 0 {
        return Err(validator::ValidationError::new("limit_zero"));
    }
    if *limit > MAX_PAGINATION_LIMIT {
        return Err(validator::ValidationError::new("limit_too_large"));
    }
    Ok(())
}

/// Custom validator for array of public keys
pub fn validate_pubkey_array(pubkeys: &[String]) -> Result<(), validator::ValidationError> {
    for pubkey in pubkeys {
        validate_pubkey(pubkey)?;
    }
    Ok(())
}

/// Custom validator for array of signatures
pub fn validate_signatures_array(signatures: &[String]) -> Result<(), validator::ValidationError> {
    for signature in signatures {
        validate_signature(signature)?;
    }
    Ok(())
}

/// Custom validator for airdrop amounts
pub fn validate_airdrop_amount(lamports: u64) -> Result<(), validator::ValidationError> {
    validate_lamports(lamports)?;

    // Additional airdrop-specific validation
    if lamports > MAX_AIRDROP_AMOUNT {
        return Err(validator::ValidationError::new("airdrop_amount_too_large"));
    }

    if lamports == 0 {
        return Err(validator::ValidationError::new("airdrop_amount_zero"));
    }

    Ok(())
}

/// Custom validator for airdrop amounts (i64 version)
pub fn validate_airdrop_amount_i64(lamports: i64) -> Result<(), validator::ValidationError> {
    // Check for negative values
    if lamports < 0 {
        return Err(validator::ValidationError::new("airdrop_amount_negative"));
    }

    // Check for zero
    if lamports == 0 {
        return Err(validator::ValidationError::new("airdrop_amount_zero"));
    }

    // Convert to u64 for other validations
    let lamports_u64 = lamports as u64;
    validate_lamports(lamports_u64)?;

    // Additional airdrop-specific validation
    if lamports_u64 > MAX_AIRDROP_AMOUNT {
        return Err(validator::ValidationError::new("airdrop_amount_too_large"));
    }

    Ok(())
}

/// Validate signature limit (1-1000)
pub fn validate_signature_limit(limit: &u16) -> Result<(), validator::ValidationError> {
    if *limit == 0 {
        return Err(validator::ValidationError::new("limit_must_be_positive"));
    }
    if *limit > MAX_PAGINATION_LIMIT as u16 {
        return Err(validator::ValidationError::new("limit_exceeds_maximum"));
    }
    Ok(())
}

/// Custom validator for transaction data based on encoding
pub fn validate_transaction_data(transaction: &str) -> Result<(), validator::ValidationError> {
    // Empty strings are handled by the length validator, so skip custom validation for them
    if transaction.is_empty() {
        return Ok(());
    }

    // Validate the encoding format - try base64 first (most common)
    if validate_base64(transaction).is_ok() {
        // Now validate that the decoded data can be parsed as a transaction
        return validate_transaction_structure_base64(transaction);
    }

    // If base64 fails, try base58
    if validate_base58(transaction).is_ok() {
        return validate_transaction_structure_base58(transaction);
    }

    // If neither encoding works, return error
    Err(validator::ValidationError::new(
        "invalid_transaction_encoding",
    ))
}

/// Validate that base64 encoded data represents a valid transaction structure
fn validate_transaction_structure_base64(
    transaction: &str,
) -> Result<(), validator::ValidationError> {
    use fastcrypto::encoding::{Base64, Encoding};

    // Decode the base64 data
    let decoded = Base64::decode(transaction)
        .map_err(|_| validator::ValidationError::new("invalid_base64_transaction"))?;

    validate_transaction_bytes(&decoded)
}

/// Validate that base58 encoded data represents a valid transaction structure  
fn validate_transaction_structure_base58(
    transaction: &str,
) -> Result<(), validator::ValidationError> {
    use fastcrypto::encoding::{Base58, Encoding};

    // Decode the base58 data
    let decoded = Base58::decode(transaction)
        .map_err(|_| validator::ValidationError::new("invalid_base58_transaction"))?;

    validate_transaction_bytes(&decoded)
}

/// Validate the raw transaction bytes represent a valid transaction structure
fn validate_transaction_bytes(transaction_bytes: &[u8]) -> Result<(), validator::ValidationError> {
    // Basic size validation - transactions should be at least some minimum size
    if transaction_bytes.len() < MIN_TRANSACTION_SIZE {
        return Err(validator::ValidationError::new("transaction_too_small"));
    }

    // Maximum transaction size check (Solana has a 1232 byte limit)
    if transaction_bytes.len() > MAX_TRANSACTION_SIZE {
        return Err(validator::ValidationError::new("transaction_too_large"));
    }

    // Try to deserialize as a VersionedTransaction to validate structure
    match bincode::deserialize::<rialo_s_sdk::transaction::VersionedTransaction>(transaction_bytes)
    {
        Ok(_) => Ok(()),
        Err(_) => {
            // If VersionedTransaction fails, try legacy Transaction format
            match bincode::deserialize::<rialo_s_sdk::transaction::Transaction>(transaction_bytes) {
                Ok(_) => Ok(()),
                Err(_) => Err(validator::ValidationError::new(
                    "invalid_transaction_structure",
                )),
            }
        }
    }
}

/// Custom validator for limit as string (some endpoints use string format)
pub fn validate_limit_string(limit: &str) -> Result<(), validator::ValidationError> {
    let limit_val: u64 = limit
        .parse()
        .map_err(|_| validator::ValidationError::new("invalid_limit_format"))?;
    validate_limit(&limit_val)?;
    Ok(())
}

/// Validate blockhash format (should be valid base58)
pub fn validate_blockhash(blockhash: &str) -> Result<(), validator::ValidationError> {
    // Solana blockhashes are base58 encoded and should be 32 bytes (44 characters in base58)
    if blockhash.len() < MIN_BLOCKHASH_LENGTH || blockhash.len() > MAX_BLOCKHASH_LENGTH {
        return Err(validator::ValidationError::new("invalid_blockhash_length"));
    }
    validate_base58(blockhash)
}

/// Validate array of addresses (public keys)
pub fn validate_addresses(addresses: &[String]) -> Result<(), validator::ValidationError> {
    for address in addresses {
        validate_pubkey(address)?;
    }
    Ok(())
}

/// Validate array of signatures
pub fn validate_signatures(signatures: &[String]) -> Result<(), validator::ValidationError> {
    for signature in signatures {
        validate_signature(signature)?;
    }
    Ok(())
}

/// Validate encoding format
pub fn validate_encoding(encoding: &str) -> Result<(), validator::ValidationError> {
    match encoding {
        "json" | "jsonParsed" | "base58" | "base64" => Ok(()),
        _ => Err(validator::ValidationError::new("invalid_encoding_format")),
    }
}

/// Validate max transaction version
pub fn validate_max_transaction_version(version: &u8) -> Result<(), validator::ValidationError> {
    if *version <= 1 {
        Ok(())
    } else {
        Err(validator::ValidationError::new(
            "invalid_max_transaction_version",
        ))
    }
}

/// Validation middleware that validates a request
pub fn validate_request<T>(request: T) -> ValidationResult<T>
where
    T: validator::Validate,
{
    request.validate().map_err(ValidationError::from)?;
    Ok(request)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_limit() {
        assert!(validate_limit(&1).is_ok());
        assert!(validate_limit(&MAX_PAGINATION_LIMIT).is_ok());
        assert!(validate_limit(&0).is_err());
        assert!(validate_limit(&(MAX_PAGINATION_LIMIT + 1)).is_err());
    }

    #[test]
    fn test_validate_nonce() {
        assert!(validate_nonce("valid_nonce").is_ok());
        assert!(validate_nonce("").is_err());
        let long_nonce = "x".repeat(65);
        assert!(validate_nonce(&long_nonce).is_err());
    }

    #[test]
    fn test_validate_encoding() {
        assert!(validate_encoding("json").is_ok());
        assert!(validate_encoding("jsonParsed").is_ok());
        assert!(validate_encoding("base58").is_ok());
        assert!(validate_encoding("base64").is_ok());
        assert!(validate_encoding("invalid").is_err());
    }

    #[test]
    fn test_validate_max_transaction_version() {
        assert!(validate_max_transaction_version(&0).is_ok());
        assert!(validate_max_transaction_version(&1).is_ok());
        assert!(validate_max_transaction_version(&2).is_err());
    }
}