use std::{
cmp::Ordering,
fmt::Display,
ops::{Deref, DerefMut},
};
use derive_more::From;
use itertools::Itertools as _;
use serde::{de::Error, Deserialize, Serialize};
use serde_with::{hex::Hex, serde_as};
use crate::{
payload::AggregatedPriceFeedData,
time::{DurationUs, FixedRate, TimestampUs},
ChannelId, Price, PriceFeedId, PriceFeedProperty, Rate,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(examples(LatestPriceRequestRepr::example1)))]
pub struct LatestPriceRequestRepr {
pub price_feed_ids: Option<Vec<PriceFeedId>>,
pub symbols: Option<Vec<String>>,
pub properties: Vec<PriceFeedProperty>,
#[serde(alias = "chains")]
pub formats: Vec<Format>,
#[serde(default)]
pub json_binary_encoding: JsonBinaryEncoding,
#[serde(default = "default_parsed")]
pub parsed: bool,
pub channel: Channel,
}
#[cfg(feature = "utoipa")]
impl LatestPriceRequestRepr {
fn example1() -> Self {
Self {
price_feed_ids: None,
symbols: Some(vec!["Crypto.BTC/USD".into()]),
properties: vec![PriceFeedProperty::Price, PriceFeedProperty::Confidence],
formats: vec![Format::Evm],
json_binary_encoding: JsonBinaryEncoding::Hex,
parsed: true,
channel: Channel::RealTime,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct LatestPriceRequest(LatestPriceRequestRepr);
impl<'de> Deserialize<'de> for LatestPriceRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = LatestPriceRequestRepr::deserialize(deserializer)?;
Self::new(value).map_err(Error::custom)
}
}
impl LatestPriceRequest {
pub fn new(value: LatestPriceRequestRepr) -> Result<Self, &'static str> {
validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
validate_optional_nonempty_vec_has_unique_elements(
&value.price_feed_ids,
"no price feed ids specified",
"duplicate price feed ids specified",
)?;
validate_optional_nonempty_vec_has_unique_elements(
&value.symbols,
"no symbols specified",
"duplicate symbols specified",
)?;
validate_formats(&value.formats)?;
validate_properties(&value.properties)?;
Ok(Self(value))
}
}
impl Deref for LatestPriceRequest {
type Target = LatestPriceRequestRepr;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for LatestPriceRequest {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct PriceRequestRepr {
pub timestamp: TimestampUs,
pub price_feed_ids: Option<Vec<PriceFeedId>>,
#[cfg_attr(feature = "utoipa", schema(default))]
pub symbols: Option<Vec<String>>,
pub properties: Vec<PriceFeedProperty>,
pub formats: Vec<Format>,
#[serde(default)]
pub json_binary_encoding: JsonBinaryEncoding,
#[serde(default = "default_parsed")]
pub parsed: bool,
pub channel: Channel,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct PriceRequest(PriceRequestRepr);
impl<'de> Deserialize<'de> for PriceRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = PriceRequestRepr::deserialize(deserializer)?;
Self::new(value).map_err(Error::custom)
}
}
impl PriceRequest {
pub fn new(value: PriceRequestRepr) -> Result<Self, &'static str> {
validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
validate_optional_nonempty_vec_has_unique_elements(
&value.price_feed_ids,
"no price feed ids specified",
"duplicate price feed ids specified",
)?;
validate_optional_nonempty_vec_has_unique_elements(
&value.symbols,
"no symbols specified",
"duplicate symbols specified",
)?;
validate_formats(&value.formats)?;
validate_properties(&value.properties)?;
Ok(Self(value))
}
}
impl Deref for PriceRequest {
type Target = PriceRequestRepr;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PriceRequest {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ReducePriceRequest {
pub payload: JsonUpdate,
pub price_feed_ids: Vec<PriceFeedId>,
}
pub type LatestPriceResponse = JsonUpdate;
pub type ReducePriceResponse = JsonUpdate;
pub type PriceResponse = JsonUpdate;
pub fn default_parsed() -> bool {
true
}
pub fn schema_default_symbols() -> Option<Vec<String>> {
None
}
pub fn schema_default_price_feed_ids() -> Option<Vec<PriceFeedId>> {
Some(vec![PriceFeedId(1)])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum DeliveryFormat {
#[default]
Json,
Binary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum Format {
Evm,
Solana,
LeEcdsa,
LeUnsigned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum JsonBinaryEncoding {
#[default]
Base64,
Hex,
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum ChannelSchemaRepr {
#[serde(rename = "real_time")]
RealTime,
#[serde(rename = "fixed_rate@50ms")]
FixedRate50ms,
#[serde(rename = "fixed_rate@200ms")]
FixedRate200ms,
#[serde(rename = "fixed_rate@1000ms")]
FixedRate1000ms,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)]
pub enum Channel {
FixedRate(FixedRate),
RealTime,
}
#[cfg(feature = "utoipa")]
impl utoipa::PartialSchema for Channel {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
ChannelSchemaRepr::schema()
}
}
#[cfg(feature = "utoipa")]
impl utoipa::ToSchema for Channel {
fn name() -> std::borrow::Cow<'static, str> {
ChannelSchemaRepr::name()
}
fn schemas(
schemas: &mut Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>,
) {
ChannelSchemaRepr::schemas(schemas)
}
}
impl PartialOrd for Channel {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let rate_left = match self {
Channel::FixedRate(rate) => rate.duration().as_micros(),
Channel::RealTime => FixedRate::MIN.duration().as_micros(),
};
let rate_right = match other {
Channel::FixedRate(rate) => rate.duration().as_micros(),
Channel::RealTime => FixedRate::MIN.duration().as_micros(),
};
Some(rate_left.cmp(&rate_right))
}
}
impl Serialize for Channel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Channel::FixedRate(fixed_rate) => serializer.serialize_str(&format!(
"fixed_rate@{}ms",
fixed_rate.duration().as_millis()
)),
Channel::RealTime => serializer.serialize_str("real_time"),
}
}
}
impl Display for Channel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Channel::FixedRate(fixed_rate) => {
write!(f, "fixed_rate@{}ms", fixed_rate.duration().as_millis())
}
Channel::RealTime => write!(f, "real_time"),
}
}
}
impl Channel {
pub fn id(&self) -> ChannelId {
match self {
Channel::FixedRate(fixed_rate) => match fixed_rate.duration().as_millis() {
50 => ChannelId::FIXED_RATE_50,
200 => ChannelId::FIXED_RATE_200,
1000 => ChannelId::FIXED_RATE_1000,
_ => panic!("unknown channel: {self:?}"),
},
Channel::RealTime => ChannelId::REAL_TIME,
}
}
}
impl TryFrom<ChannelId> for Channel {
type Error = ChannelId;
fn try_from(id: ChannelId) -> Result<Self, Self::Error> {
match id {
ChannelId::REAL_TIME => Ok(Channel::RealTime),
ChannelId::FIXED_RATE_50 => Ok(Channel::FixedRate(FixedRate::RATE_50_MS)),
ChannelId::FIXED_RATE_200 => Ok(Channel::FixedRate(FixedRate::RATE_200_MS)),
ChannelId::FIXED_RATE_1000 => Ok(Channel::FixedRate(FixedRate::RATE_1000_MS)),
_ => Err(id),
}
}
}
#[test]
fn id_supports_all_fixed_rates() {
for rate in FixedRate::ALL {
Channel::FixedRate(rate).id();
}
}
#[test]
fn from_id_round_trips_with_id() {
let all_channels = [
Channel::RealTime,
Channel::FixedRate(FixedRate::RATE_50_MS),
Channel::FixedRate(FixedRate::RATE_200_MS),
Channel::FixedRate(FixedRate::RATE_1000_MS),
];
for channel in all_channels {
assert_eq!(Channel::try_from(channel.id()), Ok(channel));
}
}
#[test]
fn from_id_returns_none_for_unknown_ids() {
assert!(Channel::try_from(ChannelId(0)).is_err());
assert!(Channel::try_from(ChannelId(5)).is_err());
assert!(Channel::try_from(ChannelId(255)).is_err());
}
#[test]
fn parse_channel_accepts_numeric_ids() {
assert_eq!(parse_channel("1"), Some(Channel::RealTime));
assert_eq!(
parse_channel("2"),
Some(Channel::FixedRate(FixedRate::RATE_50_MS))
);
assert_eq!(
parse_channel("3"),
Some(Channel::FixedRate(FixedRate::RATE_200_MS))
);
assert_eq!(
parse_channel("4"),
Some(Channel::FixedRate(FixedRate::RATE_1000_MS))
);
}
#[test]
fn parse_channel_rejects_invalid_numeric_ids() {
assert_eq!(parse_channel("0"), None);
assert_eq!(parse_channel("5"), None); assert_eq!(parse_channel("99"), None);
}
#[test]
fn channel_deserializes_from_json_string() {
let channel: Channel = serde_json::from_str(r#""3""#).unwrap();
assert_eq!(channel, Channel::FixedRate(FixedRate::RATE_200_MS));
let channel: Channel = serde_json::from_str(r#""fixed_rate@200ms""#).unwrap();
assert_eq!(channel, Channel::FixedRate(FixedRate::RATE_200_MS));
}
#[test]
fn channel_deserializes_from_json_number() {
let channel: Channel = serde_json::from_str("3").unwrap();
assert_eq!(channel, Channel::FixedRate(FixedRate::RATE_200_MS));
let channel: Channel = serde_json::from_str("1").unwrap();
assert_eq!(channel, Channel::RealTime);
}
#[test]
fn channel_rejects_invalid_json_number() {
assert!(serde_json::from_str::<Channel>("0").is_err());
assert!(serde_json::from_str::<Channel>("5").is_err());
assert!(serde_json::from_str::<Channel>("999").is_err());
}
fn parse_channel(value: &str) -> Option<Channel> {
if value == "real_time" {
Some(Channel::RealTime)
} else if let Some(rest) = value.strip_prefix("fixed_rate@") {
let ms_value = rest.strip_suffix("ms")?;
Some(Channel::FixedRate(FixedRate::from_millis(
ms_value.parse().ok()?,
)?))
} else if let Ok(id) = value.parse::<u8>() {
Channel::try_from(ChannelId(id)).ok()
} else {
None
}
}
impl<'de> Deserialize<'de> for Channel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ChannelVisitor;
impl<'de> serde::de::Visitor<'de> for ChannelVisitor {
type Value = Channel;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a channel name string or numeric channel ID")
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Channel, E> {
parse_channel(value).ok_or_else(|| E::custom("unknown channel"))
}
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Channel, E> {
let id = u8::try_from(value).map_err(|_| E::custom("channel ID out of range"))?;
Channel::try_from(ChannelId(id)).map_err(|_| E::custom("unknown channel ID"))
}
}
deserializer.deserialize_any(ChannelVisitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SubscriptionParamsRepr {
pub price_feed_ids: Option<Vec<PriceFeedId>>,
#[cfg_attr(feature = "utoipa", schema(default))]
pub symbols: Option<Vec<String>>,
pub properties: Vec<PriceFeedProperty>,
#[serde(alias = "chains")]
pub formats: Vec<Format>,
#[serde(default)]
pub delivery_format: DeliveryFormat,
#[serde(default)]
pub json_binary_encoding: JsonBinaryEncoding,
#[serde(default = "default_parsed")]
pub parsed: bool,
pub channel: Channel,
#[serde(default, alias = "ignoreInvalidFeedIds")]
pub ignore_invalid_feeds: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SubscriptionParams(SubscriptionParamsRepr);
impl<'de> Deserialize<'de> for SubscriptionParams {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = SubscriptionParamsRepr::deserialize(deserializer)?;
Self::new(value).map_err(Error::custom)
}
}
impl SubscriptionParams {
pub fn new(value: SubscriptionParamsRepr) -> Result<Self, &'static str> {
validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
validate_optional_nonempty_vec_has_unique_elements(
&value.price_feed_ids,
"no price feed ids specified",
"duplicate price feed ids specified",
)?;
validate_optional_nonempty_vec_has_unique_elements(
&value.symbols,
"no symbols specified",
"duplicate symbols specified",
)?;
validate_formats(&value.formats)?;
validate_properties(&value.properties)?;
Ok(Self(value))
}
}
impl Deref for SubscriptionParams {
type Target = SubscriptionParamsRepr;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SubscriptionParams {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct JsonBinaryData {
pub encoding: JsonBinaryEncoding,
pub data: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct JsonUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub parsed: Option<ParsedPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evm: Option<JsonBinaryData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub solana: Option<JsonBinaryData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub le_ecdsa: Option<JsonBinaryData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub le_unsigned: Option<JsonBinaryData>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ParsedPayload {
#[serde(with = "crate::serde_str::timestamp")]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub timestamp_us: TimestampUs,
pub price_feeds: Vec<ParsedFeedPayload>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ParsedFeedPayload {
pub price_feed_id: PriceFeedId,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "crate::serde_str::option_price")]
#[serde(default)]
#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
pub price: Option<Price>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "crate::serde_str::option_price")]
#[serde(default)]
#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
pub best_bid_price: Option<Price>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "crate::serde_str::option_price")]
#[serde(default)]
#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
pub best_ask_price: Option<Price>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub publisher_count: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub exponent: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub confidence: Option<Price>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub funding_rate: Option<Rate>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub funding_timestamp: Option<TimestampUs>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub funding_rate_interval: Option<DurationUs>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub market_session: Option<MarketSession>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "crate::serde_str::option_price")]
#[serde(default)]
#[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
pub ema_price: Option<Price>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub ema_confidence: Option<Price>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub feed_update_timestamp: Option<TimestampUs>,
}
impl ParsedFeedPayload {
pub fn new(
price_feed_id: PriceFeedId,
data: &AggregatedPriceFeedData,
properties: &[PriceFeedProperty],
) -> Self {
let mut output = Self {
price_feed_id,
price: None,
best_bid_price: None,
best_ask_price: None,
publisher_count: None,
exponent: None,
confidence: None,
funding_rate: None,
funding_timestamp: None,
funding_rate_interval: None,
market_session: None,
ema_price: None,
ema_confidence: None,
feed_update_timestamp: None,
};
for &property in properties {
match property {
PriceFeedProperty::Price => {
output.price = data.price;
}
PriceFeedProperty::BestBidPrice => {
output.best_bid_price = data.best_bid_price;
}
PriceFeedProperty::BestAskPrice => {
output.best_ask_price = data.best_ask_price;
}
PriceFeedProperty::PublisherCount => {
output.publisher_count = Some(data.publisher_count);
}
PriceFeedProperty::Exponent => {
output.exponent = Some(data.exponent);
}
PriceFeedProperty::Confidence => {
output.confidence = data.confidence;
}
PriceFeedProperty::FundingRate => {
output.funding_rate = data.funding_rate;
}
PriceFeedProperty::FundingTimestamp => {
output.funding_timestamp = data.funding_timestamp;
}
PriceFeedProperty::FundingRateInterval => {
output.funding_rate_interval = data.funding_rate_interval;
}
PriceFeedProperty::MarketSession => {
output.market_session = Some(data.market_session);
}
PriceFeedProperty::EmaPrice => {
output.ema_price = data.ema_price;
}
PriceFeedProperty::EmaConfidence => {
output.ema_confidence = data.ema_confidence;
}
PriceFeedProperty::FeedUpdateTimestamp => {
output.feed_update_timestamp = data.feed_update_timestamp;
}
}
}
output
}
pub fn new_full(
price_feed_id: PriceFeedId,
exponent: Option<i16>,
data: &AggregatedPriceFeedData,
) -> Self {
Self {
price_feed_id,
price: data.price,
best_bid_price: data.best_bid_price,
best_ask_price: data.best_ask_price,
publisher_count: Some(data.publisher_count),
exponent,
confidence: data.confidence,
funding_rate: data.funding_rate,
funding_timestamp: data.funding_timestamp,
funding_rate_interval: data.funding_rate_interval,
market_session: Some(data.market_session),
ema_price: data.ema_price,
ema_confidence: data.ema_confidence,
feed_update_timestamp: data.feed_update_timestamp,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum WsRequest {
Subscribe(SubscribeRequest),
Unsubscribe(UnsubscribeRequest),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SubscriptionId(pub u64);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SubscribeRequest {
pub subscription_id: SubscriptionId,
#[serde(flatten)]
pub params: SubscriptionParams,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct UnsubscribeRequest {
pub subscription_id: SubscriptionId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum WsResponse {
Error(ErrorResponse),
Subscribed(SubscribedResponse),
SubscribedWithInvalidFeedIdsIgnored(SubscribedWithInvalidFeedIdsIgnoredResponse),
Unsubscribed(UnsubscribedResponse),
SubscriptionError(SubscriptionErrorResponse),
StreamUpdated(StreamUpdatedResponse),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SubscribedResponse {
pub subscription_id: SubscriptionId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct InvalidFeedSubscriptionDetails {
pub unknown_ids: Vec<PriceFeedId>,
pub unknown_symbols: Vec<String>,
pub unsupported_channels: Vec<PriceFeedId>,
pub unstable: Vec<PriceFeedId>,
#[serde(default)]
pub not_entitled: Vec<PriceFeedId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
pub subscription_id: SubscriptionId,
pub subscribed_feed_ids: Vec<PriceFeedId>,
pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct UnsubscribedResponse {
pub subscription_id: SubscriptionId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SubscriptionErrorResponse {
pub subscription_id: SubscriptionId,
pub error: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ErrorResponse {
pub error: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct StreamUpdatedResponse {
pub subscription_id: SubscriptionId,
#[serde(flatten)]
pub payload: JsonUpdate,
}
fn validate_price_feed_ids_or_symbols(
price_feed_ids: &Option<Vec<PriceFeedId>>,
symbols: &Option<Vec<String>>,
) -> Result<(), &'static str> {
if price_feed_ids.is_none() && symbols.is_none() {
return Err("either price feed ids or symbols must be specified");
}
if price_feed_ids.is_some() && symbols.is_some() {
return Err("either price feed ids or symbols must be specified, not both");
}
Ok(())
}
fn validate_optional_nonempty_vec_has_unique_elements<T>(
vec: &Option<Vec<T>>,
empty_msg: &'static str,
duplicate_msg: &'static str,
) -> Result<(), &'static str>
where
T: Eq + std::hash::Hash,
{
if let Some(items) = vec {
if items.is_empty() {
return Err(empty_msg);
}
if !items.iter().all_unique() {
return Err(duplicate_msg);
}
}
Ok(())
}
fn validate_properties(properties: &[PriceFeedProperty]) -> Result<(), &'static str> {
if properties.is_empty() {
return Err("no properties specified");
}
if !properties.iter().all_unique() {
return Err("duplicate properties specified");
}
Ok(())
}
fn validate_formats(formats: &[Format]) -> Result<(), &'static str> {
if !formats.iter().all_unique() {
return Err("duplicate formats or chains specified");
}
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(example = "regular"))]
pub enum MarketSession {
#[default]
Regular,
PreMarket,
PostMarket,
OverNight,
Closed,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(example = "open"))]
pub enum TradingStatus {
#[default]
Open,
Closed,
Halted,
CorpAction,
}
impl From<MarketSession> for i16 {
fn from(s: MarketSession) -> i16 {
match s {
MarketSession::Regular => 0,
MarketSession::PreMarket => 1,
MarketSession::PostMarket => 2,
MarketSession::OverNight => 3,
MarketSession::Closed => 4,
}
}
}
impl TryFrom<i16> for MarketSession {
type Error = anyhow::Error;
fn try_from(value: i16) -> Result<MarketSession, Self::Error> {
match value {
0 => Ok(MarketSession::Regular),
1 => Ok(MarketSession::PreMarket),
2 => Ok(MarketSession::PostMarket),
3 => Ok(MarketSession::OverNight),
4 => Ok(MarketSession::Closed),
_ => Err(anyhow::anyhow!("invalid MarketSession value: {}", value)),
}
}
}
pub type GuardianIndex = u8;
pub type Slot = u64;
pub type MerkleTimestamp = u32;
pub type RawMerkleRoot = [u8; 20];
pub type RawMerkleSignature = [u8; 65];
pub type MerklePriceFeedId = [u8; 32];
pub type RawMerkleMessage = Vec<u8>;
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SignedMerkleRoot {
#[serde_as(as = "Hex")]
#[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
pub root: Vec<u8>,
pub slot: Slot,
pub timestamp: u32,
#[serde_as(as = "Hex")]
#[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
pub signature: Vec<u8>,
#[serde_as(as = "Vec<Hex>")]
#[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>, example = json!(["00abcdef...", "00123456..."])))]
pub messages: Vec<RawMerkleMessage>,
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct SignedGuardianSetUpgrade {
pub current_guardian_set_index: u32,
pub new_guardian_set_index: u32,
#[serde_as(as = "Vec<Hex>")]
pub new_guardian_keys: Vec<Vec<u8>>,
#[serde_as(as = "Hex")]
#[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
pub body: Vec<u8>,
#[serde_as(as = "Hex")]
#[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1a2b3c..."))]
pub signature: Vec<u8>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn signed_merkle_root_json_serialization() {
let root = SignedMerkleRoot {
root: vec![
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
],
slot: 34567890123,
timestamp: 1700000000,
signature: vec![0xaa; 65],
messages: vec![vec![0x00, 0xab, 0xcd, 0xef], vec![0x00, 0x12, 0x34, 0x56]],
};
let json = serde_json::to_value(&root).unwrap();
assert_eq!(json["root"], "0102030405060708090a0b0c0d0e0f1011121314");
assert_eq!(json["slot"], 34567890123u64);
assert_eq!(json["timestamp"], 1700000000u32);
assert_eq!(json["signature"], "aa".repeat(65));
assert_eq!(json["messages"], json!(["00abcdef", "00123456"]));
let deserialized: SignedMerkleRoot = serde_json::from_value(json).unwrap();
assert_eq!(deserialized, root);
}
#[test]
fn signed_guardian_set_upgrade_json_serialization() {
let upgrade = SignedGuardianSetUpgrade {
current_guardian_set_index: 4,
new_guardian_set_index: 5,
new_guardian_keys: vec![vec![0x11; 20], vec![0x22; 20]],
body: vec![0xde, 0xad, 0xbe, 0xef],
signature: vec![0xaa; 65],
};
let json = serde_json::to_value(&upgrade).unwrap();
assert_eq!(json["current_guardian_set_index"], 4);
assert_eq!(json["new_guardian_set_index"], 5);
let keys = json["new_guardian_keys"].as_array().unwrap();
assert_eq!(keys.len(), 2);
assert_eq!(keys[0], "11".repeat(20));
assert_eq!(keys[1], "22".repeat(20));
assert_eq!(json["body"], "deadbeef");
assert_eq!(json["signature"], "aa".repeat(65));
let deserialized: SignedGuardianSetUpgrade = serde_json::from_value(json).unwrap();
assert_eq!(deserialized, upgrade);
}
}