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}