templar_common/oracle/
pyth.rs

1//! Derived from <https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/near>.
2//! Modified for use with the Templar Protocol contracts.
3//!
4//! The original code was released under the following license:
5//!
6//! Copyright 2025 Pyth Data Association.
7//!
8//! Licensed under the Apache License, Version 2.0 (the "License");
9//! you may not use this file except in compliance with the License.
10//! You may obtain a copy of the License at
11//!
12//!     http://www.apache.org/licenses/LICENSE-2.0
13//!
14//! Unless required by applicable law or agreed to in writing, software
15//! distributed under the License is distributed on an "AS IS" BASIS,
16//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17//! See the License for the specific language governing permissions and
18//! limitations under the License.
19use std::{collections::HashMap, fmt::Display};
20
21use near_sdk::{
22    ext_contract,
23    json_types::{I64, U64},
24    near,
25};
26
27pub type OracleResponse = HashMap<PriceIdentifier, Option<Price>>;
28
29#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[near(serializers = [borsh, json])]
31pub struct PriceIdentifier(
32    #[serde(
33        serialize_with = "hex::serde::serialize",
34        deserialize_with = "hex::serde::deserialize"
35    )]
36    pub [u8; 32],
37);
38
39impl std::fmt::Debug for PriceIdentifier {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        write!(f, "{}", hex::encode(self.0))
42    }
43}
44
45impl Display for PriceIdentifier {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "{}", hex::encode(self.0))
48    }
49}
50
51/// A price with a degree of uncertainty, represented as a price +- a confidence interval.
52///
53/// The confidence interval roughly corresponds to the standard error of a normal distribution.
54/// Both the price and confidence are stored in a fixed-point numeric representation,
55/// `x * (10^expo)`, where `expo` is the exponent.
56//
57/// Please refer to the documentation at
58/// <https://docs.pyth.network/documentation/pythnet-price-feeds/best-practices>
59/// for how to use this price safely.
60#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
61#[near(serializers = [json, borsh])]
62pub struct Price {
63    pub price: I64,
64    /// Confidence interval around the price
65    pub conf: U64,
66    /// The exponent
67    pub expo: i32,
68    /// Unix timestamp of when this price was computed
69    pub publish_time: PythTimestamp,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
73#[near(serializers = [json, borsh])]
74#[serde(transparent)]
75pub struct PythTimestamp(i64);
76
77impl PythTimestamp {
78    /// Creates a `PythTimestamp` from a value in seconds.
79    pub fn from_secs(secs: i64) -> Self {
80        Self(secs)
81    }
82
83    /// Converts milliseconds to a [`PythTimestamp`], stored in whole seconds,
84    /// truncating any fractional seconds.
85    pub fn from_ms(ms: i64) -> Self {
86        Self(ms / 1000)
87    }
88
89    /// Returns the timestamp value in seconds.
90    pub fn as_secs(&self) -> i64 {
91        self.0
92    }
93
94    /// Converts a [`PythTimestamp`] (stored in whole seconds) to milliseconds
95    /// by performing a checked multiplication by 1000.
96    pub fn as_ms(&self) -> Option<i64> {
97        self.0.checked_mul(1000)
98    }
99}
100
101#[ext_contract(ext_pyth)]
102pub trait Pyth {
103    // See implementations for details, PriceIdentifier can be passed either as a 64 character
104    // hex price ID which can be found on the Pyth homepage.
105    fn price_feed_exists(&self, price_identifier: PriceIdentifier) -> bool;
106    // fn get_price(&self, price_identifier: PriceIdentifier) -> Option<Price>;
107    // fn get_price_unsafe(&self, price_identifier: PriceIdentifier) -> Option<Price>;
108    // fn get_price_no_older_than(&self, price_id: PriceIdentifier, age: u64) -> Option<Price>;
109    // fn get_ema_price(&self, price_id: PriceIdentifier) -> Option<Price>;
110    // fn get_ema_price_unsafe(&self, price_id: PriceIdentifier) -> Option<Price>;
111    // fn get_ema_price_no_older_than(&self, price_id: PriceIdentifier, age: u64) -> Option<Price>;
112    // fn list_prices(
113    //     &self,
114    //     price_ids: Vec<PriceIdentifier>,
115    // ) -> HashMap<PriceIdentifier, Option<Price>>;
116    // fn list_prices_unsafe(
117    //     &self,
118    //     price_ids: Vec<PriceIdentifier>,
119    // ) -> HashMap<PriceIdentifier, Option<Price>>;
120    // fn list_prices_no_older_than(
121    //     &self,
122    //     price_ids: Vec<PriceIdentifier>,
123    // ) -> HashMap<PriceIdentifier, Option<Price>>;
124    // fn list_ema_prices(
125    //     &self,
126    //     price_ids: Vec<PriceIdentifier>,
127    // ) -> HashMap<PriceIdentifier, Option<Price>>;
128    // fn list_ema_prices_unsafe(
129    //     &self,
130    //     price_ids: Vec<PriceIdentifier>,
131    // ) -> HashMap<PriceIdentifier, Option<Price>>;
132    fn list_ema_prices_no_older_than(
133        &self,
134        price_ids: Vec<PriceIdentifier>,
135        age: u64,
136    ) -> HashMap<PriceIdentifier, Option<Price>>;
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn can_parse_real_price() {
145        let real_price = r#"{ "conf": "2696300000", "expo": -8, "price": "7154901300000", "publish_time": 1773381271 }"#;
146
147        let parsed = near_sdk::serde_json::from_str::<Price>(real_price).unwrap();
148        assert_eq!(parsed.price.0, 7_154_901_300_000);
149        assert_eq!(parsed.conf.0, 2_696_300_000);
150        assert_eq!(parsed.expo, -8);
151        assert_eq!(parsed.publish_time.as_secs(), 1_773_381_271);
152    }
153}