rialo-api-types 0.8.0-alpha.0

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

//! Types and validation for the `getTransactions` RPC method.
//!
//! This module provides request/response structures and validation logic for retrieving
//! transactions from the blockchain. The API supports pagination and different encoding formats.

use serde::{Deserialize, Serialize};
use validator::Validate;

use super::rpc_response_context::RpcResponseContext;
use crate::validation::{
    validate_encoding, validate_max_transaction_version, validate_signature_limit,
};

/// Request parameters for the `getTransactions` RPC method
#[derive(Debug, Deserialize, Serialize, Clone, Validate)]
pub struct GetTransactionsRequest {
    /// Protocol version for forward compatibility.
    #[serde(default)]
    #[validate(custom(function = crate::validation::validate_protocol_version))]
    pub version: u16,
    /// Optional configuration for the request
    #[validate(nested)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub config: Option<GetTransactionsConfig>,
}

/// Configuration options for GetTransactions request
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)]
#[validate(schema(function = validate_transactions_config))]
pub struct GetTransactionsConfig {
    /// Maximum number of transactions to return (1-1000, default: 100)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<u16>,

    /// Start searching backward from this transaction signature
    #[serde(skip_serializing_if = "Option::is_none")]
    pub before: Option<String>,

    /// Encoding format ("json", "jsonParsed", "base58", "base64", default: "json")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub encoding: Option<String>,

    /// Maximum transaction version to return
    #[serde(rename = "maxSupportedTransactionVersion")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_supported_transaction_version: Option<u8>,
}

/// Custom validation function for GetTransactionsConfig
fn validate_transactions_config(
    config: &GetTransactionsConfig,
) -> Result<(), validator::ValidationError> {
    // Validate limit if present
    if let Some(limit) = config.limit {
        validate_signature_limit(&limit)?;
    }

    // Validate before signature if present
    if let Some(ref before) = config.before {
        crate::validation::validate_signature(before)?;
    }

    // Validate encoding if present
    if let Some(ref encoding) = config.encoding {
        validate_encoding(encoding)?;
    }

    // Validate max transaction version if present
    if let Some(ref max_version) = config.max_supported_transaction_version {
        validate_max_transaction_version(max_version)?;
    }

    Ok(())
}

/// The response message for a GetTransactions request
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetTransactionsResponse {
    pub context: RpcResponseContext,
    pub value: Vec<TransactionInfo>,
}

impl GetTransactionsResponse {
    pub fn new(value: Vec<TransactionInfo>, slot: u64) -> Self {
        Self {
            context: RpcResponseContext::new(slot),
            value,
        }
    }
}

/// Information about a transaction
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionInfo {
    /// The transaction signature (base58-encoded)
    pub signature: String,

    /// The slot in which the transaction was processed
    pub slot: u64,

    /// The block height in which the transaction was processed
    pub block_height: u64,

    /// Block time (Unix timestamp in milliseconds) when the transaction was processed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub block_time: Option<i64>,

    /// Error if transaction failed, null if transaction succeeded
    #[serde(skip_serializing_if = "Option::is_none")]
    pub err: Option<serde_json::Value>,

    /// Transaction memo
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memo: Option<String>,

    /// Transaction version
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
}

impl TransactionInfo {
    /// Create a new TransactionInfo with required fields
    pub fn new(signature: String, slot: u64) -> Self {
        Self {
            signature,
            slot,
            block_height: slot,
            block_time: None,
            err: None,
            memo: None,
            version: Some("legacy".to_string()),
        }
    }

    /// Set the block time for this transaction
    pub fn with_block_time(mut self, block_time: i64) -> Self {
        self.block_time = Some(block_time);
        self
    }

    /// Set the error for this transaction
    pub fn with_error(mut self, error: serde_json::Value) -> Self {
        self.err = Some(error);
        self
    }

    /// Set the memo for this transaction
    pub fn with_memo(mut self, memo: String) -> Self {
        self.memo = Some(memo);
        self
    }

    /// Set the version for this transaction
    pub fn with_version(mut self, version: String) -> Self {
        self.version = Some(version);
        self
    }
}

impl Default for GetTransactionsConfig {
    fn default() -> Self {
        Self {
            limit: Some(100),
            before: None,
            encoding: Some("json".to_string()),
            max_supported_transaction_version: Some(0),
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::{from_str, to_string};

    use super::*;

    #[test]
    fn test_get_transactions_request_serialization() {
        let request = GetTransactionsRequest {
            version: 0,
            config: Some(GetTransactionsConfig {
                limit: Some(10),
                before: Some("5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW".to_string()),
                encoding: Some("json".to_string()),
                max_supported_transaction_version: Some(0),
            }),
        };

        let json = to_string(&request).unwrap();
        let deserialized: GetTransactionsRequest = from_str(&json).unwrap();

        assert_eq!(
            request.config.as_ref().unwrap().limit,
            deserialized.config.as_ref().unwrap().limit
        );
    }

    #[test]
    fn test_transaction_info_creation() {
        let signature = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW".to_string();
        let slot = 12345u64;

        let info = TransactionInfo::new(signature.clone(), slot).with_block_time(1640995200000); // Updated to milliseconds

        assert_eq!(info.signature, signature);
        assert_eq!(info.slot, slot);
        assert_eq!(info.block_height, slot); // New assertion
        assert_eq!(info.block_time, Some(1640995200000)); // Updated to milliseconds
    }

    #[test]
    fn test_minimal_request() {
        let request = GetTransactionsRequest {
            version: 0,
            config: None,
        };

        let json = to_string(&request).unwrap();
        let expected = r#"{"version":0}"#;
        assert_eq!(json, expected);
    }

    #[test]
    fn test_default_config() {
        let config = GetTransactionsConfig::default();

        assert_eq!(config.limit, Some(100));
        assert_eq!(config.encoding, Some("json".to_string()));
        assert_eq!(config.max_supported_transaction_version, Some(0));
        assert!(config.before.is_none());
    }

    #[test]
    fn test_config_validation_valid() {
        use crate::validation::validate_request;

        let config = GetTransactionsConfig {
            limit: Some(500),
            before: Some("5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW".to_string()),
            encoding: Some("json".to_string()),
            max_supported_transaction_version: Some(0),
        };

        let result = validate_request(config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_config_validation_invalid_limit() {
        use crate::validation::validate_request;

        let config = GetTransactionsConfig {
            limit: Some(1001), // Invalid limit
            before: None,
            encoding: Some("json".to_string()),
            max_supported_transaction_version: Some(0),
        };

        let result = validate_request(config);
        assert!(result.is_err());
    }

    #[test]
    fn test_config_validation_invalid_encoding() {
        use crate::validation::validate_request;

        let config = GetTransactionsConfig {
            limit: Some(100),
            before: None,
            encoding: Some("invalid".to_string()), // Invalid encoding
            max_supported_transaction_version: Some(0),
        };

        let result = validate_request(config);
        assert!(result.is_err());
    }

    #[test]
    fn test_response_creation() {
        let transactions = vec![
            TransactionInfo::new(
                "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW".to_string(),
                12345,
            ),
            TransactionInfo::new(
                "3Bxs9A1vAVi8n1hQrkVK7pn94BAmFWPKKDtkK5WwRXkacPZKqVm6gmnG6WtQReajvQZ9bdXLvEEsVJgS9zqjUBxG".to_string(),
                12346,
            ),
        ];

        let response = GetTransactionsResponse::new(transactions, 12500);

        assert_eq!(response.value.len(), 2);
        assert_eq!(response.context.slot, 12500);
        assert_eq!(response.value[0].signature, "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW");
        assert_eq!(response.value[1].signature, "3Bxs9A1vAVi8n1hQrkVK7pn94BAmFWPKKDtkK5WwRXkacPZKqVm6gmnG6WtQReajvQZ9bdXLvEEsVJgS9zqjUBxG");
    }
}