pyth_client/
lib.rs

1//! A Rust library for consuming price feeds from the [pyth.network](https://pyth.network/) oracle on the Solana network.
2//!
3//! Please see the [crates.io page](https://crates.io/crates/pyth-client/) for documentation and example usage.
4#![deprecated = "This crate has been deprecated. Please use pyth-sdk-solana instead."]
5
6pub use self::price_conf::PriceConf;
7pub use self::error::PythError;
8
9mod entrypoint;
10mod error;
11mod price_conf;
12
13pub mod processor;
14pub mod instruction;
15
16use std::mem::size_of;
17use borsh::{BorshSerialize, BorshDeserialize};
18use bytemuck::{
19  cast_slice, from_bytes, try_cast_slice,
20  Pod, PodCastError, Zeroable,
21};
22
23#[cfg(target_arch = "bpf")]
24use solana_program::{clock::Clock, sysvar::Sysvar};
25
26solana_program::declare_id!("PythC11111111111111111111111111111111111111");
27
28pub const MAGIC               : u32   = 0xa1b2c3d4;
29pub const VERSION_2           : u32   = 2;
30pub const VERSION             : u32   = VERSION_2;
31pub const MAP_TABLE_SIZE      : usize = 640;
32pub const PROD_ACCT_SIZE      : usize = 512;
33pub const PROD_HDR_SIZE       : usize = 48;
34pub const PROD_ATTR_SIZE      : usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
35pub const MAX_SLOT_DIFFERENCE : u64   = 25; 
36
37/// The type of Pyth account determines what data it contains
38#[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
39#[repr(C)]
40pub enum AccountType
41{
42  Unknown,
43  Mapping,
44  Product,
45  Price
46}
47
48impl Default for AccountType {
49  fn default() -> Self {
50    AccountType::Unknown
51  }
52}
53
54/// The current status of a price feed.
55#[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
56#[repr(C)]
57pub enum PriceStatus
58{
59  /// The price feed is not currently updating for an unknown reason.
60  Unknown,
61  /// The price feed is updating as expected.
62  Trading,
63  /// The price feed is not currently updating because trading in the product has been halted.
64  Halted,
65  /// The price feed is not currently updating because an auction is setting the price.
66  Auction
67}
68
69impl Default for PriceStatus {
70  fn default() -> Self {
71      PriceStatus::Unknown
72  }
73}
74
75/// Status of any ongoing corporate actions.
76/// (still undergoing dev)
77#[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
78#[repr(C)]
79pub enum CorpAction
80{
81  NoCorpAct
82}
83
84impl Default for CorpAction {
85  fn default() -> Self {
86      CorpAction::NoCorpAct
87  }
88}
89
90/// The type of prices associated with a product -- each product may have multiple price feeds of different types.
91#[derive(Copy, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
92#[repr(C)]
93pub enum PriceType
94{
95  Unknown,
96  Price
97}
98
99impl Default for PriceType {
100  fn default() -> Self {
101      PriceType::Unknown
102  }
103}
104
105/// Public key of a Solana account
106#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
107#[repr(C)]
108pub struct AccKey
109{
110  pub val: [u8;32]
111}
112
113/// Mapping accounts form a linked-list containing the listing of all products on Pyth.
114#[derive(Copy, Clone, Debug, PartialEq, Eq)]
115#[repr(C)]
116pub struct Mapping
117{
118  /// pyth magic number
119  pub magic      : u32,
120  /// program version
121  pub ver        : u32,
122  /// account type
123  pub atype      : u32,
124  /// account used size
125  pub size       : u32,
126  /// number of product accounts
127  pub num        : u32,
128  pub unused     : u32,
129  /// next mapping account (if any)
130  pub next       : AccKey,
131  pub products   : [AccKey;MAP_TABLE_SIZE]
132}
133
134#[cfg(target_endian = "little")]
135unsafe impl Zeroable for Mapping {}
136
137#[cfg(target_endian = "little")]
138unsafe impl Pod for Mapping {}
139
140
141/// Product accounts contain metadata for a single product, such as its symbol ("Crypto.BTC/USD")
142/// and its base/quote currencies.
143#[derive(Copy, Clone, Debug, PartialEq, Eq)]
144#[repr(C)]
145pub struct Product
146{
147  /// pyth magic number
148  pub magic      : u32,
149  /// program version
150  pub ver        : u32,
151  /// account type
152  pub atype      : u32,
153  /// price account size
154  pub size       : u32,
155  /// first price account in list
156  pub px_acc     : AccKey,
157  /// key/value pairs of reference attr.
158  pub attr       : [u8;PROD_ATTR_SIZE]
159}
160
161impl Product {
162    pub fn iter(&self) -> AttributeIter {
163        AttributeIter { attrs: &self.attr }
164    }
165}
166
167#[cfg(target_endian = "little")]
168unsafe impl Zeroable for Product {}
169
170#[cfg(target_endian = "little")]
171unsafe impl Pod for Product {}
172
173/// A price and confidence at a specific slot. This struct can represent either a
174/// publisher's contribution or the outcome of price aggregation.
175#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
176#[repr(C)]
177pub struct PriceInfo
178{
179  /// the current price. 
180  /// For the aggregate price use price.get_current_price() whenever possible. It has more checks to make sure price is valid.
181  pub price      : i64,
182  /// confidence interval around the price.
183  /// For the aggregate confidence use price.get_current_price() whenever possible. It has more checks to make sure price is valid.
184  pub conf       : u64,
185  /// status of price (Trading is valid).
186  /// For the aggregate status use price.get_current_status() whenever possible.
187  /// Price data can sometimes go stale and the function handles the status in such cases.
188  pub status     : PriceStatus,
189  /// notification of any corporate action
190  pub corp_act   : CorpAction,
191  pub pub_slot   : u64
192}
193
194/// The price and confidence contributed by a specific publisher.
195#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
196#[repr(C)]
197pub struct PriceComp
198{
199  /// key of contributing publisher
200  pub publisher  : AccKey,
201  /// the price used to compute the current aggregate price
202  pub agg        : PriceInfo,
203  /// The publisher's latest price. This price will be incorporated into the aggregate price
204  /// when price aggregation runs next.
205  pub latest     : PriceInfo
206
207}
208
209/// An exponentially-weighted moving average.
210#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize)]
211#[repr(C)]
212pub struct Ema
213{
214  /// The current value of the EMA
215  pub val        : i64,
216  /// numerator state for next update
217  pub numer          : i64,
218  /// denominator state for next update
219  pub denom          : i64
220}
221
222/// Price accounts represent a continuously-updating price feed for a product.
223#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
224#[repr(C)]
225pub struct Price
226{
227  /// pyth magic number
228  pub magic          : u32,
229  /// program version
230  pub ver            : u32,
231  /// account type
232  pub atype          : u32,
233  /// price account size
234  pub size           : u32,
235  /// price or calculation type
236  pub ptype          : PriceType,
237  /// price exponent
238  pub expo           : i32,
239  /// number of component prices
240  pub num            : u32,
241  /// number of quoters that make up aggregate
242  pub num_qt         : u32,
243  /// slot of last valid (not unknown) aggregate price
244  pub last_slot      : u64,
245  /// valid slot-time of agg. price
246  pub valid_slot     : u64,
247  /// exponential moving average price
248  pub ema_price      : Ema,
249  /// exponential moving average confidence interval
250  pub ema_confidence : Ema,
251  /// space for future derived values
252  pub drv1           : i64,
253  /// space for future derived values
254  pub drv2           : i64,
255  /// product account key
256  pub prod           : AccKey,
257  /// next Price account in linked list
258  pub next           : AccKey,
259  /// valid slot of previous update
260  pub prev_slot      : u64,
261  /// aggregate price of previous update
262  pub prev_price     : i64,
263  /// confidence interval of previous update
264  pub prev_conf      : u64,
265  /// space for future derived values
266  pub drv3           : i64,
267  /// aggregate price info
268  pub agg            : PriceInfo,
269  /// price components one per quoter
270  pub comp           : [PriceComp;32]
271}
272
273#[cfg(target_endian = "little")]
274unsafe impl Zeroable for Price {}
275
276#[cfg(target_endian = "little")]
277unsafe impl Pod for Price {}
278
279impl Price {
280  /**
281   * Get the current status of the aggregate price.
282   * If this lib is used on-chain it will mark price status as unknown if price has not been updated for a while.
283   */
284  pub fn get_current_price_status(&self) -> PriceStatus {
285    #[cfg(target_arch = "bpf")]
286    if matches!(self.agg.status, PriceStatus::Trading) &&
287      Clock::get().unwrap().slot - self.agg.pub_slot > MAX_SLOT_DIFFERENCE {
288      return PriceStatus::Unknown;
289    }
290    self.agg.status
291  }
292
293  /**
294   * Get the current price and confidence interval as fixed-point numbers of the form a * 10^e.
295   * Returns a struct containing the current price, confidence interval, and the exponent for both
296   * numbers. Returns `None` if price information is currently unavailable for any reason.
297   */
298  pub fn get_current_price(&self) -> Option<PriceConf> {
299    if !matches!(self.get_current_price_status(), PriceStatus::Trading) {
300      None
301    } else {
302      Some(PriceConf {
303        price: self.agg.price,
304        conf: self.agg.conf,
305        expo: self.expo
306      })
307    }
308  }
309
310  /**
311   * Get the exponential moving average price (ema_price) and a confidence interval on the result.
312   * Returns `None` if the ema_price is currently unavailable.
313   *
314   * At the moment, the confidence interval returned by this method is computed in
315   * a somewhat questionable way, so we do not recommend using it for high-value applications.
316   */
317  pub fn get_ema_price(&self) -> Option<PriceConf> {
318    // This method currently cannot return None, but may do so in the future.
319    // Note that the ema_confidence is a positive number in i64, so safe to cast to u64.
320    Some(PriceConf { price: self.ema_price.val, conf: self.ema_confidence.val as u64, expo: self.expo })
321  }
322
323  /**
324   * Get the current price of this account in a different quote currency. If this account
325   * represents the price of the product X/Z, and `quote` represents the price of the product Y/Z,
326   * this method returns the price of X/Y. Use this method to get the price of e.g., mSOL/SOL from
327   * the mSOL/USD and SOL/USD accounts.
328   *
329   * `result_expo` determines the exponent of the result, i.e., the number of digits below the decimal
330   * point. This method returns `None` if either the price or confidence are too large to be
331   * represented with the requested exponent.
332   */
333  pub fn get_price_in_quote(&self, quote: &Price, result_expo: i32) -> Option<PriceConf> {
334    return match (self.get_current_price(), quote.get_current_price()) {
335      (Some(base_price_conf), Some(quote_price_conf)) =>
336        base_price_conf.div(&quote_price_conf)?.scale_to_exponent(result_expo),
337      (_, _) => None,
338    }
339  }
340
341  /**
342   * Get the price of a basket of currencies. Each entry in `amounts` is of the form
343   * `(price, qty, qty_expo)`, and the result is the sum of `price * qty * 10^qty_expo`.
344   * The result is returned with exponent `result_expo`.
345   *
346   * An example use case for this function is to get the value of an LP token.
347   */
348  pub fn price_basket(amounts: &[(Price, i64, i32)], result_expo: i32) -> Option<PriceConf> {
349    assert!(amounts.len() > 0);
350    let mut res = PriceConf { price: 0, conf: 0, expo: result_expo };
351    for i in 0..amounts.len() {
352      res = res.add(
353        &amounts[i].0.get_current_price()?.cmul(amounts[i].1, amounts[i].2)?.scale_to_exponent(result_expo)?
354      )?
355    }
356    Some(res)
357  }
358}
359
360#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
361struct AccKeyU64
362{
363  pub val: [u64;4]
364}
365
366#[cfg(target_endian = "little")]
367unsafe impl Zeroable for AccKeyU64 {}
368
369#[cfg(target_endian = "little")]
370unsafe impl Pod for AccKeyU64 {}
371
372impl AccKey
373{
374  pub fn is_valid( &self ) -> bool  {
375    match load::<AccKeyU64>( &self.val ) {
376      Ok(k8) => k8.val[0]!=0 || k8.val[1]!=0 || k8.val[2]!=0 || k8.val[3]!=0,
377      Err(_) => false,
378    }
379  }
380}
381
382fn load<T: Pod>(data: &[u8]) -> Result<&T, PodCastError> {
383  let size = size_of::<T>();
384  if data.len() >= size {
385    Ok(from_bytes(cast_slice::<u8, u8>(try_cast_slice(
386      &data[0..size],
387    )?)))
388  } else {
389    Err(PodCastError::SizeMismatch)
390  }
391}
392
393/** Get a `Mapping` account from the raw byte value of a Solana account. */
394pub fn load_mapping(data: &[u8]) -> Result<&Mapping, PythError> {
395  let pyth_mapping = load::<Mapping>(&data).map_err(|_| PythError::InvalidAccountData)?;
396
397  if pyth_mapping.magic != MAGIC {
398    return Err(PythError::InvalidAccountData);
399  }
400  if pyth_mapping.ver != VERSION_2 {
401    return Err(PythError::BadVersionNumber);
402  }
403  if pyth_mapping.atype != AccountType::Mapping as u32 {
404    return Err(PythError::WrongAccountType);
405  }
406
407  return Ok(pyth_mapping);
408}
409
410/** Get a `Product` account from the raw byte value of a Solana account. */
411pub fn load_product(data: &[u8]) -> Result<&Product, PythError> {
412  let pyth_product = load::<Product>(&data).map_err(|_| PythError::InvalidAccountData)?;
413
414  if pyth_product.magic != MAGIC {
415    return Err(PythError::InvalidAccountData);
416  }
417  if pyth_product.ver != VERSION_2 {
418    return Err(PythError::BadVersionNumber);
419  }
420  if pyth_product.atype != AccountType::Product as u32 {
421    return Err(PythError::WrongAccountType);
422  }
423
424  return Ok(pyth_product);
425}
426
427/** Get a `Price` account from the raw byte value of a Solana account. */
428pub fn load_price(data: &[u8]) -> Result<&Price, PythError> {
429  let pyth_price = load::<Price>(&data).map_err(|_| PythError::InvalidAccountData)?;
430
431  if pyth_price.magic != MAGIC {
432    return Err(PythError::InvalidAccountData);
433  }
434  if pyth_price.ver != VERSION_2 {
435    return Err(PythError::BadVersionNumber);
436  }
437  if pyth_price.atype != AccountType::Price as u32 {
438    return Err(PythError::WrongAccountType);
439  }
440
441  return Ok(pyth_price);
442}
443
444
445pub struct AttributeIter<'a> {
446    attrs: &'a [u8],
447}
448
449impl<'a> Iterator for AttributeIter<'a> {
450    type Item = (&'a str, &'a str);
451
452    fn next(&mut self) -> Option<Self::Item> {
453        if self.attrs.is_empty() {
454            return None;
455        }
456        let (key, data) = get_attr_str(self.attrs);
457        let (val, data) = get_attr_str(data);
458        self.attrs = data;
459        return Some((key, val));
460    }
461}
462
463fn get_attr_str(buf: &[u8]) -> (&str, &[u8]) {
464    if buf.is_empty() {
465        return ("", &[]);
466    }
467    let len = buf[0] as usize;
468    let str = std::str::from_utf8(&buf[1..len + 1]).expect("attr should be ascii or utf-8");
469    let remaining_buf = &buf[len + 1..];
470    (str, remaining_buf)
471}