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