pyth_sdk_solana/
state.rs

1//! Structures and functions for interacting with Solana on-chain account data.
2
3use borsh::{
4    BorshDeserialize,
5    BorshSerialize,
6};
7use bytemuck::{
8    cast_slice,
9    from_bytes,
10    try_cast_slice,
11    Pod,
12    PodCastError,
13    Zeroable,
14};
15use pyth_sdk::{
16    PriceIdentifier,
17    UnixTimestamp,
18};
19use solana_program::clock::Clock;
20use solana_program::pubkey::Pubkey;
21use std::cmp::min;
22use std::mem::size_of;
23
24pub use pyth_sdk::{
25    Price,
26    PriceFeed,
27};
28
29use crate::PythError;
30
31pub const MAGIC: u32 = 0xa1b2c3d4;
32pub const VERSION_2: u32 = 2;
33pub const VERSION: u32 = VERSION_2;
34pub const MAP_TABLE_SIZE: usize = 5000;
35pub const PROD_ACCT_SIZE: usize = 512;
36pub const PROD_HDR_SIZE: usize = 48;
37pub const PROD_ATTR_SIZE: usize = PROD_ACCT_SIZE - PROD_HDR_SIZE;
38
39/// The type of Pyth account determines what data it contains
40#[derive(
41    Copy,
42    Clone,
43    Debug,
44    PartialEq,
45    Eq,
46    BorshSerialize,
47    BorshDeserialize,
48    serde::Serialize,
49    serde::Deserialize,
50    Default,
51)]
52#[repr(u8)]
53pub enum AccountType {
54    #[default]
55    Unknown,
56    Mapping,
57    Product,
58    Price,
59}
60
61/// Status of any ongoing corporate actions.
62/// (still undergoing dev)
63#[derive(
64    Copy,
65    Clone,
66    Debug,
67    PartialEq,
68    Eq,
69    BorshSerialize,
70    BorshDeserialize,
71    serde::Serialize,
72    serde::Deserialize,
73    Default,
74)]
75#[repr(u8)]
76pub enum CorpAction {
77    #[default]
78    NoCorpAct,
79}
80
81/// The type of prices associated with a product -- each product may have multiple price feeds of
82/// different types.
83#[derive(
84    Copy,
85    Clone,
86    Debug,
87    PartialEq,
88    Eq,
89    BorshSerialize,
90    BorshDeserialize,
91    serde::Serialize,
92    serde::Deserialize,
93    Default,
94)]
95#[repr(u8)]
96pub enum PriceType {
97    #[default]
98    Unknown,
99    Price,
100}
101
102/// Represents availability status of a price feed.
103#[derive(
104    Copy,
105    Clone,
106    Debug,
107    PartialEq,
108    Eq,
109    BorshSerialize,
110    BorshDeserialize,
111    serde::Serialize,
112    serde::Deserialize,
113    Default,
114)]
115#[repr(u8)]
116pub enum PriceStatus {
117    /// The price feed is not currently updating for an unknown reason.
118    #[default]
119    Unknown,
120    /// The price feed is updating as expected.
121    Trading,
122    /// The price feed is not currently updating because trading in the product has been halted.
123    Halted,
124    /// The price feed is not currently updating because an auction is setting the price.
125    Auction,
126    /// A price component can be ignored if the confidence interval is too wide
127    Ignored,
128}
129
130impl std::fmt::Display for PriceStatus {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(
133            f,
134            "{}",
135            match self {
136                Self::Unknown => "unknown",
137                Self::Trading => "trading",
138                Self::Halted => "halted",
139                Self::Auction => "auction",
140                Self::Ignored => "ignored",
141            }
142        )
143    }
144}
145
146/// Mapping accounts form a linked-list containing the listing of all products on Pyth.
147#[derive(Copy, Clone, Debug, PartialEq, Eq)]
148#[repr(C)]
149pub struct MappingAccount {
150    /// pyth magic number
151    pub magic:    u32,
152    /// program version
153    pub ver:      u32,
154    /// account type
155    pub atype:    u32,
156    /// account used size
157    pub size:     u32,
158    /// number of product accounts
159    pub num:      u32,
160    pub unused:   u32,
161    /// next mapping account (if any)
162    pub next:     Pubkey,
163    pub products: [Pubkey; MAP_TABLE_SIZE],
164}
165
166#[cfg(target_endian = "little")]
167unsafe impl Zeroable for MappingAccount {
168}
169
170#[cfg(target_endian = "little")]
171unsafe impl Pod for MappingAccount {
172}
173
174/// Product accounts contain metadata for a single product, such as its symbol ("Crypto.BTC/USD")
175/// and its base/quote currencies.
176#[derive(Copy, Clone, Debug, PartialEq, Eq)]
177#[repr(C)]
178pub struct ProductAccount {
179    /// pyth magic number
180    pub magic:  u32,
181    /// program version
182    pub ver:    u32,
183    /// account type
184    pub atype:  u32,
185    /// price account size
186    pub size:   u32,
187    /// first price account in list
188    pub px_acc: Pubkey,
189    /// key/value pairs of reference attr.
190    pub attr:   [u8; PROD_ATTR_SIZE],
191}
192
193impl ProductAccount {
194    pub fn iter(&self) -> AttributeIter {
195        AttributeIter {
196            attrs: &self.attr[..min(
197                (self.size as usize).saturating_sub(PROD_HDR_SIZE),
198                PROD_ATTR_SIZE,
199            )],
200        }
201    }
202}
203
204#[cfg(target_endian = "little")]
205unsafe impl Zeroable for ProductAccount {
206}
207
208#[cfg(target_endian = "little")]
209unsafe impl Pod for ProductAccount {
210}
211
212/// A price and confidence at a specific slot. This struct can represent either a
213/// publisher's contribution or the outcome of price aggregation.
214#[derive(
215    Copy,
216    Clone,
217    Debug,
218    Default,
219    PartialEq,
220    Eq,
221    BorshSerialize,
222    BorshDeserialize,
223    serde::Serialize,
224    serde::Deserialize,
225)]
226#[repr(C)]
227pub struct PriceInfo {
228    /// the current price.
229    /// For the aggregate price use `get_price_no_older_than()` whenever possible. Accessing fields
230    /// directly might expose you to stale or invalid prices.
231    pub price:    i64,
232    /// confidence interval around the price.
233    /// For the aggregate confidence use `get_price_no_older_than()` whenever possible. Accessing
234    /// fields directly might expose you to stale or invalid prices.
235    pub conf:     u64,
236    /// status of price (Trading is valid)
237    pub status:   PriceStatus,
238    /// notification of any corporate action
239    pub corp_act: CorpAction,
240    pub pub_slot: u64,
241}
242
243/// The price and confidence contributed by a specific publisher.
244#[derive(
245    Copy,
246    Clone,
247    Debug,
248    Default,
249    PartialEq,
250    Eq,
251    BorshSerialize,
252    BorshDeserialize,
253    serde::Serialize,
254    serde::Deserialize,
255)]
256#[repr(C)]
257pub struct PriceComp {
258    /// key of contributing publisher
259    pub publisher: Pubkey,
260    /// the price used to compute the current aggregate price
261    pub agg:       PriceInfo,
262    /// The publisher's latest price. This price will be incorporated into the aggregate price
263    /// when price aggregation runs next.
264    pub latest:    PriceInfo,
265}
266
267#[deprecated = "Type is renamed to Rational, please use the new name."]
268pub type Ema = Rational;
269
270/// An number represented as both `value` and also in rational as `numer/denom`.
271#[derive(
272    Copy,
273    Clone,
274    Debug,
275    Default,
276    PartialEq,
277    Eq,
278    BorshSerialize,
279    BorshDeserialize,
280    serde::Serialize,
281    serde::Deserialize,
282)]
283#[repr(C)]
284pub struct Rational {
285    pub val:   i64,
286    pub numer: i64,
287    pub denom: i64,
288}
289
290#[repr(C)]
291#[derive(Copy, Clone, Debug, PartialEq, Eq)]
292pub struct GenericPriceAccount<const N: usize, T>
293where
294    T: Default,
295    T: Copy,
296{
297    /// pyth magic number
298    pub magic:          u32,
299    /// program version
300    pub ver:            u32,
301    /// account type
302    pub atype:          u32,
303    /// price account size
304    pub size:           u32,
305    /// price or calculation type
306    pub ptype:          PriceType,
307    /// price exponent
308    pub expo:           i32,
309    /// number of component prices
310    pub num:            u32,
311    /// number of quoters that make up aggregate
312    pub num_qt:         u32,
313    /// slot of last valid (not unknown) aggregate price
314    pub last_slot:      u64,
315    /// valid slot-time of agg. price
316    pub valid_slot:     u64,
317    /// exponentially moving average price
318    pub ema_price:      Rational,
319    /// exponentially moving average confidence interval
320    pub ema_conf:       Rational,
321    /// unix timestamp of aggregate price
322    pub timestamp:      i64,
323    /// min publishers for valid price
324    pub min_pub:        u8,
325    /// space for future derived values
326    pub drv2:           u8,
327    /// space for future derived values
328    pub drv3:           u16,
329    /// space for future derived values
330    pub drv4:           u32,
331    /// product account key
332    pub prod:           Pubkey,
333    /// next Price account in linked list
334    pub next:           Pubkey,
335    /// valid slot of previous update
336    pub prev_slot:      u64,
337    /// aggregate price of previous update with TRADING status
338    pub prev_price:     i64,
339    /// confidence interval of previous update with TRADING status
340    pub prev_conf:      u64,
341    /// unix timestamp of previous aggregate with TRADING status
342    pub prev_timestamp: i64,
343    /// aggregate price info
344    pub agg:            PriceInfo,
345    /// price components one per quoter
346    pub comp:           [PriceComp; N],
347    /// additional extended account data
348    pub extended:       T,
349}
350
351impl<const N: usize, T> Default for GenericPriceAccount<N, T>
352where
353    T: Default,
354    T: Copy,
355{
356    fn default() -> Self {
357        Self {
358            magic:          Default::default(),
359            ver:            Default::default(),
360            atype:          Default::default(),
361            size:           Default::default(),
362            ptype:          Default::default(),
363            expo:           Default::default(),
364            num:            Default::default(),
365            num_qt:         Default::default(),
366            last_slot:      Default::default(),
367            valid_slot:     Default::default(),
368            ema_price:      Default::default(),
369            ema_conf:       Default::default(),
370            timestamp:      Default::default(),
371            min_pub:        Default::default(),
372            drv2:           Default::default(),
373            drv3:           Default::default(),
374            drv4:           Default::default(),
375            prod:           Default::default(),
376            next:           Default::default(),
377            prev_slot:      Default::default(),
378            prev_price:     Default::default(),
379            prev_conf:      Default::default(),
380            prev_timestamp: Default::default(),
381            agg:            Default::default(),
382            comp:           [Default::default(); N],
383            extended:       Default::default(),
384        }
385    }
386}
387
388impl<const N: usize, T> std::ops::Deref for GenericPriceAccount<N, T>
389where
390    T: Default,
391    T: Copy,
392{
393    type Target = T;
394    fn deref(&self) -> &Self::Target {
395        &self.extended
396    }
397}
398
399#[repr(C)]
400#[derive(Copy, Clone, Debug, Default, Pod, Zeroable, PartialEq, Eq)]
401pub struct PriceCumulative {
402    /// Cumulative sum of price * slot_gap
403    pub price:          i128,
404    /// Cumulative sum of conf * slot_gap
405    pub conf:           u128,
406    /// Cumulative number of slots where the price wasn't recently updated (within
407    /// PC_MAX_SEND_LATENCY slots). This field should be used to calculate the downtime
408    /// as a percent of slots between two times `T` and `t` as follows:
409    /// `(T.num_down_slots - t.num_down_slots) / (T.agg_.pub_slot_ - t.agg_.pub_slot_)`
410    pub num_down_slots: u64,
411    /// Padding for alignment
412    pub unused:         u64,
413}
414
415#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
416pub struct PriceAccountExt {
417    pub price_cumulative: PriceCumulative,
418}
419
420/// Backwards compatibility.
421#[deprecated(note = "use an explicit SolanaPriceAccount or PythnetPriceAccount to avoid ambiguity")]
422pub type PriceAccount = GenericPriceAccount<32, ()>;
423
424/// Solana-specific Pyth account where the old 32-element publishers are present.
425pub type SolanaPriceAccount = GenericPriceAccount<32, ()>;
426
427/// Pythnet-specific Price accountw ith upgraded 64-element publishers and extended fields.
428pub type PythnetPriceAccount = GenericPriceAccount<128, PriceAccountExt>;
429
430#[cfg(target_endian = "little")]
431unsafe impl<const N: usize, T: Default + Copy> Zeroable for GenericPriceAccount<N, T> {
432}
433
434#[cfg(target_endian = "little")]
435unsafe impl<const N: usize, T: Default + Copy + 'static> Pod for GenericPriceAccount<N, T> {
436}
437
438impl<const N: usize, T> GenericPriceAccount<N, T>
439where
440    T: Default,
441    T: Copy,
442{
443    pub fn get_publish_time(&self) -> UnixTimestamp {
444        match self.agg.status {
445            PriceStatus::Trading => self.timestamp,
446            _ => self.prev_timestamp,
447        }
448    }
449
450    /// Get the last valid price as long as it was updated within `slot_threshold` slots of the
451    /// current slot.
452    pub fn get_price_no_older_than(&self, clock: &Clock, slot_threshold: u64) -> Option<Price> {
453        if self.agg.status == PriceStatus::Trading
454            && self.agg.pub_slot >= clock.slot - slot_threshold
455        {
456            return Some(Price {
457                conf:         self.agg.conf,
458                expo:         self.expo,
459                price:        self.agg.price,
460                publish_time: self.timestamp,
461            });
462        }
463
464        if self.prev_slot >= clock.slot - slot_threshold {
465            return Some(Price {
466                conf:         self.prev_conf,
467                expo:         self.expo,
468                price:        self.prev_price,
469                publish_time: self.prev_timestamp,
470            });
471        }
472
473        None
474    }
475
476    pub fn to_price_feed(&self, price_key: &Pubkey) -> PriceFeed {
477        let status = self.agg.status;
478
479        let price = match status {
480            PriceStatus::Trading => Price {
481                conf:         self.agg.conf,
482                expo:         self.expo,
483                price:        self.agg.price,
484                publish_time: self.get_publish_time(),
485            },
486            _ => Price {
487                conf:         self.prev_conf,
488                expo:         self.expo,
489                price:        self.prev_price,
490                publish_time: self.get_publish_time(),
491            },
492        };
493
494        let ema_price = Price {
495            conf:         self.ema_conf.val as u64,
496            expo:         self.expo,
497            price:        self.ema_price.val,
498            publish_time: self.get_publish_time(),
499        };
500
501        PriceFeed::new(PriceIdentifier::new(price_key.to_bytes()), price, ema_price)
502    }
503}
504
505fn load<T: Pod>(data: &[u8]) -> Result<&T, PodCastError> {
506    let size = size_of::<T>();
507    if data.len() >= size {
508        Ok(from_bytes(cast_slice::<u8, u8>(try_cast_slice(
509            &data[0..size],
510        )?)))
511    } else {
512        Err(PodCastError::SizeMismatch)
513    }
514}
515
516/// Get a `Mapping` account from the raw byte value of a Solana account.
517pub fn load_mapping_account(data: &[u8]) -> Result<&MappingAccount, PythError> {
518    let pyth_mapping = load::<MappingAccount>(data).map_err(|_| PythError::InvalidAccountData)?;
519
520    if pyth_mapping.magic != MAGIC {
521        return Err(PythError::InvalidAccountData);
522    }
523    if pyth_mapping.ver != VERSION_2 {
524        return Err(PythError::BadVersionNumber);
525    }
526    if pyth_mapping.atype != AccountType::Mapping as u32 {
527        return Err(PythError::WrongAccountType);
528    }
529
530    Ok(pyth_mapping)
531}
532
533/// Get a `Product` account from the raw byte value of a Solana account.
534pub fn load_product_account(data: &[u8]) -> Result<&ProductAccount, PythError> {
535    let pyth_product = load::<ProductAccount>(data).map_err(|_| PythError::InvalidAccountData)?;
536
537    if pyth_product.magic != MAGIC {
538        return Err(PythError::InvalidAccountData);
539    }
540    if pyth_product.ver != VERSION_2 {
541        return Err(PythError::BadVersionNumber);
542    }
543    if pyth_product.atype != AccountType::Product as u32 {
544        return Err(PythError::WrongAccountType);
545    }
546
547    Ok(pyth_product)
548}
549
550/// Get a `Price` account from the raw byte value of a Solana account.
551pub fn load_price_account<const N: usize, T: Default + Copy + 'static>(
552    data: &[u8],
553) -> Result<&GenericPriceAccount<N, T>, PythError> {
554    let pyth_price =
555        load::<GenericPriceAccount<N, T>>(data).map_err(|_| PythError::InvalidAccountData)?;
556
557    if pyth_price.magic != MAGIC {
558        return Err(PythError::InvalidAccountData);
559    }
560    if pyth_price.ver != VERSION_2 {
561        return Err(PythError::BadVersionNumber);
562    }
563    if pyth_price.atype != AccountType::Price as u32 {
564        return Err(PythError::WrongAccountType);
565    }
566
567    Ok(pyth_price)
568}
569
570pub struct AttributeIter<'a> {
571    attrs: &'a [u8],
572}
573
574impl<'a> Iterator for AttributeIter<'a> {
575    type Item = (&'a str, &'a str);
576
577    fn next(&mut self) -> Option<Self::Item> {
578        if self.attrs.is_empty() {
579            return None;
580        }
581        let (key, data) = get_attr_str(self.attrs)?;
582        let (val, data) = get_attr_str(data)?;
583        self.attrs = data;
584        Some((key, val))
585    }
586}
587
588fn get_attr_str(buf: &[u8]) -> Option<(&str, &[u8])> {
589    if buf.is_empty() {
590        return Some(("", &[]));
591    }
592    let len = buf[0] as usize;
593    let str = std::str::from_utf8(buf.get(1..len + 1)?).ok()?;
594    let remaining_buf = &buf.get(len + 1..)?;
595    Some((str, remaining_buf))
596}
597
598#[cfg(test)]
599mod test {
600    use pyth_sdk::{
601        Identifier,
602        Price,
603        PriceFeed,
604    };
605    use solana_program::clock::Clock;
606    use solana_program::pubkey::Pubkey;
607
608    use crate::state::{
609        PROD_ACCT_SIZE,
610        PROD_HDR_SIZE,
611    };
612
613    use super::{
614        PriceInfo,
615        PriceStatus,
616        Rational,
617        SolanaPriceAccount,
618    };
619
620    #[test]
621    fn test_trading_price_to_price_feed() {
622        let price_account = SolanaPriceAccount {
623            expo: 5,
624            agg: PriceInfo {
625                price: 10,
626                conf: 20,
627                status: PriceStatus::Trading,
628                ..Default::default()
629            },
630            timestamp: 200,
631            prev_timestamp: 100,
632            ema_price: Rational {
633                val: 40,
634                ..Default::default()
635            },
636            ema_conf: Rational {
637                val: 50,
638                ..Default::default()
639            },
640            prev_price: 60,
641            prev_conf: 70,
642            ..Default::default()
643        };
644
645        let pubkey = Pubkey::new_from_array([3; 32]);
646        let price_feed = price_account.to_price_feed(&pubkey);
647
648        assert_eq!(
649            price_feed,
650            PriceFeed::new(
651                Identifier::new(pubkey.to_bytes()),
652                Price {
653                    conf:         20,
654                    price:        10,
655                    expo:         5,
656                    publish_time: 200,
657                },
658                Price {
659                    conf:         50,
660                    price:        40,
661                    expo:         5,
662                    publish_time: 200,
663                }
664            )
665        );
666    }
667
668    #[test]
669    fn test_non_trading_price_to_price_feed() {
670        let price_account = SolanaPriceAccount {
671            expo: 5,
672            agg: PriceInfo {
673                price: 10,
674                conf: 20,
675                status: PriceStatus::Unknown,
676                ..Default::default()
677            },
678            timestamp: 200,
679            prev_timestamp: 100,
680            ema_price: Rational {
681                val: 40,
682                ..Default::default()
683            },
684            ema_conf: Rational {
685                val: 50,
686                ..Default::default()
687            },
688            prev_price: 60,
689            prev_conf: 70,
690            ..Default::default()
691        };
692
693        let pubkey = Pubkey::new_from_array([3; 32]);
694        let price_feed = price_account.to_price_feed(&pubkey);
695
696        assert_eq!(
697            price_feed,
698            PriceFeed::new(
699                Identifier::new(pubkey.to_bytes()),
700                Price {
701                    conf:         70,
702                    price:        60,
703                    expo:         5,
704                    publish_time: 100,
705                },
706                Price {
707                    conf:         50,
708                    price:        40,
709                    expo:         5,
710                    publish_time: 100,
711                }
712            )
713        );
714    }
715
716    #[test]
717    fn test_happy_use_latest_price_in_price_no_older_than() {
718        let price_account = SolanaPriceAccount {
719            expo: 5,
720            agg: PriceInfo {
721                price: 10,
722                conf: 20,
723                status: PriceStatus::Trading,
724                pub_slot: 1,
725                ..Default::default()
726            },
727            timestamp: 200,
728            prev_timestamp: 100,
729            prev_price: 60,
730            prev_conf: 70,
731            ..Default::default()
732        };
733
734        let clock = Clock {
735            slot: 5,
736            ..Default::default()
737        };
738
739        assert_eq!(
740            price_account.get_price_no_older_than(&clock, 4),
741            Some(Price {
742                conf:         20,
743                expo:         5,
744                price:        10,
745                publish_time: 200,
746            })
747        );
748    }
749
750    #[test]
751    fn test_happy_use_prev_price_in_price_no_older_than() {
752        let price_account = SolanaPriceAccount {
753            expo: 5,
754            agg: PriceInfo {
755                price: 10,
756                conf: 20,
757                status: PriceStatus::Unknown,
758                pub_slot: 3,
759                ..Default::default()
760            },
761            timestamp: 200,
762            prev_timestamp: 100,
763            prev_price: 60,
764            prev_conf: 70,
765            prev_slot: 1,
766            ..Default::default()
767        };
768
769        let clock = Clock {
770            slot: 5,
771            ..Default::default()
772        };
773
774        assert_eq!(
775            price_account.get_price_no_older_than(&clock, 4),
776            Some(Price {
777                conf:         70,
778                expo:         5,
779                price:        60,
780                publish_time: 100,
781            })
782        );
783    }
784
785    #[test]
786    fn test_sad_cur_price_unknown_in_price_no_older_than() {
787        let price_account = SolanaPriceAccount {
788            expo: 5,
789            agg: PriceInfo {
790                price: 10,
791                conf: 20,
792                status: PriceStatus::Unknown,
793                pub_slot: 3,
794                ..Default::default()
795            },
796            timestamp: 200,
797            prev_timestamp: 100,
798            prev_price: 60,
799            prev_conf: 70,
800            prev_slot: 1,
801            ..Default::default()
802        };
803
804        let clock = Clock {
805            slot: 5,
806            ..Default::default()
807        };
808
809        // current price is unknown, prev price is too stale
810        assert_eq!(price_account.get_price_no_older_than(&clock, 3), None);
811    }
812
813    #[test]
814    fn test_sad_cur_price_stale_in_price_no_older_than() {
815        let price_account = SolanaPriceAccount {
816            expo: 5,
817            agg: PriceInfo {
818                price: 10,
819                conf: 20,
820                status: PriceStatus::Trading,
821                pub_slot: 3,
822                ..Default::default()
823            },
824            timestamp: 200,
825            prev_timestamp: 100,
826            prev_price: 60,
827            prev_conf: 70,
828            prev_slot: 1,
829            ..Default::default()
830        };
831
832        let clock = Clock {
833            slot: 5,
834            ..Default::default()
835        };
836
837        assert_eq!(price_account.get_price_no_older_than(&clock, 1), None);
838    }
839
840    #[test]
841    fn test_price_feed_representations_equal() {
842        #[repr(C)]
843        #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
844        pub struct OldPriceAccount {
845            pub magic:          u32,
846            pub ver:            u32,
847            pub atype:          u32,
848            pub size:           u32,
849            pub ptype:          crate::state::PriceType,
850            pub expo:           i32,
851            pub num:            u32,
852            pub num_qt:         u32,
853            pub last_slot:      u64,
854            pub valid_slot:     u64,
855            pub ema_price:      Rational,
856            pub ema_conf:       Rational,
857            pub timestamp:      i64,
858            pub min_pub:        u8,
859            pub drv2:           u8,
860            pub drv3:           u16,
861            pub drv4:           u32,
862            pub prod:           Pubkey,
863            pub next:           Pubkey,
864            pub prev_slot:      u64,
865            pub prev_price:     i64,
866            pub prev_conf:      u64,
867            pub prev_timestamp: i64,
868            pub agg:            PriceInfo,
869            pub comp:           [crate::state::PriceComp; 32],
870        }
871
872        // Would be better to fuzz this but better than no check.
873        let old = OldPriceAccount {
874            magic:          1,
875            ver:            2,
876            atype:          3,
877            size:           4,
878            ptype:          crate::state::PriceType::Price,
879            expo:           5,
880            num:            6,
881            num_qt:         7,
882            last_slot:      8,
883            valid_slot:     9,
884            ema_price:      Rational {
885                val:   1,
886                numer: 2,
887                denom: 3,
888            },
889            ema_conf:       Rational {
890                val:   1,
891                numer: 2,
892                denom: 3,
893            },
894            timestamp:      12,
895            min_pub:        13,
896            drv2:           14,
897            drv3:           15,
898            drv4:           16,
899            prod:           Pubkey::new_from_array([1; 32]),
900            next:           Pubkey::new_from_array([2; 32]),
901            prev_slot:      19,
902            prev_price:     20,
903            prev_conf:      21,
904            prev_timestamp: 22,
905            agg:            PriceInfo {
906                price:    1,
907                conf:     2,
908                status:   PriceStatus::Trading,
909                corp_act: crate::state::CorpAction::NoCorpAct,
910                pub_slot: 5,
911            },
912            comp:           [Default::default(); 32],
913        };
914
915        let new = super::SolanaPriceAccount {
916            magic:          1,
917            ver:            2,
918            atype:          3,
919            size:           4,
920            ptype:          crate::state::PriceType::Price,
921            expo:           5,
922            num:            6,
923            num_qt:         7,
924            last_slot:      8,
925            valid_slot:     9,
926            ema_price:      Rational {
927                val:   1,
928                numer: 2,
929                denom: 3,
930            },
931            ema_conf:       Rational {
932                val:   1,
933                numer: 2,
934                denom: 3,
935            },
936            timestamp:      12,
937            min_pub:        13,
938            drv2:           14,
939            drv3:           15,
940            drv4:           16,
941            prod:           Pubkey::new_from_array([1; 32]),
942            next:           Pubkey::new_from_array([2; 32]),
943            prev_slot:      19,
944            prev_price:     20,
945            prev_conf:      21,
946            prev_timestamp: 22,
947            agg:            PriceInfo {
948                price:    1,
949                conf:     2,
950                status:   PriceStatus::Trading,
951                corp_act: crate::state::CorpAction::NoCorpAct,
952                pub_slot: 5,
953            },
954            comp:           [Default::default(); 32],
955            extended:       (),
956        };
957
958        // Equal Sized?
959        assert_eq!(
960            std::mem::size_of::<OldPriceAccount>(),
961            std::mem::size_of::<super::SolanaPriceAccount>(),
962        );
963
964        // Equal Byte Representation?
965        unsafe {
966            let old_b = std::slice::from_raw_parts(
967                &old as *const OldPriceAccount as *const u8,
968                std::mem::size_of::<OldPriceAccount>(),
969            );
970            let new_b = std::slice::from_raw_parts(
971                &new as *const super::SolanaPriceAccount as *const u8,
972                std::mem::size_of::<super::SolanaPriceAccount>(),
973            );
974            assert_eq!(old_b, new_b);
975        }
976    }
977
978    #[test]
979    fn test_product_account_iter_works() {
980        let mut product = super::ProductAccount {
981            magic:  1,
982            ver:    2,
983            atype:  super::AccountType::Product as u32,
984            size:   PROD_HDR_SIZE as u32 + 10,
985            px_acc: Pubkey::new_from_array([3; 32]),
986            attr:   [0; super::PROD_ATTR_SIZE],
987        };
988
989        // Set some attributes
990        product.attr[0] = 3; // key length
991        product.attr[1..4].copy_from_slice(b"key");
992        product.attr[4] = 5; // value length
993        product.attr[5..10].copy_from_slice(b"value");
994
995        let mut iter = product.iter();
996        assert_eq!(iter.next(), Some(("key", "value")));
997        assert_eq!(iter.next(), None);
998
999        // Check that the iterator does not panic on size misconfiguration
1000        product.size = PROD_HDR_SIZE as u32 - 10; // Invalid size
1001        let mut iter = product.iter();
1002        assert_eq!(iter.next(), None); // Should not panic, just return None
1003
1004        product.size = PROD_ACCT_SIZE as u32 + 10; // Reset size to a size larger than the account size
1005        let mut iter = product.iter();
1006        assert_eq!(iter.next(), Some(("key", "value")));
1007        while iter.next().is_some() {} // Consume the iterator
1008
1009        // Check that invalid len stops the iterator. This behaviour is not perfect as it
1010        // stops reading attributes after the first invalid one but is just a safety measure.
1011        // In this case, we set the length byte to 255 which goes beyond the size of the
1012        // product account.
1013        product.attr[10] = 255;
1014        for i in 11..266 {
1015            product.attr[i] = b'a';
1016        }
1017        product.attr[266] = 255;
1018        for i in 267..super::PROD_ATTR_SIZE {
1019            product.attr[i] = b'b';
1020        }
1021        let mut iter = product.iter();
1022        assert_eq!(iter.next(), Some(("key", "value")));
1023        assert_eq!(iter.next(), None); // No more attributes because it stopped reading the invalid value
1024
1025        // Make sure if the value size was set to a smaller value, it would work fine
1026        product.attr[266] = 10;
1027        let mut iter = product.iter();
1028        assert_eq!(iter.next(), Some(("key", "value")));
1029        let (key, val) = iter.next().unwrap();
1030        assert_eq!(key.len(), 255);
1031        for byte in key.as_bytes() {
1032            assert_eq!(byte, &b'a');
1033        }
1034        assert_eq!(val, "bbbbbbbbbb"); // No more attributes because it stopped reading the invalid value
1035
1036        // Check that iterator stops on non-UTF8 attributes. This behaviour is not
1037        // perfect as it stops reading attributes after the first non-UTF8 one but
1038        // is just a safety measure.
1039        product.attr[1..4].copy_from_slice(b"\xff\xfe\xfa");
1040        let mut iter = product.iter();
1041        assert_eq!(iter.next(), None); // Should not panic, just return None
1042    }
1043}