templar_common/oracle/
lazer.rs

1//! Pyth Lazer adapter feed data — the native, feed-id-keyed shape the `contract/pyth-lazer`
2//! adapter stores and serves. The adapter is a pure store-and-serve oracle: it hands back the raw
3//! [`FeedData`], and each consumer (the proxy-oracle's `Lazer` source, the gateway) projects it to
4//! a [`pyth::Price`] itself, mirroring [`redstone::FeedData::to_pyth_price`](super::redstone::FeedData).
5//! Freshness is likewise the consumer's concern; the adapter applies no age filter on reads.
6
7use std::collections::HashMap;
8
9use near_sdk::{
10    ext_contract,
11    json_types::{I64, U64},
12    near,
13};
14use templar_primitives::time::Nanoseconds;
15
16use crate::oracle::pyth::{self, PythTimestamp};
17
18/// Response shape of the adapter's feed-id-keyed read (`get_feeds_data`): the raw stored feed for
19/// each requested `u32` feed id (`None` when the feed is absent).
20pub type FeedDataResponse = HashMap<u32, Option<FeedData>>;
21
22/// EMA price/confidence for a feed, following the same fixed-point `expo` as the spot price.
23#[derive(Clone, Debug, PartialEq, Eq)]
24#[near(serializers = [borsh, json])]
25pub struct EmaData {
26    pub price: I64,
27    pub conf: U64,
28}
29
30/// The latest stored data for one Lazer feed. Prices/exponent follow the Pyth fixed-point
31/// convention (`value * 10^expo`); the timestamp is stored as [`Nanoseconds`].
32#[derive(Clone, Debug, PartialEq, Eq)]
33#[near(serializers = [borsh, json])]
34pub struct FeedData {
35    pub price: I64,
36    pub conf: U64,
37    /// EMA data. The adapter's stateful storage path requires it, so a stored feed always carries
38    /// EMA; it is never synthesized from spot.
39    pub ema: EmaData,
40    pub expo: i32,
41    /// Per-feed publish time in nanoseconds. The `_ns` suffix marks the unit for JSON consumers
42    /// (the [`Nanoseconds`] type is erased in JSON).
43    pub publish_time_ns: Nanoseconds,
44}
45
46impl FeedData {
47    /// Build a Pyth [`Price`](pyth::Price) from a `(price, conf)` pair using this feed's exponent
48    /// and publish time. `None` if the publish time cannot be represented as a [`PythTimestamp`].
49    fn to_price(&self, price: I64, conf: U64) -> Option<pyth::Price> {
50        Some(pyth::Price {
51            price,
52            conf,
53            expo: self.expo,
54            publish_time: PythTimestamp::try_from_time(self.publish_time_ns)?,
55        })
56    }
57
58    /// Spot [`Price`](pyth::Price) projection.
59    pub fn to_pyth_price(&self) -> Option<pyth::Price> {
60        self.to_price(self.price, self.conf)
61    }
62
63    /// EMA [`Price`](pyth::Price) projection (the form the proxy-oracle's `Lazer` source consumes).
64    pub fn to_ema_price(&self) -> Option<pyth::Price> {
65        self.to_price(self.ema.price, self.ema.conf)
66    }
67}
68
69/// Feed-id-keyed read ABI of the Pyth Lazer adapter (`contract/pyth-lazer`). Feeds are addressed
70/// by their native `u32` id; the adapter serves the raw stored [`FeedData`] and the consumer
71/// projects it (mirroring the RedStone adapter, which serves [`redstone::FeedData`](super::redstone::FeedData)).
72#[ext_contract(ext_pyth_lazer)]
73pub trait PythLazer {
74    fn get_feeds_data(&self, feed_ids: Vec<u32>) -> FeedDataResponse;
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn feed(price: i64, ema: i64, conf: u64, publish_s: u64) -> FeedData {
82        FeedData {
83            price: I64(price),
84            conf: U64(conf),
85            ema: EmaData {
86                price: I64(ema),
87                conf: U64(conf),
88            },
89            expo: -8,
90            publish_time_ns: Nanoseconds::from_secs(publish_s),
91        }
92    }
93
94    #[test]
95    fn spot_and_ema_projections_use_the_right_mantissa() {
96        let feed = feed(123_456, 123_000, 50, 1_700_000_000);
97
98        let spot = feed.to_pyth_price().unwrap();
99        assert_eq!(spot.price.0, 123_456);
100        assert_eq!(spot.conf.0, 50);
101        assert_eq!(spot.expo, -8);
102        assert_eq!(spot.publish_time.as_secs(), 1_700_000_000);
103
104        let ema = feed.to_ema_price().unwrap();
105        assert_eq!(ema.price.0, 123_000);
106        assert_eq!(ema.conf.0, 50);
107        assert_eq!(ema.expo, -8);
108        assert_eq!(ema.publish_time.as_secs(), 1_700_000_000);
109    }
110}