diff --git a/Cargo.lock b/Cargo.lock index 048f0956da..5dc69aa3ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5693,7 +5693,7 @@ dependencies = [ [[package]] name = "pyth-lazer-client" -version = "8.0.1" +version = "8.1.0" dependencies = [ "alloy-primitives 0.8.25", "anyhow", diff --git a/lazer/sdk/js/package.json b/lazer/sdk/js/package.json index 478b38e77d..85dece8006 100644 --- a/lazer/sdk/js/package.json +++ b/lazer/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-lazer-sdk", - "version": "3.0.0", + "version": "3.0.1", "description": "Pyth Lazer SDK", "publishConfig": { "access": "public" diff --git a/lazer/sdk/js/src/protocol.ts b/lazer/sdk/js/src/protocol.ts index 697b6f2b70..30a1bb83ab 100644 --- a/lazer/sdk/js/src/protocol.ts +++ b/lazer/sdk/js/src/protocol.ts @@ -50,6 +50,7 @@ export type JsonBinaryData = { export type InvalidFeedSubscriptionDetails = { unknownIds: number[]; + unknownSymbols: string[]; unsupportedChannels: number[]; unstable: number[]; }; diff --git a/lazer/sdk/rust/client/Cargo.toml b/lazer/sdk/rust/client/Cargo.toml index 11d544ee04..062046ad9d 100644 --- a/lazer/sdk/rust/client/Cargo.toml +++ b/lazer/sdk/rust/client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyth-lazer-client" -version = "8.0.1" +version = "8.1.0" edition = "2021" description = "A Rust client for Pyth Lazer" license = "Apache-2.0" diff --git a/lazer/sdk/rust/protocol/src/api.rs b/lazer/sdk/rust/protocol/src/api.rs index 27aa8a83e1..228b6efd52 100644 --- a/lazer/sdk/rust/protocol/src/api.rs +++ b/lazer/sdk/rust/protocol/src/api.rs @@ -16,7 +16,8 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct LatestPriceRequest { +pub struct LatestPriceRequestRepr { + // Either price feed ids or symbols must be specified. pub price_feed_ids: Option>, pub symbols: Option>, pub properties: Vec, @@ -32,9 +33,55 @@ pub struct LatestPriceRequest { pub channel: Channel, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LatestPriceRequest(LatestPriceRequestRepr); + +impl<'de> Deserialize<'de> for LatestPriceRequest { + fn deserialize(deserializer: D) -> Result + 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 { + 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")] -pub struct PriceRequest { +pub struct PriceRequestRepr { pub timestamp: TimestampUs, // Either price feed ids or symbols must be specified. pub price_feed_ids: Option>, @@ -50,6 +97,52 @@ pub struct PriceRequest { pub channel: Channel, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PriceRequest(PriceRequestRepr); + +impl<'de> Deserialize<'de> for PriceRequest { + fn deserialize(deserializer: D) -> Result + 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 { + 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")] pub struct ReducePriceRequest { @@ -221,40 +314,19 @@ impl<'de> Deserialize<'de> for SubscriptionParams { impl SubscriptionParams { pub fn new(value: SubscriptionParamsRepr) -> Result { - if value.price_feed_ids.is_none() && value.symbols.is_none() { - return Err("either price feed ids or symbols must be specified"); - } - if value.price_feed_ids.is_some() && value.symbols.is_some() { - return Err("either price feed ids or symbols must be specified, not both"); - } - - if let Some(ref ids) = value.price_feed_ids { - if ids.is_empty() { - return Err("no price feed ids specified"); - } - if !ids.iter().all_unique() { - return Err("duplicate price feed ids specified"); - } - } - - if let Some(ref symbols) = value.symbols { - if symbols.is_empty() { - return Err("no symbols specified"); - } - if !symbols.iter().all_unique() { - return Err("duplicate symbols specified"); - } - } - - if !value.formats.iter().all_unique() { - return Err("duplicate formats or chains specified"); - } - if value.properties.is_empty() { - return Err("no properties specified"); - } - if !value.properties.iter().all_unique() { - return Err("duplicate properties specified"); - } + 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)) } } @@ -467,6 +539,7 @@ pub struct SubscribedResponse { #[serde(rename_all = "camelCase")] pub struct InvalidFeedSubscriptionDetails { pub unknown_ids: Vec, + pub unknown_symbols: Vec, pub unsupported_channels: Vec, pub unstable: Vec, } @@ -511,3 +584,53 @@ pub struct StreamUpdatedResponse { #[serde(flatten)] pub payload: JsonUpdate, } + +// Common validation functions +fn validate_price_feed_ids_or_symbols( + price_feed_ids: &Option>, + symbols: &Option>, +) -> 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( + vec: &Option>, + empty_msg: &'static str, + duplicate_msg: &'static str, +) -> Result<(), &'static str> +where + T: Eq + std::hash::Hash, +{ + if let Some(ref 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(()) +}