templar_common/oracle/redstone/
feed_data.rs

1use near_sdk::json_types::{I64, U64};
2use primitive_types::U256;
3
4use crate::{oracle::pyth, time::Nanoseconds};
5
6use super::SerializableU256;
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
9#[near_sdk::near(serializers = [json, borsh])]
10pub struct FeedData {
11    pub price: SerializableU256,
12    /// Package timestamp in nanoseconds since Unix epoch.
13    pub package_timestamp: Nanoseconds,
14    /// Write timestamp in nanoseconds since Unix epoch.
15    pub write_timestamp: Nanoseconds,
16}
17
18impl FeedData {
19    /// Converts this [`FeedData`] to a [`pyth::Price`], with the confidence
20    /// set to zero, because RedStone does not provide confidence intervals.
21    pub fn to_pyth_price(&self) -> Option<pyth::Price> {
22        let (price, exponent) = approximate_u256(self.price.into());
23        Some(pyth::Price {
24            price: I64(price),
25            conf: U64(0),
26            expo: exponent.checked_sub(super::DECIMALS)?,
27            // Publish time is in seconds, but the RedStone data uses nanoseconds.
28            publish_time: self.package_timestamp.try_to_pyth()?,
29        })
30    }
31}
32
33/// Use instead of `U256::exp10` to avoid stack overflow for large exponents,
34/// since `U256::exp10` uses linear-time recursion.
35fn u256_exp10(mut exponent: u32) -> U256 {
36    if exponent == 0 {
37        return U256::one();
38    }
39    let mut y = U256::one();
40    let mut x = U256::from(10);
41
42    while exponent > 1 {
43        if exponent % 2 == 1 {
44            y *= x;
45        }
46        x *= x;
47        exponent >>= 1;
48    }
49
50    x * y
51}
52
53/// Converts a [`U256`] to an `i64` mantissa and an `i32` exponent.
54///
55/// Rounds down (floor).
56#[allow(
57    clippy::cast_possible_wrap,
58    clippy::cast_possible_truncation,
59    reason = "guaranteed safe"
60)]
61fn approximate_u256(input: U256) -> (i64, i32) {
62    const I64_MAX: U256 = U256([0x7FFF_FFFF_FFFF_FFFF, 0, 0, 0]);
63
64    let mut n = input;
65    let mut exponent = 0;
66
67    if let Some(b) = n.bits().checked_sub(64) {
68        // 103_873_643 / 345_060_773 ~= log(2)/log(10)
69        let e = b * 103_873_643 / 345_060_773;
70        let modulus = u256_exp10(e as u32);
71        n /= modulus;
72        exponent += e;
73    }
74
75    while n > I64_MAX {
76        n /= 10;
77        exponent += 1;
78    }
79
80    (n.low_u64() as i64, exponent as i32)
81}
82
83#[allow(clippy::cast_sign_loss)]
84#[cfg(test)]
85mod tests {
86    use near_sdk::serde_json;
87
88    use super::*;
89
90    #[rstest::rstest]
91    #[case::zero(U256::zero())]
92    #[case::one(U256::one())]
93    #[case::max(U256::MAX)]
94    #[case::normal_fits_i64(U256::from(777_777_777_777_777_777_i64))]
95    #[case::large_power_of_2(U256::from(2).pow(255.into()))]
96    #[case::large_other(U256::from(123_945).pow(12.into()))]
97    fn approximation(#[case] x: U256) {
98        let (n, e) = approximate_u256(x);
99        eprintln!("{n}*10^{e} ~= {x}");
100        assert_eq!(U256::from(n), x / U256::exp10(e as usize));
101    }
102
103    #[rstest::rstest]
104    fn approximation_exp10() {
105        for i in 0..=77_u32 {
106            let v = U256::exp10(i as usize);
107            let (n, e) = approximate_u256(v);
108            eprintln!("{i}:\t{n} * 10^{e}");
109            assert_eq!(n.ilog10() + e as u32, i);
110        }
111    }
112
113    #[test]
114    fn json() {
115        let fd = FeedData {
116            price: U256::from(3333).into(),
117            package_timestamp: Nanoseconds::from_ms(5555),
118            write_timestamp: Nanoseconds::from_ms(6666),
119        };
120
121        let serialized = serde_json::to_string(&fd).unwrap();
122
123        eprintln!("{serialized}");
124
125        assert_eq!(
126            serialized,
127            r#"{"price":"3333","package_timestamp":"5555000000","write_timestamp":"6666000000"}"#,
128        );
129
130        let deserialized: FeedData = serde_json::from_str(&serialized).unwrap();
131
132        assert_eq!(fd, deserialized);
133    }
134}