pythnet_sdk/
messages.rs

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