switchboard-on-demand 0.1.10

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::clock::Clock;
use solana_program::pubkey::Pubkey;
use std::cell::{Ref, RefMut};
use num::integer::Roots;

#[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 {
    pub fn parse<'info>(
        data: Ref<'info, &mut [u8]>,
    ) -> Result<Ref<'info, Self>, OnDemandError> {
        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: &&mut [u8]| {
            bytemuck::from_bytes(&data[8..std::mem::size_of::<Self>() + 8])
        }))
    }

    pub fn parse_mut<'info>(
        data: RefMut<'info, &mut [u8]>,
    ) -> Result<RefMut<'info, Self>, OnDemandError> {
        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()
    }

    /// 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_with_timestamp(
        queue: Pubkey,
        feed_hash: [u8; 32],
        result: i128,
        slothash: [u8; 32],
        max_variance: u64,
        min_responses: u32,
        timestamp: u64,
    ) -> [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.update(timestamp.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
    /// * `only_positive` - if true, only positive values are considered
    /// # returns
    /// * ``Result<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));
    }

    /// # method
    /// range
    /// Returns the range of values of the submissions in the last `max_staleness` slots.
    /// The range is defined as the maximum absolute difference between the median and the minimum or maximum value.
    /// If there are no submissions in the last `max_staleness` slots, returns an error.
    /// # arguments
    /// * `clock` - the clock to use for the current slot
    /// * `max_staleness` - the maximum number of slots to consider
    /// # returns
    /// * `Result<Decimal>` - the range of the submissions in the last `max_staleness` slots
    pub fn range(
        &self,
        clock: &Clock,
        max_staleness: u64,
    ) -> Result<Decimal, OnDemandError> {
        let mut submissions = self
            .submissions
            .iter()
            .take_while(|s| !s.is_empty())
            .filter(|s| s.slot > clock.slot - max_staleness)
            .map(|s| s.value)
            .collect::<Vec<_>>();
        let median = lower_bound_median(&mut submissions)
            .ok_or(OnDemandError::NotEnoughSamples)?;
        let (min, max) = find_min_max(&submissions)
            .ok_or(OnDemandError::NotEnoughSamples)?;
        let lower_range = median - min;
        let upper_range = max - median;
        assert!(lower_range >= 0);
        assert!(upper_range >= 0);
        let range = std::cmp::max(lower_range, upper_range);
        Ok(Decimal::from_i128_with_scale(range, 18))
    }

    /// # method
    /// std_deviation
    /// Returns the standard deviation of the submissions in the last `max_staleness` slots.
    /// If there are no submissions in the last `max_staleness` slots, returns an error.
    /// # arguments
    /// * `clock` - the clock to use for the current slot
    /// * `max_staleness` - the maximum number of slots to consider
    /// # returns
    /// * `Result<Decimal>` - the standard deviation of the submissions in the last `max_staleness` slots
    pub fn std_deviation(
        &self,
        clock: &Clock,
        max_staleness: u64,
    ) -> Result<Decimal, OnDemandError> {
        let submissions = self
            .submissions
            .iter()
            .take_while(|s| !s.is_empty())
            .filter(|s| s.slot > clock.slot - max_staleness)
            .map(|s| s.value)
            .collect::<Vec<_>>();
        standard_deviation(&submissions)
            .ok_or(OnDemandError::NotEnoughSamples)
            .map(|std_dev| Decimal::from_i128_with_scale(std_dev, 18))
    }

}

fn standard_deviation(numbers: &[i128]) -> Option<i128> {
    let len = numbers.len() as i128;
    if len == 0 {
        return None;
    }

    let mean = mean(numbers)?;

    let variance: i128 = numbers.iter()
        .map(|&value| {
            let diff = value - mean;
            diff * diff
        })
        .sum::<i128>() / len;

    Some(variance.sqrt())
}

fn mean(numbers: &[i128]) -> Option<i128> {
    let len = numbers.len() as i128;
    if len == 0 {
        return None;
    }

    let sum: i128 = numbers.iter().sum();
    Some(sum / len)
}

// 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])
}

fn find_min_max(decimals: &[i128]) -> Option<(i128, i128)> {
    let min = decimals.iter().min()?;
    let max = decimals.iter().max()?;
    Some((*min, *max))
}