pyth_lazer_protocol/
api.rs

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