Skip to content

Commit 05e2dba

Browse files
committed
feat(lazer-protocol): add SymbolV3 type & validation, add strum::EnumString to InstrumentType for string parsing, add tests
1 parent ebf5caf commit 05e2dba

File tree

5 files changed

+437
-5
lines changed

5 files changed

+437
-5
lines changed

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lazer/sdk/rust/protocol/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ chrono = "0.4.41"
2121
humantime = "2.2.0"
2222
hex = "0.4.3"
2323
thiserror = "2.0.12"
24+
strum = { version = "0.27.2", features = ["derive"] }
2425

2526
[dev-dependencies]
2627
bincode = "1.3.3"

lazer/sdk/rust/protocol/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ mod rate;
2121
mod serde_price_as_i64;
2222
mod serde_str;
2323
mod symbol_state;
24+
/// Validated symbol type for `source.instrument_type.base/quote` format.
25+
pub mod symbol_v3;
2426
/// Lazer's types for time representation.
2527
pub mod time;
2628

@@ -34,6 +36,7 @@ pub use crate::{
3436
price::{Price, PriceError},
3537
rate::{Rate, RateError},
3638
symbol_state::SymbolState,
39+
symbol_v3::SymbolV3,
3740
};
3841

3942
#[derive(

lazer/sdk/rust/protocol/src/metadata.rs

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
//! Types describing Lazer's metadata APIs.
22
3+
use crate::time::TimestampUs;
34
use crate::FeedKind;
4-
use crate::{symbol_state::SymbolState, PriceFeedId};
5+
use crate::{symbol_state::SymbolState, PriceFeedId, SymbolV3};
56
use serde::{Deserialize, Serialize};
7+
use strum::{Display, EnumString};
68

79
/// 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)]
911
#[serde(rename_all = "lowercase")]
12+
#[strum(serialize_all = "lowercase")]
1013
pub enum InstrumentType {
1114
/// Spot price
1215
Spot,
1316
/// Redemption rate
1417
#[serde(rename = "redemptionrate")]
18+
#[strum(serialize = "redemptionrate")]
1519
RedemptionRate,
1620
/// Funding rate
1721
#[serde(rename = "fundingrate")]
22+
#[strum(serialize = "fundingrate")]
1823
FundingRate,
1924
/// Future price
2025
Future,
@@ -58,7 +63,7 @@ pub struct FeedResponseV3 {
5863
/// Unique human-readable identifier for a feed.
5964
/// Format: `source.instrument_type.base/quote`
6065
/// 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,
6267
/// Description of the feed pair.
6368
/// Example: `"Pyth Network Aggregate Price for spot BTC/USD"`
6469
pub description: String,
@@ -106,7 +111,7 @@ pub struct FeedResponseV3 {
106111
/// ISO datetime after which the feed will no longer produce prices because the underlying market has expired.
107112
/// Example: `"2025-10-03T11:08:10.089998603Z"`
108113
#[serde(skip_serializing_if = "Option::is_none")]
109-
pub feed_expiry: Option<String>,
114+
pub feed_expiry: Option<TimestampUs>,
110115
/// The nature of the data produced by the feed.
111116
/// Examples: `"price"`, `"fundingRate"`
112117
pub feed_kind: FeedKind,
@@ -134,5 +139,84 @@ pub struct AssetResponseV3 {
134139
/// Primary or canonical listing exchange, when applicable.
135140
/// Example: `"NASDAQ"`
136141
#[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+
}
138222
}

0 commit comments

Comments
 (0)