Skip to content
Open
32 changes: 27 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-lazer-solana-contract"
version = "0.7.1"
version = "0.7.3"
edition = "2021"
description = "Pyth Lazer Solana contract and SDK."
license = "Apache-2.0"
Expand All @@ -19,7 +19,7 @@ no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]

[dependencies]
pyth-lazer-protocol = { path = "../../../../sdk/rust/protocol", version = "0.18.1" }
pyth-lazer-protocol = { path = "../../../../sdk/rust/protocol", version = "0.19.0" }

anchor-lang = "0.31.1"
bytemuck = { version = "1.20.0", features = ["derive"] }
Expand Down
4 changes: 2 additions & 2 deletions lazer/publisher_sdk/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[package]
name = "pyth-lazer-publisher-sdk"
version = "0.16.1"
version = "0.16.2"
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.18.1", path = "../../sdk/rust/protocol" }
pyth-lazer-protocol = { version = "0.19.0", path = "../../sdk/rust/protocol" }
anyhow = "1.0.98"
protobuf = "3.7.2"
serde_json = "1.0.140"
Expand Down
4 changes: 2 additions & 2 deletions lazer/sdk/rust/client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "pyth-lazer-client"
version = "8.4.1"
version = "8.4.2"
edition = "2021"
description = "A Rust client for Pyth Lazer"
license = "Apache-2.0"

[dependencies]
pyth-lazer-protocol = { path = "../protocol", version = "0.18.1" }
pyth-lazer-protocol = { path = "../protocol", version = "0.19.0" }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
futures-util = "0.3"
Expand Down
3 changes: 2 additions & 1 deletion lazer/sdk/rust/protocol/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-lazer-protocol"
version = "0.18.1"
version = "0.19.0"
edition = "2021"
description = "Pyth Lazer SDK - protocol types."
license = "Apache-2.0"
Expand All @@ -21,6 +21,7 @@ chrono = "0.4.41"
humantime = "2.2.0"
hex = "0.4.3"
thiserror = "2.0.12"
strum = { version = "0.27.2", features = ["derive"] }

[dev-dependencies]
bincode = "1.3.3"
Expand Down
6 changes: 6 additions & 0 deletions lazer/sdk/rust/protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ mod feed_kind;
pub mod jrpc;
/// Types describing Lazer's verifiable messages containing signature and payload.
pub mod message;
/// Types describing Lazer's feed & asset metadata catalog APIs.
pub mod metadata;
/// Types describing Lazer's message payload.
pub mod payload;
mod price;
Expand All @@ -19,6 +21,8 @@ mod rate;
mod serde_price_as_i64;
mod serde_str;
mod symbol_state;
/// Validated symbol type for `source.instrument_type.base/quote` format.
pub mod symbol_v3;
/// Lazer's types for time representation.
pub mod time;

Expand All @@ -28,9 +32,11 @@ use serde::{Deserialize, Serialize};
pub use crate::{
dynamic_value::DynamicValue,
feed_kind::FeedKind,
metadata::{AssetClass, AssetResponseV3, FeedResponseV3, InstrumentType},
price::{Price, PriceError},
rate::{Rate, RateError},
symbol_state::SymbolState,
symbol_v3::SymbolV3,
};

#[derive(
Expand Down
209 changes: 209 additions & 0 deletions lazer/sdk/rust/protocol/src/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//! Types describing Lazer's metadata APIs.

use crate::time::{DurationUs, TimestampUs};
use crate::PriceFeedId;
use serde::{Deserialize, Serialize};

/// The pricing context or type of instrument for a feed.
/// This is an internal type and should not be used by clients as it is non-exhaustive.
/// The API response can evolve to contain additional variants that are not listed here.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InstrumentType {
/// Spot price
Spot,
/// Redemption rate
#[serde(rename = "redemptionrate")]
RedemptionRate,
/// Funding rate
#[serde(rename = "fundingrate")]
FundingRate,
/// Future price
Future,
/// Net Asset Value
Nav,
/// Time-weighted average price
Twap,
}

/// High-level asset class.
/// This is an internal type and should not be used by clients as it is non-exhaustive.
/// The API response can evolve to contain additional variants that are not listed here.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AssetClass {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the reasons for opting in a dynamic metadata was to not go through a code change when new asset classes are added to the system. I know that users (on both sides) rely on these, and that's probably why you opted for explicit definition here. But if that's the case, you might actually remove any dynamic field and make everything very explicit. Being in the middle (some explicit metadata, some implicit dynamic) is probably worse.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should stick with our decision and use fully dynamic metadata in the protocols. In Rust that would be BTreeMap<String, serde_value::Value>. We can revisit it later if we feel like the metadata structure is very stable and future-proof, but I doubt that it will happen soon.

Copy link
Contributor Author

@tejasbadadare tejasbadadare Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah my main goal in adding these explicit types is to ensure end users can depend on a stable API contract across different versions. I.e. the different types like the existing SymbolResponse and the new FeedResponseV3 can handle the differences between v1 and v3 representations derived from the same internal dynamic metadata representation.

/// Cryptocurrency
Crypto,
/// Foreign exchange
Fx,
/// Equity
Equity,
/// Metal
Metal,
/// Rates
Rates,
/// Commodity
Commodity,
}

/// Feed metadata as returned by the v3 metadata API.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FeedResponseV3 {
/// Unique integer identifier for a feed. Known as `pyth_lazer_id` in V1 API.
/// Example: `1`
pub id: PriceFeedId,
/// Short feed name.
/// Example: `"Bitcoin / US Dollar"`
pub name: String,
/// Unique human-readable identifier for a feed.
/// Format: `source.instrument_type.base/quote`
/// Examples: `"pyth.spot.btc/usd"`, `"pyth.redemptionrate.alp/usd"`, `"binance.fundingrate.btc/usdt"`, `"pyth.future.emz5/usd"`
pub symbol: String,
/// Description of the feed pair.
/// Example: `"Pyth Network Aggregate Price for spot BTC/USD"`
pub description: String,
/// The Asset ID of the base asset.
/// Example: `"BTC"`
pub base_asset_id: String,
/// The Asset ID of the quote asset.
/// Example: `"USD"`
#[serde(skip_serializing_if = "Option::is_none")]
pub quote_asset_id: Option<String>,
/// The pricing context. Should be one of the values in the InstrumentType enum.
/// Example: `"spot"`
pub instrument_type: String,
/// Aggregator or producer of the prices.
/// Examples: `"pyth"`, `"binance"`
pub source: String,
/// The trading schedule of the feed's market, in Pythnet format.
/// Example: `"America/New_York;O,O,O,O,O,O,O;"`
pub schedule: String,
/// Power-of-ten exponent. Scale the `price` mantissa value by `10^exponent` to get the decimal representation.
/// Example: `-8`
pub exponent: i16,
/// Funding rate interval. Only applies to feeds with instrument type `funding_rate`.
/// Example: `10`
#[serde(skip_serializing_if = "Option::is_none")]
pub update_interval: Option<DurationUs>,
/// The minimum number of publishers contributing component prices to the aggregate price.
/// Example: `3`
pub min_publishers: u16,
/// Status of the feed.
/// Example: `"active"`
pub state: String,
/// High-level asset class. One of crypto, fx, equity, metal, rates, nav, commodity, funding-rate.
/// Should be one of the values in the AssetClass enum.
/// Example: `"crypto"`
pub asset_type: String,
/// CoinMarketCap asset identifier.
/// Example: `"123"`
#[serde(skip_serializing_if = "Option::is_none")]
pub cmc_id: Option<u32>,
/// Pythnet feed identifier. 32 bytes, represented in hex.
/// Example: `"e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"`
pub pythnet_id: String,
/// Nasdaq symbol identifier.
/// Example: `"ADSK"`
#[serde(skip_serializing_if = "Option::is_none")]
pub nasdaq_symbol: Option<String>,
/// ISO datetime after which the feed will no longer produce prices because the underlying market has expired.
/// Example: `"2025-10-03T11:08:10.089998603Z"`
#[serde(skip_serializing_if = "Option::is_none")]
pub feed_expiry: Option<TimestampUs>,
/// The nature of the data produced by the feed.
/// Examples: `"price"`, `"fundingRate"`
pub feed_kind: String,
}

/// Asset metadata as returned by the v3 metadata API.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AssetResponseV3 {
/// Unique identifier for an asset.
/// Example: `"BTC"`
pub id: String,
/// A short, human-readable code that identifies an asset. Not guaranteed to be unique.
/// Example: `"BTC"`
pub ticker: String,
/// Full human-readable name of the asset.
/// Example: `"Bitcoin"`
pub full_name: String,
/// High-level asset class.
/// Example: `"crypto"`
pub class: String,
/// More granular categorization within class.
/// Example: `"stablecoin"`
#[serde(skip_serializing_if = "Option::is_none")]
pub subclass: Option<String>,
/// Primary or canonical listing exchange, when applicable.
/// Example: `"NASDAQ"`
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_exchange: Option<String>,
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use crate::SymbolV3;

use super::*;

#[test]
fn test_feed_response_v3_json_serde_roundtrip() {
use crate::PriceFeedId;

let symbol = SymbolV3::new(
"pyth".to_string(),
"spot".to_string(),
"btc".to_string(),
Some("usd".to_string()),
);

let feed_response = FeedResponseV3 {
id: PriceFeedId(1),
name: "Bitcoin / US Dollar".to_string(),
symbol: symbol.as_string(),
description: "Pyth Network Aggregate Price for spot BTC/USD".to_string(),
base_asset_id: "BTC".to_string(),
quote_asset_id: Some("USD".to_string()),
instrument_type: "spot".to_string(),
source: "pyth".to_string(),
schedule: "America/New_York;O,O,O,O,O,O,O;".to_string(),
exponent: -8,
update_interval: Some(DurationUs::from_secs_u32(10)),
min_publishers: 3,
state: "stable".to_string(),
asset_type: "crypto".to_string(),
cmc_id: Some(1),
pythnet_id: "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
.to_string(),
nasdaq_symbol: None,
feed_expiry: None,
feed_kind: "price".to_string(),
};

// Test JSON serialization
let json =
serde_json::to_string(&feed_response).expect("Failed to serialize FeedResponseV3");
let expected_json = r#"{"id":1,"name":"Bitcoin / US Dollar","symbol":"pyth.spot.btc/usd","description":"Pyth Network Aggregate Price for spot BTC/USD","base_asset_id":"BTC","quote_asset_id":"USD","instrument_type":"spot","source":"pyth","schedule":"America/New_York;O,O,O,O,O,O,O;","exponent":-8,"update_interval":10000000,"min_publishers":3,"state":"stable","asset_type":"crypto","cmc_id":1,"pythnet_id":"e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43","feed_kind":"price"}"#;
assert_eq!(
json, expected_json,
"Serialized JSON does not match expected output"
);

// Test JSON deserialization
let deserialized: FeedResponseV3 =
serde_json::from_str(&json).expect("Failed to deserialize FeedResponseV3");

// Ensure the entire structure matches
assert_eq!(deserialized, feed_response);

// Test SymbolV3 deserialization
assert_eq!(deserialized.symbol, "pyth.spot.btc/usd");
let symbol = SymbolV3::from_str(&deserialized.symbol).unwrap();
assert_eq!(symbol.source, "pyth");
assert_eq!(symbol.instrument_type, "spot");
assert_eq!(symbol.base, "btc");
assert_eq!(symbol.quote, Some("usd".to_string()));
}
}
Loading