pyth_sdk/
lib.rs

1use borsh::{
2    BorshDeserialize,
3    BorshSerialize,
4};
5
6use hex::FromHexError;
7use schemars::JsonSchema;
8use std::fmt;
9
10pub mod utils;
11
12mod price;
13pub use price::Price;
14
15#[derive(
16    Copy,
17    Clone,
18    Default,
19    PartialEq,
20    Eq,
21    PartialOrd,
22    Ord,
23    Hash,
24    BorshSerialize,
25    BorshDeserialize,
26    serde::Serialize,
27    serde::Deserialize,
28    JsonSchema,
29)]
30#[repr(C)]
31pub struct Identifier(
32    #[serde(with = "hex")]
33    #[schemars(with = "String")]
34    [u8; 32],
35);
36
37impl Identifier {
38    pub fn new(bytes: [u8; 32]) -> Identifier {
39        Identifier(bytes)
40    }
41
42    pub fn to_bytes(&self) -> [u8; 32] {
43        self.0
44    }
45
46    pub fn to_hex(&self) -> String {
47        hex::encode(self.0)
48    }
49
50    pub fn from_hex<T: AsRef<[u8]>>(s: T) -> Result<Identifier, FromHexError> {
51        let mut bytes = [0u8; 32];
52        hex::decode_to_slice(s, &mut bytes)?;
53        Ok(Identifier::new(bytes))
54    }
55}
56
57impl fmt::Debug for Identifier {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "0x{}", self.to_hex())
60    }
61}
62
63impl fmt::Display for Identifier {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "0x{}", self.to_hex())
66    }
67}
68
69impl AsRef<[u8]> for Identifier {
70    fn as_ref(&self) -> &[u8] {
71        &self.0[..]
72    }
73}
74
75/// Consists of 32 bytes and it is currently based on largest Public Key size on various
76/// blockchains.
77pub type PriceIdentifier = Identifier;
78
79/// Consists of 32 bytes and it is currently based on largest Public Key size on various
80/// blockchains.
81pub type ProductIdentifier = Identifier;
82
83/// Unix Timestamp is represented as number of seconds passed since Unix epoch (00:00:00 UTC on 1
84/// Jan 1970). It is a signed integer because it's the standard in Unix systems and allows easier
85/// time difference.
86pub type UnixTimestamp = i64;
87pub type DurationInSeconds = u64;
88
89/// Represents a current aggregation price from pyth publisher feeds.
90#[derive(
91    Copy,
92    Clone,
93    Debug,
94    Default,
95    PartialEq,
96    Eq,
97    BorshSerialize,
98    BorshDeserialize,
99    serde::Serialize,
100    serde::Deserialize,
101    JsonSchema,
102)]
103#[repr(C)]
104pub struct PriceFeed {
105    /// Unique identifier for this price.
106    pub id:    PriceIdentifier,
107    /// Price.
108    price:     Price,
109    /// Exponentially-weighted moving average (EMA) price.
110    ema_price: Price,
111}
112
113impl PriceFeed {
114    /// Constructs a new Price Feed
115    #[allow(clippy::too_many_arguments)]
116    pub fn new(id: PriceIdentifier, price: Price, ema_price: Price) -> PriceFeed {
117        PriceFeed {
118            id,
119            price,
120            ema_price,
121        }
122    }
123
124
125    /// Get the "unchecked" price and confidence interval as fixed-point numbers of the form
126    /// a * 10^e along with its publish time.
127    ///
128    /// Returns a `Price` struct containing the current price, confidence interval, and the exponent
129    /// for both numbers, and publish time. This method returns the latest price which may be from
130    /// arbitrarily far in the past, and the caller should probably check the timestamp before using
131    /// it.
132    ///
133    /// Please consider using `get_price_no_older_than` when possible.
134    pub fn get_price_unchecked(&self) -> Price {
135        self.price
136    }
137
138
139    /// Get the "unchecked" exponentially-weighted moving average (EMA) price and a confidence
140    /// interval on the result along with its publish time.
141    ///
142    /// Returns the latest EMA price value which may be from arbitrarily far in the past, and the
143    /// caller should probably check the timestamp before using it.
144    ///
145    /// At the moment, the confidence interval returned by this method is computed in
146    /// a somewhat questionable way, so we do not recommend using it for high-value applications.
147    ///
148    /// Please consider using `get_ema_price_no_older_than` when possible.
149    pub fn get_ema_price_unchecked(&self) -> Price {
150        self.ema_price
151    }
152
153    /// Get the price as long as it was updated within `age` seconds of the
154    /// `current_time`.
155    ///
156    /// This function is a sanity-checked version of `get_price_unchecked` which is
157    /// useful in applications that require a sufficiently-recent price. Returns `None` if the
158    /// price wasn't updated sufficiently recently.
159    ///
160    /// Returns a struct containing the latest available price, confidence interval and the exponent
161    /// for both numbers, or `None` if no price update occurred within `age` seconds of the
162    /// `current_time`.
163    pub fn get_price_no_older_than(
164        &self,
165        current_time: UnixTimestamp,
166        age: DurationInSeconds,
167    ) -> Option<Price> {
168        let price = self.get_price_unchecked();
169
170        let time_diff_abs = (price.publish_time - current_time).abs() as u64;
171
172        if time_diff_abs > age {
173            return None;
174        }
175
176        Some(price)
177    }
178
179    /// Get the exponentially-weighted moving average (EMA) price as long as it was updated within
180    /// `age` seconds of the `current_time`.
181    ///
182    /// This function is a sanity-checked version of `get_ema_price_unchecked` which is useful in
183    /// applications that require a sufficiently-recent price. Returns `None` if the price
184    /// wasn't updated sufficiently recently.
185    ///
186    /// Returns a struct containing the EMA price, confidence interval and the exponent
187    /// for both numbers, or `None` if no price update occurred within `age` seconds of the
188    /// `current_time`.
189    pub fn get_ema_price_no_older_than(
190        &self,
191        current_time: UnixTimestamp,
192        age: DurationInSeconds,
193    ) -> Option<Price> {
194        let price = self.get_ema_price_unchecked();
195
196        let time_diff_abs = (price.publish_time - current_time).abs() as u64;
197
198        if time_diff_abs > age {
199            return None;
200        }
201
202        Some(price)
203    }
204}
205#[cfg(test)]
206mod test {
207    use super::*;
208
209    #[test]
210    pub fn test_ser_then_deser_default() {
211        let price_feed = PriceFeed::default();
212        let ser = serde_json::to_string(&price_feed).unwrap();
213        let deser: PriceFeed = serde_json::from_str(&ser).unwrap();
214        assert_eq!(price_feed, deser);
215    }
216
217    #[test]
218    pub fn test_ser_large_number() {
219        let price_feed = PriceFeed {
220            ema_price: Price {
221                conf: 1_234_567_000_000_000_789,
222                ..Price::default()
223            },
224            ..PriceFeed::default()
225        };
226        let price_feed_json = serde_json::to_value(price_feed).unwrap();
227        assert_eq!(
228            price_feed_json["ema_price"]["conf"].as_str(),
229            Some("1234567000000000789")
230        );
231    }
232
233    #[test]
234    pub fn test_deser_large_number() {
235        let mut price_feed_json = serde_json::to_value(PriceFeed::default()).unwrap();
236        price_feed_json["price"]["price"] =
237            serde_json::Value::String(String::from("1000000000000000123"));
238        let p: PriceFeed = serde_json::from_value(price_feed_json).unwrap();
239        assert_eq!(p.get_price_unchecked().price, 1_000_000_000_000_000_123);
240    }
241
242    #[test]
243    pub fn test_ser_id_length_32_bytes() {
244        let mut price_feed = PriceFeed::default();
245        price_feed.id.0[0] = 106; // 0x6a
246        let price_feed_json = serde_json::to_value(price_feed).unwrap();
247        let id_str = price_feed_json["id"].as_str().unwrap();
248        assert_eq!(id_str.len(), 64);
249        assert_eq!(
250            id_str,
251            "6a00000000000000000000000000000000000000000000000000000000000000"
252        );
253    }
254
255    #[test]
256    pub fn test_deser_invalid_id_length_fails() {
257        let mut price_feed_json = serde_json::to_value(PriceFeed::default()).unwrap();
258        price_feed_json["id"] = serde_json::Value::String(String::from("1234567890"));
259        assert!(serde_json::from_value::<PriceFeed>(price_feed_json).is_err());
260    }
261
262    #[test]
263    pub fn test_identifier_from_hex_ok() {
264        let id = Identifier::from_hex(
265            "0a3f000000000000000000000000000000000000000000000000000000000000",
266        )
267        .unwrap();
268        assert_eq!(id.to_bytes()[0], 10);
269    }
270
271    #[test]
272    pub fn test_identifier_from_hex_invalid_err() {
273        let try_parse_odd = Identifier::from_hex("010"); // odd length
274        assert_eq!(try_parse_odd, Err(FromHexError::OddLength));
275
276        let try_parse_invalid_len = Identifier::from_hex("0a"); // length should be 32 bytes, 64
277        assert_eq!(
278            try_parse_invalid_len,
279            Err(FromHexError::InvalidStringLength)
280        );
281    }
282
283    #[test]
284    pub fn test_identifier_debug_fmt() {
285        let mut id = Identifier::default();
286        id.0[0] = 10;
287
288        let id_str = format!("{:?}", id);
289        assert_eq!(
290            id_str,
291            "0x0a00000000000000000000000000000000000000000000000000000000000000"
292        );
293    }
294
295    #[test]
296    pub fn test_identifier_display_fmt() {
297        let mut id = Identifier::default();
298        id.0[0] = 10;
299
300        let id_str = format!("{}", id);
301        assert_eq!(
302            id_str,
303            "0x0a00000000000000000000000000000000000000000000000000000000000000"
304        );
305    }
306}