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