switchboard-on-demand 0.1.2

A Rust library to interact with the Switchboard Solana program.
use crate::anchor_traits::*;
use crate::cfg_client;
#[allow(unused_imports)]
use crate::impl_account_deserialize;
use crate::OnDemandError;
use rust_decimal::Decimal;
use sha2::{Digest, Sha256};
use solana_program::account_info::AccountInfo;
use solana_program::clock::Clock;
use solana_program::pubkey::Pubkey;
use std::cell::{Ref, RefMut};

#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PullFeedAccountData {
    /// The oracle submissions for this feed.
    pub submissions: [OracleSubmission; 32],
    /// The public key of the authority that can update the feed hash that
    /// this account will use for registering updates.
    pub authority: Pubkey,
    /// The public key of the queue which oracles must be bound to in order to
    /// submit data to this feed.
    pub queue: Pubkey,
    /// SHA-256 hash of the job schema oracles will execute to produce data
    /// for this feed.
    pub feed_hash: [u8; 32],
    /// The slot at which this account was initialized.
    pub initialized_at: i64,
    pub permissions: u64,
    pub max_variance: u64,
    pub min_responses: u32,
    padding1: [u8; 4],
    pub _ebuf: [u8; 1024],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct OracleSubmission {
    /// The public key of the oracle that submitted this value.
    pub oracle: Pubkey,
    /// The slot at which this value was signed.
    pub slot: u64,
    padding: [u8; 8],
    /// The value that was submitted.
    pub value: i128,
}
impl OracleSubmission {
    pub fn is_empty(&self) -> bool {
        self.slot == 0
    }
}
impl Discriminator for PullFeedAccountData {
    const DISCRIMINATOR: [u8; 8] = [196, 27, 108, 196, 10, 215, 219, 40];
}

cfg_client! {
    impl_account_deserialize!(PullFeedAccountData);
}

impl PullFeedAccountData {
    /// Returns the deserialized Switchboard feed account
    ///
    /// # Arguments
    ///
    /// * `account_info` - A Solana AccountInfo referencing an existing Switchboard PullFeed account
    ///
    /// # Examples
    ///
    /// ```ignore
    /// use switchboard_on_demand::PullFeedAccountData;
    ///
    /// let feed = PullFeedAccountData::new(account_info)?;
    /// ```
    pub fn parse<'info>(
        account_info: &'info AccountInfo<'info>,
    ) -> Result<Ref<'info, Self>, OnDemandError> {
        let data = account_info
            .try_borrow_data()
            .map_err(|_| OnDemandError::AccountBorrowError)?;
        if data.len() < Self::discriminator().len() {
            return Err(OnDemandError::InvalidDiscriminator);
        }

        let mut disc_bytes = [0u8; 8];
        disc_bytes.copy_from_slice(&data[..8]);
        if disc_bytes != Self::discriminator() {
            return Err(OnDemandError::InvalidDiscriminator);
        }

        Ok(Ref::map(data, |data| {
            bytemuck::from_bytes(&data[8..std::mem::size_of::<Self>() + 8])
        }))
    }

    /// Returns the deserialized Switchboard feed account
    ///
    /// # Arguments
    ///
    /// * `account_info` - A Solana AccountInfo referencing an existing Switchboard PullFeed account
    ///
    /// # Examples
    ///
    /// ```ignore
    /// use switchboard_on_demand::PullFeedAccountData;
    ///
    /// let mut feed = PullFeedAccountData::new_mut(account_info)?;
    /// ```
    pub fn parse_mut<'info>(
        account_info: &'info AccountInfo<'info>,
    ) -> Result<RefMut<'info, Self>, OnDemandError> {
        let data = account_info
            .try_borrow_mut_data()
            .map_err(|_| OnDemandError::AccountBorrowError)?;
        if data.len() < Self::discriminator().len() {
            return Err(OnDemandError::InvalidDiscriminator);
        }

        let mut disc_bytes = [0u8; 8];
        disc_bytes.copy_from_slice(&data[..8]);
        if disc_bytes != Self::discriminator() {
            return Err(OnDemandError::InvalidDiscriminator);
        }

        Ok(RefMut::map(data, |data: &mut &mut [u8]| {
            bytemuck::from_bytes_mut(&mut data[8..std::mem::size_of::<Self>() + 8])
        }))
    }

    /// Generate a checksum for the given feed hash, result, slothash, max_variance and min_responses
    /// This is signed by the oracle and used to verify that the data submitted by the oracles is valid.
    pub fn generate_checksum(&self, result: i128, slothash: [u8; 32]) -> [u8; 32] {
        Self::generate_checksum_inner(
            self.queue,
            self.feed_hash,
            result,
            slothash,
            self.max_variance,
            self.min_responses,
        )
    }

    /// Generate a checksum for the given feed hash, result, slothash, max_variance and min_responses
    /// This is signed by the oracle and used to verify that the data submitted by the oracles is valid.
    pub fn generate_checksum_inner(
        queue: Pubkey,
        feed_hash: [u8; 32],
        result: i128,
        slothash: [u8; 32],
        max_variance: u64,
        min_responses: u32,
    ) -> [u8; 32] {
        let mut hasher = Sha256::new();
        hasher.update(queue.to_bytes());
        hasher.update(feed_hash);
        hasher.update(result.to_le_bytes());
        hasher.update(slothash);
        hasher.update(max_variance.to_le_bytes());
        hasher.update(min_responses.to_le_bytes());
        hasher.finalize().to_vec().try_into().unwrap()
    }

    /// **method**
    /// get_value
    /// Returns the median value of the submissions in the last `max_staleness` slots.
    /// If there are fewer than `min_samples` submissions, returns an error.
    /// **arguments**
    /// * `clock` - the clock to use for the current slot
    /// * `max_staleness` - the maximum number of slots to consider
    /// * `min_samples` - the minimum number of samples required to return a value
    /// **returns**
    /// * `Ok(Decimal)` - the median value of the submissions in the last `max_staleness` slots
    pub fn get_value(
        &self,
        clock: &Clock,
        max_staleness: u64,
        min_samples: u32,
        only_positive: bool,
    ) -> Result<Decimal, OnDemandError> {
        let submissions = self
            .submissions
            .iter()
            .take_while(|s| !s.is_empty())
            .filter(|s| s.slot > clock.slot - max_staleness)
            .collect::<Vec<_>>();
        if submissions.len() < min_samples as usize {
            return Err(OnDemandError::NotEnoughSamples);
        }
        let median =
            lower_bound_median(&mut submissions.iter().map(|s| s.value).collect::<Vec<_>>())
                .ok_or(OnDemandError::NotEnoughSamples)?;
        if only_positive && median <= 0 {
            return Err(OnDemandError::IllegalFeedValue);
        }

        return Ok(Decimal::from_i128_with_scale(median, 18));
    }
}

// takes the rounded down median of a list of numbers
pub fn lower_bound_median(numbers: &mut Vec<i128>) -> Option<i128> {
    numbers.sort(); // Sort the numbers in ascending order.

    let len = numbers.len();
    if len == 0 {
        return None; // Return None for an empty list.
    }
    Some(numbers[len / 2])
}