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