pythnet_sdk/
messages.rs

1#[cfg(feature = "solana-program")]
2use anchor_lang::{
3    AnchorDeserialize,
4    AnchorSerialize,
5};
6#[cfg(not(feature = "solana-program"))]
7use borsh::{
8    BorshDeserialize,
9    BorshSerialize,
10};
11#[cfg(feature = "quickcheck")]
12use quickcheck::Arbitrary;
13use {
14    borsh::BorshSchema,
15    serde::{
16        Deserialize,
17        Serialize,
18    },
19};
20
21/// Message format for sending data to other chains via the accumulator program
22/// When serialized with PythNet serialization format, each message starts with a unique
23/// 1-byte discriminator, followed by the serialized struct data in the definition(s) below.
24///
25/// Messages are forward-compatible. You may add new fields to messages after all previously
26/// defined fields. All code for parsing messages must ignore any extraneous bytes at the end of
27/// the message (which could be fields that the code does not yet understand).
28///
29/// The oracle is not using the Message enum due to the contract size limit and
30/// some of the methods for PriceFeedMessage and TwapMessage are not used by the oracle
31/// for the same reason. Rust compiler doesn't include the unused methods in the contract.
32/// Once we start using the unused structs and methods, the contract size will increase.
33#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
34#[cfg_attr(
35    feature = "strum",
36    derive(strum::EnumDiscriminants),
37    strum_discriminants(name(MessageType)),
38    strum_discriminants(vis(pub)),
39    strum_discriminants(derive(
40        Hash,
41        strum::EnumIter,
42        strum::EnumString,
43        strum::IntoStaticStr,
44        strum::Display,
45        Serialize,
46        Deserialize
47    ))
48)]
49
50pub enum Message {
51    PriceFeedMessage(PriceFeedMessage),
52    TwapMessage(TwapMessage),
53}
54
55impl Message {
56    pub fn publish_time(&self) -> i64 {
57        match self {
58            Self::PriceFeedMessage(msg) => msg.publish_time,
59            Self::TwapMessage(msg) => msg.publish_time,
60        }
61    }
62
63    pub fn feed_id(&self) -> FeedId {
64        match self {
65            Self::PriceFeedMessage(msg) => msg.feed_id,
66            Self::TwapMessage(msg) => msg.feed_id,
67        }
68    }
69}
70
71#[cfg(feature = "quickcheck")]
72impl Arbitrary for Message {
73    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
74        match u8::arbitrary(g) % 2 {
75            0 => Message::PriceFeedMessage(Arbitrary::arbitrary(g)),
76            _ => Message::TwapMessage(Arbitrary::arbitrary(g)),
77        }
78    }
79}
80
81/// Id of a feed producing the message. One feed produces one or more messages.
82pub type FeedId = [u8; 32];
83
84#[repr(C)]
85#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, BorshSchema)]
86#[cfg_attr(feature = "solana-program", derive(AnchorSerialize, AnchorDeserialize))]
87#[cfg_attr(
88    not(feature = "solana-program"),
89    derive(BorshSerialize, BorshDeserialize)
90)]
91
92pub struct PriceFeedMessage {
93    pub feed_id:           FeedId,
94    pub price:             i64,
95    pub conf:              u64,
96    pub exponent:          i32,
97    /// The timestamp of this price update in seconds
98    pub publish_time:      i64,
99    /// The timestamp of the previous price update. This field is intended to allow users to
100    /// identify the single unique price update for any moment in time:
101    /// for any time t, the unique update is the one such that prev_publish_time < t <= publish_time.
102    ///
103    /// Note that there may not be such an update while we are migrating to the new message-sending logic,
104    /// as some price updates on pythnet may not be sent to other chains (because the message-sending
105    /// logic may not have triggered). We can solve this problem by making the message-sending mandatory
106    /// (which we can do once publishers have migrated over).
107    ///
108    /// Additionally, this field may be equal to publish_time if the message is sent on a slot where
109    /// where the aggregation was unsuccesful. This problem will go away once all publishers have
110    /// migrated over to a recent version of pyth-agent.
111    pub prev_publish_time: i64,
112    pub ema_price:         i64,
113    pub ema_conf:          u64,
114}
115
116#[cfg(feature = "quickcheck")]
117impl Arbitrary for PriceFeedMessage {
118    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
119        let mut id = [0u8; 32];
120        for item in &mut id {
121            *item = u8::arbitrary(g);
122        }
123
124        let publish_time = i64::arbitrary(g);
125
126        PriceFeedMessage {
127            id,
128            price: i64::arbitrary(g),
129            conf: u64::arbitrary(g),
130            exponent: i32::arbitrary(g),
131            publish_time,
132            prev_publish_time: publish_time.saturating_sub(i64::arbitrary(g)),
133            ema_price: i64::arbitrary(g),
134            ema_conf: u64::arbitrary(g),
135        }
136    }
137}
138
139/// Message format for sending Twap data via the accumulator program
140#[repr(C)]
141#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
142pub struct TwapMessage {
143    pub feed_id:           FeedId,
144    pub cumulative_price:  i128,
145    pub cumulative_conf:   u128,
146    pub num_down_slots:    u64,
147    pub exponent:          i32,
148    pub publish_time:      i64,
149    pub prev_publish_time: i64,
150    pub publish_slot:      u64,
151}
152
153#[cfg(feature = "quickcheck")]
154impl Arbitrary for TwapMessage {
155    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
156        let mut id = [0u8; 32];
157        for item in &mut id {
158            *item = u8::arbitrary(g);
159        }
160
161        let publish_time = i64::arbitrary(g);
162
163        TwapMessage {
164            id,
165            cumulative_price: i128::arbitrary(g),
166            cumulative_conf: u128::arbitrary(g),
167            num_down_slots: u64::arbitrary(g),
168            exponent: i32::arbitrary(g),
169            publish_time,
170            prev_publish_time: publish_time.saturating_sub(i64::arbitrary(g)),
171            publish_slot: u64::arbitrary(g),
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178
179    use crate::{
180        messages::{
181            Message,
182            PriceFeedMessage,
183        },
184        wire::Serializer,
185    };
186
187    // Test if additional payload to the end of a message is forward compatible
188    #[test]
189    fn test_forward_compatibility() {
190        use {
191            serde::Serialize,
192            std::iter,
193        };
194        let msg = Message::PriceFeedMessage(PriceFeedMessage {
195            feed_id:           [1u8; 32],
196            price:             1,
197            conf:              1,
198            exponent:          1,
199            publish_time:      1,
200            prev_publish_time: 1,
201            ema_price:         1,
202            ema_conf:          1,
203        });
204        let mut buffer = Vec::new();
205        let mut cursor = std::io::Cursor::new(&mut buffer);
206        let mut serializer: Serializer<_, byteorder::LE> = Serializer::new(&mut cursor);
207        msg.serialize(&mut serializer).unwrap();
208        buffer.extend(iter::repeat(0).take(10));
209        let deserialized = crate::wire::from_slice::<byteorder::LE, Message>(&buffer).unwrap();
210        assert_eq!(deserialized, msg);
211    }
212}