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