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