Skip to main content

pyth_lazer_protocol/
api.rs

1use std::{
2    cmp::Ordering,
3    fmt::Display,
4    ops::{Deref, DerefMut},
5};
6
7use derive_more::From;
8use itertools::Itertools as _;
9use serde::{de::Error, Deserialize, Serialize};
10use serde_with::{hex::Hex, serde_as};
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)]
19#[serde(rename_all = "camelCase")]
20#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
21#[cfg_attr(feature = "utoipa", schema(examples(LatestPriceRequestRepr::example1)))]
22pub struct LatestPriceRequestRepr {
23    /// List of feed IDs.
24    /// Either feed ids or symbols must be specified.
25    pub price_feed_ids: Option<Vec<PriceFeedId>>,
26    /// List of feed symbols.
27    /// Either feed ids or symbols must be specified.
28    pub symbols: Option<Vec<String>>,
29    /// List of feed properties the sender is interested in.
30    pub properties: Vec<PriceFeedProperty>,
31    // "chains" was renamed to "formats". "chains" is still supported for compatibility.
32    /// Requested formats of the payload.
33    #[serde(alias = "chains")]
34    pub formats: Vec<Format>,
35    #[serde(default)]
36    pub json_binary_encoding: JsonBinaryEncoding,
37    /// If `true`, the response will contain a JSON object containing
38    /// all data of the update.
39    #[serde(default = "default_parsed")]
40    pub parsed: bool,
41    /// Channel determines frequency of updates.
42    pub channel: Channel,
43}
44
45#[cfg(feature = "utoipa")]
46impl LatestPriceRequestRepr {
47    fn example1() -> Self {
48        Self {
49            price_feed_ids: None,
50            symbols: Some(vec!["Crypto.BTC/USD".into()]),
51            properties: vec![PriceFeedProperty::Price, PriceFeedProperty::Confidence],
52            formats: vec![Format::Evm],
53            json_binary_encoding: JsonBinaryEncoding::Hex,
54            parsed: true,
55            channel: Channel::RealTime,
56        }
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
61#[serde(rename_all = "camelCase")]
62#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
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)]
108#[serde(rename_all = "camelCase")]
109#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
110pub struct PriceRequestRepr {
111    /// Requested timestamp of the update.
112    pub timestamp: TimestampUs,
113    /// List of feed IDs.
114    /// Either feed ids or symbols must be specified.
115    pub price_feed_ids: Option<Vec<PriceFeedId>>,
116    /// List of feed symbols.
117    /// Either feed ids or symbols must be specified.
118    #[cfg_attr(feature = "utoipa", schema(default))]
119    pub symbols: Option<Vec<String>>,
120    /// List of feed properties the sender is interested in.
121    pub properties: Vec<PriceFeedProperty>,
122    /// Requested formats of the payload.
123    pub formats: Vec<Format>,
124    #[serde(default)]
125    pub json_binary_encoding: JsonBinaryEncoding,
126    /// If `true`, the stream update will contain a JSON object containing
127    /// all data of the update.
128    #[serde(default = "default_parsed")]
129    pub parsed: bool,
130    /// Channel determines frequency of updates.
131    pub channel: Channel,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
135#[serde(rename_all = "camelCase")]
136#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
137pub struct PriceRequest(PriceRequestRepr);
138
139impl<'de> Deserialize<'de> for PriceRequest {
140    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141    where
142        D: serde::Deserializer<'de>,
143    {
144        let value = PriceRequestRepr::deserialize(deserializer)?;
145        Self::new(value).map_err(Error::custom)
146    }
147}
148
149impl PriceRequest {
150    pub fn new(value: PriceRequestRepr) -> Result<Self, &'static str> {
151        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
152        validate_optional_nonempty_vec_has_unique_elements(
153            &value.price_feed_ids,
154            "no price feed ids specified",
155            "duplicate price feed ids specified",
156        )?;
157        validate_optional_nonempty_vec_has_unique_elements(
158            &value.symbols,
159            "no symbols specified",
160            "duplicate symbols specified",
161        )?;
162        validate_formats(&value.formats)?;
163        validate_properties(&value.properties)?;
164        Ok(Self(value))
165    }
166}
167
168impl Deref for PriceRequest {
169    type Target = PriceRequestRepr;
170
171    fn deref(&self) -> &Self::Target {
172        &self.0
173    }
174}
175impl DerefMut for PriceRequest {
176    fn deref_mut(&mut self) -> &mut Self::Target {
177        &mut self.0
178    }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
184pub struct ReducePriceRequest {
185    /// Feed update previously received from WebSocket or from "Fetch price"
186    /// or "Fetch latest price" endpoints.
187    pub payload: JsonUpdate,
188    /// List of feeds that should be preserved in the output update.
189    pub price_feed_ids: Vec<PriceFeedId>,
190}
191
192pub type LatestPriceResponse = JsonUpdate;
193pub type ReducePriceResponse = JsonUpdate;
194pub type PriceResponse = JsonUpdate;
195
196pub fn default_parsed() -> bool {
197    true
198}
199
200pub fn schema_default_symbols() -> Option<Vec<String>> {
201    None
202}
203pub fn schema_default_price_feed_ids() -> Option<Vec<PriceFeedId>> {
204    Some(vec![PriceFeedId(1)])
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
210pub enum DeliveryFormat {
211    /// Deliver stream updates as JSON text messages.
212    #[default]
213    Json,
214    /// Deliver stream updates as binary messages.
215    Binary,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
221pub enum Format {
222    Evm,
223    Solana,
224    LeEcdsa,
225    LeUnsigned,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
231pub enum JsonBinaryEncoding {
232    #[default]
233    Base64,
234    Hex,
235}
236
237#[derive(Serialize, Deserialize)]
238#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
239pub enum ChannelSchemaRepr {
240    #[serde(rename = "real_time")]
241    RealTime,
242    #[serde(rename = "fixed_rate@50ms")]
243    FixedRate50ms,
244    #[serde(rename = "fixed_rate@200ms")]
245    FixedRate200ms,
246    #[serde(rename = "fixed_rate@1000ms")]
247    FixedRate1000ms,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)]
251pub enum Channel {
252    FixedRate(FixedRate),
253    RealTime,
254}
255
256#[cfg(feature = "utoipa")]
257impl utoipa::PartialSchema for Channel {
258    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
259        ChannelSchemaRepr::schema()
260    }
261}
262
263#[cfg(feature = "utoipa")]
264impl utoipa::ToSchema for Channel {
265    fn name() -> std::borrow::Cow<'static, str> {
266        ChannelSchemaRepr::name()
267    }
268
269    fn schemas(
270        schemas: &mut Vec<(
271            String,
272            utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
273        )>,
274    ) {
275        ChannelSchemaRepr::schemas(schemas)
276    }
277}
278
279impl PartialOrd for Channel {
280    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
281        let rate_left = match self {
282            Channel::FixedRate(rate) => rate.duration().as_micros(),
283            Channel::RealTime => FixedRate::MIN.duration().as_micros(),
284        };
285        let rate_right = match other {
286            Channel::FixedRate(rate) => rate.duration().as_micros(),
287            Channel::RealTime => FixedRate::MIN.duration().as_micros(),
288        };
289        Some(rate_left.cmp(&rate_right))
290    }
291}
292
293impl Serialize for Channel {
294    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
295    where
296        S: serde::Serializer,
297    {
298        match self {
299            Channel::FixedRate(fixed_rate) => serializer.serialize_str(&format!(
300                "fixed_rate@{}ms",
301                fixed_rate.duration().as_millis()
302            )),
303            Channel::RealTime => serializer.serialize_str("real_time"),
304        }
305    }
306}
307
308impl Display for Channel {
309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        match self {
311            Channel::FixedRate(fixed_rate) => {
312                write!(f, "fixed_rate@{}ms", fixed_rate.duration().as_millis())
313            }
314            Channel::RealTime => write!(f, "real_time"),
315        }
316    }
317}
318
319impl Channel {
320    pub fn id(&self) -> ChannelId {
321        match self {
322            Channel::FixedRate(fixed_rate) => match fixed_rate.duration().as_millis() {
323                50 => ChannelId::FIXED_RATE_50,
324                200 => ChannelId::FIXED_RATE_200,
325                1000 => ChannelId::FIXED_RATE_1000,
326                _ => panic!("unknown channel: {self:?}"),
327            },
328            Channel::RealTime => ChannelId::REAL_TIME,
329        }
330    }
331}
332
333impl TryFrom<ChannelId> for Channel {
334    type Error = ChannelId;
335
336    fn try_from(id: ChannelId) -> Result<Self, Self::Error> {
337        match id {
338            ChannelId::REAL_TIME => Ok(Channel::RealTime),
339            ChannelId::FIXED_RATE_50 => Ok(Channel::FixedRate(FixedRate::RATE_50_MS)),
340            ChannelId::FIXED_RATE_200 => Ok(Channel::FixedRate(FixedRate::RATE_200_MS)),
341            ChannelId::FIXED_RATE_1000 => Ok(Channel::FixedRate(FixedRate::RATE_1000_MS)),
342            _ => Err(id),
343        }
344    }
345}
346
347#[test]
348fn id_supports_all_fixed_rates() {
349    for rate in FixedRate::ALL {
350        Channel::FixedRate(rate).id();
351    }
352}
353
354#[test]
355fn from_id_round_trips_with_id() {
356    let all_channels = [
357        Channel::RealTime,
358        Channel::FixedRate(FixedRate::RATE_50_MS),
359        Channel::FixedRate(FixedRate::RATE_200_MS),
360        Channel::FixedRate(FixedRate::RATE_1000_MS),
361    ];
362    for channel in all_channels {
363        assert_eq!(Channel::try_from(channel.id()), Ok(channel));
364    }
365}
366
367#[test]
368fn from_id_returns_none_for_unknown_ids() {
369    assert!(Channel::try_from(ChannelId(0)).is_err());
370    assert!(Channel::try_from(ChannelId(5)).is_err());
371    assert!(Channel::try_from(ChannelId(255)).is_err());
372}
373
374#[test]
375fn parse_channel_accepts_numeric_ids() {
376    assert_eq!(parse_channel("1"), Some(Channel::RealTime));
377    assert_eq!(
378        parse_channel("2"),
379        Some(Channel::FixedRate(FixedRate::RATE_50_MS))
380    );
381    assert_eq!(
382        parse_channel("3"),
383        Some(Channel::FixedRate(FixedRate::RATE_200_MS))
384    );
385    assert_eq!(
386        parse_channel("4"),
387        Some(Channel::FixedRate(FixedRate::RATE_1000_MS))
388    );
389}
390
391#[test]
392fn parse_channel_rejects_invalid_numeric_ids() {
393    assert_eq!(parse_channel("0"), None);
394    assert_eq!(parse_channel("5"), None); // Unsupported channel ID for now. Remove this test when we add support for it.
395    assert_eq!(parse_channel("99"), None);
396}
397
398#[test]
399fn channel_deserializes_from_json_string() {
400    let channel: Channel = serde_json::from_str(r#""3""#).unwrap();
401    assert_eq!(channel, Channel::FixedRate(FixedRate::RATE_200_MS));
402
403    let channel: Channel = serde_json::from_str(r#""fixed_rate@200ms""#).unwrap();
404    assert_eq!(channel, Channel::FixedRate(FixedRate::RATE_200_MS));
405}
406
407#[test]
408fn channel_deserializes_from_json_number() {
409    let channel: Channel = serde_json::from_str("3").unwrap();
410    assert_eq!(channel, Channel::FixedRate(FixedRate::RATE_200_MS));
411
412    let channel: Channel = serde_json::from_str("1").unwrap();
413    assert_eq!(channel, Channel::RealTime);
414}
415
416#[test]
417fn channel_rejects_invalid_json_number() {
418    assert!(serde_json::from_str::<Channel>("0").is_err());
419    assert!(serde_json::from_str::<Channel>("5").is_err());
420    assert!(serde_json::from_str::<Channel>("999").is_err());
421}
422
423fn parse_channel(value: &str) -> Option<Channel> {
424    if value == "real_time" {
425        Some(Channel::RealTime)
426    } else if let Some(rest) = value.strip_prefix("fixed_rate@") {
427        let ms_value = rest.strip_suffix("ms")?;
428        Some(Channel::FixedRate(FixedRate::from_millis(
429            ms_value.parse().ok()?,
430        )?))
431    } else if let Ok(id) = value.parse::<u8>() {
432        Channel::try_from(ChannelId(id)).ok()
433    } else {
434        None
435    }
436}
437
438impl<'de> Deserialize<'de> for Channel {
439    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
440    where
441        D: serde::Deserializer<'de>,
442    {
443        struct ChannelVisitor;
444
445        impl<'de> serde::de::Visitor<'de> for ChannelVisitor {
446            type Value = Channel;
447
448            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
449                formatter.write_str("a channel name string or numeric channel ID")
450            }
451
452            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Channel, E> {
453                parse_channel(value).ok_or_else(|| E::custom("unknown channel"))
454            }
455
456            fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Channel, E> {
457                let id = u8::try_from(value).map_err(|_| E::custom("channel ID out of range"))?;
458                Channel::try_from(ChannelId(id)).map_err(|_| E::custom("unknown channel ID"))
459            }
460        }
461
462        deserializer.deserialize_any(ChannelVisitor)
463    }
464}
465
466#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
469pub struct SubscriptionParamsRepr {
470    /// List of feed IDs.
471    /// Either feed ids or symbols must be specified.
472    pub price_feed_ids: Option<Vec<PriceFeedId>>,
473    /// List of feed symbols.
474    /// Either feed ids or symbols must be specified.
475    #[cfg_attr(feature = "utoipa", schema(default))]
476    pub symbols: Option<Vec<String>>,
477    /// List of feed properties the sender is interested in.
478    pub properties: Vec<PriceFeedProperty>,
479    /// Requested formats of the payload.
480    /// As part of each feed update, the server will send on-chain payloads required
481    /// to validate these price updates on the specified chains.
482    #[serde(alias = "chains")]
483    pub formats: Vec<Format>,
484    /// If `json` is selected, the server will send price updates as JSON objects
485    /// (the on-chain payload will be encoded according to the `jsonBinaryEncoding` property).
486    /// If `binary` is selected, the server will send price updates as binary messages.
487    #[serde(default)]
488    pub delivery_format: DeliveryFormat,
489    /// For `deliveryFormat == "json"`, the on-chain payload will be encoded using the specified encoding.
490    /// This option has no effect for  `deliveryFormat == "binary"`.
491    #[serde(default)]
492    pub json_binary_encoding: JsonBinaryEncoding,
493    /// If `true`, the stream update will contain a `parsed` JSON field containing
494    /// all data of the update.
495    #[serde(default = "default_parsed")]
496    pub parsed: bool,
497    /// Channel determines frequency of updates.
498    pub channel: Channel,
499    /// If true, the subscription will ignore invalid feed IDs and subscribe to any valid feeds.
500    /// Otherwise, the entire subscription will fail if any feed is invalid.
501    #[serde(default, alias = "ignoreInvalidFeedIds")]
502    pub ignore_invalid_feeds: bool,
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
506#[serde(rename_all = "camelCase")]
507#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
508pub struct SubscriptionParams(SubscriptionParamsRepr);
509
510impl<'de> Deserialize<'de> for SubscriptionParams {
511    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
512    where
513        D: serde::Deserializer<'de>,
514    {
515        let value = SubscriptionParamsRepr::deserialize(deserializer)?;
516        Self::new(value).map_err(Error::custom)
517    }
518}
519
520impl SubscriptionParams {
521    pub fn new(value: SubscriptionParamsRepr) -> Result<Self, &'static str> {
522        validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
523        validate_optional_nonempty_vec_has_unique_elements(
524            &value.price_feed_ids,
525            "no price feed ids specified",
526            "duplicate price feed ids specified",
527        )?;
528        validate_optional_nonempty_vec_has_unique_elements(
529            &value.symbols,
530            "no symbols specified",
531            "duplicate symbols specified",
532        )?;
533        validate_formats(&value.formats)?;
534        validate_properties(&value.properties)?;
535        Ok(Self(value))
536    }
537}
538
539impl Deref for SubscriptionParams {
540    type Target = SubscriptionParamsRepr;
541
542    fn deref(&self) -> &Self::Target {
543        &self.0
544    }
545}
546impl DerefMut for SubscriptionParams {
547    fn deref_mut(&mut self) -> &mut Self::Target {
548        &mut self.0
549    }
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
553#[serde(rename_all = "camelCase")]
554#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
555pub struct JsonBinaryData {
556    /// Encoding of the data. It will be the same as `jsonBinaryEncoding` specified in the `SubscriptionRequest`.
557    pub encoding: JsonBinaryEncoding,
558    /// Binary data encoded in base64 or hex, depending on the requested encoding.
559    pub data: String,
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
563#[serde(rename_all = "camelCase")]
564#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
565pub struct JsonUpdate {
566    /// Parsed representation of the price update.
567    /// Present unless `parsed = false` is specified in subscription params.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub parsed: Option<ParsedPayload>,
570    /// Signed on-chain payload for EVM. Only present if `Evm` is present in `formats` in subscription params.
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub evm: Option<JsonBinaryData>,
573    /// Signed on-chain payload for Solana. Only present if `Solana` is present in `formats` in subscription params.
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub solana: Option<JsonBinaryData>,
576    /// Signed binary payload for off-chain verification. Only present if `LeEcdsa` is present in `formats` in subscription params.
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub le_ecdsa: Option<JsonBinaryData>,
579    /// Unsigned binary payload. Only present if `LeUnsigned` is present in `formats` in subscription params.
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub le_unsigned: Option<JsonBinaryData>,
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
585#[serde(rename_all = "camelCase")]
586#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
587pub struct ParsedPayload {
588    /// Unix timestamp associated with the update (with microsecond precision).
589    #[serde(with = "crate::serde_str::timestamp")]
590    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
591    pub timestamp_us: TimestampUs,
592    /// Values of the update for each feed.
593    pub price_feeds: Vec<ParsedFeedPayload>,
594}
595
596/// Parsed representation of a feed update.
597#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
598#[serde(rename_all = "camelCase")]
599#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
600pub struct ParsedFeedPayload {
601    /// Feed ID.
602    pub price_feed_id: PriceFeedId,
603    /// For price feeds: main price. For funding rate feeds: funding price.
604    /// Only present if the `price` property was specified
605    /// in the `SubscriptionRequest` and the value is currently available for this price feed.
606    #[serde(skip_serializing_if = "Option::is_none")]
607    #[serde(with = "crate::serde_str::option_price")]
608    #[serde(default)]
609    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
610    pub price: Option<Price>,
611    /// Best bid price for this price feed. Only present if the `bestBidPrice` property
612    /// was specified in the `SubscriptionRequest` and this is a price feed and
613    /// the value is currently available for this price feed.
614    #[serde(skip_serializing_if = "Option::is_none")]
615    #[serde(with = "crate::serde_str::option_price")]
616    #[serde(default)]
617    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
618    pub best_bid_price: Option<Price>,
619    /// Best ask price for this price feed. Only present if the `bestAskPrice` property was
620    /// specified in the `SubscriptionRequest` and this is a price feed and
621    /// the value is currently available for this price feed.
622    #[serde(skip_serializing_if = "Option::is_none")]
623    #[serde(with = "crate::serde_str::option_price")]
624    #[serde(default)]
625    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
626    pub best_ask_price: Option<Price>,
627    /// Number of publishers contributing to this feed update. Only present if the `publisherCount`
628    /// property was specified in the `SubscriptionRequest`.
629    #[serde(skip_serializing_if = "Option::is_none")]
630    #[serde(default)]
631    pub publisher_count: Option<u16>,
632    /// Exponent for this feed. Only present if the `exponent` property was specified
633    /// in the `SubscriptionRequest`. Each decimal field provided by the feed (price, fundingRate, etc)
634    /// returns the mantissa of the value. The actual value can be calculated as
635    /// `mantissa * 10^exponent`.
636    #[serde(skip_serializing_if = "Option::is_none")]
637    #[serde(default)]
638    pub exponent: Option<i16>,
639    /// Confidence for this price feed. Only present if the `confidence` property was
640    /// specified in the `SubscriptionRequest` and this is a price feed and
641    /// the value is currently available for this price feed.
642    #[serde(skip_serializing_if = "Option::is_none")]
643    #[serde(default)]
644    pub confidence: Option<Price>,
645    /// Perpetual future funding rate for this feed.
646    /// Only present if the `fundingRate` property was specified in the `SubscriptionRequest`
647    /// and this is a funding rate feed
648    /// and the value is currently available for this price feed.
649    #[serde(skip_serializing_if = "Option::is_none")]
650    #[serde(default)]
651    pub funding_rate: Option<Rate>,
652    /// Most recent perpetual future funding rate timestamp for this feed.
653    /// Only present if the `fundingTimestamp` property was specified in the `SubscriptionRequest`
654    /// and this is a funding rate feed
655    /// and the value is currently available for this price feed.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    #[serde(default)]
658    pub funding_timestamp: Option<TimestampUs>,
659    /// Duration, in microseconds, between consecutive funding rate updates for this price feed.
660    /// Only present if the `fundingRateInterval` property was requested in the `SubscriptionRequest`
661    /// and this is a funding rate feed and the value is defined for that feed.
662    #[serde(skip_serializing_if = "Option::is_none")]
663    #[serde(default)]
664    pub funding_rate_interval: Option<DurationUs>,
665    /// Market session for this price feed. Only present if the `marketSession` property was specified
666    /// in the `SubscriptionRequest`.
667    #[serde(skip_serializing_if = "Option::is_none")]
668    #[serde(default)]
669    pub market_session: Option<MarketSession>,
670    /// Exponential moving average of the main price for this price feeds.
671    /// Only present if the `emaPrice` property was specified
672    /// in the `SubscriptionRequest`  and this is a price feed
673    /// and the value is currently available for this price feed.
674    #[serde(skip_serializing_if = "Option::is_none")]
675    #[serde(with = "crate::serde_str::option_price")]
676    #[serde(default)]
677    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
678    pub ema_price: Option<Price>,
679    /// Exponential moving average of the confidence for this price feeds.
680    /// Only present if the `emaConfidence` property was specified
681    /// in the `SubscriptionRequest`  and this is a price feed
682    /// and the value is currently available for this price feed.
683    #[serde(skip_serializing_if = "Option::is_none")]
684    #[serde(default)]
685    pub ema_confidence: Option<Price>,
686    #[serde(skip_serializing_if = "Option::is_none")]
687    #[serde(default)]
688    pub feed_update_timestamp: Option<TimestampUs>,
689    // More fields may be added later.
690}
691
692impl ParsedFeedPayload {
693    pub fn new(
694        price_feed_id: PriceFeedId,
695        data: &AggregatedPriceFeedData,
696        properties: &[PriceFeedProperty],
697    ) -> Self {
698        let mut output = Self {
699            price_feed_id,
700            price: None,
701            best_bid_price: None,
702            best_ask_price: None,
703            publisher_count: None,
704            exponent: None,
705            confidence: None,
706            funding_rate: None,
707            funding_timestamp: None,
708            funding_rate_interval: None,
709            market_session: None,
710            ema_price: None,
711            ema_confidence: None,
712            feed_update_timestamp: None,
713        };
714        for &property in properties {
715            match property {
716                PriceFeedProperty::Price => {
717                    output.price = data.price;
718                }
719                PriceFeedProperty::BestBidPrice => {
720                    output.best_bid_price = data.best_bid_price;
721                }
722                PriceFeedProperty::BestAskPrice => {
723                    output.best_ask_price = data.best_ask_price;
724                }
725                PriceFeedProperty::PublisherCount => {
726                    output.publisher_count = Some(data.publisher_count);
727                }
728                PriceFeedProperty::Exponent => {
729                    output.exponent = Some(data.exponent);
730                }
731                PriceFeedProperty::Confidence => {
732                    output.confidence = data.confidence;
733                }
734                PriceFeedProperty::FundingRate => {
735                    output.funding_rate = data.funding_rate;
736                }
737                PriceFeedProperty::FundingTimestamp => {
738                    output.funding_timestamp = data.funding_timestamp;
739                }
740                PriceFeedProperty::FundingRateInterval => {
741                    output.funding_rate_interval = data.funding_rate_interval;
742                }
743                PriceFeedProperty::MarketSession => {
744                    output.market_session = Some(data.market_session);
745                }
746                PriceFeedProperty::EmaPrice => {
747                    output.ema_price = data.ema_price;
748                }
749                PriceFeedProperty::EmaConfidence => {
750                    output.ema_confidence = data.ema_confidence;
751                }
752                PriceFeedProperty::FeedUpdateTimestamp => {
753                    output.feed_update_timestamp = data.feed_update_timestamp;
754                }
755            }
756        }
757        output
758    }
759
760    pub fn new_full(
761        price_feed_id: PriceFeedId,
762        exponent: Option<i16>,
763        data: &AggregatedPriceFeedData,
764    ) -> Self {
765        Self {
766            price_feed_id,
767            price: data.price,
768            best_bid_price: data.best_bid_price,
769            best_ask_price: data.best_ask_price,
770            publisher_count: Some(data.publisher_count),
771            exponent,
772            confidence: data.confidence,
773            funding_rate: data.funding_rate,
774            funding_timestamp: data.funding_timestamp,
775            funding_rate_interval: data.funding_rate_interval,
776            market_session: Some(data.market_session),
777            ema_price: data.ema_price,
778            ema_confidence: data.ema_confidence,
779            feed_update_timestamp: data.feed_update_timestamp,
780        }
781    }
782}
783
784/// A WebSocket JSON message sent from the client to the server.
785#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
786#[serde(tag = "type")]
787#[serde(rename_all = "camelCase")]
788#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
789pub enum WsRequest {
790    Subscribe(SubscribeRequest),
791    Unsubscribe(UnsubscribeRequest),
792}
793
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
795#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
796pub struct SubscriptionId(pub u64);
797
798/// A subscription request.
799///
800/// After a successful subscription, the server will respond with a `SubscribedResponse`
801/// or `SubscribedWithInvalidFeedIdsIgnoredResponse` message,
802/// followed by `StreamUpdatedResponse` messages.
803/// If a subscription cannot be made, the server will respond with a `SubscriptionError`
804/// message containing the error message.
805#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
806#[serde(rename_all = "camelCase")]
807#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
808pub struct SubscribeRequest {
809    /// A number chosen by the client to identify the new subscription.
810    /// This identifier will be sent back in any responses related to this subscription.
811    pub subscription_id: SubscriptionId,
812    /// Properties of the new subscription.
813    #[serde(flatten)]
814    pub params: SubscriptionParams,
815}
816
817/// An unsubscription request.
818///
819/// After a successful unsubscription, the server will respond with a `UnsubscribedResponse` message
820/// and stop sending `SubscriptionErrorResponse` messages for that subscription.
821/// If the unsubscription cannot be made, the server will respond with a `SubscriptionError` message
822/// containing the error text.
823#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
824#[serde(rename_all = "camelCase")]
825#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
826pub struct UnsubscribeRequest {
827    /// ID of the subscription that should be canceled.
828    pub subscription_id: SubscriptionId,
829}
830
831/// A WebSocket JSON message sent from the server to the client.
832#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From)]
833#[serde(tag = "type")]
834#[serde(rename_all = "camelCase")]
835#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
836pub enum WsResponse {
837    Error(ErrorResponse),
838    Subscribed(SubscribedResponse),
839    SubscribedWithInvalidFeedIdsIgnored(SubscribedWithInvalidFeedIdsIgnoredResponse),
840    Unsubscribed(UnsubscribedResponse),
841    SubscriptionError(SubscriptionErrorResponse),
842    StreamUpdated(StreamUpdatedResponse),
843}
844
845/// Sent from the server when a subscription succeeded and all specified feeds were valid.
846#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
847#[serde(rename_all = "camelCase")]
848#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
849pub struct SubscribedResponse {
850    pub subscription_id: SubscriptionId,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
854#[serde(rename_all = "camelCase")]
855#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
856pub struct InvalidFeedSubscriptionDetails {
857    /// List of price feed IDs that could not be found.
858    pub unknown_ids: Vec<PriceFeedId>,
859    /// List of price feed symbols that could not be found.
860    pub unknown_symbols: Vec<String>,
861    /// List of price feed IDs that do not support the requested channel.
862    pub unsupported_channels: Vec<PriceFeedId>,
863    /// List of unstable price feed IDs. Unstable feeds are not available for subscription.
864    pub unstable: Vec<PriceFeedId>,
865    /// List of price feed IDs that the API key is not entitled to access.
866    #[serde(default)]
867    pub not_entitled: Vec<PriceFeedId>,
868}
869
870/// Sent from the server when a subscription succeeded, but
871/// some of the  specified feeds were invalid.
872#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
873#[serde(rename_all = "camelCase")]
874#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
875pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
876    /// The value specified in the corresponding `SubscribeRequest`.
877    pub subscription_id: SubscriptionId,
878    /// IDs of valid feeds included in the established subscription.
879    pub subscribed_feed_ids: Vec<PriceFeedId>,
880    /// Map of failed feed IDs categorized by failure reason.
881    pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
882}
883
884/// Notification of a successful unsubscription.
885#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
886#[serde(rename_all = "camelCase")]
887#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
888pub struct UnsubscribedResponse {
889    /// The value specified in the corresponding `SubscribeRequest`.
890    pub subscription_id: SubscriptionId,
891}
892
893/// Sent from the server if the requested subscription or unsubscription request
894/// could not be fulfilled.
895#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
896#[serde(rename_all = "camelCase")]
897#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
898pub struct SubscriptionErrorResponse {
899    /// The value specified in the corresponding `SubscribeRequest`.
900    pub subscription_id: SubscriptionId,
901    /// Text of the error.
902    pub error: String,
903}
904
905/// Sent from the server if an internal error occured while serving data for an existing subscription,
906/// or a client request sent a bad request.
907#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
908#[serde(rename_all = "camelCase")]
909#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
910pub struct ErrorResponse {
911    /// Text of the error.
912    pub error: String,
913}
914
915/// Sent from the server when new data is available for an existing subscription
916/// (only if `delivery_format == Json`).
917#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
918#[serde(rename_all = "camelCase")]
919#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
920pub struct StreamUpdatedResponse {
921    /// The value specified in the corresponding `SubscribeRequest`.
922    pub subscription_id: SubscriptionId,
923    /// Content of the update.
924    #[serde(flatten)]
925    pub payload: JsonUpdate,
926}
927
928// Common validation functions
929fn validate_price_feed_ids_or_symbols(
930    price_feed_ids: &Option<Vec<PriceFeedId>>,
931    symbols: &Option<Vec<String>>,
932) -> Result<(), &'static str> {
933    if price_feed_ids.is_none() && symbols.is_none() {
934        return Err("either price feed ids or symbols must be specified");
935    }
936    if price_feed_ids.is_some() && symbols.is_some() {
937        return Err("either price feed ids or symbols must be specified, not both");
938    }
939    Ok(())
940}
941
942fn validate_optional_nonempty_vec_has_unique_elements<T>(
943    vec: &Option<Vec<T>>,
944    empty_msg: &'static str,
945    duplicate_msg: &'static str,
946) -> Result<(), &'static str>
947where
948    T: Eq + std::hash::Hash,
949{
950    if let Some(items) = vec {
951        if items.is_empty() {
952            return Err(empty_msg);
953        }
954        if !items.iter().all_unique() {
955            return Err(duplicate_msg);
956        }
957    }
958    Ok(())
959}
960
961fn validate_properties(properties: &[PriceFeedProperty]) -> Result<(), &'static str> {
962    if properties.is_empty() {
963        return Err("no properties specified");
964    }
965    if !properties.iter().all_unique() {
966        return Err("duplicate properties specified");
967    }
968    Ok(())
969}
970
971fn validate_formats(formats: &[Format]) -> Result<(), &'static str> {
972    if !formats.iter().all_unique() {
973        return Err("duplicate formats or chains specified");
974    }
975    Ok(())
976}
977
978#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, Default)]
979#[serde(rename_all = "camelCase")]
980#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
981#[cfg_attr(feature = "utoipa", schema(example = "regular"))]
982pub enum MarketSession {
983    #[default]
984    Regular,
985    PreMarket,
986    PostMarket,
987    OverNight,
988    Closed,
989}
990
991#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, Default)]
992#[serde(rename_all = "camelCase")]
993#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
994#[cfg_attr(feature = "utoipa", schema(example = "open"))]
995pub enum TradingStatus {
996    #[default]
997    Open,
998    Closed,
999    Halted,
1000    CorpAction,
1001}
1002
1003impl From<MarketSession> for i16 {
1004    fn from(s: MarketSession) -> i16 {
1005        match s {
1006            MarketSession::Regular => 0,
1007            MarketSession::PreMarket => 1,
1008            MarketSession::PostMarket => 2,
1009            MarketSession::OverNight => 3,
1010            MarketSession::Closed => 4,
1011        }
1012    }
1013}
1014
1015impl TryFrom<i16> for MarketSession {
1016    type Error = anyhow::Error;
1017
1018    fn try_from(value: i16) -> Result<MarketSession, Self::Error> {
1019        match value {
1020            0 => Ok(MarketSession::Regular),
1021            1 => Ok(MarketSession::PreMarket),
1022            2 => Ok(MarketSession::PostMarket),
1023            3 => Ok(MarketSession::OverNight),
1024            4 => Ok(MarketSession::Closed),
1025            _ => Err(anyhow::anyhow!("invalid MarketSession value: {}", value)),
1026        }
1027    }
1028}
1029
1030pub type GuardianIndex = u8;
1031pub type Slot = u64;
1032pub type MerkleTimestamp = u32;
1033pub type RawMerkleRoot = [u8; 20];
1034pub type RawMerkleSignature = [u8; 65];
1035pub type MerklePriceFeedId = [u8; 32];
1036pub type RawMerkleMessage = Vec<u8>;
1037
1038#[serde_as]
1039#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1040#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1041pub struct SignedMerkleRoot {
1042    /// Hex-encoded 20-byte Keccak160 merkle root
1043    #[serde_as(as = "Hex")]
1044    #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
1045    pub root: Vec<u8>,
1046
1047    pub slot: Slot,
1048    pub timestamp: u32,
1049    pub channel: Channel,
1050
1051    /// Hex-encoded 65-byte ECDSA signature (r || s || v)
1052    #[serde_as(as = "Hex")]
1053    #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
1054    pub signature: Vec<u8>,
1055
1056    #[serde_as(as = "Vec<Hex>")]
1057    #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>, example = json!(["00abcdef...", "00123456..."])))]
1058    pub messages: Vec<RawMerkleMessage>,
1059}
1060
1061#[serde_as]
1062#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1063#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1064pub struct SignedGuardianSetUpgrade {
1065    /// Current guardian set index (the signing set)
1066    pub current_guardian_set_index: u32,
1067    /// New guardian set index
1068    pub new_guardian_set_index: u32,
1069    /// Hex-encoded new guardian keys (20 bytes each)
1070    #[serde_as(as = "Vec<Hex>")]
1071    pub new_guardian_keys: Vec<Vec<u8>>,
1072    /// Hex-encoded serialized VAA body bytes (for downstream VAA assembly)
1073    #[serde_as(as = "Hex")]
1074    #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
1075    pub body: Vec<u8>,
1076    /// Hex-encoded 65-byte ECDSA signature (r || s || v) over the body digest
1077    #[serde_as(as = "Hex")]
1078    #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
1079    pub signature: Vec<u8>,
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085    use serde_json::json;
1086
1087    #[test]
1088    fn signed_merkle_root_json_serialization() {
1089        let root = SignedMerkleRoot {
1090            root: vec![
1091                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1092                0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
1093            ],
1094            slot: 34567890123,
1095            timestamp: 1700000000,
1096            channel: Channel::FixedRate(FixedRate::RATE_200_MS),
1097            signature: vec![0xaa; 65],
1098            messages: vec![vec![0x00, 0xab, 0xcd, 0xef], vec![0x00, 0x12, 0x34, 0x56]],
1099        };
1100
1101        let json = serde_json::to_value(&root).unwrap();
1102
1103        // root and signature are hex-encoded strings (no 0x prefix, lowercase)
1104        assert_eq!(json["root"], "0102030405060708090a0b0c0d0e0f1011121314");
1105        assert_eq!(json["slot"], 34567890123u64);
1106        assert_eq!(json["timestamp"], 1700000000u32);
1107        assert_eq!(
1108            json["channel"],
1109            Channel::FixedRate(FixedRate::RATE_200_MS).to_string()
1110        );
1111        assert_eq!(json["signature"], "aa".repeat(65));
1112        assert_eq!(json["messages"], json!(["00abcdef", "00123456"]));
1113
1114        // round-trip
1115        let deserialized: SignedMerkleRoot = serde_json::from_value(json).unwrap();
1116        assert_eq!(deserialized, root);
1117    }
1118
1119    #[test]
1120    fn signed_guardian_set_upgrade_json_serialization() {
1121        let upgrade = SignedGuardianSetUpgrade {
1122            current_guardian_set_index: 4,
1123            new_guardian_set_index: 5,
1124            new_guardian_keys: vec![vec![0x11; 20], vec![0x22; 20]],
1125            body: vec![0xde, 0xad, 0xbe, 0xef],
1126            signature: vec![0xaa; 65],
1127        };
1128
1129        let json = serde_json::to_value(&upgrade).unwrap();
1130
1131        assert_eq!(json["current_guardian_set_index"], 4);
1132        assert_eq!(json["new_guardian_set_index"], 5);
1133        let keys = json["new_guardian_keys"].as_array().unwrap();
1134        assert_eq!(keys.len(), 2);
1135        assert_eq!(keys[0], "11".repeat(20));
1136        assert_eq!(keys[1], "22".repeat(20));
1137        assert_eq!(json["body"], "deadbeef");
1138        assert_eq!(json["signature"], "aa".repeat(65));
1139
1140        // round-trip
1141        let deserialized: SignedGuardianSetUpgrade = serde_json::from_value(json).unwrap();
1142        assert_eq!(deserialized, upgrade);
1143    }
1144}