pyth_lazer_protocol/
api.rs

1use std::{
2    cmp::Ordering,
3    fmt::Display,
4    ops::{Deref, DerefMut},
5};
6
7use derive_more::derive::From;
8use itertools::Itertools as _;
9use serde::{de::Error, Deserialize, Serialize};
10
11use crate::{
12    payload::AggregatedPriceFeedData,
13    time::{DurationUs, FixedRate, TimestampUs},
14    ChannelId, Price, PriceFeedId, PriceFeedProperty, Rate,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct LatestPriceRequestRepr {
20    // Either price feed ids or symbols must be specified.
21    pub price_feed_ids: Option<Vec<PriceFeedId>>,
22    pub symbols: Option<Vec<String>>,
23    pub properties: Vec<PriceFeedProperty>,
24    // "chains" was renamed to "formats". "chains" is still supported for compatibility.
25    #[serde(alias = "chains")]
26    pub formats: Vec<Format>,
27    #[serde(default)]
28    pub json_binary_encoding: JsonBinaryEncoding,
29    /// If `true`, the stream update will contain a JSON object containing
30    /// all data of the update.
31    #[serde(default = "default_parsed")]
32    pub parsed: bool,
33    pub channel: Channel,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct LatestPriceRequest(LatestPriceRequestRepr);
39
40impl<'de> Deserialize<'de> for LatestPriceRequest {
41    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42    where
43        D: serde::Deserializer<'de>,
44    {
45        let value = LatestPriceRequestRepr::deserialize(deserializer)?;
46        Self::new(value).map_err(Error::custom)
47    }
48}
49
50impl LatestPriceRequest {
51    pub fn new(value: LatestPriceRequestRepr) -> Result<Self, &'static str> {
52        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
53        validate_optional_nonempty_vec_has_unique_elements(
54            &value.price_feed_ids,
55            "no price feed ids specified",
56            "duplicate price feed ids specified",
57        )?;
58        validate_optional_nonempty_vec_has_unique_elements(
59            &value.symbols,
60            "no symbols specified",
61            "duplicate symbols specified",
62        )?;
63        validate_formats(&value.formats)?;
64        validate_properties(&value.properties)?;
65        Ok(Self(value))
66    }
67}
68
69impl Deref for LatestPriceRequest {
70    type Target = LatestPriceRequestRepr;
71
72    fn deref(&self) -> &Self::Target {
73        &self.0
74    }
75}
76impl DerefMut for LatestPriceRequest {
77    fn deref_mut(&mut self) -> &mut Self::Target {
78        &mut self.0
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct PriceRequestRepr {
85    pub timestamp: TimestampUs,
86    // Either price feed ids or symbols must be specified.
87    pub price_feed_ids: Option<Vec<PriceFeedId>>,
88    pub symbols: Option<Vec<String>>,
89    pub properties: Vec<PriceFeedProperty>,
90    pub formats: Vec<Format>,
91    #[serde(default)]
92    pub json_binary_encoding: JsonBinaryEncoding,
93    /// If `true`, the stream update will contain a JSON object containing
94    /// all data of the update.
95    #[serde(default = "default_parsed")]
96    pub parsed: bool,
97    pub channel: Channel,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct PriceRequest(PriceRequestRepr);
103
104impl<'de> Deserialize<'de> for PriceRequest {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: serde::Deserializer<'de>,
108    {
109        let value = PriceRequestRepr::deserialize(deserializer)?;
110        Self::new(value).map_err(Error::custom)
111    }
112}
113
114impl PriceRequest {
115    pub fn new(value: PriceRequestRepr) -> Result<Self, &'static str> {
116        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
117        validate_optional_nonempty_vec_has_unique_elements(
118            &value.price_feed_ids,
119            "no price feed ids specified",
120            "duplicate price feed ids specified",
121        )?;
122        validate_optional_nonempty_vec_has_unique_elements(
123            &value.symbols,
124            "no symbols specified",
125            "duplicate symbols specified",
126        )?;
127        validate_formats(&value.formats)?;
128        validate_properties(&value.properties)?;
129        Ok(Self(value))
130    }
131}
132
133impl Deref for PriceRequest {
134    type Target = PriceRequestRepr;
135
136    fn deref(&self) -> &Self::Target {
137        &self.0
138    }
139}
140impl DerefMut for PriceRequest {
141    fn deref_mut(&mut self) -> &mut Self::Target {
142        &mut self.0
143    }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct ReducePriceRequest {
149    pub payload: JsonUpdate,
150    pub price_feed_ids: Vec<PriceFeedId>,
151}
152
153pub type LatestPriceResponse = JsonUpdate;
154pub type ReducePriceResponse = JsonUpdate;
155pub type PriceResponse = JsonUpdate;
156
157pub fn default_parsed() -> bool {
158    true
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub enum DeliveryFormat {
164    /// Deliver stream updates as JSON text messages.
165    #[default]
166    Json,
167    /// Deliver stream updates as binary messages.
168    Binary,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub enum Format {
174    Evm,
175    Solana,
176    LeEcdsa,
177    LeUnsigned,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub enum JsonBinaryEncoding {
183    #[default]
184    Base64,
185    Hex,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)]
189pub enum Channel {
190    FixedRate(FixedRate),
191    RealTime,
192}
193
194impl PartialOrd for Channel {
195    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
196        let rate_left = match self {
197            Channel::FixedRate(rate) => rate.duration().as_micros(),
198            Channel::RealTime => FixedRate::MIN.duration().as_micros(),
199        };
200        let rate_right = match other {
201            Channel::FixedRate(rate) => rate.duration().as_micros(),
202            Channel::RealTime => FixedRate::MIN.duration().as_micros(),
203        };
204        Some(rate_left.cmp(&rate_right))
205    }
206}
207
208impl Serialize for Channel {
209    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210    where
211        S: serde::Serializer,
212    {
213        match self {
214            Channel::FixedRate(fixed_rate) => serializer.serialize_str(&format!(
215                "fixed_rate@{}ms",
216                fixed_rate.duration().as_millis()
217            )),
218            Channel::RealTime => serializer.serialize_str("real_time"),
219        }
220    }
221}
222
223impl Display for Channel {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        match self {
226            Channel::FixedRate(fixed_rate) => {
227                write!(f, "fixed_rate@{}ms", fixed_rate.duration().as_millis())
228            }
229            Channel::RealTime => write!(f, "real_time"),
230        }
231    }
232}
233
234impl Channel {
235    pub fn id(&self) -> ChannelId {
236        match self {
237            Channel::FixedRate(fixed_rate) => match fixed_rate.duration().as_millis() {
238                50 => ChannelId::FIXED_RATE_50,
239                200 => ChannelId::FIXED_RATE_200,
240                1000 => ChannelId::FIXED_RATE_1000,
241                _ => panic!("unknown channel: {self:?}"),
242            },
243            Channel::RealTime => ChannelId::REAL_TIME,
244        }
245    }
246}
247
248#[test]
249fn id_supports_all_fixed_rates() {
250    for rate in FixedRate::ALL {
251        Channel::FixedRate(rate).id();
252    }
253}
254
255fn parse_channel(value: &str) -> Option<Channel> {
256    if value == "real_time" {
257        Some(Channel::RealTime)
258    } else if let Some(rest) = value.strip_prefix("fixed_rate@") {
259        let ms_value = rest.strip_suffix("ms")?;
260        Some(Channel::FixedRate(FixedRate::from_millis(
261            ms_value.parse().ok()?,
262        )?))
263    } else {
264        None
265    }
266}
267
268impl<'de> Deserialize<'de> for Channel {
269    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
270    where
271        D: serde::Deserializer<'de>,
272    {
273        let value = <String>::deserialize(deserializer)?;
274        parse_channel(&value).ok_or_else(|| Error::custom("unknown channel"))
275    }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct SubscriptionParamsRepr {
281    // Either price feed ids or symbols must be specified.
282    pub price_feed_ids: Option<Vec<PriceFeedId>>,
283    pub symbols: Option<Vec<String>>,
284    pub properties: Vec<PriceFeedProperty>,
285    // "chains" was renamed to "formats". "chains" is still supported for compatibility.
286    #[serde(alias = "chains")]
287    pub formats: Vec<Format>,
288    #[serde(default)]
289    pub delivery_format: DeliveryFormat,
290    #[serde(default)]
291    pub json_binary_encoding: JsonBinaryEncoding,
292    /// If `true`, the stream update will contain a `parsed` JSON field containing
293    /// all data of the update.
294    #[serde(default = "default_parsed")]
295    pub parsed: bool,
296    pub channel: Channel,
297    // "ignoreInvalidFeedIds" was renamed to "ignoreInvalidFeeds". "ignoreInvalidFeedIds" is still supported for compatibility.
298    #[serde(default, alias = "ignoreInvalidFeedIds")]
299    pub ignore_invalid_feeds: bool,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
303#[serde(rename_all = "camelCase")]
304pub struct SubscriptionParams(SubscriptionParamsRepr);
305
306impl<'de> Deserialize<'de> for SubscriptionParams {
307    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
308    where
309        D: serde::Deserializer<'de>,
310    {
311        let value = SubscriptionParamsRepr::deserialize(deserializer)?;
312        Self::new(value).map_err(Error::custom)
313    }
314}
315
316impl SubscriptionParams {
317    pub fn new(value: SubscriptionParamsRepr) -> Result<Self, &'static str> {
318        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
319        validate_optional_nonempty_vec_has_unique_elements(
320            &value.price_feed_ids,
321            "no price feed ids specified",
322            "duplicate price feed ids specified",
323        )?;
324        validate_optional_nonempty_vec_has_unique_elements(
325            &value.symbols,
326            "no symbols specified",
327            "duplicate symbols specified",
328        )?;
329        validate_formats(&value.formats)?;
330        validate_properties(&value.properties)?;
331        Ok(Self(value))
332    }
333}
334
335impl Deref for SubscriptionParams {
336    type Target = SubscriptionParamsRepr;
337
338    fn deref(&self) -> &Self::Target {
339        &self.0
340    }
341}
342impl DerefMut for SubscriptionParams {
343    fn deref_mut(&mut self) -> &mut Self::Target {
344        &mut self.0
345    }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct JsonBinaryData {
351    pub encoding: JsonBinaryEncoding,
352    pub data: String,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct JsonUpdate {
358    /// Present unless `parsed = false` is specified in subscription params.
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub parsed: Option<ParsedPayload>,
361    /// Only present if `Evm` is present in `formats` in subscription params.
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub evm: Option<JsonBinaryData>,
364    /// Only present if `Solana` is present in `formats` in subscription params.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub solana: Option<JsonBinaryData>,
367    /// Only present if `LeEcdsa` is present in `formats` in subscription params.
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub le_ecdsa: Option<JsonBinaryData>,
370    /// Only present if `LeUnsigned` is present in `formats` in subscription params.
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub le_unsigned: Option<JsonBinaryData>,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
376#[serde(rename_all = "camelCase")]
377pub struct ParsedPayload {
378    #[serde(with = "crate::serde_str::timestamp")]
379    pub timestamp_us: TimestampUs,
380    pub price_feeds: Vec<ParsedFeedPayload>,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct ParsedFeedPayload {
386    pub price_feed_id: PriceFeedId,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    #[serde(with = "crate::serde_str::option_price")]
389    #[serde(default)]
390    pub price: Option<Price>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    #[serde(with = "crate::serde_str::option_price")]
393    #[serde(default)]
394    pub best_bid_price: Option<Price>,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    #[serde(with = "crate::serde_str::option_price")]
397    #[serde(default)]
398    pub best_ask_price: Option<Price>,
399    #[serde(skip_serializing_if = "Option::is_none")]
400    #[serde(default)]
401    pub publisher_count: Option<u16>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    #[serde(default)]
404    pub exponent: Option<i16>,
405    #[serde(skip_serializing_if = "Option::is_none")]
406    #[serde(default)]
407    pub confidence: Option<Price>,
408    #[serde(skip_serializing_if = "Option::is_none")]
409    #[serde(default)]
410    pub funding_rate: Option<Rate>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    #[serde(default)]
413    pub funding_timestamp: Option<TimestampUs>,
414    // More fields may be added later.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    #[serde(default)]
417    pub funding_rate_interval: Option<DurationUs>,
418}
419
420impl ParsedFeedPayload {
421    pub fn new(
422        price_feed_id: PriceFeedId,
423        exponent: Option<i16>,
424        data: &AggregatedPriceFeedData,
425        properties: &[PriceFeedProperty],
426    ) -> Self {
427        let mut output = Self {
428            price_feed_id,
429            price: None,
430            best_bid_price: None,
431            best_ask_price: None,
432            publisher_count: None,
433            exponent: None,
434            confidence: None,
435            funding_rate: None,
436            funding_timestamp: None,
437            funding_rate_interval: None,
438        };
439        for &property in properties {
440            match property {
441                PriceFeedProperty::Price => {
442                    output.price = data.price;
443                }
444                PriceFeedProperty::BestBidPrice => {
445                    output.best_bid_price = data.best_bid_price;
446                }
447                PriceFeedProperty::BestAskPrice => {
448                    output.best_ask_price = data.best_ask_price;
449                }
450                PriceFeedProperty::PublisherCount => {
451                    output.publisher_count = Some(data.publisher_count);
452                }
453                PriceFeedProperty::Exponent => {
454                    output.exponent = exponent;
455                }
456                PriceFeedProperty::Confidence => {
457                    output.confidence = data.confidence;
458                }
459                PriceFeedProperty::FundingRate => {
460                    output.funding_rate = data.funding_rate;
461                }
462                PriceFeedProperty::FundingTimestamp => {
463                    output.funding_timestamp = data.funding_timestamp;
464                }
465                PriceFeedProperty::FundingRateInterval => {
466                    output.funding_rate_interval = data.funding_rate_interval;
467                }
468            }
469        }
470        output
471    }
472
473    pub fn new_full(
474        price_feed_id: PriceFeedId,
475        exponent: Option<i16>,
476        data: &AggregatedPriceFeedData,
477    ) -> Self {
478        Self {
479            price_feed_id,
480            price: data.price,
481            best_bid_price: data.best_bid_price,
482            best_ask_price: data.best_ask_price,
483            publisher_count: Some(data.publisher_count),
484            exponent,
485            confidence: data.confidence,
486            funding_rate: data.funding_rate,
487            funding_timestamp: data.funding_timestamp,
488            funding_rate_interval: data.funding_rate_interval,
489        }
490    }
491}
492
493/// A request sent from the client to the server.
494#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
495#[serde(tag = "type")]
496#[serde(rename_all = "camelCase")]
497pub enum WsRequest {
498    Subscribe(SubscribeRequest),
499    Unsubscribe(UnsubscribeRequest),
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
503pub struct SubscriptionId(pub u64);
504
505#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
506#[serde(rename_all = "camelCase")]
507pub struct SubscribeRequest {
508    pub subscription_id: SubscriptionId,
509    #[serde(flatten)]
510    pub params: SubscriptionParams,
511}
512
513#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
514#[serde(rename_all = "camelCase")]
515pub struct UnsubscribeRequest {
516    pub subscription_id: SubscriptionId,
517}
518
519/// A JSON response sent from the server to the client.
520#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From)]
521#[serde(tag = "type")]
522#[serde(rename_all = "camelCase")]
523pub enum WsResponse {
524    Error(ErrorResponse),
525    Subscribed(SubscribedResponse),
526    SubscribedWithInvalidFeedIdsIgnored(SubscribedWithInvalidFeedIdsIgnoredResponse),
527    Unsubscribed(UnsubscribedResponse),
528    SubscriptionError(SubscriptionErrorResponse),
529    StreamUpdated(StreamUpdatedResponse),
530}
531
532/// Sent from the server after a successul subscription.
533#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct SubscribedResponse {
536    pub subscription_id: SubscriptionId,
537}
538
539#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct InvalidFeedSubscriptionDetails {
542    pub unknown_ids: Vec<PriceFeedId>,
543    pub unknown_symbols: Vec<String>,
544    pub unsupported_channels: Vec<PriceFeedId>,
545    pub unstable: Vec<PriceFeedId>,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
549#[serde(rename_all = "camelCase")]
550pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
551    pub subscription_id: SubscriptionId,
552    pub subscribed_feed_ids: Vec<PriceFeedId>,
553    pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
554}
555
556#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
557#[serde(rename_all = "camelCase")]
558pub struct UnsubscribedResponse {
559    pub subscription_id: SubscriptionId,
560}
561
562/// Sent from the server if the requested subscription or unsubscription request
563/// could not be fulfilled.
564#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
565#[serde(rename_all = "camelCase")]
566pub struct SubscriptionErrorResponse {
567    pub subscription_id: SubscriptionId,
568    pub error: String,
569}
570
571/// Sent from the server if an internal error occured while serving data for an existing subscription,
572/// or a client request sent a bad request.
573#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
574#[serde(rename_all = "camelCase")]
575pub struct ErrorResponse {
576    pub error: String,
577}
578
579/// Sent from the server when new data is available for an existing subscription
580/// (only if `delivery_format == Json`).
581#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
582#[serde(rename_all = "camelCase")]
583pub struct StreamUpdatedResponse {
584    pub subscription_id: SubscriptionId,
585    #[serde(flatten)]
586    pub payload: JsonUpdate,
587}
588
589// Common validation functions
590fn validate_price_feed_ids_or_symbols(
591    price_feed_ids: &Option<Vec<PriceFeedId>>,
592    symbols: &Option<Vec<String>>,
593) -> Result<(), &'static str> {
594    if price_feed_ids.is_none() && symbols.is_none() {
595        return Err("either price feed ids or symbols must be specified");
596    }
597    if price_feed_ids.is_some() && symbols.is_some() {
598        return Err("either price feed ids or symbols must be specified, not both");
599    }
600    Ok(())
601}
602
603fn validate_optional_nonempty_vec_has_unique_elements<T>(
604    vec: &Option<Vec<T>>,
605    empty_msg: &'static str,
606    duplicate_msg: &'static str,
607) -> Result<(), &'static str>
608where
609    T: Eq + std::hash::Hash,
610{
611    if let Some(ref items) = vec {
612        if items.is_empty() {
613            return Err(empty_msg);
614        }
615        if !items.iter().all_unique() {
616            return Err(duplicate_msg);
617        }
618    }
619    Ok(())
620}
621
622fn validate_properties(properties: &[PriceFeedProperty]) -> Result<(), &'static str> {
623    if properties.is_empty() {
624        return Err("no properties specified");
625    }
626    if !properties.iter().all_unique() {
627        return Err("duplicate properties specified");
628    }
629    Ok(())
630}
631
632fn validate_formats(formats: &[Format]) -> Result<(), &'static str> {
633    if !formats.iter().all_unique() {
634        return Err("duplicate formats or chains specified");
635    }
636    Ok(())
637}