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