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

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

use super::rpc_response_context::RpcResponseContext;

/// The response message for a getTransaction request.
/// Solana specification: <https://solana.com/docs/rpc/http/gettransaction>
#[derive(Serialize, Deserialize, Debug)]
pub struct GetTransactionResponse {
    #[serde(default)]
    pub version: u16,
    pub context: RpcResponseContext,

    /// The slot this transaction was processed in.
    pub block_height: u64,

    /// Estimated production time, as Unix timestamp (seconds since the Unix epoch) of when the
    /// transaction was processed. None if not available.
    pub block_time: Option<i64>,

    /// The transaction data object.
    pub transaction: Transaction,

    /// The transaction status metadata object.
    pub meta: TransactionStatusMetadata,
}

/// The transaction object data.
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
    /// A list of signatures applied to the transaction in base58.
    pub signatures: Vec<String>,
    /// The content of the transaction.
    pub message: TransactionMessage,
    /// Transaction valid from (milliseconds since Unix epoch).
    pub valid_from: i64,
}

/// The transaction message object.
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TransactionMessage {
    /// List of public keys used by the transaction.
    /// The first `message.header.num_required_signatures` public keys must sign the transaction.
    pub account_keys: Vec<String>,
    /// Details the account types and signatures required by the transaction.
    pub header: MessageHeader,
    ///  List of program instructions that will be executed in the transaction.
    pub instructions: Vec<Instruction>,
}

/// The transaction header object.
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessageHeader {
    /// The total number of signatures required to make the transaction valid.
    pub num_required_signatures: u8,
    /// The last `num_readonly_signed_accounts` of the signed keys are read-only accounts.
    pub num_readonly_signed_accounts: u8,
    ///  The last `num_readonly_unsigned_accounts` of the unsigned keys are read-only accounts.
    pub num_readonly_unsigned_accounts: u8,
}

/// The transaction instruction object.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Instruction {
    /// List of ordered indices into the message.account_keys array indicating which accounts to
    /// pass to the program.
    pub accounts: Vec<u8>,
    /// The program input data encoded in a base-58 string.
    pub data: String,
    /// Index into the message.account_keys array indicating the program account that executes this
    /// instruction.
    pub program_id_index: u8,
}

/// The transaction status metadata.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionStatusMetadata {
    /// Error if transaction failed, None if transaction succeeded.
    pub err: Option<serde_json::Value>,
    /// Fee this transaction was charged, as u64 integer.
    pub fee: u64,
    /// Array of string log messages or None if log message recording was not enabled during
    /// this transaction
    pub log_messages: Option<Vec<String>>,
    /// Inner instructions recorded during execution, grouped by top-level instruction index.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub inner_instructions: Option<Vec<InnerInstructionEntry>>,
    /// Compute units consumed by this transaction, if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub compute_units_consumed: Option<u64>,
}

/// Inner instruction entry paired with the top-level instruction index that produced it.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct InnerInstructionEntry {
    pub instruction_index: u8,
    pub instruction: Instruction,
}

/// Request for getTransaction RPC call
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Validate)]
pub struct GetTransactionRequest {
    #[serde(default)]
    #[validate(custom(function = crate::validation::validate_protocol_version))]
    pub version: u16,
    /// Transaction signature (base58-encoded)
    #[validate(length(min = 1, message = "Signature cannot be empty"))]
    #[validate(custom(function = crate::validation::validate_signature))]
    pub signature: String,
}

impl GetTransactionRequest {
    pub fn new(signature: String) -> Self {
        Self {
            version: 0,
            signature,
        }
    }
}

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

    fn sample_metadata() -> TransactionStatusMetadata {
        TransactionStatusMetadata {
            err: None,
            fee: 5_000,
            log_messages: Some(vec!["Program log: invoke".to_string()]),
            inner_instructions: Some(vec![InnerInstructionEntry {
                instruction_index: 0,
                instruction: Instruction {
                    accounts: vec![0, 1],
                    data: "abc".to_string(),
                    program_id_index: 2,
                },
            }]),
            compute_units_consumed: Some(1_234),
        }
    }

    #[test]
    fn transaction_status_metadata_serializes_as_camelcase() {
        let meta = sample_metadata();
        let value = serde_json::to_value(&meta).expect("serialize");
        let obj = value.as_object().expect("object");

        // camelCase keys must be present.
        assert!(
            obj.contains_key("logMessages"),
            "missing logMessages: {obj:?}"
        );
        assert!(
            obj.contains_key("innerInstructions"),
            "missing innerInstructions: {obj:?}"
        );
        assert!(
            obj.contains_key("computeUnitsConsumed"),
            "missing computeUnitsConsumed: {obj:?}"
        );

        // snake_case keys must NOT leak through.
        assert!(!obj.contains_key("log_messages"));
        assert!(!obj.contains_key("inner_instructions"));
        assert!(!obj.contains_key("compute_units_consumed"));
    }

    #[test]
    fn transaction_status_metadata_survives_round_trip() {
        let original = sample_metadata();
        let value = serde_json::to_value(&original).expect("serialize");
        let round_tripped: TransactionStatusMetadata =
            serde_json::from_value(value).expect("deserialize");

        assert_eq!(round_tripped.err, original.err);
        assert_eq!(round_tripped.fee, original.fee);
        assert_eq!(round_tripped.log_messages, original.log_messages);
        assert_eq!(
            round_tripped.compute_units_consumed,
            original.compute_units_consumed
        );
        assert_eq!(
            round_tripped.inner_instructions.as_ref().map(|v| v.len()),
            original.inner_instructions.as_ref().map(|v| v.len())
        );
    }

    #[test]
    fn transaction_status_metadata_deserializes_camelcase_wire_format() {
        // The Solana JSON-RPC wire format uses camelCase for the metadata-
        // level fields (logMessages, innerInstructions, computeUnitsConsumed).
        let wire = serde_json::json!({
            "err": null,
            "fee": 5_000,
            "logMessages": ["Program log: invoke"],
            "innerInstructions": [],
            "computeUnitsConsumed": 1_234
        });

        let meta: TransactionStatusMetadata =
            serde_json::from_value(wire).expect("parse camelCase wire format");

        assert_eq!(meta.fee, 5_000);
        assert_eq!(
            meta.log_messages.as_deref(),
            Some(["Program log: invoke".to_string()].as_slice())
        );
        assert_eq!(meta.compute_units_consumed, Some(1_234));
        assert_eq!(meta.inner_instructions.as_ref().map(|v| v.len()), Some(0));
    }

    #[test]
    fn instruction_serializes_as_camelcase() {
        let ix = Instruction {
            accounts: vec![0, 1],
            data: "abc".to_string(),
            program_id_index: 2,
        };
        let value = serde_json::to_value(&ix).expect("serialize");
        let obj = value.as_object().expect("object");

        assert!(
            obj.contains_key("programIdIndex"),
            "Instruction must serialize program_id_index as programIdIndex to match \
             the Solana JSON-RPC wire format and the CDK Value bridge reader at \
             conversions.rs:142; got {obj:?}"
        );
        assert!(
            !obj.contains_key("program_id_index"),
            "snake_case key must not leak: {obj:?}"
        );
    }

    #[test]
    fn message_header_serializes_as_camelcase() {
        let header = MessageHeader {
            num_required_signatures: 1,
            num_readonly_signed_accounts: 0,
            num_readonly_unsigned_accounts: 1,
        };
        let value = serde_json::to_value(&header).expect("serialize");
        let obj = value.as_object().expect("object");

        assert!(obj.contains_key("numRequiredSignatures"), "{obj:?}");
        assert!(obj.contains_key("numReadonlySignedAccounts"), "{obj:?}");
        assert!(obj.contains_key("numReadonlyUnsignedAccounts"), "{obj:?}");
        assert!(!obj.contains_key("num_required_signatures"));
        assert!(!obj.contains_key("num_readonly_signed_accounts"));
        assert!(!obj.contains_key("num_readonly_unsigned_accounts"));
    }

    #[test]
    fn transaction_message_serializes_as_camelcase() {
        let msg = TransactionMessage {
            account_keys: vec!["k1".to_string(), "k2".to_string()],
            header: MessageHeader {
                num_required_signatures: 1,
                num_readonly_signed_accounts: 0,
                num_readonly_unsigned_accounts: 1,
            },
            instructions: vec![Instruction {
                accounts: vec![0, 1],
                data: "abc".to_string(),
                program_id_index: 2,
            }],
        };
        let value = serde_json::to_value(&msg).expect("serialize");
        let obj = value.as_object().expect("object");

        assert!(obj.contains_key("accountKeys"), "{obj:?}");
        assert!(!obj.contains_key("account_keys"));

        // Nested instruction must also be camelCase.
        let instructions = obj
            .get("instructions")
            .and_then(|v| v.as_array())
            .expect("instructions array");
        let first = instructions
            .first()
            .and_then(|v| v.as_object())
            .expect("first instruction object");
        assert!(
            first.contains_key("programIdIndex"),
            "nested Instruction must still serialize as camelCase: {first:?}"
        );
    }

    #[test]
    fn transaction_serializes_valid_from_as_camelcase() {
        let tx = Transaction {
            signatures: vec!["sig".to_string()],
            message: TransactionMessage {
                account_keys: vec![],
                header: MessageHeader {
                    num_required_signatures: 0,
                    num_readonly_signed_accounts: 0,
                    num_readonly_unsigned_accounts: 0,
                },
                instructions: vec![],
            },
            valid_from: 1_700_000_000,
        };
        let value = serde_json::to_value(&tx).expect("serialize");
        let obj = value.as_object().expect("object");

        assert!(
            obj.contains_key("validFrom"),
            "Transaction must serialize valid_from as validFrom to match the CDK \
             Value bridge reader at conversions.rs:259; got {obj:?}"
        );
        assert!(
            !obj.contains_key("valid_from"),
            "snake_case key must not leak: {obj:?}"
        );
    }

    #[test]
    fn inner_instructions_deserialize_populated_camelcase_wire() {
        // Full camelCase wire shape including the nested Instruction fields
        // (programIdIndex). Exercises the end-to-end deserializer path the
        // CDK relies on when it roundtrips an api-type `TransactionStatusMetadata`
        // through `serde_json::Value`.
        let wire = serde_json::json!({
            "err": null,
            "fee": 5_000,
            "logMessages": ["Program log: invoke"],
            "innerInstructions": [{
                "instructionIndex": 0,
                "instruction": {
                    "accounts": [0, 1],
                    "data": "abc",
                    "programIdIndex": 2
                }
            }],
            "computeUnitsConsumed": 1_234
        });

        let meta: TransactionStatusMetadata =
            serde_json::from_value(wire).expect("parse populated camelCase wire format");

        let inner = meta
            .inner_instructions
            .as_ref()
            .expect("innerInstructions present");
        assert_eq!(inner.len(), 1);
        assert_eq!(inner[0].instruction_index, 0);
        assert_eq!(inner[0].instruction.program_id_index, 2);
        assert_eq!(inner[0].instruction.accounts, vec![0, 1]);
        assert_eq!(inner[0].instruction.data, "abc");
    }
}