|
1 | 1 | //! Types describing Lazer's metadata APIs.
|
2 | 2 |
|
| 3 | +use crate::time::TimestampUs; |
3 | 4 | use crate::FeedKind;
|
4 |
| -use crate::{symbol_state::SymbolState, PriceFeedId}; |
| 5 | +use crate::{symbol_state::SymbolState, PriceFeedId, SymbolV3}; |
5 | 6 | use serde::{Deserialize, Serialize};
|
| 7 | +use strum::{Display, EnumString}; |
6 | 8 |
|
7 | 9 | /// The pricing context or type of instrument for a feed.
|
8 |
| -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 10 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)] |
9 | 11 | #[serde(rename_all = "lowercase")]
|
| 12 | +#[strum(serialize_all = "lowercase")] |
10 | 13 | pub enum InstrumentType {
|
11 | 14 | /// Spot price
|
12 | 15 | Spot,
|
13 | 16 | /// Redemption rate
|
14 | 17 | #[serde(rename = "redemptionrate")]
|
| 18 | + #[strum(serialize = "redemptionrate")] |
15 | 19 | RedemptionRate,
|
16 | 20 | /// Funding rate
|
17 | 21 | #[serde(rename = "fundingrate")]
|
| 22 | + #[strum(serialize = "fundingrate")] |
18 | 23 | FundingRate,
|
19 | 24 | /// Future price
|
20 | 25 | Future,
|
@@ -58,7 +63,7 @@ pub struct FeedResponseV3 {
|
58 | 63 | /// Unique human-readable identifier for a feed.
|
59 | 64 | /// Format: `source.instrument_type.base/quote`
|
60 | 65 | /// Examples: `"pyth.spot.btc/usd"`, `"pyth.redemptionrate.alp/usd"`, `"binance.fundingrate.btc/usdt"`, `"pyth.future.emz5/usd"`
|
61 |
| - pub symbol: String, |
| 66 | + pub symbol: SymbolV3, |
62 | 67 | /// Description of the feed pair.
|
63 | 68 | /// Example: `"Pyth Network Aggregate Price for spot BTC/USD"`
|
64 | 69 | pub description: String,
|
@@ -106,7 +111,7 @@ pub struct FeedResponseV3 {
|
106 | 111 | /// ISO datetime after which the feed will no longer produce prices because the underlying market has expired.
|
107 | 112 | /// Example: `"2025-10-03T11:08:10.089998603Z"`
|
108 | 113 | #[serde(skip_serializing_if = "Option::is_none")]
|
109 |
| - pub feed_expiry: Option<String>, |
| 114 | + pub feed_expiry: Option<TimestampUs>, |
110 | 115 | /// The nature of the data produced by the feed.
|
111 | 116 | /// Examples: `"price"`, `"fundingRate"`
|
112 | 117 | pub feed_kind: FeedKind,
|
@@ -134,5 +139,84 @@ pub struct AssetResponseV3 {
|
134 | 139 | /// Primary or canonical listing exchange, when applicable.
|
135 | 140 | /// Example: `"NASDAQ"`
|
136 | 141 | #[serde(skip_serializing_if = "Option::is_none")]
|
137 |
| - pub listing_exchange: Option<String>, |
| 142 | + pub primary_exchange: Option<String>, |
| 143 | +} |
| 144 | + |
| 145 | +#[cfg(test)] |
| 146 | +mod tests { |
| 147 | + use super::*; |
| 148 | + |
| 149 | + #[test] |
| 150 | + fn test_instrument_type_roundtrip() { |
| 151 | + let types = vec![ |
| 152 | + InstrumentType::Spot, |
| 153 | + InstrumentType::RedemptionRate, |
| 154 | + InstrumentType::FundingRate, |
| 155 | + InstrumentType::Future, |
| 156 | + InstrumentType::Nav, |
| 157 | + InstrumentType::Twap, |
| 158 | + ]; |
| 159 | + |
| 160 | + for instrument_type in types { |
| 161 | + let string_repr = instrument_type.to_string(); |
| 162 | + let parsed = string_repr.parse::<InstrumentType>().unwrap(); |
| 163 | + assert_eq!(parsed, instrument_type); |
| 164 | + } |
| 165 | + |
| 166 | + // Test invalid values |
| 167 | + assert!("invalid".parse::<InstrumentType>().is_err()); |
| 168 | + assert!("SPOT".parse::<InstrumentType>().is_err()); // case sensitive |
| 169 | + } |
| 170 | + |
| 171 | + #[test] |
| 172 | + fn test_feed_response_v3_roundtrip() { |
| 173 | + use crate::{symbol_state::SymbolState, FeedKind, PriceFeedId}; |
| 174 | + |
| 175 | + let symbol = SymbolV3::new( |
| 176 | + "pyth".to_string(), |
| 177 | + InstrumentType::Spot, |
| 178 | + "btc".to_string(), |
| 179 | + "usd".to_string(), |
| 180 | + ); |
| 181 | + |
| 182 | + let feed_response = FeedResponseV3 { |
| 183 | + id: PriceFeedId(1), |
| 184 | + name: "Bitcoin / US Dollar".to_string(), |
| 185 | + symbol, |
| 186 | + description: "Pyth Network Aggregate Price for spot BTC/USD".to_string(), |
| 187 | + base_asset_id: "BTC".to_string(), |
| 188 | + quote_asset_id: "USD".to_string(), |
| 189 | + instrument_type: InstrumentType::Spot, |
| 190 | + source: "pyth".to_string(), |
| 191 | + schedule: "America/New_York;O,O,O,O,O,O,O;".to_string(), |
| 192 | + exponent: -8, |
| 193 | + update_interval_seconds: 10, |
| 194 | + min_publishers: 3, |
| 195 | + state: SymbolState::Stable, |
| 196 | + asset_type: AssetClass::Crypto, |
| 197 | + cmc_id: Some("1".to_string()), |
| 198 | + pythnet_id: "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" |
| 199 | + .to_string(), |
| 200 | + nasdaq_symbol: None, |
| 201 | + feed_expiry: None, |
| 202 | + feed_kind: FeedKind::Price, |
| 203 | + }; |
| 204 | + |
| 205 | + // Test JSON serialization |
| 206 | + let json = |
| 207 | + serde_json::to_string(&feed_response).expect("Failed to serialize FeedResponseV3"); |
| 208 | + assert!(json.contains("\"symbol\":\"pyth.spot.btc/usd\"")); |
| 209 | + |
| 210 | + // Test JSON deserialization |
| 211 | + let deserialized: FeedResponseV3 = |
| 212 | + serde_json::from_str(&json).expect("Failed to deserialize FeedResponseV3"); |
| 213 | + assert_eq!(deserialized.symbol.as_string(), "pyth.spot.btc/usd"); |
| 214 | + assert_eq!(deserialized.symbol.source, "pyth"); |
| 215 | + assert_eq!(deserialized.symbol.instrument_type, InstrumentType::Spot); |
| 216 | + assert_eq!(deserialized.symbol.base, "btc"); |
| 217 | + assert_eq!(deserialized.symbol.quote, "usd"); |
| 218 | + |
| 219 | + // Ensure the entire structure matches |
| 220 | + assert_eq!(deserialized, feed_response); |
| 221 | + } |
138 | 222 | }
|
0 commit comments