From 48521b5c886e83bede873d8920917aea4444bd61 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Mon, 14 Jul 2025 14:55:08 +0100 Subject: [PATCH 1/8] feat(lazer): update metadata proto, rename and clarify docs --- lazer/publisher_sdk/proto/dynamic_value.proto | 2 +- .../proto/governance_instruction.proto | 109 ++++++++++++------ .../proto/publisher_update.proto | 4 +- .../proto/pyth_lazer_transaction.proto | 2 +- lazer/publisher_sdk/proto/state.proto | 89 +++++++------- .../proto/transaction_envelope.proto | 2 +- 6 files changed, 123 insertions(+), 85 deletions(-) diff --git a/lazer/publisher_sdk/proto/dynamic_value.proto b/lazer/publisher_sdk/proto/dynamic_value.proto index 6d15d73a6b..14800e784f 100644 --- a/lazer/publisher_sdk/proto/dynamic_value.proto +++ b/lazer/publisher_sdk/proto/dynamic_value.proto @@ -3,7 +3,7 @@ syntax = "proto3"; import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; -package pyth_lazer_transaction; +package pyth_lazer; // A dynamically typed value similar to `google.protobuf.Value` // but supporting more types. diff --git a/lazer/publisher_sdk/proto/governance_instruction.proto b/lazer/publisher_sdk/proto/governance_instruction.proto index 9f655a32e6..ef94e2351d 100644 --- a/lazer/publisher_sdk/proto/governance_instruction.proto +++ b/lazer/publisher_sdk/proto/governance_instruction.proto @@ -5,11 +5,12 @@ import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; import "dynamic_value.proto"; +import "state.proto"; // If any field documented as `[required]` is not present in the instruction, // the instruction will be rejected. -package pyth_lazer_transaction; +package pyth_lazer; // Representation of a complete governance instruction. This value will be signed // by a governance source. @@ -130,9 +131,10 @@ message Permissions { enum UpdateFeedAction { // Required by protobuf. Instruction will be rejected if this value is encountered. UPDATE_FEED_ACTION_UNSPECIFIED = 0; - UPDATE_FEED_METADATA = 101; - ACTIVATE_FEED = 102; - DEACTIVATE_FEED = 103; + UPDATE_FEED_PROPERTIES = 101; + UPDATE_FEED_METADATA = 102; + ENABLE_FEED_IN_SHARD = 103; + DISABLE_FEED_IN_SHARD = 104; REMOVE_FEED = 199; } @@ -302,17 +304,31 @@ message SetPublisherActive { optional bool is_active = 1; } -// Feed is inactive when added, meaning that it will be available to publishers but not to consumers. +// Add a new feed. Refer to `Feed` message fields documentation. message AddFeed { - // [required] ID of the feed. Must be unique (within the shard). + // [required] optional uint32 feed_id = 1; - // [required] Feed metadata. Some properties are required (name, exponent, etc.). - // Known properties must have the expected type. - // Additional arbitrary properties are allowed. - // (TODO: document known metadata properties) - optional DynamicValue.Map metadata = 2; - // IDs of publishers enabled for this feed. - repeated uint32 permissioned_publishers = 3; + // [required] + optional DynamicValue.Map metadata = 3; + // [required] + optional string name = 101; + // [required] + optional sint32 exponent = 102; + // [required] + optional uint32 min_publishers = 103; + // [required] + optional google.protobuf.Duration min_rate = 104; + // [required] + optional google.protobuf.Duration expiry_time = 105; + // [required] + optional string market_schedule = 106; + // [required] + optional FeedState state = 107; + // [required] + optional bool is_enabled_in_shard = 201; + + // TODO: IDs of publishers enabled for this feed. + // repeated uint32 permissioned_publishers = 3; } message UpdateFeed { @@ -321,13 +337,38 @@ message UpdateFeed { // [required] // Note: when adding a new variant here, update `Permissions` as well. oneof action { - UpdateFeedMetadata update_feed_metadata = 101; - ActivateFeed activate_feed = 102; - DeactivateFeed deactivate_feed = 103; + UpdateFeedProperties update_feed_properties = 101; + UpdateFeedMetadata update_feed_metadata = 102; + EnableFeedInShard enable_feed_in_shard = 103; + DisableFeedInShard disable_feed_in_shard = 104; google.protobuf.Empty remove_feed = 199; } } +// Update a feed's properties. The feed will be updated with values present in each field. +// If a value is not supplied, the corresponding property will remain unchanged. +// Refer to `Feed` message fields documentation. +message UpdateFeedProperties { + // [optional] + optional DynamicValue.Map metadata = 3; + // [optional] + optional string name = 101; + // [optional] + optional sint32 exponent = 102; + // [optional] + optional uint32 min_publishers = 103; + // [optional] + optional google.protobuf.Duration min_rate = 104; + // [optional] + optional google.protobuf.Duration expiry_time = 105; + // [optional] + optional string market_schedule = 106; + // [optional] + optional FeedState state = 107; + // [optional] + optional bool is_enabled_in_shard = 201; +} + message UpdateFeedMetadata { // [required] Property name. optional string name = 1; @@ -335,29 +376,29 @@ message UpdateFeedMetadata { optional DynamicValue value = 2; } -// Set the feed as active or shedule an activation. -// If there was already a pending activation or deactivation, it will be cleared +// Set the feed as enabled in this shard or shedule it for a certain timestamp. +// If there was already a pending status change, it will be cleared // when this governance instruction is processed. -// Warning: there must never be two feeds with the same name active at the same time +// Warning: there must never be two feeds with the same name enabled at the same time // within a shard group. This cannot be enforced within a shard. When a feed needs to be -// moved between shards, use `activation_timestamp` and `deactivation_timestamp` -// to deactivate it in the old shard and activate it in the new shard at the same time. -message ActivateFeed { - // [optional] If provided, the feed will activate at the specified timestamp. - // If `activation_timestamp` is already passed or if it's unset, - // the feed will be activated immediately when this +// moved between shards, use `enable_in_shard_timestamp` and `disable_in_shard_timestamp` +// to disable it in the old shard and enable it in the new shard at the same time. +message EnableFeedInShard { + // [optional] If provided, the feed will be enabled at the specified timestamp. + // If `enable_in_shard_timestamp` is already passed or if it's unset, + // the feed will be enabled immediately when this // governance instruction is processed. - optional google.protobuf.Timestamp activation_timestamp = 1; + optional google.protobuf.Timestamp enable_in_shard_timestamp = 1; } -// Set the feed as inactive or shedule a deactivation. -// If there was already a pending activation or deactivation, it will be cleared +// Set the feed as disabled in this shard or shedule it for a certain timestamp. +// If there was already a pending status change, it will be cleared // when this governance instruction is processed. -// See also: `ActivateFeed` docs. -message DeactivateFeed { - // [optional] If provided, the feed will deactivate at the specified timestamp. - // If `deactivation_timestamp` is already passed or if it's unset, - // the feed will be deactivated immediately when this +// See also: `EnableFeedInShard` docs. +message DisableFeedInShard { + // [optional] If provided, the feed will be disabled at the specified timestamp. + // If `disable_in_shard_timestamp` is already passed or if it's unset, + // the feed will be disabled immediately when this // governance instruction is processed. - optional google.protobuf.Timestamp deactivation_timestamp = 1; + optional google.protobuf.Timestamp disable_in_shard_timestamp = 1; } diff --git a/lazer/publisher_sdk/proto/publisher_update.proto b/lazer/publisher_sdk/proto/publisher_update.proto index 916837dedc..6bf249ce8d 100644 --- a/lazer/publisher_sdk/proto/publisher_update.proto +++ b/lazer/publisher_sdk/proto/publisher_update.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package pyth_lazer_transaction; +package pyth_lazer; import "google/protobuf/timestamp.proto"; @@ -28,7 +28,7 @@ message FeedUpdate { // [required] timestamp when this data was first acquired or generated optional google.protobuf.Timestamp source_timestamp = 2; - // [required] one type of update containing specific data + // [required] one type of update containing specific data oneof update { PriceUpdate price_update = 3; FundingRateUpdate funding_rate_update = 4; diff --git a/lazer/publisher_sdk/proto/pyth_lazer_transaction.proto b/lazer/publisher_sdk/proto/pyth_lazer_transaction.proto index f755c4b7a6..deb75adef3 100644 --- a/lazer/publisher_sdk/proto/pyth_lazer_transaction.proto +++ b/lazer/publisher_sdk/proto/pyth_lazer_transaction.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package pyth_lazer_transaction; +package pyth_lazer; import "publisher_update.proto"; import "governance_instruction.proto"; diff --git a/lazer/publisher_sdk/proto/state.proto b/lazer/publisher_sdk/proto/state.proto index d98e42f246..82721582cd 100644 --- a/lazer/publisher_sdk/proto/state.proto +++ b/lazer/publisher_sdk/proto/state.proto @@ -1,9 +1,11 @@ syntax = "proto3"; -package lazer; +package pyth_lazer; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; +import "dynamic_value.proto"; + // All optional fields should always be set unless documented otherwise. // State of a Pyth Lazer shard. @@ -45,67 +47,62 @@ message Publisher { } enum FeedState { - COMING_SOON = 0; // Default value + // Default value. Feeds in this state are not available to consumers. + COMING_SOON = 0; + // A fully available feed. STABLE = 1; + // Inactive feeds are not available to consumers or publishers. INACTIVE = 2; } -// Static data for a feed. -message FeedMetadata { - // [required] ID of the price feed. - optional uint32 price_feed_id = 1; - // [required] Feed name. - optional string name = 2; - // [required] Feed symbol. - optional string symbol = 3; - // [required] Feed description. - optional string description = 4; - // [required] Feed asset type. - optional string asset_type = 5; +// An item of the state describing a feed. +message Feed { + // [required] ID of the feed. + optional uint32 feed_id = 1; + // Additional state per publisher. + // If an eligible publisher is not listed here, the corresponding state should be considered empty. + repeated FeedPublisherState per_publisher = 2; + // [required] Additional metadata values. These values will be exposed in the APIs, but + // are not directly used in the aggregator. + optional DynamicValue.Map metadata = 3; + + // [required] A readable feed name. It must be unique across all feeds in the shard. + // Used for logs, metrics, feed search API, TradingView API. + optional string name = 101; // [required] Exponent applied to all price and rate values for this feed. // Actual value is `mantissa * 10 ^ exponent`. // Restricted to int16. - optional sint32 exponent = 6; - // [optional] CoinMarketCap ID. Can be absent if there is no CoinMarketCap ID for this symbol. - optional uint32 cmc_id = 7; - // [optional] Funding rate interval. Only present for funding rate feeds. - optional google.protobuf.Duration funding_rate_interval = 8; + optional sint32 exponent = 102; // [required] Minimal number of publisher prices required to produce an aggregate. - optional uint32 min_publishers = 9; + optional uint32 min_publishers = 103; // [required] Minimal rate of aggregation performed by the aggregator for this feed. // Cannot be lower than the shard's top level `State.min_rate`. - optional google.protobuf.Duration min_rate = 10; + optional google.protobuf.Duration min_rate = 104; // [required] Time after which the publisher update is discarded. - optional google.protobuf.Duration expiry_time = 11; - // [required] If true, the feed is visible to the consumers. This can be used to prepare and verify - // new feeds before releasing them. This can also be used to migrate a feed from - // one shard to another. If a feed is present in + optional google.protobuf.Duration expiry_time = 105; + // [required] Market schedule in Pythnet format. + optional string market_schedule = 106; + // [required] Feed state + optional FeedState state = 107; + + + // [required] Feed status in the current shard. Disabled feeds will not be visible in + // the consumer API for the current shard. This setting should only be used + // to migrate a feed from one shard to another. + // + // If a feed is present in // multiple shards, it must only be active in one of them at each time. // To enforce this, `pending_activation` and `pending_deactivation` fields // can be used to deactivate a feed in one shard and activate it in another shard // at the same instant. - optional bool is_activated = 12; - // [optional] ID of the corresponding price feed in Hermes (Pythnet). - optional string hermes_id = 13; - // [optional] Quote currency of the asset. - optional string quote_currency = 14; - // [optional] Market schedule in Pythnet format. - // If absent, the default schedule is used (market is always open). - optional string market_schedule = 15; - // [required] Feed state - optional FeedState state = 16; -} + optional bool is_enabled_in_shard = 201; + // [optional] If present, the aggregator will enable the feed in the current shard + // at the specified instant. + optional google.protobuf.Timestamp enable_in_shard_timestamp = 202; + // [optional] If present, the aggregator will disable the feed in the current shard + // at the specified instant. + optional google.protobuf.Timestamp disable_in_shard_timestamp = 203; -// An item of the state describing a feed. -message Feed { - optional FeedMetadata metadata = 1; - // [optional] If present, the aggregator will activate the feed at the specified instant. - optional google.protobuf.Timestamp pending_activation = 2; - // [optional] If present, the aggregator will deactivate the feed at the specified instant. - optional google.protobuf.Timestamp pending_deactivation = 3; - // Additional state per publisher. - // If an eligible publisher is not listed here, the corresponding state should be considered empty. - repeated FeedPublisherState per_publisher = 4; // TODO: list of permissioned publisher IDs. } diff --git a/lazer/publisher_sdk/proto/transaction_envelope.proto b/lazer/publisher_sdk/proto/transaction_envelope.proto index 750f98606d..d6c5f1056f 100644 --- a/lazer/publisher_sdk/proto/transaction_envelope.proto +++ b/lazer/publisher_sdk/proto/transaction_envelope.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package pyth_lazer_transaction; +package pyth_lazer; import "google/protobuf/timestamp.proto"; import "pyth_lazer_transaction.proto"; From b1e808e28898c483ff231e5a54c4850ed08f7086 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Wed, 16 Jul 2025 10:59:47 +0100 Subject: [PATCH 2/8] feat(lazer): add DynamicValue parsed type --- Cargo.lock | 4 +- lazer/publisher_sdk/rust/Cargo.toml | 4 +- lazer/publisher_sdk/rust/src/dynamic_value.rs | 225 ++++++++++++++++++ lazer/publisher_sdk/rust/src/lib.rs | 163 ++----------- lazer/sdk/rust/protocol/Cargo.toml | 2 +- lazer/sdk/rust/protocol/src/router.rs | 14 +- 6 files changed, 266 insertions(+), 146 deletions(-) create mode 100644 lazer/publisher_sdk/rust/src/dynamic_value.rs diff --git a/Cargo.lock b/Cargo.lock index 92df432fe6..4688e8d985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5713,12 +5713,14 @@ name = "pyth-lazer-publisher-sdk" version = "0.1.7" dependencies = [ "anyhow", + "derive_more 2.0.1", "fs-err", "humantime", "protobuf", "protobuf-codegen", "pyth-lazer-protocol 0.9.0", - "serde-value", + "serde", + "serde_json", "tracing", ] diff --git a/lazer/publisher_sdk/rust/Cargo.toml b/lazer/publisher_sdk/rust/Cargo.toml index aaa919aae5..f8b7a29c77 100644 --- a/lazer/publisher_sdk/rust/Cargo.toml +++ b/lazer/publisher_sdk/rust/Cargo.toml @@ -10,9 +10,11 @@ repository = "https://github.com/pyth-network/pyth-crosschain" pyth-lazer-protocol = { version = "0.9.0", path = "../../sdk/rust/protocol" } anyhow = "1.0.98" protobuf = "3.7.2" -serde-value = "0.7.0" humantime = "2.2.0" tracing = "0.1.41" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +derive_more = { version = "2.0.1", features = ["from"] } [build-dependencies] fs-err = "3.1.0" diff --git a/lazer/publisher_sdk/rust/src/dynamic_value.rs b/lazer/publisher_sdk/rust/src/dynamic_value.rs new file mode 100644 index 0000000000..7a7e5acac9 --- /dev/null +++ b/lazer/publisher_sdk/rust/src/dynamic_value.rs @@ -0,0 +1,225 @@ +use std::collections::BTreeMap; + +use crate::protobuf::dynamic_value::{dynamic_value, DynamicValue as ProtobufDynamicValue}; +use ::protobuf::MessageField; +use anyhow::{ensure, Context}; +use derive_more::From; +use pyth_lazer_protocol::time::{DurationUs, TimestampUs}; +use serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, +}; + +#[derive(Debug, Clone, PartialEq, From)] +pub enum DynamicValue { + String(String), + F64(f64), + U64(u64), + I64(i64), + Bool(bool), + Timestamp(TimestampUs), + Duration(DurationUs), + Bytes(Vec), + List(Vec), + Map(BTreeMap), +} + +impl From for ProtobufDynamicValue { + fn from(value: DynamicValue) -> Self { + let converted = match value { + DynamicValue::Bool(value) => dynamic_value::Value::BoolValue(value), + DynamicValue::U64(value) => dynamic_value::Value::UintValue(value), + DynamicValue::I64(value) => dynamic_value::Value::IntValue(value), + DynamicValue::F64(value) => dynamic_value::Value::DoubleValue(value), + DynamicValue::String(value) => dynamic_value::Value::StringValue(value), + DynamicValue::Bytes(value) => dynamic_value::Value::BytesValue(value), + DynamicValue::Timestamp(value) => dynamic_value::Value::TimestampValue(value.into()), + DynamicValue::Duration(value) => dynamic_value::Value::DurationValue(value.into()), + DynamicValue::List(values) => dynamic_value::Value::List(values.into()), + DynamicValue::Map(values) => dynamic_value::Value::Map(values.into()), + }; + ProtobufDynamicValue { + value: Some(converted), + special_fields: Default::default(), + } + } +} + +impl From<&DynamicValue> for ProtobufDynamicValue { + fn from(value: &DynamicValue) -> Self { + let converted = match value { + DynamicValue::Bool(value) => dynamic_value::Value::BoolValue(*value), + DynamicValue::U64(value) => dynamic_value::Value::UintValue(*value), + DynamicValue::I64(value) => dynamic_value::Value::IntValue(*value), + DynamicValue::F64(value) => dynamic_value::Value::DoubleValue(*value), + DynamicValue::String(value) => dynamic_value::Value::StringValue(value.clone()), + DynamicValue::Bytes(value) => dynamic_value::Value::BytesValue(value.clone()), + DynamicValue::Timestamp(value) => dynamic_value::Value::TimestampValue((*value).into()), + DynamicValue::Duration(value) => dynamic_value::Value::DurationValue((*value).into()), + DynamicValue::List(values) => dynamic_value::Value::List(values.into()), + DynamicValue::Map(values) => dynamic_value::Value::Map(values.into()), + }; + ProtobufDynamicValue { + value: Some(converted), + special_fields: Default::default(), + } + } +} + +impl From> for dynamic_value::Map { + fn from(values: BTreeMap) -> Self { + let mut items = Vec::new(); + for (key, value) in values { + items.push(dynamic_value::MapItem { + key: Some(key), + value: MessageField::some(value.into()), + special_fields: Default::default(), + }) + } + dynamic_value::Map { + items, + special_fields: Default::default(), + } + } +} + +impl From<&BTreeMap> for dynamic_value::Map { + fn from(values: &BTreeMap) -> Self { + let mut items = Vec::new(); + for (key, value) in values { + items.push(dynamic_value::MapItem { + key: Some(key.clone()), + value: MessageField::some(value.into()), + special_fields: Default::default(), + }) + } + dynamic_value::Map { + items, + special_fields: Default::default(), + } + } +} + +impl From> for dynamic_value::List { + fn from(values: Vec) -> Self { + let mut items = Vec::new(); + for value in values { + items.push(value.into()); + } + dynamic_value::List { + items, + special_fields: Default::default(), + } + } +} + +impl From<&[DynamicValue]> for dynamic_value::List { + fn from(values: &[DynamicValue]) -> Self { + let mut items = Vec::new(); + for value in values { + items.push(value.into()); + } + dynamic_value::List { + items, + special_fields: Default::default(), + } + } +} + +impl From<&Vec> for dynamic_value::List { + fn from(value: &Vec) -> Self { + let value: &[DynamicValue] = value; + value.into() + } +} + +impl TryFrom for DynamicValue { + type Error = anyhow::Error; + + fn try_from(value: ProtobufDynamicValue) -> Result { + let value = value.value.context("missing DynamicValue.value")?; + match value { + dynamic_value::Value::StringValue(value) => Ok(DynamicValue::String(value)), + dynamic_value::Value::DoubleValue(value) => Ok(DynamicValue::F64(value)), + dynamic_value::Value::UintValue(value) => Ok(DynamicValue::U64(value)), + dynamic_value::Value::IntValue(value) => Ok(DynamicValue::I64(value)), + dynamic_value::Value::BoolValue(value) => Ok(DynamicValue::Bool(value)), + dynamic_value::Value::BytesValue(value) => Ok(DynamicValue::Bytes(value)), + dynamic_value::Value::DurationValue(value) => { + let v: DurationUs = value.try_into()?; + Ok(DynamicValue::Duration(v)) + } + dynamic_value::Value::TimestampValue(ts) => { + let ts = TimestampUs::try_from(&ts)?; + Ok(DynamicValue::Timestamp(ts)) + } + dynamic_value::Value::List(list) => Ok(DynamicValue::List(list.try_into()?)), + dynamic_value::Value::Map(map) => Ok(DynamicValue::Map(map.try_into()?)), + } + } +} + +impl TryFrom for BTreeMap { + type Error = anyhow::Error; + + fn try_from(value: dynamic_value::Map) -> Result { + let mut output = BTreeMap::new(); + for item in value.items { + let key = item.key.context("missing DynamicValue.MapItem.key")?; + let value = item + .value + .into_option() + .context("missing DynamicValue.MapItem.value")? + .try_into()?; + let old = output.insert(key, value); + ensure!(old.is_none(), "duplicate DynamicValue.MapItem.key"); + } + Ok(output) + } +} + +impl TryFrom for Vec { + type Error = anyhow::Error; + + fn try_from(value: dynamic_value::List) -> Result { + let mut output = Vec::new(); + for item in value.items { + output.push(item.try_into()?); + } + Ok(output) + } +} + +impl Serialize for DynamicValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + DynamicValue::String(v) => serializer.serialize_str(v), + DynamicValue::F64(v) => serializer.serialize_f64(*v), + DynamicValue::U64(v) => serializer.serialize_u64(*v), + DynamicValue::I64(v) => serializer.serialize_i64(*v), + DynamicValue::Bool(v) => serializer.serialize_bool(*v), + DynamicValue::Timestamp(v) => serializer.serialize_u64(v.as_micros()), + DynamicValue::Duration(v) => { + serializer.serialize_str(&humantime::format_duration((*v).into()).to_string()) + } + DynamicValue::Bytes(v) => serializer.serialize_bytes(v), + DynamicValue::List(v) => { + let mut seq_serializer = serializer.serialize_seq(Some(v.len()))?; + for element in v { + seq_serializer.serialize_element(element)?; + } + seq_serializer.end() + } + DynamicValue::Map(map) => { + let mut map_serializer = serializer.serialize_map(Some(map.len()))?; + for (k, v) in map { + map_serializer.serialize_entry(k, v)?; + } + map_serializer.end() + } + } + } +} diff --git a/lazer/publisher_sdk/rust/src/lib.rs b/lazer/publisher_sdk/rust/src/lib.rs index a88260c643..f17fd208e4 100644 --- a/lazer/publisher_sdk/rust/src/lib.rs +++ b/lazer/publisher_sdk/rust/src/lib.rs @@ -1,13 +1,8 @@ -use std::{collections::BTreeMap, time::Duration}; - use crate::publisher_update::feed_update::Update; use crate::publisher_update::{FeedUpdate, FundingRateUpdate, PriceUpdate}; -use ::protobuf::MessageField; -use anyhow::{bail, ensure, Context}; -use humantime::format_duration; -use protobuf::dynamic_value::{dynamic_value, DynamicValue}; +use crate::state::FeedState; use pyth_lazer_protocol::jrpc::{FeedUpdateParams, UpdateParams}; -use pyth_lazer_protocol::time::TimestampUs; +use pyth_lazer_protocol::symbol_state::SymbolState; pub mod transaction_envelope { pub use crate::protobuf::transaction_envelope::*; @@ -34,139 +29,9 @@ mod protobuf { include!(concat!(env!("OUT_DIR"), "/protobuf/mod.rs")); } -impl DynamicValue { - pub fn try_option_from_serde(value: serde_value::Value) -> anyhow::Result> { - match value { - serde_value::Value::Option(value) => { - if let Some(value) = value { - Ok(Some((*value).try_into()?)) - } else { - Ok(None) - } - } - value => Ok(Some(value.try_into()?)), - } - } - - pub fn to_timestamp(&self) -> anyhow::Result { - let value = self.value.as_ref().context("missing DynamicValue.value")?; - match value { - dynamic_value::Value::TimestampValue(ts) => Ok(ts.try_into()?), - _ => bail!("expected timestamp, got {:?}", self), - } - } - - pub fn to_duration(&self) -> anyhow::Result { - let value = self.value.as_ref().context("missing DynamicValue.value")?; - match value { - dynamic_value::Value::DurationValue(duration) => Ok(duration.clone().into()), - _ => bail!("expected duration, got {:?}", self), - } - } -} - -impl TryFrom for DynamicValue { - type Error = anyhow::Error; - - fn try_from(value: serde_value::Value) -> Result { - let converted = match value { - serde_value::Value::Bool(value) => dynamic_value::Value::BoolValue(value), - serde_value::Value::U8(value) => dynamic_value::Value::UintValue(value.into()), - serde_value::Value::U16(value) => dynamic_value::Value::UintValue(value.into()), - serde_value::Value::U32(value) => dynamic_value::Value::UintValue(value.into()), - serde_value::Value::U64(value) => dynamic_value::Value::UintValue(value), - serde_value::Value::I8(value) => dynamic_value::Value::IntValue(value.into()), - serde_value::Value::I16(value) => dynamic_value::Value::IntValue(value.into()), - serde_value::Value::I32(value) => dynamic_value::Value::IntValue(value.into()), - serde_value::Value::I64(value) => dynamic_value::Value::IntValue(value), - serde_value::Value::F32(value) => dynamic_value::Value::DoubleValue(value.into()), - serde_value::Value::F64(value) => dynamic_value::Value::DoubleValue(value), - serde_value::Value::Char(value) => dynamic_value::Value::StringValue(value.to_string()), - serde_value::Value::String(value) => dynamic_value::Value::StringValue(value), - serde_value::Value::Bytes(value) => dynamic_value::Value::BytesValue(value), - serde_value::Value::Seq(values) => { - let mut items = Vec::new(); - for value in values { - items.push(value.try_into()?); - } - dynamic_value::Value::List(dynamic_value::List { - items, - special_fields: Default::default(), - }) - } - serde_value::Value::Map(values) => { - let mut items = Vec::new(); - for (key, value) in values { - let key = match key { - serde_value::Value::String(key) => key, - _ => bail!("unsupported key type: expected string, got {:?}", key), - }; - items.push(dynamic_value::MapItem { - key: Some(key), - value: MessageField::some(value.try_into()?), - special_fields: Default::default(), - }) - } - dynamic_value::Value::Map(dynamic_value::Map { - items, - special_fields: Default::default(), - }) - } - serde_value::Value::Unit - | serde_value::Value::Option(_) - | serde_value::Value::Newtype(_) => bail!("unsupported type: {:?}", value), - }; - Ok(DynamicValue { - value: Some(converted), - special_fields: Default::default(), - }) - } -} - -impl TryFrom for serde_value::Value { - type Error = anyhow::Error; +mod dynamic_value; - fn try_from(value: DynamicValue) -> Result { - let value = value.value.context("missing DynamicValue.value")?; - match value { - dynamic_value::Value::StringValue(value) => Ok(serde_value::Value::String(value)), - dynamic_value::Value::DoubleValue(value) => Ok(serde_value::Value::F64(value)), - dynamic_value::Value::UintValue(value) => Ok(serde_value::Value::U64(value)), - dynamic_value::Value::IntValue(value) => Ok(serde_value::Value::I64(value)), - dynamic_value::Value::BoolValue(value) => Ok(serde_value::Value::Bool(value)), - dynamic_value::Value::BytesValue(value) => Ok(serde_value::Value::Bytes(value)), - dynamic_value::Value::DurationValue(duration) => { - let s: Duration = duration.into(); - Ok(serde_value::Value::String(format_duration(s).to_string())) - } - dynamic_value::Value::TimestampValue(ts) => { - let ts = TimestampUs::try_from(&ts)?; - Ok(serde_value::Value::U64(ts.as_micros())) - } - dynamic_value::Value::List(list) => { - let mut output = Vec::new(); - for item in list.items { - output.push(item.try_into()?); - } - Ok(serde_value::Value::Seq(output)) - } - dynamic_value::Value::Map(map) => { - let mut output = BTreeMap::new(); - for item in map.items { - let key = item.key.context("missing DynamicValue.MapItem.key")?; - let value = item - .value - .into_option() - .context("missing DynamicValue.MapItem.value")? - .try_into()?; - let old = output.insert(serde_value::Value::String(key), value); - ensure!(old.is_none(), "duplicate DynamicValue.MapItem.key"); - } - Ok(serde_value::Value::Map(output)) - } - } - } -} +pub use crate::dynamic_value::DynamicValue; impl From for FeedUpdate { fn from(value: FeedUpdateParams) -> Self { @@ -202,3 +67,23 @@ impl From for Update { } } } + +impl From for SymbolState { + fn from(value: FeedState) -> Self { + match value { + FeedState::COMING_SOON => SymbolState::ComingSoon, + FeedState::STABLE => SymbolState::Stable, + FeedState::INACTIVE => SymbolState::Inactive, + } + } +} + +impl From for FeedState { + fn from(value: SymbolState) -> Self { + match value { + SymbolState::ComingSoon => FeedState::COMING_SOON, + SymbolState::Stable => FeedState::STABLE, + SymbolState::Inactive => FeedState::INACTIVE, + } + } +} diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index 283e5ea170..ea8761ae71 100644 --- a/lazer/sdk/rust/protocol/Cargo.toml +++ b/lazer/sdk/rust/protocol/Cargo.toml @@ -11,7 +11,7 @@ byteorder = "1.5.0" anyhow = "1.0.89" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0" -derive_more = { version = "1.0.0", features = ["from"] } +derive_more = { version = "1.0.0", features = ["from", "into"] } itertools = "0.13.0" rust_decimal = "1.36.0" protobuf = "3.7.2" diff --git a/lazer/sdk/rust/protocol/src/router.rs b/lazer/sdk/rust/protocol/src/router.rs index 3150260bf6..dd0decb0a7 100644 --- a/lazer/sdk/rust/protocol/src/router.rs +++ b/lazer/sdk/rust/protocol/src/router.rs @@ -6,7 +6,7 @@ use { time::{DurationUs, TimestampUs}, }, anyhow::{bail, Context}, - derive_more::derive::From, + derive_more::derive::{From, Into}, itertools::Itertools, protobuf::well_known_types::duration::Duration as ProtobufDuration, rust_decimal::{prelude::FromPrimitive, Decimal}, @@ -18,13 +18,19 @@ use { }, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into, +)] pub struct PublisherId(pub u16); -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into, +)] pub struct PriceFeedId(pub u32); -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into, +)] pub struct ChannelId(pub u8); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] From 1cb5d89b91ed958fe2c8912057946f170b5184eb Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Wed, 16 Jul 2025 11:47:23 +0100 Subject: [PATCH 3/8] test(lazer): add DynamicValue tests --- Cargo.lock | 1 + lazer/publisher_sdk/rust/Cargo.toml | 1 + lazer/publisher_sdk/rust/src/dynamic_value.rs | 5 +- .../rust/src/dynamic_value/tests.rs | 147 ++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 lazer/publisher_sdk/rust/src/dynamic_value/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 4688e8d985..75bc120487 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5715,6 +5715,7 @@ dependencies = [ "anyhow", "derive_more 2.0.1", "fs-err", + "hex", "humantime", "protobuf", "protobuf-codegen", diff --git a/lazer/publisher_sdk/rust/Cargo.toml b/lazer/publisher_sdk/rust/Cargo.toml index f8b7a29c77..3e88658b1a 100644 --- a/lazer/publisher_sdk/rust/Cargo.toml +++ b/lazer/publisher_sdk/rust/Cargo.toml @@ -15,6 +15,7 @@ tracing = "0.1.41" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" derive_more = { version = "2.0.1", features = ["from"] } +hex = "0.4.3" [build-dependencies] fs-err = "3.1.0" diff --git a/lazer/publisher_sdk/rust/src/dynamic_value.rs b/lazer/publisher_sdk/rust/src/dynamic_value.rs index 7a7e5acac9..027e010efc 100644 --- a/lazer/publisher_sdk/rust/src/dynamic_value.rs +++ b/lazer/publisher_sdk/rust/src/dynamic_value.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +mod tests; + use std::collections::BTreeMap; use crate::protobuf::dynamic_value::{dynamic_value, DynamicValue as ProtobufDynamicValue}; @@ -205,7 +208,7 @@ impl Serialize for DynamicValue { DynamicValue::Duration(v) => { serializer.serialize_str(&humantime::format_duration((*v).into()).to_string()) } - DynamicValue::Bytes(v) => serializer.serialize_bytes(v), + DynamicValue::Bytes(v) => serializer.serialize_str(&hex::encode(v)), DynamicValue::List(v) => { let mut seq_serializer = serializer.serialize_seq(Some(v.len()))?; for element in v { diff --git a/lazer/publisher_sdk/rust/src/dynamic_value/tests.rs b/lazer/publisher_sdk/rust/src/dynamic_value/tests.rs new file mode 100644 index 0000000000..76751c9e06 --- /dev/null +++ b/lazer/publisher_sdk/rust/src/dynamic_value/tests.rs @@ -0,0 +1,147 @@ +use std::collections::BTreeMap; + +use protobuf::{ + well_known_types::{duration::Duration, timestamp::Timestamp}, + MessageField, +}; + +use crate::{ + protobuf::dynamic_value::{ + dynamic_value::{List, Map, MapItem, Value}, + DynamicValue as ProtobufDynamicValue, + }, + DynamicValue, +}; + +#[test] +fn dynamic_value_serializes() { + let mut map = BTreeMap::new(); + map.insert( + "int1".to_owned(), + ProtobufDynamicValue { + value: Some(Value::IntValue(42)), + special_fields: Default::default(), + }, + ); + + map.insert( + "bool2".to_owned(), + ProtobufDynamicValue { + value: Some(Value::BoolValue(true)), + special_fields: Default::default(), + }, + ); + + map.insert( + "str3".to_owned(), + ProtobufDynamicValue { + value: Some(Value::StringValue("abc".into())), + special_fields: Default::default(), + }, + ); + + map.insert( + "double4".to_owned(), + ProtobufDynamicValue { + value: Some(Value::DoubleValue(42.0)), + special_fields: Default::default(), + }, + ); + + map.insert( + "uint5".to_owned(), + ProtobufDynamicValue { + value: Some(Value::UintValue(42)), + special_fields: Default::default(), + }, + ); + + map.insert( + "bytes6".to_owned(), + ProtobufDynamicValue { + value: Some(Value::BytesValue(b"\xAB\xCD\xEF".into())), + special_fields: Default::default(), + }, + ); + + map.insert( + "duration7".to_owned(), + ProtobufDynamicValue { + value: Some(Value::DurationValue(Duration { + seconds: 12, + nanos: 345678000, + special_fields: Default::default(), + })), + special_fields: Default::default(), + }, + ); + + map.insert( + "timestamp8".to_owned(), + ProtobufDynamicValue { + value: Some(Value::TimestampValue(Timestamp { + seconds: 12, + nanos: 345678000, + special_fields: Default::default(), + })), + special_fields: Default::default(), + }, + ); + + map.insert( + "list9".to_owned(), + ProtobufDynamicValue { + value: Some(Value::List(List { + items: vec![ + ProtobufDynamicValue { + value: Some(Value::StringValue("item1".into())), + special_fields: Default::default(), + }, + ProtobufDynamicValue { + value: Some(Value::StringValue("item2".into())), + special_fields: Default::default(), + }, + ], + special_fields: Default::default(), + })), + special_fields: Default::default(), + }, + ); + let map = Map { + items: map + .into_iter() + .map(|(k, v)| MapItem { + key: Some(k), + value: MessageField::some(v), + special_fields: Default::default(), + }) + .collect(), + special_fields: Default::default(), + }; + + let converted: BTreeMap = map.clone().try_into().unwrap(); + + let json = serde_json::to_string_pretty(&converted).unwrap(); + println!("{json}"); + assert_eq!( + json, + r#"{ + "bool2": true, + "bytes6": "abcdef", + "double4": 42.0, + "duration7": "12s 345ms 678us", + "int1": 42, + "list9": [ + "item1", + "item2" + ], + "str3": "abc", + "timestamp8": 12345678, + "uint5": 42 +}"# + ); + + // Check roundtrip + let reversed: Map = converted.into(); + assert_eq!(map, reversed); +} From 0129da156370e08910be775cff24b90c0e76786e Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Thu, 17 Jul 2025 10:50:13 +0100 Subject: [PATCH 4/8] feat(lazer): add FeedKind to protocol --- lazer/publisher_sdk/proto/state.proto | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lazer/publisher_sdk/proto/state.proto b/lazer/publisher_sdk/proto/state.proto index 82721582cd..a7fecfe540 100644 --- a/lazer/publisher_sdk/proto/state.proto +++ b/lazer/publisher_sdk/proto/state.proto @@ -55,6 +55,16 @@ enum FeedState { INACTIVE = 2; } +// Feed kind determines the set of data fields available in the feed. +// It also determines the kind of data accepted from publishers for this feed +// (`PriceUpdate` or `FundingRateUpdate`). +enum FeedKind { + // Fields: price, best_bid_price, best_ask_price + PRICE = 0; + // Fields: price, rate. + FUNDING_RATE = 1; +} + // An item of the state describing a feed. message Feed { // [required] ID of the feed. @@ -82,8 +92,10 @@ message Feed { optional google.protobuf.Duration expiry_time = 105; // [required] Market schedule in Pythnet format. optional string market_schedule = 106; - // [required] Feed state + // [required] Feed state. optional FeedState state = 107; + // [required] Feed kind. + optional FeedKind kind = 108; // [required] Feed status in the current shard. Disabled feeds will not be visible in @@ -92,7 +104,7 @@ message Feed { // // If a feed is present in // multiple shards, it must only be active in one of them at each time. - // To enforce this, `pending_activation` and `pending_deactivation` fields + // To enforce this, `enable_in_shard_timestamp` and `disable_in_shard_timestamp` fields // can be used to deactivate a feed in one shard and activate it in another shard // at the same instant. optional bool is_enabled_in_shard = 201; From f8c719b2e0bc037fde8387bfa223e6d014387075 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Thu, 17 Jul 2025 10:59:27 +0100 Subject: [PATCH 5/8] feat(lazer): add feed kind to protocol --- lazer/publisher_sdk/rust/src/lib.rs | 19 +++++++++++++++++++ lazer/sdk/rust/protocol/src/feed_kind.rs | 20 ++++++++++++++++++++ lazer/sdk/rust/protocol/src/lib.rs | 3 +++ 3 files changed, 42 insertions(+) create mode 100644 lazer/sdk/rust/protocol/src/feed_kind.rs diff --git a/lazer/publisher_sdk/rust/src/lib.rs b/lazer/publisher_sdk/rust/src/lib.rs index f17fd208e4..4cc1435ae7 100644 --- a/lazer/publisher_sdk/rust/src/lib.rs +++ b/lazer/publisher_sdk/rust/src/lib.rs @@ -3,6 +3,7 @@ use crate::publisher_update::{FeedUpdate, FundingRateUpdate, PriceUpdate}; use crate::state::FeedState; use pyth_lazer_protocol::jrpc::{FeedUpdateParams, UpdateParams}; use pyth_lazer_protocol::symbol_state::SymbolState; +use pyth_lazer_protocol::FeedKind; pub mod transaction_envelope { pub use crate::protobuf::transaction_envelope::*; @@ -87,3 +88,21 @@ impl From for FeedState { } } } + +impl From for protobuf::state::FeedKind { + fn from(value: FeedKind) -> Self { + match value { + FeedKind::Price => protobuf::state::FeedKind::PRICE, + FeedKind::FundingRate => protobuf::state::FeedKind::FUNDING_RATE, + } + } +} + +impl From for FeedKind { + fn from(value: protobuf::state::FeedKind) -> Self { + match value { + protobuf::state::FeedKind::PRICE => FeedKind::Price, + protobuf::state::FeedKind::FUNDING_RATE => FeedKind::FundingRate, + } + } +} diff --git a/lazer/sdk/rust/protocol/src/feed_kind.rs b/lazer/sdk/rust/protocol/src/feed_kind.rs new file mode 100644 index 0000000000..7e8a85eb92 --- /dev/null +++ b/lazer/sdk/rust/protocol/src/feed_kind.rs @@ -0,0 +1,20 @@ +use { + serde::{Deserialize, Serialize}, + std::fmt::Display, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum FeedKind { + Price, + FundingRate, +} + +impl Display for FeedKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FeedKind::Price => write!(f, "price"), + FeedKind::FundingRate => write!(f, "fundingRate"), + } + } +} diff --git a/lazer/sdk/rust/protocol/src/lib.rs b/lazer/sdk/rust/protocol/src/lib.rs index cec19e11de..44d904afbc 100644 --- a/lazer/sdk/rust/protocol/src/lib.rs +++ b/lazer/sdk/rust/protocol/src/lib.rs @@ -2,6 +2,7 @@ pub mod api; pub mod binary_update; +mod feed_kind; pub mod jrpc; pub mod message; pub mod payload; @@ -13,6 +14,8 @@ pub mod subscription; pub mod symbol_state; pub mod time; +pub use crate::feed_kind::FeedKind; + #[test] fn magics_in_big_endian() { use crate::{ From 10547594fced13e3c5e733c5ebd39c31084cf1b4 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Thu, 17 Jul 2025 13:37:43 +0100 Subject: [PATCH 6/8] fix(lazer): expose dynamic_value protobuf as rust module, move type to protocol --- Cargo.lock | 1 + ...amic_value.rs => convert_dynamic_value.rs} | 56 +------------------ .../tests.rs | 10 ++-- lazer/publisher_sdk/rust/src/lib.rs | 8 ++- lazer/sdk/rust/protocol/Cargo.toml | 2 + lazer/sdk/rust/protocol/src/dynamic_value.rs | 56 +++++++++++++++++++ lazer/sdk/rust/protocol/src/lib.rs | 3 +- 7 files changed, 73 insertions(+), 63 deletions(-) rename lazer/publisher_sdk/rust/src/{dynamic_value.rs => convert_dynamic_value.rs} (78%) rename lazer/publisher_sdk/rust/src/{dynamic_value => convert_dynamic_value}/tests.rs (95%) create mode 100644 lazer/sdk/rust/protocol/src/dynamic_value.rs diff --git a/Cargo.lock b/Cargo.lock index 75bc120487..31f3fadb99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5698,6 +5698,7 @@ dependencies = [ "derive_more 1.0.0", "ed25519-dalek 2.1.1", "hex", + "humantime", "humantime-serde", "itertools 0.13.0", "libsecp256k1 0.7.2", diff --git a/lazer/publisher_sdk/rust/src/dynamic_value.rs b/lazer/publisher_sdk/rust/src/convert_dynamic_value.rs similarity index 78% rename from lazer/publisher_sdk/rust/src/dynamic_value.rs rename to lazer/publisher_sdk/rust/src/convert_dynamic_value.rs index 027e010efc..2936f8d6af 100644 --- a/lazer/publisher_sdk/rust/src/dynamic_value.rs +++ b/lazer/publisher_sdk/rust/src/convert_dynamic_value.rs @@ -6,27 +6,11 @@ use std::collections::BTreeMap; use crate::protobuf::dynamic_value::{dynamic_value, DynamicValue as ProtobufDynamicValue}; use ::protobuf::MessageField; use anyhow::{ensure, Context}; -use derive_more::From; -use pyth_lazer_protocol::time::{DurationUs, TimestampUs}; -use serde::{ - ser::{SerializeMap, SerializeSeq}, - Serialize, +use pyth_lazer_protocol::{ + time::{DurationUs, TimestampUs}, + DynamicValue, }; -#[derive(Debug, Clone, PartialEq, From)] -pub enum DynamicValue { - String(String), - F64(f64), - U64(u64), - I64(i64), - Bool(bool), - Timestamp(TimestampUs), - Duration(DurationUs), - Bytes(Vec), - List(Vec), - Map(BTreeMap), -} - impl From for ProtobufDynamicValue { fn from(value: DynamicValue) -> Self { let converted = match value { @@ -192,37 +176,3 @@ impl TryFrom for Vec { Ok(output) } } - -impl Serialize for DynamicValue { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - DynamicValue::String(v) => serializer.serialize_str(v), - DynamicValue::F64(v) => serializer.serialize_f64(*v), - DynamicValue::U64(v) => serializer.serialize_u64(*v), - DynamicValue::I64(v) => serializer.serialize_i64(*v), - DynamicValue::Bool(v) => serializer.serialize_bool(*v), - DynamicValue::Timestamp(v) => serializer.serialize_u64(v.as_micros()), - DynamicValue::Duration(v) => { - serializer.serialize_str(&humantime::format_duration((*v).into()).to_string()) - } - DynamicValue::Bytes(v) => serializer.serialize_str(&hex::encode(v)), - DynamicValue::List(v) => { - let mut seq_serializer = serializer.serialize_seq(Some(v.len()))?; - for element in v { - seq_serializer.serialize_element(element)?; - } - seq_serializer.end() - } - DynamicValue::Map(map) => { - let mut map_serializer = serializer.serialize_map(Some(map.len()))?; - for (k, v) in map { - map_serializer.serialize_entry(k, v)?; - } - map_serializer.end() - } - } - } -} diff --git a/lazer/publisher_sdk/rust/src/dynamic_value/tests.rs b/lazer/publisher_sdk/rust/src/convert_dynamic_value/tests.rs similarity index 95% rename from lazer/publisher_sdk/rust/src/dynamic_value/tests.rs rename to lazer/publisher_sdk/rust/src/convert_dynamic_value/tests.rs index 76751c9e06..7928b002e4 100644 --- a/lazer/publisher_sdk/rust/src/dynamic_value/tests.rs +++ b/lazer/publisher_sdk/rust/src/convert_dynamic_value/tests.rs @@ -4,13 +4,11 @@ use protobuf::{ well_known_types::{duration::Duration, timestamp::Timestamp}, MessageField, }; +use pyth_lazer_protocol::DynamicValue; -use crate::{ - protobuf::dynamic_value::{ - dynamic_value::{List, Map, MapItem, Value}, - DynamicValue as ProtobufDynamicValue, - }, - DynamicValue, +use crate::protobuf::dynamic_value::{ + dynamic_value::{List, Map, MapItem, Value}, + DynamicValue as ProtobufDynamicValue, }; #[test] diff --git a/lazer/publisher_sdk/rust/src/lib.rs b/lazer/publisher_sdk/rust/src/lib.rs index 4cc1435ae7..81ac139ed0 100644 --- a/lazer/publisher_sdk/rust/src/lib.rs +++ b/lazer/publisher_sdk/rust/src/lib.rs @@ -25,14 +25,16 @@ pub mod state { pub use crate::protobuf::state::*; } +pub mod dynamic_value { + pub use crate::protobuf::dynamic_value::*; +} + #[allow(rustdoc::broken_intra_doc_links)] mod protobuf { include!(concat!(env!("OUT_DIR"), "/protobuf/mod.rs")); } -mod dynamic_value; - -pub use crate::dynamic_value::DynamicValue; +mod convert_dynamic_value; impl From for FeedUpdate { fn from(value: FeedUpdateParams) -> Self { diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index ea8761ae71..cd1fef3aae 100644 --- a/lazer/sdk/rust/protocol/Cargo.toml +++ b/lazer/sdk/rust/protocol/Cargo.toml @@ -18,6 +18,8 @@ protobuf = "3.7.2" humantime-serde = "1.1.1" mry = { version = "0.13.0", features = ["serde"], optional = true } chrono = "0.4.41" +humantime = "2.2.0" +hex = "0.4.3" [dev-dependencies] bincode = "1.3.3" diff --git a/lazer/sdk/rust/protocol/src/dynamic_value.rs b/lazer/sdk/rust/protocol/src/dynamic_value.rs new file mode 100644 index 0000000000..ab954311f1 --- /dev/null +++ b/lazer/sdk/rust/protocol/src/dynamic_value.rs @@ -0,0 +1,56 @@ +use std::collections::BTreeMap; + +use crate::time::{DurationUs, TimestampUs}; +use derive_more::From; +use serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, +}; + +#[derive(Debug, Clone, PartialEq, From)] +pub enum DynamicValue { + String(String), + F64(f64), + U64(u64), + I64(i64), + Bool(bool), + Timestamp(TimestampUs), + Duration(DurationUs), + Bytes(Vec), + List(Vec), + Map(BTreeMap), +} + +impl Serialize for DynamicValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + DynamicValue::String(v) => serializer.serialize_str(v), + DynamicValue::F64(v) => serializer.serialize_f64(*v), + DynamicValue::U64(v) => serializer.serialize_u64(*v), + DynamicValue::I64(v) => serializer.serialize_i64(*v), + DynamicValue::Bool(v) => serializer.serialize_bool(*v), + DynamicValue::Timestamp(v) => serializer.serialize_u64(v.as_micros()), + DynamicValue::Duration(v) => { + serializer.serialize_str(&humantime::format_duration((*v).into()).to_string()) + } + DynamicValue::Bytes(v) => serializer.serialize_str(&hex::encode(v)), + DynamicValue::List(v) => { + let mut seq_serializer = serializer.serialize_seq(Some(v.len()))?; + for element in v { + seq_serializer.serialize_element(element)?; + } + seq_serializer.end() + } + DynamicValue::Map(map) => { + let mut map_serializer = serializer.serialize_map(Some(map.len()))?; + for (k, v) in map { + map_serializer.serialize_entry(k, v)?; + } + map_serializer.end() + } + } + } +} diff --git a/lazer/sdk/rust/protocol/src/lib.rs b/lazer/sdk/rust/protocol/src/lib.rs index 44d904afbc..21e94b4a4b 100644 --- a/lazer/sdk/rust/protocol/src/lib.rs +++ b/lazer/sdk/rust/protocol/src/lib.rs @@ -2,6 +2,7 @@ pub mod api; pub mod binary_update; +mod dynamic_value; mod feed_kind; pub mod jrpc; pub mod message; @@ -14,7 +15,7 @@ pub mod subscription; pub mod symbol_state; pub mod time; -pub use crate::feed_kind::FeedKind; +pub use crate::{dynamic_value::DynamicValue, feed_kind::FeedKind}; #[test] fn magics_in_big_endian() { From b1512a812fde8d111657f4b326c86a9363a1ef85 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Mon, 21 Jul 2025 11:03:07 +0100 Subject: [PATCH 7/8] fix: add FeedKind to AddFeed --- lazer/publisher_sdk/proto/governance_instruction.proto | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lazer/publisher_sdk/proto/governance_instruction.proto b/lazer/publisher_sdk/proto/governance_instruction.proto index ef94e2351d..eada3b465f 100644 --- a/lazer/publisher_sdk/proto/governance_instruction.proto +++ b/lazer/publisher_sdk/proto/governance_instruction.proto @@ -325,6 +325,8 @@ message AddFeed { // [required] optional FeedState state = 107; // [required] + optional FeedKind kind = 108; + // [required] optional bool is_enabled_in_shard = 201; // TODO: IDs of publishers enabled for this feed. From 06a3661f01ee981095e45e8e35f49882405e6fb0 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Mon, 21 Jul 2025 11:41:23 +0100 Subject: [PATCH 8/8] doc(lazer): improve proto comments --- lazer/publisher_sdk/proto/state.proto | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lazer/publisher_sdk/proto/state.proto b/lazer/publisher_sdk/proto/state.proto index a7fecfe540..b13569e211 100644 --- a/lazer/publisher_sdk/proto/state.proto +++ b/lazer/publisher_sdk/proto/state.proto @@ -48,10 +48,12 @@ message Publisher { enum FeedState { // Default value. Feeds in this state are not available to consumers. + // `COMING_SOON` feeds are expected to become stable in the future. COMING_SOON = 0; // A fully available feed. STABLE = 1; - // Inactive feeds are not available to consumers or publishers. + // Inactive feeds are not available to consumers. + // `INACTIVE` feeds are not expected to become stable again. INACTIVE = 2; }