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}