diff --git a/lazer/Cargo.lock b/lazer/Cargo.lock index 203248d549..977b27f93d 100644 --- a/lazer/Cargo.lock +++ b/lazer/Cargo.lock @@ -3772,7 +3772,7 @@ dependencies = [ "futures-util", "hex", "libsecp256k1 0.7.1", - "pyth-lazer-protocol 0.6.0", + "pyth-lazer-protocol 0.6.1", "serde", "serde_json", "tokio", @@ -3799,7 +3799,7 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.6.0" +version = "0.6.1" dependencies = [ "alloy-primitives", "anyhow", diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index 3f69199087..912fbda9b7 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.6.0" +version = "0.6.1" edition = "2021" description = "Pyth Lazer SDK - protocol types." license = "Apache-2.0" diff --git a/lazer/sdk/rust/protocol/src/payload.rs b/lazer/sdk/rust/protocol/src/payload.rs index 7adc528e66..bd5a750c0d 100644 --- a/lazer/sdk/rust/protocol/src/payload.rs +++ b/lazer/sdk/rust/protocol/src/payload.rs @@ -2,7 +2,7 @@ use { super::router::{PriceFeedId, PriceFeedProperty, TimestampUs}, - crate::router::{ChannelId, Price}, + crate::router::{ChannelId, Price, Rate}, anyhow::bail, byteorder::{ByteOrder, ReadBytesExt, WriteBytesExt, BE, LE}, serde::{Deserialize, Serialize}, @@ -33,9 +33,11 @@ pub enum PayloadPropertyValue { Price(Option), BestBidPrice(Option), BestAskPrice(Option), - PublisherCount(Option), + PublisherCount(u16), Exponent(i16), Confidence(Option), + FundingRate(Option), + FundingTimestamp(Option), } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -43,8 +45,10 @@ pub struct AggregatedPriceFeedData { pub price: Option, pub best_bid_price: Option, pub best_ask_price: Option, - pub publisher_count: Option, + pub publisher_count: u16, pub confidence: Option, + pub funding_rate: Option, + pub funding_timestamp: Option, } /// First bytes of a payload's encoding @@ -84,6 +88,12 @@ impl PayloadData { PriceFeedProperty::Confidence => { PayloadPropertyValue::Confidence(feed.confidence) } + PriceFeedProperty::FundingRate => { + PayloadPropertyValue::FundingRate(feed.funding_rate) + } + PriceFeedProperty::FundingTimestamp => { + PayloadPropertyValue::FundingTimestamp(feed.funding_timestamp) + } }) .collect(), }) @@ -115,7 +125,7 @@ impl PayloadData { } PayloadPropertyValue::PublisherCount(count) => { writer.write_u8(PriceFeedProperty::PublisherCount as u8)?; - write_option_u16::(&mut writer, *count)?; + writer.write_u16::(*count)?; } PayloadPropertyValue::Exponent(exponent) => { writer.write_u8(PriceFeedProperty::Exponent as u8)?; @@ -125,6 +135,14 @@ impl PayloadData { writer.write_u8(PriceFeedProperty::Confidence as u8)?; write_option_price::(&mut writer, *confidence)?; } + PayloadPropertyValue::FundingRate(rate) => { + writer.write_u8(PriceFeedProperty::FundingRate as u8)?; + write_option_rate::(&mut writer, *rate)?; + } + PayloadPropertyValue::FundingTimestamp(timestamp) => { + writer.write_u8(PriceFeedProperty::FundingTimestamp as u8)?; + write_option_timestamp::(&mut writer, *timestamp)?; + } } } } @@ -164,11 +182,17 @@ impl PayloadData { } else if property == PriceFeedProperty::BestAskPrice as u8 { PayloadPropertyValue::BestAskPrice(read_option_price::(&mut reader)?) } else if property == PriceFeedProperty::PublisherCount as u8 { - PayloadPropertyValue::PublisherCount(read_option_u16::(&mut reader)?) + PayloadPropertyValue::PublisherCount(reader.read_u16::()?) } else if property == PriceFeedProperty::Exponent as u8 { PayloadPropertyValue::Exponent(reader.read_i16::()?) } else if property == PriceFeedProperty::Confidence as u8 { PayloadPropertyValue::Confidence(read_option_price::(&mut reader)?) + } else if property == PriceFeedProperty::FundingRate as u8 { + PayloadPropertyValue::FundingRate(read_option_rate::(&mut reader)?) + } else if property == PriceFeedProperty::FundingTimestamp as u8 { + PayloadPropertyValue::FundingTimestamp(read_option_timestamp::( + &mut reader, + )?) } else { bail!("unknown property"); }; @@ -196,14 +220,54 @@ fn read_option_price(mut reader: impl Read) -> std::io::Result( +fn write_option_rate( + mut writer: impl Write, + value: Option, +) -> std::io::Result<()> { + match value { + Some(value) => { + writer.write_u8(1)?; + writer.write_i64::(value.0) + } + None => { + writer.write_u8(0)?; + Ok(()) + } + } +} + +fn read_option_rate(mut reader: impl Read) -> std::io::Result> { + let present = reader.read_u8()? != 0; + if present { + Ok(Some(Rate(reader.read_i64::()?))) + } else { + Ok(None) + } +} + +fn write_option_timestamp( mut writer: impl Write, - value: Option, + value: Option, ) -> std::io::Result<()> { - writer.write_u16::(value.unwrap_or(0)) + match value { + Some(value) => { + writer.write_u8(1)?; + writer.write_u64::(value.0) + } + None => { + writer.write_u8(0)?; + Ok(()) + } + } } -fn read_option_u16(mut reader: impl Read) -> std::io::Result> { - let value = reader.read_u16::()?; - Ok(Some(value)) +fn read_option_timestamp( + mut reader: impl Read, +) -> std::io::Result> { + let present = reader.read_u8()? != 0; + if present { + Ok(Some(TimestampUs(reader.read_u64::()?))) + } else { + Ok(None) + } } diff --git a/lazer/sdk/rust/protocol/src/publisher.rs b/lazer/sdk/rust/protocol/src/publisher.rs index cef637f72b..fccbe67f7d 100644 --- a/lazer/sdk/rust/protocol/src/publisher.rs +++ b/lazer/sdk/rust/protocol/src/publisher.rs @@ -3,7 +3,7 @@ //! eliminating WebSocket overhead. use { - super::router::{Price, PriceFeedId, TimestampUs}, + super::router::{Price, PriceFeedId, Rate, TimestampUs}, derive_more::derive::From, serde::{Deserialize, Serialize}, }; @@ -12,7 +12,33 @@ use { /// from the publisher to the router. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PriceFeedData { +pub struct PriceFeedDataV2 { + pub price_feed_id: PriceFeedId, + /// Timestamp of the last update provided by the source of the prices + /// (like an exchange). If unavailable, this value is set to `publisher_timestamp_us`. + pub source_timestamp_us: TimestampUs, + /// Timestamp of the last update provided by the publisher. + pub publisher_timestamp_us: TimestampUs, + /// Last known value of the best executable price of this price feed. + /// `None` if no value is currently available. + pub price: Option, + /// Last known value of the best bid price of this price feed. + /// `None` if no value is currently available. + pub best_bid_price: Option, + /// Last known value of the best ask price of this price feed. + /// `None` if no value is currently available. + pub best_ask_price: Option, + /// Last known value of the funding rate of this feed. + /// `None` if no value is currently available. + pub funding_rate: Option, +} + +/// Old Represents a binary (bincode-serialized) stream update sent +/// from the publisher to the router. +/// Superseded by `PriceFeedData`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PriceFeedDataV1 { pub price_feed_id: PriceFeedId, /// Timestamp of the last update provided by the source of the prices /// (like an exchange). If unavailable, this value is set to `publisher_timestamp_us`. @@ -33,6 +59,20 @@ pub struct PriceFeedData { pub best_ask_price: Option, } +impl From for PriceFeedDataV2 { + fn from(v0: PriceFeedDataV1) -> Self { + Self { + price_feed_id: v0.price_feed_id, + source_timestamp_us: v0.source_timestamp_us, + publisher_timestamp_us: v0.publisher_timestamp_us, + price: v0.price, + best_bid_price: v0.best_bid_price, + best_ask_price: v0.best_ask_price, + funding_rate: None, + } + } +} + /// A response sent from the server to the publisher client. /// Currently only serde errors are reported back to the client. #[derive(Debug, Clone, Serialize, Deserialize, From)] @@ -49,7 +89,7 @@ pub struct UpdateDeserializationErrorResponse { } #[test] -fn price_feed_data_serde() { +fn price_feed_data_v1_serde() { let data = [ 1, 0, 0, 0, // price_feed_id 2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us @@ -59,7 +99,7 @@ fn price_feed_data_serde() { 6, 2, 0, 0, 0, 0, 0, 0, // best_ask_price ]; - let expected = PriceFeedData { + let expected = PriceFeedDataV1 { price_feed_id: PriceFeedId(1), source_timestamp_us: TimestampUs(2), publisher_timestamp_us: TimestampUs(3), @@ -68,7 +108,7 @@ fn price_feed_data_serde() { best_ask_price: Some(Price((2 * 256 + 6).try_into().unwrap())), }; assert_eq!( - bincode::deserialize::(&data).unwrap(), + bincode::deserialize::(&data).unwrap(), expected ); assert_eq!(bincode::serialize(&expected).unwrap(), data); @@ -81,16 +121,68 @@ fn price_feed_data_serde() { 0, 0, 0, 0, 0, 0, 0, 0, // best_bid_price 0, 0, 0, 0, 0, 0, 0, 0, // best_ask_price ]; - let expected2 = PriceFeedData { + let expected2 = PriceFeedDataV1 { + price_feed_id: PriceFeedId(1), + source_timestamp_us: TimestampUs(2), + publisher_timestamp_us: TimestampUs(3), + price: Some(Price(4.try_into().unwrap())), + best_bid_price: None, + best_ask_price: None, + }; + assert_eq!( + bincode::deserialize::(&data2).unwrap(), + expected2 + ); + assert_eq!(bincode::serialize(&expected2).unwrap(), data2); +} + +#[test] +fn price_feed_data_v2_serde() { + let data = [ + 1, 0, 0, 0, // price_feed_id + 2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us + 3, 0, 0, 0, 0, 0, 0, 0, // publisher_timestamp_us + 1, 4, 0, 0, 0, 0, 0, 0, 0, // price + 1, 5, 0, 0, 0, 0, 0, 0, 0, // best_bid_price + 1, 6, 2, 0, 0, 0, 0, 0, 0, // best_ask_price + 0, // funding_rate + ]; + + let expected = PriceFeedDataV2 { + price_feed_id: PriceFeedId(1), + source_timestamp_us: TimestampUs(2), + publisher_timestamp_us: TimestampUs(3), + price: Some(Price(4.try_into().unwrap())), + best_bid_price: Some(Price(5.try_into().unwrap())), + best_ask_price: Some(Price((2 * 256 + 6).try_into().unwrap())), + funding_rate: None, + }; + assert_eq!( + bincode::deserialize::(&data).unwrap(), + expected + ); + assert_eq!(bincode::serialize(&expected).unwrap(), data); + + let data2 = [ + 1, 0, 0, 0, // price_feed_id + 2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us + 3, 0, 0, 0, 0, 0, 0, 0, // publisher_timestamp_us + 1, 4, 0, 0, 0, 0, 0, 0, 0, // price + 0, // best_bid_price + 0, // best_ask_price + 1, 7, 3, 0, 0, 0, 0, 0, 0, // funding_rate + ]; + let expected2 = PriceFeedDataV2 { price_feed_id: PriceFeedId(1), source_timestamp_us: TimestampUs(2), publisher_timestamp_us: TimestampUs(3), price: Some(Price(4.try_into().unwrap())), best_bid_price: None, best_ask_price: None, + funding_rate: Some(Rate(3 * 256 + 7)), }; assert_eq!( - bincode::deserialize::(&data2).unwrap(), + bincode::deserialize::(&data2).unwrap(), expected2 ); assert_eq!(bincode::serialize(&expected2).unwrap(), data2); diff --git a/lazer/sdk/rust/protocol/src/router.rs b/lazer/sdk/rust/protocol/src/router.rs index 858f44cb8c..f8e7491734 100644 --- a/lazer/sdk/rust/protocol/src/router.rs +++ b/lazer/sdk/rust/protocol/src/router.rs @@ -41,6 +41,33 @@ impl TimestampUs { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[repr(transparent)] +pub struct Rate(pub i64); + +impl Rate { + pub fn parse_str(value: &str, exponent: u32) -> anyhow::Result { + let value: Decimal = value.parse()?; + let coef = 10i64.checked_pow(exponent).context("overflow")?; + let coef = Decimal::from_i64(coef).context("overflow")?; + let value = value.checked_mul(coef).context("overflow")?; + if !value.is_integer() { + bail!("price value is more precise than available exponent"); + } + let value: i64 = value.try_into().context("overflow")?; + Ok(Self(value)) + } + + pub fn from_f64(value: f64, exponent: u32) -> anyhow::Result { + let value = Decimal::from_f64(value).context("overflow")?; + let coef = 10i64.checked_pow(exponent).context("overflow")?; + let coef = Decimal::from_i64(coef).context("overflow")?; + let value = value.checked_mul(coef).context("overflow")?; + let value: i64 = value.try_into().context("overflow")?; + Ok(Self(value)) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[repr(transparent)] pub struct Price(pub NonZeroI64); @@ -79,6 +106,12 @@ impl Price { Ok(self.0.get() as f64 / 10i64.checked_pow(exponent).context("overflow")? as f64) } + pub fn from_f64(value: f64, exponent: u32) -> anyhow::Result { + let value = (value * 10f64.powi(exponent as i32)) as i64; + let value = NonZeroI64::new(value).context("zero price is unsupported")?; + Ok(Self(value)) + } + pub fn mul(self, rhs: Price, rhs_exponent: u32) -> anyhow::Result { let left_value = i128::from(self.0.get()); let right_value = i128::from(rhs.0.get()); @@ -142,6 +175,8 @@ pub enum PriceFeedProperty { PublisherCount, Exponent, Confidence, + FundingRate, + FundingTimestamp, // More fields may be added later. } @@ -394,13 +429,6 @@ pub struct ParsedPayload { pub price_feeds: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NatsPayload { - pub payload: ParsedPayload, - pub channel: Channel, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ParsedFeedPayload { @@ -426,6 +454,12 @@ pub struct ParsedFeedPayload { #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub confidence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub funding_rate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub funding_timestamp: Option, // More fields may be added later. } @@ -444,6 +478,8 @@ impl ParsedFeedPayload { publisher_count: None, exponent: None, confidence: None, + funding_rate: None, + funding_timestamp: None, }; for &property in properties { match property { @@ -457,7 +493,7 @@ impl ParsedFeedPayload { output.best_ask_price = data.best_ask_price; } PriceFeedProperty::PublisherCount => { - output.publisher_count = data.publisher_count; + output.publisher_count = Some(data.publisher_count); } PriceFeedProperty::Exponent => { output.exponent = exponent; @@ -465,6 +501,12 @@ impl ParsedFeedPayload { PriceFeedProperty::Confidence => { output.confidence = data.confidence; } + PriceFeedProperty::FundingRate => { + output.funding_rate = data.funding_rate; + } + PriceFeedProperty::FundingTimestamp => { + output.funding_timestamp = data.funding_timestamp; + } } } output @@ -480,9 +522,11 @@ impl ParsedFeedPayload { price: data.price, best_bid_price: data.best_bid_price, best_ask_price: data.best_ask_price, - publisher_count: data.publisher_count, + publisher_count: Some(data.publisher_count), exponent, confidence: data.confidence, + funding_rate: data.funding_rate, + funding_timestamp: data.funding_timestamp, } } }