1use {
2 crate::api::{Channel, MerklePriceFeedId},
3 anyhow::{anyhow, bail, Result},
4 serde::{Deserialize, Serialize},
5 serde_with::{serde_as, DisplayFromStr},
6 std::fmt,
7};
8
9#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
10#[cfg_attr(
11 feature = "utoipa",
12 schema(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")
13)]
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(transparent)]
16pub struct PriceIdInput(pub String);
25
26impl PriceIdInput {
27 fn is_valid(&self) -> bool {
28 let normalized = self.normalize();
29 normalized.0.len() == 64 && normalized.0.bytes().all(|byte| byte.is_ascii_hexdigit())
30 }
31
32 fn normalize(&self) -> Self {
33 let normalized = self
34 .0
35 .strip_prefix("0x")
36 .or_else(|| self.0.strip_prefix("0X"))
37 .unwrap_or(&self.0);
38 Self(normalized.to_string())
39 }
40
41 pub fn parse(&self) -> Result<MerklePriceFeedId> {
42 if !self.is_valid() {
43 bail!("Invalid price id: {}", self.0);
44 }
45 let normalized = self.normalize();
46 let bytes = hex::decode(normalized.0)
47 .map_err(|e| anyhow!("Failed to decode price id: {}, error: {}", self.0, e))?;
48 bytes
49 .try_into()
50 .map_err(|_| anyhow!("Invalid price length: {}", self.0))
51 }
52}
53
54#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum AssetType {
58 Crypto,
59 Fx,
60 Equity,
61 Metal,
62 Rates,
63 CryptoRedemptionRate,
64 Commodities,
65 CryptoIndex,
66 CryptoNav,
67 Eco,
68 Kalshi,
69}
70
71impl fmt::Display for AssetType {
72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73 match self {
74 AssetType::Crypto => write!(f, "crypto"),
75 AssetType::Fx => write!(f, "fx"),
76 AssetType::Equity => write!(f, "equity"),
77 AssetType::Metal => write!(f, "metal"),
78 AssetType::Rates => write!(f, "rates"),
79 AssetType::CryptoRedemptionRate => write!(f, "crypto_redemption_rate"),
80 AssetType::Commodities => write!(f, "commodities"),
81 AssetType::CryptoIndex => write!(f, "crypto_index"),
82 AssetType::CryptoNav => write!(f, "crypto_nav"),
83 AssetType::Eco => write!(f, "eco"),
84 AssetType::Kalshi => write!(f, "kalshi"),
85 }
86 }
87}
88
89#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
90#[cfg_attr(
91 feature = "utoipa",
92 schema(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")
93)]
94#[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)]
95#[serde(transparent)]
96pub struct RpcPriceIdentifier(pub String);
97
98#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
99#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
100pub struct ParsedPriceUpdate {
101 pub ema_price: RpcPrice,
102 pub id: RpcPriceIdentifier,
103 pub metadata: RpcPriceFeedMetadataV2,
104 pub price: RpcPrice,
105}
106
107#[serde_as]
108#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
109#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
110pub struct RpcPrice {
117 #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "509500001"))]
119 #[serde_as(as = "DisplayFromStr")]
120 pub conf: u64,
121 #[cfg_attr(feature = "utoipa", schema(example = -8))]
124 pub expo: i32,
125 #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "2920679499999"))]
127 #[serde_as(as = "DisplayFromStr")]
128 pub price: i64,
129 #[cfg_attr(feature = "utoipa", schema(example = 1717632000))]
132 pub publish_time: i64,
133}
134
135#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
136#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
137pub struct RpcPriceFeedMetadataV2 {
138 #[cfg_attr(feature = "utoipa", schema(example = 1717632000))]
139 pub prev_publish_time: i64,
140 #[cfg_attr(feature = "utoipa", schema(example = 1717632000))]
141 pub proof_available_time: i64,
142 #[cfg_attr(feature = "utoipa", schema(minimum = 0, example = 85480034))]
143 pub slot: i64,
144}
145
146#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct PriceFeedAttributes {
149 pub asset_type: String,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub base: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub cms_symbol: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub country: Option<String>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub cqs_symbol: Option<String>,
158 pub description: String,
159 pub display_symbol: String,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub generic_symbol: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub nasdaq_symbol: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub publish_interval: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub quote_currency: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub schedule: Option<String>,
170 pub symbol: String,
171 pub min_channel: Channel,
172}
173
174#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct PriceFeedMetadata {
177 pub id: RpcPriceIdentifier,
178 pub attributes: PriceFeedAttributes,
179}
180
181#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
182#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
183pub struct WsPriceFeed {
184 pub id: RpcPriceIdentifier,
185 pub price: RpcPrice,
186 pub ema_price: RpcPrice,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub metadata: Option<WsPriceFeedMetadata>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub vaa: Option<String>,
191}
192
193#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
194#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
195pub struct WsPriceFeedMetadata {
196 pub slot: u64,
197 pub emitter_chain: u16,
198 pub price_service_receive_time: i64,
199 pub prev_publish_time: i64,
200}
201
202#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(tag = "type")]
206pub enum HermesWsClientMessage {
207 #[serde(rename = "subscribe")]
208 Subscribe {
209 ids: Vec<PriceIdInput>,
210 #[serde(default)]
211 verbose: bool,
212 #[serde(default)]
213 binary: bool,
214 #[serde(default)]
215 #[allow(dead_code)]
216 allow_out_of_order: bool,
218 #[serde(default)]
219 ignore_invalid_price_ids: bool,
220 },
221 #[serde(rename = "unsubscribe")]
222 Unsubscribe { ids: Vec<PriceIdInput> },
223}
224
225#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
227#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
228#[serde(tag = "type")]
229#[allow(clippy::large_enum_variant)]
230pub enum HermesWsServerMessage {
232 #[serde(rename = "response")]
233 Response(HermesWsServerResponse),
234 #[serde(rename = "price_update")]
235 PriceUpdate { price_feed: WsPriceFeed },
236}
237
238#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
240#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
241#[serde(tag = "status")]
242pub enum HermesWsServerResponse {
243 #[serde(rename = "success")]
244 Success,
245 #[serde(rename = "error")]
246 Err { error: String },
247}
248
249#[cfg(test)]
250mod tests {
251 use super::PriceIdInput;
252
253 #[test]
254 fn validates_price_id_with_and_without_prefix() {
255 let valid = &PriceIdInput(
256 "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43".to_string(),
257 );
258 assert!(valid.is_valid());
259
260 let valid = &PriceIdInput(
261 "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43".to_string(),
262 );
263 assert!(valid.is_valid());
264
265 let valid = &PriceIdInput(
266 "0Xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43".to_string(),
267 );
268 assert!(valid.is_valid());
269 }
270
271 #[test]
272 fn rejects_invalid_price_id() {
273 let invalid = &PriceIdInput("abc123".to_string());
274 assert!(!invalid.is_valid());
275
276 let invalid = &PriceIdInput(
277 "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b4z".to_string(),
278 );
279 assert!(!invalid.is_valid());
280 }
281}