switchboard-on-demand 0.10.2

A Rust library to interact with the Switchboard Solana program.
Documentation
//! Packed feed information data structures for oracle quotes
//!
//! This module defines zero-copy data structures for efficiently storing and accessing
//! oracle feed data within quotes. The structures use `#[repr(packed)]` to ensure
//! consistent memory layout across platforms and minimize space usage.

use faster_hex::hex_string;
use rust_decimal::prelude::*;

use crate::prelude::*;

/// Packed quote header containing the signed slot hash
///
/// This header is signed by all oracles in the quote and contains the slot hash
/// that is used to validate the quote's freshness against the slot hash sysvar.
///
/// Size: 32 bytes
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(packed)]
pub struct PackedQuoteHeader {
    /// The 32-byte slot hash that was signed by all oracles in the quote
    pub signed_slothash: [u8; 32],
}

// Custom Borsh implementation for packed struct
impl borsh::BorshSerialize for PackedQuoteHeader {
    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
        // Read values without taking references (safe for packed structs)
        let slothash = self.signed_slothash;
        writer.write_all(&slothash)?;
        Ok(())
    }
}

impl borsh::BorshDeserialize for PackedQuoteHeader {
    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
        let mut signed_slothash = [0u8; 32];
        reader.read_exact(&mut signed_slothash)?;
        Ok(Self { signed_slothash })
    }
}

#[cfg(feature = "anchor")]
impl anchor_lang::AnchorDeserialize for PackedQuoteHeader {
    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
        <Self as borsh::BorshDeserialize>::deserialize_reader(reader)
    }
}

#[cfg(feature = "anchor")]
impl anchor_lang::AnchorSerialize for PackedQuoteHeader {
    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
        <Self as borsh::BorshSerialize>::serialize(self, writer)
    }
}

#[cfg(feature = "idl-build")]
impl anchor_lang::IdlBuild for PackedQuoteHeader {}

/// Packed feed information containing ID, value, and validation requirements
///
/// This structure stores individual feed data within a quote. The layout is optimized
/// for compatibility with JavaScript serialization, with the feed ID first, followed
/// by the value and minimum sample requirement.
///
/// Size: 49 bytes (32 + 16 + 1)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(packed)]
pub struct PackedFeedInfo {
    /// 32-byte unique identifier for this feed
    pub feed_id: [u8; 32],
    /// Feed value as a fixed-point integer (scaled by PRECISION)
    pub feed_value: i128,
    /// Minimum number of oracle samples required for this feed to be considered valid
    pub min_oracle_samples: u8,
}

// Custom Borsh implementation for packed struct
impl borsh::BorshSerialize for PackedFeedInfo {
    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
        // Read values without taking references (safe for packed structs)
        let feed_id = self.feed_id;
        let feed_value = self.feed_value;
        let min_oracle_samples = self.min_oracle_samples;

        writer.write_all(&feed_id)?;
        writer.write_all(&feed_value.to_le_bytes())?;
        writer.write_all(&[min_oracle_samples])?;
        Ok(())
    }
}

impl borsh::BorshDeserialize for PackedFeedInfo {
    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
        let mut feed_id = [0u8; 32];
        reader.read_exact(&mut feed_id)?;

        let mut value_bytes = [0u8; 16];
        reader.read_exact(&mut value_bytes)?;
        let feed_value = i128::from_le_bytes(value_bytes);

        let mut min_samples = [0u8; 1];
        reader.read_exact(&mut min_samples)?;
        let min_oracle_samples = min_samples[0];

        Ok(Self {
            feed_id,
            feed_value,
            min_oracle_samples,
        })
    }
}

#[cfg(feature = "anchor")]
impl anchor_lang::AnchorDeserialize for PackedFeedInfo {
    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
        <Self as borsh::BorshDeserialize>::deserialize_reader(reader)
    }
}

#[cfg(feature = "anchor")]
impl anchor_lang::AnchorSerialize for PackedFeedInfo {
    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
        <Self as borsh::BorshSerialize>::serialize(self, writer)
    }
}

#[cfg(feature = "idl-build")]
impl anchor_lang::IdlBuild for PackedFeedInfo {}

impl PackedFeedInfo {
    /// The size in bytes of this packed structure
    pub const PACKED_SIZE: usize = 49;

    /// Returns a reference to the 32-byte feed ID
    #[inline(always)]
    pub fn feed_id(&self) -> &[u8; 32] {
        &self.feed_id
    }

    /// Returns the feed ID as a hexadecimal string with "0x" prefix
    ///
    /// This is useful for displaying feed IDs in a human-readable format
    /// or for use in APIs that expect hex-encoded identifiers.
    #[inline(always)]
    pub fn hex_id(&self) -> String {
        String::from("0x") + &hex_string(self.feed_id())
    }

    /// Returns the raw feed value as a fixed-point integer
    ///
    /// This value is scaled by the program-wide `PRECISION` constant.
    /// Use [`value()`](Self::value) to get the human-readable decimal form.
    #[inline(always)]
    pub fn feed_value(&self) -> i128 {
        self.feed_value
    }

    /// Returns the feed value as a `Decimal`, scaled using the program-wide `PRECISION`.
    ///
    /// This converts the raw fixed-point integer into a human-readable decimal form.
    /// For example, if the raw value is 115525650000000000000000 and PRECISION is 18,
    /// this will return approximately 115525.65.
    #[inline(always)]
    pub fn value(&self) -> Decimal {
        Decimal::from_i128_with_scale(self.feed_value(), PRECISION).normalize()
    }

    /// Returns the minimum number of oracle samples required for this feed
    #[inline(always)]
    pub fn min_oracle_samples(&self) -> u8 {
        self.min_oracle_samples
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn test_packed_feed_info_layout() {
        // Simulate JavaScript serialized data: feed_id (32) + feed_value (16) + min_oracle_samples (1)
        let mut data = [0u8; 49];

        // Feed ID (32 bytes) - some test hash
        data[0..32].fill(0x42);

        // Feed value at offset 32: 115525650000000000000000 (i128, scaled by 18 decimals)
        // This should decode to approximately 115525.65
        let feed_value_scaled: i128 = 115525650000000000000000i128;
        let value_bytes = feed_value_scaled.to_le_bytes();
        data[32..48].copy_from_slice(&value_bytes);

        // Min oracle samples (1 byte) at offset 48
        data[48] = 5;

        // Cast to PackedFeedInfo
        let feed_info = unsafe { &*(data.as_ptr() as *const PackedFeedInfo) };

        // Test reading the feed value
        let raw_value = feed_info.feed_value();
        println!("Raw feed value: {}", raw_value);
        println!("Expected: {}", feed_value_scaled);
        assert_eq!(raw_value, feed_value_scaled);

        // Test the decimal conversion
        let decimal_value = feed_info.value();
        println!("Decimal value: {}", decimal_value);

        // Test other fields
        assert_eq!(feed_info.min_oracle_samples(), 5);
        assert_eq!(feed_info.feed_id()[0], 0x42);

        println!(
            "✅ Test passed! Feed value reads correctly: {}",
            decimal_value
        );
    }
}