From a1eea8553465d15dcb1eeef67d51911e25c093b4 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Thu, 18 Sep 2025 17:15:54 -0700 Subject: [PATCH 1/5] feat(protocol): add unknown_symbols feed subscription response type --- lazer/sdk/rust/protocol/src/api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lazer/sdk/rust/protocol/src/api.rs b/lazer/sdk/rust/protocol/src/api.rs index 27aa8a83e1..3bcde57df7 100644 --- a/lazer/sdk/rust/protocol/src/api.rs +++ b/lazer/sdk/rust/protocol/src/api.rs @@ -467,6 +467,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, } From 6f9d6dabfedc349bf2a4b2ca76e20a84ee642d0e Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Thu, 18 Sep 2025 20:08:25 -0700 Subject: [PATCH 2/5] feat(pyth-lazer-protocol): bump proto version --- Cargo.lock | 10 +++++----- lazer/publisher_sdk/rust/Cargo.toml | 4 ++-- lazer/sdk/rust/client/Cargo.toml | 2 +- lazer/sdk/rust/protocol/Cargo.toml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d2251bf1c..a939340cfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5693,7 +5693,7 @@ dependencies = [ [[package]] name = "pyth-lazer-client" -version = "8.0.0" +version = "8.1.0" dependencies = [ "alloy-primitives 0.8.25", "anyhow", @@ -5711,7 +5711,7 @@ dependencies = [ "hex", "humantime-serde", "libsecp256k1 0.7.2", - "pyth-lazer-protocol 0.15.0", + "pyth-lazer-protocol 0.15.1", "reqwest 0.12.23", "serde", "serde_json", @@ -5746,7 +5746,7 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.15.0" +version = "0.15.1" dependencies = [ "alloy-primitives 0.8.25", "anyhow", @@ -5786,13 +5786,13 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" -version = "0.12.0" +version = "0.12.1" dependencies = [ "anyhow", "fs-err", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.15.0", + "pyth-lazer-protocol 0.15.1", "serde_json", ] diff --git a/lazer/publisher_sdk/rust/Cargo.toml b/lazer/publisher_sdk/rust/Cargo.toml index b1cbc3d1e5..a805bef5fe 100644 --- a/lazer/publisher_sdk/rust/Cargo.toml +++ b/lazer/publisher_sdk/rust/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "pyth-lazer-publisher-sdk" -version = "0.12.0" +version = "0.12.1" edition = "2021" description = "Pyth Lazer Publisher SDK types." license = "Apache-2.0" repository = "https://github.com/pyth-network/pyth-crosschain" [dependencies] -pyth-lazer-protocol = { version = "0.15.0", path = "../../sdk/rust/protocol" } +pyth-lazer-protocol = { version = "0.15.1", path = "../../sdk/rust/protocol" } anyhow = "1.0.98" protobuf = "3.7.2" serde_json = "1.0.140" diff --git a/lazer/sdk/rust/client/Cargo.toml b/lazer/sdk/rust/client/Cargo.toml index f93285720c..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.0" +version = "8.1.0" edition = "2021" description = "A Rust client for Pyth Lazer" license = "Apache-2.0" diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index c95e0e85c0..f73a098bc9 100644 --- a/lazer/sdk/rust/protocol/Cargo.toml +++ b/lazer/sdk/rust/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyth-lazer-protocol" -version = "0.15.0" +version = "0.15.1" edition = "2021" description = "Pyth Lazer SDK - protocol types." license = "Apache-2.0" From e18e4621f2ac031bdc43404ddf6ac7a10e1572cd Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Thu, 18 Sep 2025 20:20:04 -0700 Subject: [PATCH 3/5] feat(pyth-lazer-protocol): add deserializers with validation for LatestPriceRequest and PriceRequest --- lazer/sdk/rust/protocol/src/api.rs | 139 ++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/lazer/sdk/rust/protocol/src/api.rs b/lazer/sdk/rust/protocol/src/api.rs index 3bcde57df7..82519a2096 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,76 @@ 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 { + 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"); + } + 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 +118,73 @@ 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 { + 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"); + } + 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 { From 126d9df0b3b96cf3cf08d25dab8cddb866553f94 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Fri, 19 Sep 2025 10:33:57 -0700 Subject: [PATCH 4/5] extract out validation functions --- lazer/sdk/rust/protocol/src/api.rs | 191 ++++++++++++++--------------- 1 file changed, 89 insertions(+), 102 deletions(-) diff --git a/lazer/sdk/rust/protocol/src/api.rs b/lazer/sdk/rust/protocol/src/api.rs index 82519a2096..228b6efd52 100644 --- a/lazer/sdk/rust/protocol/src/api.rs +++ b/lazer/sdk/rust/protocol/src/api.rs @@ -49,40 +49,19 @@ impl<'de> Deserialize<'de> for LatestPriceRequest { impl LatestPriceRequest { pub fn new(value: LatestPriceRequestRepr) -> 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)) } } @@ -134,40 +113,19 @@ impl<'de> Deserialize<'de> for PriceRequest { impl PriceRequest { pub fn new(value: PriceRequestRepr) -> 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)) } } @@ -356,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)) } } @@ -647,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(()) +} From dac106f297b2558ad494741ae64018985216575c Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Mon, 22 Sep 2025 16:46:15 -0700 Subject: [PATCH 5/5] feat(lazer/js/sdk): add unknown_symbols to InvalidFeedSubscriptionDetails --- Cargo.lock | 36 ++++++++++++++++++------------------ lazer/sdk/js/package.json | 2 +- lazer/sdk/js/src/protocol.ts | 1 + 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e90b742d20..5dc69aa3ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5656,7 +5656,7 @@ dependencies = [ [[package]] name = "pyth-lazer-agent" -version = "0.5.0" +version = "0.5.1" dependencies = [ "anyhow", "backoff", @@ -5674,8 +5674,8 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol 0.14.0", - "pyth-lazer-publisher-sdk 0.10.0", + "pyth-lazer-protocol 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-publisher-sdk 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.12.23", "serde", "serde_json", @@ -5725,18 +5725,23 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91b3e69c264b2ad80b5943df86c606daae63b13f93062abcc008c09a9e2e621e" +version = "0.15.1" dependencies = [ + "alloy-primitives 0.8.25", "anyhow", + "assert_float_eq", + "bincode 1.3.3", + "bs58", "byteorder", "chrono", "derive_more 1.0.0", + "ed25519-dalek 2.1.1", "hex", "humantime", "humantime-serde", "itertools 0.13.0", + "libsecp256k1 0.7.2", + "mry", "protobuf", "rust_decimal", "serde", @@ -5747,22 +5752,17 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d91dc5606c70529bf14769034738bc8773d359b4313be3c44449dd3b442096d" dependencies = [ - "alloy-primitives 0.8.25", "anyhow", - "assert_float_eq", - "bincode 1.3.3", - "bs58", "byteorder", "chrono", "derive_more 1.0.0", - "ed25519-dalek 2.1.1", "hex", "humantime", "humantime-serde", "itertools 0.13.0", - "libsecp256k1 0.7.2", - "mry", "protobuf", "rust_decimal", "serde", @@ -5772,27 +5772,27 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f83b818450d72f6f6db5a9d98e90d2668971da14363820829998290d913f80" +version = "0.12.1" dependencies = [ "anyhow", "fs-err", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.14.0", + "pyth-lazer-protocol 0.15.1", "serde_json", ] [[package]] name = "pyth-lazer-publisher-sdk" version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b5f8284182d0acb4afa3c8289727511e36f59ab15b52d850aa2e32ffe0684f" dependencies = [ "anyhow", "fs-err", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.15.1", + "pyth-lazer-protocol 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json", ] diff --git a/lazer/sdk/js/package.json b/lazer/sdk/js/package.json index 6c412d5281..7f209389df 100644 --- a/lazer/sdk/js/package.json +++ b/lazer/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-lazer-sdk", - "version": "2.0.0", + "version": "2.1.0", "description": "Pyth Lazer SDK", "publishConfig": { "access": "public" diff --git a/lazer/sdk/js/src/protocol.ts b/lazer/sdk/js/src/protocol.ts index 747a4cf3bf..a5ba7e7f51 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[]; };