1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use pyth_sdk_solana::state::SolanaPriceAccount;
use serde::{Deserialize, Serialize};
use solana_program::account_info::AccountInfo;
use solana_program::clock::Clock;
use solana_program::sysvar::Sysvar;
use crate::accounts::oracles::oracle_params::{DataLen, Endian, OracleParams};
use crate::accounts::oracles::oracle_provider::OracleProvider;
use crate::dvl_error::DvlError;
use crate::errors::*;

pub const ORACLE_PARAMS_QUANTITY: usize = 3;
pub const ORACLES_DATA_SIZE: usize = 216;
pub const ORACLES_DATA_COUNT: usize = 8;

#[repr(C)]
#[derive(Copy, Clone, Serialize, Deserialize)]
pub struct OracleData {
    pub base_ticker: [u8; 8],              // The base currency ticker, e.g., BTC in BTC/USD
    pub relative_ticker: [u8; 8],          // The quote currency ticker, e.g., USD in BTC/USD
    pub configured: bool,                  // Indicates if the oracle is configured and operational
    pub use_relative_oracle: bool,         // Flag to indicate whether to use the relative_oracle_num
    pub relative_oracle_num: u8,           // Index of the reference oracle for price recalculations
    pub reserved: u8,                      // Reserved space for future use
    pub max_price_deviation: u32,          // Maximum absolute deviation for the price
    pub params: [OracleParams; ORACLE_PARAMS_QUANTITY], // Oracle parameters, 192 bytes, offset=24
}

impl OracleData {
    /// Calculates the asset price based on oracle data
    /// Returns Result<(f64, DvlError), ProgramError> where f64 is the average price
    pub fn get_asset_price(&self, ext_oracles_accounts: &[AccountInfo]) -> Result<f64, DvlError> {
        let current_time = Clock::get().
            map_err(|_| DvlError::new(ContractError::TimeReadError))?
            .unix_timestamp;
        let mut prices = Vec::<f64>::new();

        for account in ext_oracles_accounts {
            for oracle_param in self.params.iter() {
                if account.key == &oracle_param.account {
                    if !oracle_param.enabled {
                        continue;
                    }
                    match oracle_param.provider {
                        OracleProvider::Custom => {
                            let timestamp_offset = oracle_param.timestamp.offset as usize;
                            let ext_oracle_data = account.try_borrow_data().
                                map_err(|_| DvlError::new_with_account(AccountTag::ExternalOracle, ContractError::AccountSize))?;
                            let timestamp = Self::read_value(&ext_oracle_data[timestamp_offset..], oracle_param.timestamp.data_len, oracle_param.timestamp.endian)
                                .map_err(|_| DvlError::new_with_account(AccountTag::Oracle, ContractError::TimeReadError))? as i64;

                            if (current_time - timestamp).abs() > oracle_param.max_timestamp_diff_sec as i64 {
                                continue; // Skip this oracle due to time difference
                            }

                            let mantissa_offset = oracle_param.mantissa.offset as usize;
                            let mantissa = Self::read_value(&ext_oracle_data[mantissa_offset..], oracle_param.mantissa.data_len, oracle_param.mantissa.endian)?;

                            let exponent_offset = oracle_param.exponent.offset as usize;
                            let exponent = Self::read_value(&ext_oracle_data[exponent_offset..], oracle_param.exponent.data_len, oracle_param.exponent.endian)? as i32;

                            let price = mantissa as f64 * 10f64.powi(-exponent);
                            prices.push(price);
                        }
                        OracleProvider::Switchboard => {
                            continue;}
                        OracleProvider::Pyth => {
                            let price_feed = SolanaPriceAccount::account_info_to_feed( &account ).
                                map_err(|_| {
                                    DvlError::new_with_account(AccountTag::Oracle, ContractError::AssetPriceUnavailable)
                                })?;
                            let current_price = price_feed.get_price_no_older_than(current_time, oracle_param.max_timestamp_diff_sec as u64).unwrap();
                            prices.push(current_price.price as f64 * 10f64.powi(current_price.expo));
                        }
                    }
                }
            }
        }

        return match prices.len() {
            0 => Err(DvlError::new(ContractError::AssetPriceUnavailable)),
            2 => {
                if (prices[1] - prices[0]).abs() > self.max_price_deviation as f64 {
                    return Err(DvlError::new(ContractError::PriceDiscrepancyError))
                }
                Ok(prices[0])
            }
            3 => {
                let max_deviation = self.max_price_deviation as f64;
                if (prices[1] - prices[0]).abs() > max_deviation || (prices[2] - prices[1]).abs() > max_deviation {
                    return Err(DvlError::new(ContractError::PriceDiscrepancyError))
                }
                Ok(prices[0])
            }
            _ => Ok(prices[0])
        };
    }

    /// Reads value from data slice according to DataLen and Endian
    fn read_value(data: &[u8], data_len: DataLen, endian: Endian) -> Result<u128, DvlError> {
        let val = match data_len {
            DataLen::U8 => data[0] as u128,
            DataLen::U32 => {
                if endian == Endian::LE {
                    u32::from_le_bytes(data[..4].try_into().unwrap()) as u128
                } else {
                    u32::from_be_bytes(data[..4].try_into().unwrap()) as u128
                }
            },
            DataLen::U64 => {
                if endian == Endian::LE {
                    u64::from_le_bytes(data[..8].try_into().unwrap()) as u128
                } else {
                    u64::from_be_bytes(data[..8].try_into().unwrap()) as u128
                }
            },
            DataLen::U128 => {
                if endian == Endian::LE {
                    u128::from_le_bytes(data[..16].try_into().unwrap())
                } else {
                    u128::from_be_bytes(data[..16].try_into().unwrap())
                }
            },
            _ => return Err(DvlError::new(ContractError::ComputationError)), // Unsupported data length
        };
        Ok(val)
    }

}

#[cfg(test)]
impl Default for OracleData {
    fn default() -> Self {
        Self {
            base_ticker: [0; 8],
            relative_ticker: [0; 8],
            configured: false,
            use_relative_oracle: false,
            relative_oracle_num: 0,
            max_price_deviation: 0,
            reserved: 0,
            params: [OracleParams::default(); ORACLE_PARAMS_QUANTITY],
        }
    }
}

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

    #[test]
    fn test_instruments_account_offsets() {
        // checking total size
        assert_eq!(mem::size_of::<OracleData>(), ORACLES_DATA_SIZE);
    }
}