Skip to main content

pyth_lazer_protocol/
api.rs

1use {
2    std::{
3        cmp::Ordering,
4        fmt::Display,
5        ops::{Deref, DerefMut},
6    },
7    utoipa::PartialSchema,
8};
9
10use derive_more::derive::From;
11use itertools::Itertools as _;
12use serde::{de::Error, Deserialize, Serialize};
13use utoipa::ToSchema;
14
15use crate::{
16    payload::AggregatedPriceFeedData,
17    time::{DurationUs, FixedRate, TimestampUs},
18    ChannelId, Price, PriceFeedId, PriceFeedProperty, Rate,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
22#[serde(rename_all = "camelCase")]
23#[schema(examples(LatestPriceRequestRepr::example1))]
24pub struct LatestPriceRequestRepr {
25    /// List of feed IDs.
26    /// Either feed ids or symbols must be specified.
27    pub price_feed_ids: Option<Vec<PriceFeedId>>,
28    /// List of feed symbols.
29    /// Either feed ids or symbols must be specified.
30    pub symbols: Option<Vec<String>>,
31    /// List of feed properties the sender is interested in.
32    pub properties: Vec<PriceFeedProperty>,
33    // "chains" was renamed to "formats". "chains" is still supported for compatibility.
34    /// Requested formats of the payload.
35    #[serde(alias = "chains")]
36    pub formats: Vec<Format>,
37    #[serde(default)]
38    pub json_binary_encoding: JsonBinaryEncoding,
39    /// If `true`, the response will contain a JSON object containing
40    /// all data of the update.
41    #[serde(default = "default_parsed")]
42    pub parsed: bool,
43    /// Channel determines frequency of updates.
44    pub channel: Channel,
45}
46
47impl LatestPriceRequestRepr {
48    fn example1() -> Self {
49        Self {
50            price_feed_ids: None,
51            symbols: Some(vec!["Crypto.BTC/USD".into()]),
52            properties: vec![PriceFeedProperty::Price, PriceFeedProperty::Confidence],
53            formats: vec![Format::Evm],
54            json_binary_encoding: JsonBinaryEncoding::Hex,
55            parsed: true,
56            channel: Channel::RealTime,
57        }
58    }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
62#[serde(rename_all = "camelCase")]
63pub struct LatestPriceRequest(LatestPriceRequestRepr);
64
65impl<'de> Deserialize<'de> for LatestPriceRequest {
66    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67    where
68        D: serde::Deserializer<'de>,
69    {
70        let value = LatestPriceRequestRepr::deserialize(deserializer)?;
71        Self::new(value).map_err(Error::custom)
72    }
73}
74
75impl LatestPriceRequest {
76    pub fn new(value: LatestPriceRequestRepr) -> Result<Self, &'static str> {
77        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
78        validate_optional_nonempty_vec_has_unique_elements(
79            &value.price_feed_ids,
80            "no price feed ids specified",
81            "duplicate price feed ids specified",
82        )?;
83        validate_optional_nonempty_vec_has_unique_elements(
84            &value.symbols,
85            "no symbols specified",
86            "duplicate symbols specified",
87        )?;
88        validate_formats(&value.formats)?;
89        validate_properties(&value.properties)?;
90        Ok(Self(value))
91    }
92}
93
94impl Deref for LatestPriceRequest {
95    type Target = LatestPriceRequestRepr;
96
97    fn deref(&self) -> &Self::Target {
98        &self.0
99    }
100}
101impl DerefMut for LatestPriceRequest {
102    fn deref_mut(&mut self) -> &mut Self::Target {
103        &mut self.0
104    }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
108#[serde(rename_all = "camelCase")]
109pub struct PriceRequestRepr {
110    /// Requested timestamp of the update.
111    pub timestamp: TimestampUs,
112    /// List of feed IDs.
113    /// Either feed ids or symbols must be specified.
114    pub price_feed_ids: Option<Vec<PriceFeedId>>,
115    /// List of feed symbols.
116    /// Either feed ids or symbols must be specified.
117    #[schema(default)]
118    pub symbols: Option<Vec<String>>,
119    /// List of feed properties the sender is interested in.
120    pub properties: Vec<PriceFeedProperty>,
121    /// Requested formats of the payload.
122    pub formats: Vec<Format>,
123    #[serde(default)]
124    pub json_binary_encoding: JsonBinaryEncoding,
125    /// If `true`, the stream update will contain a JSON object containing
126    /// all data of the update.
127    #[serde(default = "default_parsed")]
128    pub parsed: bool,
129    /// Channel determines frequency of updates.
130    pub channel: Channel,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
134#[serde(rename_all = "camelCase")]
135pub struct PriceRequest(PriceRequestRepr);
136
137impl<'de> Deserialize<'de> for PriceRequest {
138    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
139    where
140        D: serde::Deserializer<'de>,
141    {
142        let value = PriceRequestRepr::deserialize(deserializer)?;
143        Self::new(value).map_err(Error::custom)
144    }
145}
146
147impl PriceRequest {
148    pub fn new(value: PriceRequestRepr) -> Result<Self, &'static str> {
149        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
150        validate_optional_nonempty_vec_has_unique_elements(
151            &value.price_feed_ids,
152            "no price feed ids specified",
153            "duplicate price feed ids specified",
154        )?;
155        validate_optional_nonempty_vec_has_unique_elements(
156            &value.symbols,
157            "no symbols specified",
158            "duplicate symbols specified",
159        )?;
160        validate_formats(&value.formats)?;
161        validate_properties(&value.properties)?;
162        Ok(Self(value))
163    }
164}
165
166impl Deref for PriceRequest {
167    type Target = PriceRequestRepr;
168
169    fn deref(&self) -> &Self::Target {
170        &self.0
171    }
172}
173impl DerefMut for PriceRequest {
174    fn deref_mut(&mut self) -> &mut Self::Target {
175        &mut self.0
176    }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
180#[serde(rename_all = "camelCase")]
181pub struct ReducePriceRequest {
182    /// Feed update previously received from WebSocket or from "Fetch price"
183    /// or "Fetch latest price" endpoints.
184    pub payload: JsonUpdate,
185    /// List of feeds that should be preserved in the output update.
186    pub price_feed_ids: Vec<PriceFeedId>,
187}
188
189pub type LatestPriceResponse = JsonUpdate;
190pub type ReducePriceResponse = JsonUpdate;
191pub type PriceResponse = JsonUpdate;
192
193pub fn default_parsed() -> bool {
194    true
195}
196
197pub fn schema_default_symbols() -> Option<Vec<String>> {
198    None
199}
200pub fn schema_default_price_feed_ids() -> Option<Vec<PriceFeedId>> {
201    Some(vec![PriceFeedId(1)])
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
205#[serde(rename_all = "camelCase")]
206pub enum DeliveryFormat {
207    /// Deliver stream updates as JSON text messages.
208    #[default]
209    Json,
210    /// Deliver stream updates as binary messages.
211    Binary,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
215#[serde(rename_all = "camelCase")]
216pub enum Format {
217    Evm,
218    Solana,
219    LeEcdsa,
220    LeUnsigned,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
224#[serde(rename_all = "camelCase")]
225pub enum JsonBinaryEncoding {
226    #[default]
227    Base64,
228    Hex,
229}
230
231#[derive(Serialize, Deserialize, ToSchema)]
232pub enum ChannelSchemaRepr {
233    #[serde(rename = "real_time")]
234    RealTime,
235    #[serde(rename = "fixed_rate@50ms")]
236    FixedRate50ms,
237    #[serde(rename = "fixed_rate@200ms")]
238    FixedRate200ms,
239    #[serde(rename = "fixed_rate@1000ms")]
240    FixedRate1000ms,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)]
244pub enum Channel {
245    FixedRate(FixedRate),
246    RealTime,
247}
248
249impl PartialSchema for Channel {
250    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
251        ChannelSchemaRepr::schema()
252    }
253}
254
255impl ToSchema for Channel {
256    fn name() -> std::borrow::Cow<'static, str> {
257        ChannelSchemaRepr::name()
258    }
259
260    fn schemas(
261        schemas: &mut Vec<(
262            String,
263            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
264        )>,
265    ) {
266        ChannelSchemaRepr::schemas(schemas)
267    }
268}
269
270impl PartialOrd for Channel {
271    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
272        let rate_left = match self {
273            Channel::FixedRate(rate) => rate.duration().as_micros(),
274            Channel::RealTime => FixedRate::MIN.duration().as_micros(),
275        };
276        let rate_right = match other {
277            Channel::FixedRate(rate) => rate.duration().as_micros(),
278            Channel::RealTime => FixedRate::MIN.duration().as_micros(),
279        };
280        Some(rate_left.cmp(&rate_right))
281    }
282}
283
284impl Serialize for Channel {
285    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
286    where
287        S: serde::Serializer,
288    {
289        match self {
290            Channel::FixedRate(fixed_rate) => serializer.serialize_str(&format!(
291                "fixed_rate@{}ms",
292                fixed_rate.duration().as_millis()
293            )),
294            Channel::RealTime => serializer.serialize_str("real_time"),
295        }
296    }
297}
298
299impl Display for Channel {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            Channel::FixedRate(fixed_rate) => {
303                write!(f, "fixed_rate@{}ms", fixed_rate.duration().as_millis())
304            }
305            Channel::RealTime => write!(f, "real_time"),
306        }
307    }
308}
309
310impl Channel {
311    pub fn id(&self) -> ChannelId {
312        match self {
313            Channel::FixedRate(fixed_rate) => match fixed_rate.duration().as_millis() {
314                50 => ChannelId::FIXED_RATE_50,
315                200 => ChannelId::FIXED_RATE_200,
316                1000 => ChannelId::FIXED_RATE_1000,
317                _ => panic!("unknown channel: {self:?}"),
318            },
319            Channel::RealTime => ChannelId::REAL_TIME,
320        }
321    }
322}
323
324#[test]
325fn id_supports_all_fixed_rates() {
326    for rate in FixedRate::ALL {
327        Channel::FixedRate(rate).id();
328    }
329}
330
331fn parse_channel(value: &str) -> Option<Channel> {
332    if value == "real_time" {
333        Some(Channel::RealTime)
334    } else if let Some(rest) = value.strip_prefix("fixed_rate@") {
335        let ms_value = rest.strip_suffix("ms")?;
336        Some(Channel::FixedRate(FixedRate::from_millis(
337            ms_value.parse().ok()?,
338        )?))
339    } else {
340        None
341    }
342}
343
344impl<'de> Deserialize<'de> for Channel {
345    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
346    where
347        D: serde::Deserializer<'de>,
348    {
349        let value = <String>::deserialize(deserializer)?;
350        parse_channel(&value).ok_or_else(|| Error::custom("unknown channel"))
351    }
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
355#[serde(rename_all = "camelCase")]
356pub struct SubscriptionParamsRepr {
357    /// List of feed IDs.
358    /// Either feed ids or symbols must be specified.
359    pub price_feed_ids: Option<Vec<PriceFeedId>>,
360    /// List of feed symbols.
361    /// Either feed ids or symbols must be specified.
362    #[schema(default)]
363    pub symbols: Option<Vec<String>>,
364    /// List of feed properties the sender is interested in.
365    pub properties: Vec<PriceFeedProperty>,
366    /// Requested formats of the payload.
367    /// As part of each feed update, the server will send on-chain payloads required
368    /// to validate these price updates on the specified chains.
369    #[serde(alias = "chains")]
370    pub formats: Vec<Format>,
371    /// If `json` is selected, the server will send price updates as JSON objects
372    /// (the on-chain payload will be encoded according to the `jsonBinaryEncoding` property).
373    /// If `binary` is selected, the server will send price updates as binary messages.
374    #[serde(default)]
375    pub delivery_format: DeliveryFormat,
376    /// For `deliveryFormat == "json"`, the on-chain payload will be encoded using the specified encoding.
377    /// This option has no effect for  `deliveryFormat == "binary"`.
378    #[serde(default)]
379    pub json_binary_encoding: JsonBinaryEncoding,
380    /// If `true`, the stream update will contain a `parsed` JSON field containing
381    /// all data of the update.
382    #[serde(default = "default_parsed")]
383    pub parsed: bool,
384    /// Channel determines frequency of updates.
385    pub channel: Channel,
386    /// If true, the subscription will ignore invalid feed IDs and subscribe to any valid feeds.
387    /// Otherwise, the entire subscription will fail if any feed is invalid.
388    #[serde(default, alias = "ignoreInvalidFeedIds")]
389    pub ignore_invalid_feeds: bool,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
393#[serde(rename_all = "camelCase")]
394pub struct SubscriptionParams(SubscriptionParamsRepr);
395
396impl<'de> Deserialize<'de> for SubscriptionParams {
397    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398    where
399        D: serde::Deserializer<'de>,
400    {
401        let value = SubscriptionParamsRepr::deserialize(deserializer)?;
402        Self::new(value).map_err(Error::custom)
403    }
404}
405
406impl SubscriptionParams {
407    pub fn new(value: SubscriptionParamsRepr) -> Result<Self, &'static str> {
408        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
409        validate_optional_nonempty_vec_has_unique_elements(
410            &value.price_feed_ids,
411            "no price feed ids specified",
412            "duplicate price feed ids specified",
413        )?;
414        validate_optional_nonempty_vec_has_unique_elements(
415            &value.symbols,
416            "no symbols specified",
417            "duplicate symbols specified",
418        )?;
419        validate_formats(&value.formats)?;
420        validate_properties(&value.properties)?;
421        Ok(Self(value))
422    }
423}
424
425impl Deref for SubscriptionParams {
426    type Target = SubscriptionParamsRepr;
427
428    fn deref(&self) -> &Self::Target {
429        &self.0
430    }
431}
432impl DerefMut for SubscriptionParams {
433    fn deref_mut(&mut self) -> &mut Self::Target {
434        &mut self.0
435    }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
439#[serde(rename_all = "camelCase")]
440pub struct JsonBinaryData {
441    /// Encoding of the data. It will be the same as `jsonBinaryEncoding` specified in the `SubscriptionRequest`.
442    pub encoding: JsonBinaryEncoding,
443    /// Binary data encoded in base64 or hex, depending on the requested encoding.
444    pub data: String,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
448#[serde(rename_all = "camelCase")]
449pub struct JsonUpdate {
450    /// Parsed representation of the price update.
451    /// Present unless `parsed = false` is specified in subscription params.
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub parsed: Option<ParsedPayload>,
454    /// Signed on-chain payload for EVM. Only present if `Evm` is present in `formats` in subscription params.
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub evm: Option<JsonBinaryData>,
457    /// Signed on-chain payload for Solana. Only present if `Solana` is present in `formats` in subscription params.
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub solana: Option<JsonBinaryData>,
460    /// Signed binary payload for off-chain verification. Only present if `LeEcdsa` is present in `formats` in subscription params.
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub le_ecdsa: Option<JsonBinaryData>,
463    /// Unsigned binary payload. Only present if `LeUnsigned` is present in `formats` in subscription params.
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub le_unsigned: Option<JsonBinaryData>,
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
469#[serde(rename_all = "camelCase")]
470pub struct ParsedPayload {
471    /// Unix timestamp associated with the update (with microsecond precision).
472    #[serde(with = "crate::serde_str::timestamp")]
473    #[schema(value_type = String)]
474    pub timestamp_us: TimestampUs,
475    /// Values of the update for each feed.
476    pub price_feeds: Vec<ParsedFeedPayload>,
477}
478
479/// Parsed representation of a feed update.
480#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
481#[serde(rename_all = "camelCase")]
482pub struct ParsedFeedPayload {
483    /// Feed ID.
484    pub price_feed_id: PriceFeedId,
485    /// For price feeds: main price. For funding rate feeds: funding price.
486    /// Only present if the `price` property was specified
487    /// in the `SubscriptionRequest` and the value is currently available for this price feed.
488    #[serde(skip_serializing_if = "Option::is_none")]
489    #[serde(with = "crate::serde_str::option_price")]
490    #[serde(default)]
491    #[schema(value_type = Option<String>)]
492    pub price: Option<Price>,
493    /// Best bid price for this price feed. Only present if the `bestBidPrice` property
494    /// was specified in the `SubscriptionRequest` and this is a price feed and
495    /// the value is currently available for this price feed.
496    #[serde(skip_serializing_if = "Option::is_none")]
497    #[serde(with = "crate::serde_str::option_price")]
498    #[serde(default)]
499    #[schema(value_type = Option<String>)]
500    pub best_bid_price: Option<Price>,
501    /// Best ask price for this price feed. Only present if the `bestAskPrice` property was
502    /// specified in the `SubscriptionRequest` and this is a price feed and
503    /// the value is currently available for this price feed.
504    #[serde(skip_serializing_if = "Option::is_none")]
505    #[serde(with = "crate::serde_str::option_price")]
506    #[serde(default)]
507    #[schema(value_type = Option<String>)]
508    pub best_ask_price: Option<Price>,
509    /// Number of publishers contributing to this feed update. Only present if the `publisherCount`
510    /// property was specified in the `SubscriptionRequest`.
511    #[serde(skip_serializing_if = "Option::is_none")]
512    #[serde(default)]
513    pub publisher_count: Option<u16>,
514    /// Exponent for this feed. Only present if the `exponent` property was specified
515    /// in the `SubscriptionRequest`. Each decimal field provided by the feed (price, fundingRate, etc)
516    /// returns the mantissa of the value. The actual value can be calculated as
517    /// `mantissa * 10^exponent`.
518    #[serde(skip_serializing_if = "Option::is_none")]
519    #[serde(default)]
520    pub exponent: Option<i16>,
521    /// Confidence for this price feed. Only present if the `confidence` property was
522    /// specified in the `SubscriptionRequest` and this is a price feed and
523    /// the value is currently available for this price feed.
524    #[serde(skip_serializing_if = "Option::is_none")]
525    #[serde(default)]
526    pub confidence: Option<Price>,
527    /// Perpetual future funding rate for this feed.
528    /// Only present if the `fundingRate` property was specified in the `SubscriptionRequest`
529    /// and this is a funding rate feed
530    /// and the value is currently available for this price feed.
531    #[serde(skip_serializing_if = "Option::is_none")]
532    #[serde(default)]
533    pub funding_rate: Option<Rate>,
534    /// Most recent perpetual future funding rate timestamp for this feed.
535    /// Only present if the `fundingTimestamp` property was specified in the `SubscriptionRequest`
536    /// and this is a funding rate feed
537    /// and the value is currently available for this price feed.
538    #[serde(skip_serializing_if = "Option::is_none")]
539    #[serde(default)]
540    pub funding_timestamp: Option<TimestampUs>,
541    /// Duration, in microseconds, between consecutive funding rate updates for this price feed.
542    /// Only present if the `fundingRateInterval` property was requested in the `SubscriptionRequest`
543    /// and this is a funding rate feed and the value is defined for that feed.
544    #[serde(skip_serializing_if = "Option::is_none")]
545    #[serde(default)]
546    pub funding_rate_interval: Option<DurationUs>,
547    /// Market session for this price feed. Only present if the `marketSession` property was specified
548    /// in the `SubscriptionRequest`.
549    #[serde(skip_serializing_if = "Option::is_none")]
550    #[serde(default)]
551    pub market_session: Option<MarketSession>,
552    /// Exponential moving average of the main price for this price feeds.
553    /// Only present if the `emaPrice` property was specified
554    /// in the `SubscriptionRequest`  and this is a price feed
555    /// and the value is currently available for this price feed.
556    #[serde(skip_serializing_if = "Option::is_none")]
557    #[serde(with = "crate::serde_str::option_price")]
558    #[serde(default)]
559    #[schema(value_type = Option<String>)]
560    pub ema_price: Option<Price>,
561    /// Exponential moving average of the confidence for this price feeds.
562    /// Only present if the `emaConfidence` property was specified
563    /// in the `SubscriptionRequest`  and this is a price feed
564    /// and the value is currently available for this price feed.
565    #[serde(skip_serializing_if = "Option::is_none")]
566    #[serde(default)]
567    pub ema_confidence: Option<Price>,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    #[serde(default)]
570    pub feed_update_timestamp: Option<TimestampUs>,
571    // More fields may be added later.
572}
573
574impl ParsedFeedPayload {
575    pub fn new(
576        price_feed_id: PriceFeedId,
577        data: &AggregatedPriceFeedData,
578        properties: &[PriceFeedProperty],
579    ) -> Self {
580        let mut output = Self {
581            price_feed_id,
582            price: None,
583            best_bid_price: None,
584            best_ask_price: None,
585            publisher_count: None,
586            exponent: None,
587            confidence: None,
588            funding_rate: None,
589            funding_timestamp: None,
590            funding_rate_interval: None,
591            market_session: None,
592            ema_price: None,
593            ema_confidence: None,
594            feed_update_timestamp: None,
595        };
596        for &property in properties {
597            match property {
598                PriceFeedProperty::Price => {
599                    output.price = data.price;
600                }
601                PriceFeedProperty::BestBidPrice => {
602                    output.best_bid_price = data.best_bid_price;
603                }
604                PriceFeedProperty::BestAskPrice => {
605                    output.best_ask_price = data.best_ask_price;
606                }
607                PriceFeedProperty::PublisherCount => {
608                    output.publisher_count = Some(data.publisher_count);
609                }
610                PriceFeedProperty::Exponent => {
611                    output.exponent = Some(data.exponent);
612                }
613                PriceFeedProperty::Confidence => {
614                    output.confidence = data.confidence;
615                }
616                PriceFeedProperty::FundingRate => {
617                    output.funding_rate = data.funding_rate;
618                }
619                PriceFeedProperty::FundingTimestamp => {
620                    output.funding_timestamp = data.funding_timestamp;
621                }
622                PriceFeedProperty::FundingRateInterval => {
623                    output.funding_rate_interval = data.funding_rate_interval;
624                }
625                PriceFeedProperty::MarketSession => {
626                    output.market_session = Some(data.market_session);
627                }
628                PriceFeedProperty::EmaPrice => {
629                    output.ema_price = data.ema_price;
630                }
631                PriceFeedProperty::EmaConfidence => {
632                    output.ema_confidence = data.ema_confidence;
633                }
634                PriceFeedProperty::FeedUpdateTimestamp => {
635                    output.feed_update_timestamp = data.feed_update_timestamp;
636                }
637            }
638        }
639        output
640    }
641
642    pub fn new_full(
643        price_feed_id: PriceFeedId,
644        exponent: Option<i16>,
645        data: &AggregatedPriceFeedData,
646    ) -> Self {
647        Self {
648            price_feed_id,
649            price: data.price,
650            best_bid_price: data.best_bid_price,
651            best_ask_price: data.best_ask_price,
652            publisher_count: Some(data.publisher_count),
653            exponent,
654            confidence: data.confidence,
655            funding_rate: data.funding_rate,
656            funding_timestamp: data.funding_timestamp,
657            funding_rate_interval: data.funding_rate_interval,
658            market_session: Some(data.market_session),
659            ema_price: data.ema_price,
660            ema_confidence: data.ema_confidence,
661            feed_update_timestamp: data.feed_update_timestamp,
662        }
663    }
664}
665
666/// A WebSocket JSON message sent from the client to the server.
667#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
668#[serde(tag = "type")]
669#[serde(rename_all = "camelCase")]
670pub enum WsRequest {
671    Subscribe(SubscribeRequest),
672    Unsubscribe(UnsubscribeRequest),
673}
674
675#[derive(
676    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
677)]
678pub struct SubscriptionId(pub u64);
679
680/// A subscription request.
681///
682/// After a successful subscription, the server will respond with a `SubscribedResponse`
683/// or `SubscribedWithInvalidFeedIdsIgnoredResponse` message,
684/// followed by `StreamUpdatedResponse` messages.
685/// If a subscription cannot be made, the server will respond with a `SubscriptionError`
686/// message containing the error message.
687#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
688#[serde(rename_all = "camelCase")]
689pub struct SubscribeRequest {
690    /// A number chosen by the client to identify the new subscription.
691    /// This identifier will be sent back in any responses related to this subscription.
692    pub subscription_id: SubscriptionId,
693    /// Properties of the new subscription.
694    #[serde(flatten)]
695    pub params: SubscriptionParams,
696}
697
698/// An unsubscription request.
699///
700/// After a successful unsubscription, the server will respond with a `UnsubscribedResponse` message
701/// and stop sending `SubscriptionErrorResponse` messages for that subscription.
702/// If the unsubscription cannot be made, the server will respond with a `SubscriptionError` message
703/// containing the error text.
704#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
705#[serde(rename_all = "camelCase")]
706pub struct UnsubscribeRequest {
707    /// ID of the subscription that should be canceled.
708    pub subscription_id: SubscriptionId,
709}
710
711/// A WebSocket JSON message sent from the server to the client.
712#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From, ToSchema)]
713#[serde(tag = "type")]
714#[serde(rename_all = "camelCase")]
715pub enum WsResponse {
716    Error(ErrorResponse),
717    Subscribed(SubscribedResponse),
718    SubscribedWithInvalidFeedIdsIgnored(SubscribedWithInvalidFeedIdsIgnoredResponse),
719    Unsubscribed(UnsubscribedResponse),
720    SubscriptionError(SubscriptionErrorResponse),
721    StreamUpdated(StreamUpdatedResponse),
722}
723
724/// Sent from the server when a subscription succeeded and all specified feeds were valid.
725#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
726#[serde(rename_all = "camelCase")]
727pub struct SubscribedResponse {
728    pub subscription_id: SubscriptionId,
729}
730
731#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
732#[serde(rename_all = "camelCase")]
733pub struct InvalidFeedSubscriptionDetails {
734    /// List of price feed IDs that could not be found.
735    pub unknown_ids: Vec<PriceFeedId>,
736    /// List of price feed symbols that could not be found.
737    pub unknown_symbols: Vec<String>,
738    /// List of price feed IDs that do not support the requested channel.
739    pub unsupported_channels: Vec<PriceFeedId>,
740    /// List of unstable price feed IDs. Unstable feeds are not available for subscription.
741    pub unstable: Vec<PriceFeedId>,
742}
743
744/// Sent from the server when a subscription succeeded, but
745/// some of the  specified feeds were invalid.
746#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
747#[serde(rename_all = "camelCase")]
748pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
749    /// The value specified in the corresponding `SubscribeRequest`.
750    pub subscription_id: SubscriptionId,
751    /// IDs of valid feeds included in the established subscription.
752    pub subscribed_feed_ids: Vec<PriceFeedId>,
753    /// Map of failed feed IDs categorized by failure reason.
754    pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
755}
756
757/// Notification of a successful unsubscription.
758#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
759#[serde(rename_all = "camelCase")]
760pub struct UnsubscribedResponse {
761    /// The value specified in the corresponding `SubscribeRequest`.
762    pub subscription_id: SubscriptionId,
763}
764
765/// Sent from the server if the requested subscription or unsubscription request
766/// could not be fulfilled.
767#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
768#[serde(rename_all = "camelCase")]
769pub struct SubscriptionErrorResponse {
770    /// The value specified in the corresponding `SubscribeRequest`.
771    pub subscription_id: SubscriptionId,
772    /// Text of the error.
773    pub error: String,
774}
775
776/// Sent from the server if an internal error occured while serving data for an existing subscription,
777/// or a client request sent a bad request.
778#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
779#[serde(rename_all = "camelCase")]
780pub struct ErrorResponse {
781    /// Text of the error.
782    pub error: String,
783}
784
785/// Sent from the server when new data is available for an existing subscription
786/// (only if `delivery_format == Json`).
787#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
788#[serde(rename_all = "camelCase")]
789pub struct StreamUpdatedResponse {
790    /// The value specified in the corresponding `SubscribeRequest`.
791    pub subscription_id: SubscriptionId,
792    /// Content of the update.
793    #[serde(flatten)]
794    pub payload: JsonUpdate,
795}
796
797// Common validation functions
798fn validate_price_feed_ids_or_symbols(
799    price_feed_ids: &Option<Vec<PriceFeedId>>,
800    symbols: &Option<Vec<String>>,
801) -> Result<(), &'static str> {
802    if price_feed_ids.is_none() && symbols.is_none() {
803        return Err("either price feed ids or symbols must be specified");
804    }
805    if price_feed_ids.is_some() && symbols.is_some() {
806        return Err("either price feed ids or symbols must be specified, not both");
807    }
808    Ok(())
809}
810
811fn validate_optional_nonempty_vec_has_unique_elements<T>(
812    vec: &Option<Vec<T>>,
813    empty_msg: &'static str,
814    duplicate_msg: &'static str,
815) -> Result<(), &'static str>
816where
817    T: Eq + std::hash::Hash,
818{
819    if let Some(items) = vec {
820        if items.is_empty() {
821            return Err(empty_msg);
822        }
823        if !items.iter().all_unique() {
824            return Err(duplicate_msg);
825        }
826    }
827    Ok(())
828}
829
830fn validate_properties(properties: &[PriceFeedProperty]) -> Result<(), &'static str> {
831    if properties.is_empty() {
832        return Err("no properties specified");
833    }
834    if !properties.iter().all_unique() {
835        return Err("duplicate properties specified");
836    }
837    Ok(())
838}
839
840fn validate_formats(formats: &[Format]) -> Result<(), &'static str> {
841    if !formats.iter().all_unique() {
842        return Err("duplicate formats or chains specified");
843    }
844    Ok(())
845}
846
847#[derive(
848    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema, Default,
849)]
850#[serde(rename_all = "camelCase")]
851#[schema(example = "regular")]
852pub enum MarketSession {
853    #[default]
854    Regular,
855    PreMarket,
856    PostMarket,
857    OverNight,
858    Closed,
859}
860
861#[derive(
862    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema, Default,
863)]
864#[serde(rename_all = "camelCase")]
865#[schema(example = "open")]
866pub enum TradingStatus {
867    #[default]
868    Open,
869    Closed,
870    Halted,
871    CorpAction,
872}
873
874impl From<MarketSession> for i16 {
875    fn from(s: MarketSession) -> i16 {
876        match s {
877            MarketSession::Regular => 0,
878            MarketSession::PreMarket => 1,
879            MarketSession::PostMarket => 2,
880            MarketSession::OverNight => 3,
881            MarketSession::Closed => 4,
882        }
883    }
884}
885
886impl TryFrom<i16> for MarketSession {
887    type Error = anyhow::Error;
888
889    fn try_from(value: i16) -> Result<MarketSession, Self::Error> {
890        match value {
891            0 => Ok(MarketSession::Regular),
892            1 => Ok(MarketSession::PreMarket),
893            2 => Ok(MarketSession::PostMarket),
894            3 => Ok(MarketSession::OverNight),
895            4 => Ok(MarketSession::Closed),
896            _ => Err(anyhow::anyhow!("invalid MarketSession value: {}", value)),
897        }
898    }
899}