From dac9d93cfa8cee84c5322a2b58daef698aec8959 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Mon, 7 Jul 2025 15:56:32 +0100 Subject: [PATCH 01/19] feat(pyth-lazer) Create schema for lazer agent JRPC endpoint --- lazer/publisher_sdk/rust/src/lib.rs | 38 ++++ lazer/sdk/rust/protocol/src/jrpc.rs | 238 ++++++++++++++++++++++++++ lazer/sdk/rust/protocol/src/lib.rs | 5 +- lazer/sdk/rust/protocol/src/router.rs | 37 +++- 4 files changed, 309 insertions(+), 9 deletions(-) create mode 100644 lazer/sdk/rust/protocol/src/jrpc.rs diff --git a/lazer/publisher_sdk/rust/src/lib.rs b/lazer/publisher_sdk/rust/src/lib.rs index bc341e2c5d..1a565e0800 100644 --- a/lazer/publisher_sdk/rust/src/lib.rs +++ b/lazer/publisher_sdk/rust/src/lib.rs @@ -1,9 +1,12 @@ 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 pyth_lazer_protocol::jrpc::{FeedUpdateParams, UpdateParams}; use pyth_lazer_protocol::router::TimestampUs; pub mod transaction_envelope { @@ -164,3 +167,38 @@ impl TryFrom for serde_value::Value { } } } + +impl From for FeedUpdate { + fn from(value: FeedUpdateParams) -> Self { + FeedUpdate { + feed_id: Some(value.feed_id.0), + source_timestamp: value.source_timestamp.into(), + update: Some(value.update.into()), + special_fields: Default::default(), + } + } +} + +impl From for Update { + fn from(value: UpdateParams) -> Self { + match value { + UpdateParams::PriceUpdate { + price, + best_bid_price, + best_ask_price, + } => Update::PriceUpdate(PriceUpdate { + price: Some(price.0.into()), + best_bid_price: Some(best_bid_price.0.into()), + best_ask_price: Some(best_ask_price.0.into()), + special_fields: Default::default(), + }), + UpdateParams::FundingRateUpdate { price, rate } => { + Update::FundingRateUpdate(FundingRateUpdate { + price: Some(price.0.into()), + rate: Some(rate.0.into()), + special_fields: Default::default(), + }) + } + } + } +} diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs new file mode 100644 index 0000000000..be5459aaf6 --- /dev/null +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -0,0 +1,238 @@ +use std::time::Duration; +use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct PythLazerAgentJrpcV1 { + pub jsonrpc: JsonRpcVersion, + #[serde(flatten)] + pub params: JrpcParams, + pub id: i64, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "method", content = "params")] +pub enum JrpcParams { + #[serde(rename = "send_updates")] + SendUpdates(FeedUpdateParams), + #[serde(rename = "get_symbols")] + GetMetadata(GetMetadataParams), +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct FeedUpdateParams { + pub feed_id: PriceFeedId, + pub source_timestamp: TimestampUs, + pub update: UpdateParams, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "type")] +pub enum UpdateParams { + #[serde(rename = "price")] + PriceUpdate { + price: Price, + best_bid_price: Price, + best_ask_price: Price, + }, + #[serde(rename = "funding_rate")] + FundingRateUpdate { price: Price, rate: Rate }, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Filter { + name: Option, + asset_type: Option, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct GetMetadataParams { + filters: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum JsonRpcVersion { + #[serde(rename = "2.0")] + V2, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct JrpcResponse { + pub jsonrpc: JsonRpcVersion, + pub result: T, + pub id: i64, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ErrorResponse { + pub message: String +} + +#[derive(Serialize, Deserialize)] +struct SymbolMetadata { + pub asset_type: String, + pub cmc_id: i64, + pub description: String, + pub exponent: i64, + pub hermes_id: String, + #[serde(default, with = "humantime_serde", alias = "interval")] + pub interval: Option, + pub min_channel: String, + pub min_publishers: i64, + pub name: String, + pub pyth_lazer_id: i64, + pub schedule: String, + pub state: String, + pub symbol: String, +} + +#[cfg(test)] +mod tests { + use crate::jrpc::JrpcParams::{GetMetadata, SendUpdates}; + use crate::jrpc::{ + FeedUpdateParams, Filter, GetMetadataParams, JsonRpcVersion, PythLazerAgentJrpcV1, + UpdateParams, + }; + use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; + + #[test] + fn test_send_updates_price() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "send_updates", + "params": { + "feed_id": 1, + "source_timestamp": 124214124124, + + "update": { + "type": "price", + "price": 1234567890, + "best_bid_price": 1234567891, + "best_ask_price": 1234567892 + } + }, + "id": 1 + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: SendUpdates(FeedUpdateParams { + feed_id: PriceFeedId(1), + source_timestamp: TimestampUs(124214124124), + update: UpdateParams::PriceUpdate { + price: Price::from_integer(1234567890, 0).unwrap(), + best_bid_price: Price::from_integer(1234567891, 0).unwrap(), + best_ask_price: Price::from_integer(1234567892, 0).unwrap(), + }, + }), + id: 1, + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } + + #[test] + fn test_send_updates_funding_rate() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "send_updates", + "params": { + "feed_id": 1, + "source_timestamp": 124214124124, + + "update": { + "type": "funding_rate", + "price": 1234567890, + "rate": 1234567891 + } + }, + "id": 1 + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: SendUpdates(FeedUpdateParams { + feed_id: PriceFeedId(1), + source_timestamp: TimestampUs(124214124124), + update: UpdateParams::FundingRateUpdate { + price: Price::from_integer(1234567890, 0).unwrap(), + rate: Rate::from_integer(1234567891, 0).unwrap(), + }, + }), + id: 1, + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } + #[test] + fn test_send_get_symbols() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "get_symbols", + "params": { + "filters": [ + {"name": "BTC/USD"}, + {"asset_type": "crypto"} + ] + }, + "id": 1 + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: GetMetadata(GetMetadataParams { + filters: Some(vec![ + Filter { + name: Some("BTC/USD".to_string()), + asset_type: None, + }, + Filter { + name: None, + asset_type: Some("crypto".to_string()), + }, + ]), + }), + id: 1, + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } + + #[test] + fn test_get_symbols_without_filters() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "get_symbols", + "params": {}, + "id": 1 + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: GetMetadata(GetMetadataParams { filters: None }), + id: 1, + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } +} diff --git a/lazer/sdk/rust/protocol/src/lib.rs b/lazer/sdk/rust/protocol/src/lib.rs index d10bedeebc..ded13bec8c 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; +pub mod jrpc; pub mod message; pub mod payload; pub mod publisher; @@ -23,7 +24,7 @@ fn magics_in_big_endian() { }; // The values listed in this test can be used when reading the magic headers in BE format - // (e.g. on EVM). + // (e.g., on EVM). assert_eq!(u32::swap_bytes(BINARY_UPDATE_FORMAT_MAGIC), 1937213467); assert_eq!(u32::swap_bytes(PAYLOAD_FORMAT_MAGIC), 1976813459); @@ -44,6 +45,6 @@ fn magics_in_big_endian() { LE_UNSIGNED_FORMAT_MAGIC, ] { // Required to distinguish between byte orders. - assert!(u32::swap_bytes(magic) != magic); + assert_ne!(u32::swap_bytes(magic), magic); } } diff --git a/lazer/sdk/rust/protocol/src/router.rs b/lazer/sdk/rust/protocol/src/router.rs index 356b7dab88..41d7bf7dc7 100644 --- a/lazer/sdk/rust/protocol/src/router.rs +++ b/lazer/sdk/rust/protocol/src/router.rs @@ -1,5 +1,6 @@ -//! WebSocket JSON protocol types for API the router provides to consumers and publishers. +//! WebSocket JSON protocol types for the API the router provides to consumers and publishers. +use protobuf::MessageField; use { crate::payload::AggregatedPriceFeedData, anyhow::{bail, Context}, @@ -37,6 +38,26 @@ impl TryFrom<&Timestamp> for TimestampUs { } } +impl Into for TimestampUs { + fn into(self) -> Timestamp { + Timestamp { + #[allow( + clippy::cast_possible_wrap, + reason = "u64 to i64 after this division can never overflow because the value cannot be too big" + )] + seconds: (self.0 / 1_000_000) as i64, + nanos: (self.0 % 1_000_000) as i32 * 1000, + special_fields: Default::default(), + } + } +} + +impl Into> for TimestampUs { + fn into(self) -> MessageField { + MessageField::some(self.into()) + } +} + impl TimestampUs { pub fn now() -> Self { let value = SystemTime::now() @@ -304,7 +325,7 @@ impl<'de> Deserialize<'de> for Channel { D: serde::Deserializer<'de>, { let value = ::deserialize(deserializer)?; - parse_channel(&value).ok_or_else(|| D::Error::custom("unknown channel")) + parse_channel(&value).ok_or_else(|| Error::custom("unknown channel")) } } @@ -341,12 +362,14 @@ fn fixed_rate_values() { "values must be unique and sorted" ); for value in FixedRate::ALL { - assert!( - 1000 % value.ms == 0, + assert_eq!( + 1000 % value.ms, + 0, "1 s must contain whole number of intervals" ); - assert!( - value.value_us() % FixedRate::MIN.value_us() == 0, + assert_eq!( + value.value_us() % FixedRate::MIN.value_us(), + 0, "the interval's borders must be a subset of the minimal interval's borders" ); } @@ -383,7 +406,7 @@ impl<'de> Deserialize<'de> for SubscriptionParams { D: serde::Deserializer<'de>, { let value = SubscriptionParamsRepr::deserialize(deserializer)?; - Self::new(value).map_err(D::Error::custom) + Self::new(value).map_err(Error::custom) } } From e7b4a1b37669a1135c8a7c738768088351ae1c1b Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Mon, 7 Jul 2025 15:58:11 +0100 Subject: [PATCH 02/19] feat(pyth-lazer) Implement JRPC endpoint for the lazer agent --- apps/pyth-lazer-agent/Cargo.lock | 361 +++++++++++++++++- apps/pyth-lazer-agent/Cargo.toml | 7 +- apps/pyth-lazer-agent/src/http_server.rs | 156 +++++++- apps/pyth-lazer-agent/src/jrpc_handle.rs | 59 +++ apps/pyth-lazer-agent/src/lazer_publisher.rs | 2 +- apps/pyth-lazer-agent/src/main.rs | 1 + apps/pyth-lazer-agent/src/publisher_handle.rs | 185 ++------- apps/pyth-lazer-agent/src/relayer_session.rs | 5 +- lazer/Cargo.lock | 11 + lazer/sdk/rust/protocol/Cargo.toml | 1 + 10 files changed, 611 insertions(+), 177 deletions(-) create mode 100644 apps/pyth-lazer-agent/src/jrpc_handle.rs diff --git a/apps/pyth-lazer-agent/Cargo.lock b/apps/pyth-lazer-agent/Cargo.lock index 081a98b5ac..6c9f286026 100644 --- a/apps/pyth-lazer-agent/Cargo.lock +++ b/apps/pyth-lazer-agent/Cargo.lock @@ -116,6 +116,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -884,6 +890,25 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1012,6 +1037,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1020,6 +1046,39 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -1028,13 +1087,24 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", + "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1163,6 +1233,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1266,6 +1352,12 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1663,6 +1755,7 @@ dependencies = [ "protobuf", "pyth-lazer-protocol", "pyth-lazer-publisher-sdk", + "reqwest", "serde", "serde_json", "soketto", @@ -1679,13 +1772,12 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9bdf4e2ba853a8b437309487542e742c7d094d8db189db194cb538f2be02ecd" dependencies = [ "anyhow", "base64 0.22.1", "byteorder", "derive_more", + "humantime-serde", "itertools", "protobuf", "rust_decimal", @@ -1696,8 +1788,6 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e633db28ca38210de8ab3e99d5bd85ad8cae08a08bb0292506340ee9d1c718" dependencies = [ "anyhow", "fs-err", @@ -1892,6 +1982,60 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -2001,6 +2145,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -2114,6 +2291,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2428,6 +2617,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2439,6 +2637,27 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -2580,6 +2799,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.26.2" @@ -2642,6 +2871,51 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -2722,6 +2996,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tungstenite" version = "0.26.2" @@ -2765,6 +3045,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "unty" version = "0.0.4" @@ -2835,6 +3121,15 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -2882,6 +3177,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -2914,6 +3222,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "4.4.2" @@ -2948,6 +3266,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index 188b2149a5..b514ad8ee3 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.2" edition = "2024" [dependencies] -pyth-lazer-publisher-sdk = "0.1.5" -pyth-lazer-protocol = "0.7.2" +pyth-lazer-publisher-sdk = { path = "../../lazer/publisher_sdk/rust" } +pyth-lazer-protocol = {path = "../../lazer/sdk/rust/protocol"} anyhow = "1.0.98" backoff = "0.4.0" @@ -20,7 +20,7 @@ futures-util = "0.3.31" http = "1.3.1" http-body-util = "0.1.3" humantime-serde = "1.1.1" -hyper = { version = "1.6.0", features = ["http1", "server"] } +hyper = { version = "1.6.0", features = ["http1", "server", "client"] } hyper-util = { version = "0.1.10", features = ["tokio"] } protobuf = "3.7.2" serde = { version = "1.0.219", features = ["derive"] } @@ -33,6 +33,7 @@ tokio-util = { version = "0.7.14", features = ["compat"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } url = { version = "2.5.4", features = ["serde"] } +reqwest = "0.12.22" [dev-dependencies] tempfile = "3.20.0" diff --git a/apps/pyth-lazer-agent/src/http_server.rs b/apps/pyth-lazer-agent/src/http_server.rs index 235fdb2af7..a0923b3b6e 100644 --- a/apps/pyth-lazer-agent/src/http_server.rs +++ b/apps/pyth-lazer-agent/src/http_server.rs @@ -1,32 +1,46 @@ -use anyhow::{Context, Result}; +use crate::jrpc_handle::jrpc_handler_inner; +use crate::publisher_handle::publisher_inner_handler; +use crate::websocket_utils::{handle_websocket_error, send_text}; +use crate::{ + config::Config, lazer_publisher::LazerPublisher, publisher_handle::PublisherConnectionContext, +}; +use anyhow::{Context, Result, bail}; +use futures_util::io::{BufReader, BufWriter}; +use hyper::body::Incoming; use hyper::{Response, StatusCode, body::Bytes, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; +use pyth_lazer_protocol::publisher::{ServerResponse, UpdateDeserializationErrorResponse}; use soketto::{ BoxedError, handshake::http::{Server, is_upgrade_request}, }; +use std::fmt::Debug; +use std::pin::Pin; use std::{io, net::SocketAddr}; use tokio::net::{TcpListener, TcpStream}; -use tracing::{debug, info, instrument, warn}; - -use crate::{ - config::Config, - lazer_publisher::LazerPublisher, - publisher_handle::{PublisherConnectionContext, handle_publisher}, -}; +use tokio::{pin, select}; +use tokio_util::compat::TokioAsyncReadCompatExt; +use tracing::{debug, error, info, instrument, warn}; type FullBody = http_body_util::Full; +pub type InnerHandlerResult = Pin>> + Send>>; -#[derive(Debug)] -pub enum Request { +#[derive(Debug, Copy, Clone)] +pub enum PublisherRequest { PublisherV1, PublisherV2, } -pub struct RelayerRequest(pub http::Request); +pub enum Request { + PublisherRequest(PublisherRequest), + JrpcV1, +} -const PUBLISHER_WS_URI: &str = "/v1/publisher"; +pub struct RelayerRequest(pub http::Request); + +const PUBLISHER_WS_URI_V1: &str = "/v1/publisher"; const PUBLISHER_WS_URI_V2: &str = "/v2/publisher"; +const JRPC_WS_URI_V1: &str = "/v1/jprc"; const READINESS_PROBE_PATH: &str = "/ready"; const LIVENESS_PROBE_PATH: &str = "/live"; @@ -38,8 +52,9 @@ pub async fn run(config: Config, lazer_publisher: LazerPublisher) -> Result<()> loop { let stream_addr = listener.accept().await; let lazer_publisher_clone = lazer_publisher.clone(); + let config = config.clone(); tokio::spawn(async { - if let Err(err) = try_handle_connection(stream_addr, lazer_publisher_clone).await { + if let Err(err) = try_handle_connection(config, stream_addr, lazer_publisher_clone).await { warn!("error while handling connection: {err:?}"); } }); @@ -47,6 +62,7 @@ pub async fn run(config: Config, lazer_publisher: LazerPublisher) -> Result<()> } async fn try_handle_connection( + config: Config, stream_addr: io::Result<(TcpStream, SocketAddr)>, lazer_publisher: LazerPublisher, ) -> Result<()> { @@ -58,7 +74,7 @@ async fn try_handle_connection( TokioIo::new(stream), service_fn(move |r| { let request = RelayerRequest(r); - request_handler(request, remote_addr, lazer_publisher.clone()) + request_handler(config.clone(), request, remote_addr, lazer_publisher.clone()) }), ) .with_upgrades() @@ -68,6 +84,7 @@ async fn try_handle_connection( #[instrument(skip_all, fields(component = "http_server", remote_addr = remote_addr.to_string()))] async fn request_handler( + config: Config, request: RelayerRequest, remote_addr: SocketAddr, lazer_publisher: LazerPublisher, @@ -75,8 +92,9 @@ async fn request_handler( let path = request.0.uri().path(); let request_type = match path { - PUBLISHER_WS_URI => Request::PublisherV1, - PUBLISHER_WS_URI_V2 => Request::PublisherV2, + PUBLISHER_WS_URI_V1 => Request::PublisherRequest(PublisherRequest::PublisherV1), + PUBLISHER_WS_URI_V2 => Request::PublisherRequest(PublisherRequest::PublisherV2), + JRPC_WS_URI_V1 => Request::JrpcV1, LIVENESS_PROBE_PATH => { let response = Response::builder().status(StatusCode::OK); return Ok(response.body(FullBody::default())?); @@ -113,17 +131,32 @@ async fn request_handler( Ok(response) => { info!("accepted connection from publisher"); match request_type { - Request::PublisherV1 | Request::PublisherV2 => { + Request::PublisherRequest(publisher_request_type) => { let publisher_connection_context = PublisherConnectionContext { - request_type, + request_type: publisher_request_type, _remote_addr: remote_addr, }; - tokio::spawn(handle_publisher( + + tokio::spawn(handle_ws( + config, server, request.0, + lazer_publisher, publisher_connection_context, + publisher_inner_handler, + )); + Ok(response.map(|()| FullBody::default())) + } + Request::JrpcV1 => { + tokio::spawn(handle_ws( + config, + server, + request.0, lazer_publisher, + (), + jrpc_handler_inner, )); + Ok(response.map(|()| FullBody::default())) } } @@ -137,3 +170,88 @@ async fn request_handler( } } } + +#[instrument( + skip(server, request, lazer_publisher), + fields(component = "publisher_ws") +)] +async fn handle_ws( + config: Config, + server: Server, + request: http::Request, + lazer_publisher: LazerPublisher, + context: T, + inner_handler: fn(Config, Vec, LazerPublisher, T) -> InnerHandlerResult, +) { + if let Err(err) = try_handle_ws(config, server, request, lazer_publisher, context, inner_handler).await + { + handle_websocket_error(err); + } +} + +#[instrument( + skip(server, request, lazer_publisher), + fields(component = "publisher_ws") +)] +async fn try_handle_ws( + config: Config, + server: Server, + request: http::Request, + lazer_publisher: LazerPublisher, + context: T, + inner_handler: fn(Config, Vec, LazerPublisher, T) -> InnerHandlerResult, +) -> Result<()> { + let stream = hyper::upgrade::on(request).await?; + let io = TokioIo::new(stream); + let stream = BufReader::new(BufWriter::new(io.compat())); + let (mut ws_sender, mut ws_receiver) = server.into_builder(stream).finish(); + + let mut receive_buf = Vec::new(); + + let mut error_count = 0u32; + const MAX_ERROR_LOG: u32 = 10u32; + const MAX_ERROR_DISCONNECT: u32 = 100u32; + + loop { + receive_buf.clear(); + { + // soketto is not cancel-safe, so we need to store the future and poll it + // in the inner loop. + let receive = async { ws_receiver.receive(&mut receive_buf).await }; + pin!(receive); + loop { + select! { + _result = &mut receive => { + break + } + } + } + } + + match inner_handler(config.clone(), receive_buf.clone(), lazer_publisher.clone(), context).await { + Ok(response) => { + if let Some(response) = response { + send_text(&mut ws_sender, &response).await?; + } + } + Err(err) => { + error_count += 1; + if error_count <= MAX_ERROR_LOG { + warn!("Error decoding message error: {err}"); + } + if error_count >= MAX_ERROR_DISCONNECT { + error!("Error threshold reached; disconnecting"); + bail!("Error threshold reached"); + } + let error_json = &serde_json::to_string::( + &UpdateDeserializationErrorResponse { + error: format!("failed to parse a binary message: {err}"), + } + .into(), + )?; + send_text(&mut ws_sender, error_json).await?; + continue; + } + } + } +} diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs new file mode 100644 index 0000000000..9600dd535a --- /dev/null +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -0,0 +1,59 @@ +use anyhow::{bail, Error}; +use futures_util::future::err; +use reqwest::Client; +use pyth_lazer_protocol::jrpc::{GetMetadataParams, JrpcResponse, JsonRpcVersion, JrpcParams, PythLazerAgentJrpcV1}; +use crate::config::Config; +use crate::http_server::InnerHandlerResult; +use crate::lazer_publisher::LazerPublisher; + +pub fn jrpc_handler_inner(config: Config, receive_buf: Vec, lazer_publisher: LazerPublisher, _context: ()) -> InnerHandlerResult { + Box::pin(async move { + let (data, _) = bincode::serde::decode_from_slice::( + &receive_buf, + bincode::config::legacy(), + )?; + + match data.params { + JrpcParams::SendUpdates(update_params) => { + lazer_publisher + .push_feed_update(update_params.into()) + .await?; + + return Ok(Some(serde_json::to_string::>(&JrpcResponse { + jsonrpc: JsonRpcVersion::V2, + result: (), + id: data.id, + })?)) + } + JrpcParams::GetMetadata(params) => { + match get_metadata(config, params).await { + Ok(result) => { + return Ok(Some(serde_json::to_string::>(&JrpcResponse { + jsonrpc: JsonRpcVersion::V2, + result: result, + id: data.id, + })?)) + } + Err(_) => {} + } + } + } + + Ok(None) + }) +} + +async fn get_metadata(config: Config, params: GetMetadataParams) -> Result, anyhow::Error> { + let client = Client::new(); + + let resp = client + .post("https://httpbin.org/post") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let + Ok(None) +} diff --git a/apps/pyth-lazer-agent/src/lazer_publisher.rs b/apps/pyth-lazer-agent/src/lazer_publisher.rs index b556f2d765..b275489b88 100644 --- a/apps/pyth-lazer-agent/src/lazer_publisher.rs +++ b/apps/pyth-lazer-agent/src/lazer_publisher.rs @@ -24,7 +24,7 @@ use tokio::{ }; use tracing::error; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct LazerPublisher { sender: Sender, pub(crate) is_ready: Arc, diff --git a/apps/pyth-lazer-agent/src/main.rs b/apps/pyth-lazer-agent/src/main.rs index a6082b747c..9de0c4b480 100644 --- a/apps/pyth-lazer-agent/src/main.rs +++ b/apps/pyth-lazer-agent/src/main.rs @@ -12,6 +12,7 @@ mod lazer_publisher; mod publisher_handle; mod relayer_session; mod websocket_utils; +mod jrpc_handle; #[derive(Parser)] #[command(version)] diff --git a/apps/pyth-lazer-agent/src/publisher_handle.rs b/apps/pyth-lazer-agent/src/publisher_handle.rs index 2e0ee35876..672d37a6ac 100644 --- a/apps/pyth-lazer-agent/src/publisher_handle.rs +++ b/apps/pyth-lazer-agent/src/publisher_handle.rs @@ -1,173 +1,62 @@ use std::net::SocketAddr; -use anyhow::bail; -use futures_util::io::{BufReader, BufWriter}; -use hyper_util::rt::TokioIo; -use protobuf::MessageField; -use protobuf::well_known_types::timestamp::Timestamp; -use pyth_lazer_protocol::publisher::{ - PriceFeedDataV1, PriceFeedDataV2, ServerResponse, UpdateDeserializationErrorResponse, -}; +use pyth_lazer_protocol::publisher::{PriceFeedDataV1, PriceFeedDataV2}; use pyth_lazer_publisher_sdk::publisher_update::feed_update::Update; use pyth_lazer_publisher_sdk::publisher_update::{FeedUpdate, FundingRateUpdate, PriceUpdate}; -use soketto::handshake::http::Server; -use tokio::{pin, select}; -use tokio_util::compat::TokioAsyncReadCompatExt; -use tracing::{error, instrument, warn}; -use crate::{ - http_server, - lazer_publisher::LazerPublisher, - websocket_utils::{handle_websocket_error, send_text}, -}; +use crate::{http_server, lazer_publisher::LazerPublisher}; +use crate::config::Config; +use crate::http_server::InnerHandlerResult; +#[derive(Debug, Copy, Clone)] pub struct PublisherConnectionContext { - pub request_type: http_server::Request, + pub request_type: http_server::PublisherRequest, pub _remote_addr: SocketAddr, } -#[instrument( - skip(server, request, lazer_publisher, context), - fields(component = "publisher_ws") -)] -pub async fn handle_publisher( - server: Server, - request: hyper::Request, - context: PublisherConnectionContext, - lazer_publisher: LazerPublisher, -) { - if let Err(err) = try_handle_publisher(server, request, context, lazer_publisher).await { - handle_websocket_error(err); - } -} - -#[instrument( - skip(server, request, lazer_publisher, context), - fields(component = "publisher_ws") -)] -async fn try_handle_publisher( - server: Server, - request: hyper::Request, - context: PublisherConnectionContext, - lazer_publisher: LazerPublisher, -) -> anyhow::Result<()> { - let stream = hyper::upgrade::on(request).await?; - let io = TokioIo::new(stream); - let stream = BufReader::new(BufWriter::new(io.compat())); - let (mut ws_sender, mut ws_receiver) = server.into_builder(stream).finish(); - - let mut receive_buf = Vec::new(); - - let mut error_count = 0u32; - const MAX_ERROR_LOG: u32 = 10u32; - const MAX_ERROR_DISCONNECT: u32 = 100u32; - - loop { - receive_buf.clear(); - { - // soketto is not cancel-safe, so we need to store the future and poll it - // in the inner loop. - let receive = async { ws_receiver.receive(&mut receive_buf).await }; - pin!(receive); - #[allow(clippy::never_loop)] // false positive - loop { - select! { - _result = &mut receive => { - break - } - } - } - } +pub(crate) fn publisher_inner_handler(_: Config, receive_buf: Vec, lazer_publisher: LazerPublisher, context: PublisherConnectionContext) -> InnerHandlerResult { + Box::pin(async move { // reply with an error if we can't parse the binary update let feed_update: FeedUpdate = match context.request_type { - http_server::Request::PublisherV1 => { - match bincode::serde::decode_from_slice::( + http_server::PublisherRequest::PublisherV1 => { + let (data, _) = bincode::serde::decode_from_slice::( &receive_buf, bincode::config::legacy(), - ) { - Ok((data, _)) => { - let source_timestamp = MessageField::some(Timestamp { - seconds: (data.source_timestamp_us.0 / 1_000_000) as i64, - nanos: (data.source_timestamp_us.0 % 1_000_000 * 1000) as i32, - special_fields: Default::default(), - }); - FeedUpdate { - feed_id: Some(data.price_feed_id.0), - source_timestamp, - update: Some(Update::PriceUpdate(PriceUpdate { - price: data.price.map(|p| p.0.get()), - best_bid_price: data.best_bid_price.map(|p| p.0.get()), - best_ask_price: data.best_ask_price.map(|p| p.0.get()), - ..PriceUpdate::default() - })), - special_fields: Default::default(), - } - } - Err(err) => { - error_count += 1; - if error_count <= MAX_ERROR_LOG { - warn!("Error decoding v1 update error: {:?}", err); - } - if error_count >= MAX_ERROR_DISCONNECT { - error!("Error threshold reached; disconnecting",); - bail!("Error threshold reached"); - } - let error_json = &serde_json::to_string::( - &UpdateDeserializationErrorResponse { - error: format!("failed to parse binary update: {err}"), - } - .into(), - )?; - send_text(&mut ws_sender, error_json).await?; - continue; - } + )?; + + FeedUpdate { + feed_id: Some(data.price_feed_id.0), + source_timestamp: data.source_timestamp_us.into(), + update: Some(Update::PriceUpdate(PriceUpdate { + price: data.price.map(|p| p.0.get()), + best_bid_price: data.best_bid_price.map(|p| p.0.get()), + best_ask_price: data.best_ask_price.map(|p| p.0.get()), + ..PriceUpdate::default() + })), + special_fields: Default::default(), } } - http_server::Request::PublisherV2 => { - match bincode::serde::decode_from_slice::( + http_server::PublisherRequest::PublisherV2 => { + let (data, _) = bincode::serde::decode_from_slice::( &receive_buf, bincode::config::legacy(), - ) { - Ok((data, _)) => { - let source_timestamp = MessageField::some(Timestamp { - seconds: (data.source_timestamp_us.0 / 1_000_000) as i64, - nanos: (data.source_timestamp_us.0 % 1_000_000 * 1000) as i32, - special_fields: Default::default(), - }); - FeedUpdate { - feed_id: Some(data.price_feed_id.0), - source_timestamp, - update: Some(Update::FundingRateUpdate(FundingRateUpdate { - price: data.price.map(|p| p.0.get()), - rate: data.funding_rate.map(|r| r.0), - ..FundingRateUpdate::default() - })), - special_fields: Default::default(), - } - } - Err(err) => { - error_count += 1; - if error_count <= MAX_ERROR_LOG { - warn!("Error decoding v2 update error: {:?}", err); - } - if error_count >= MAX_ERROR_DISCONNECT { - error!("Error threshold reached; disconnecting"); - bail!("Error threshold reached"); - } - let error_json = &serde_json::to_string::( - &UpdateDeserializationErrorResponse { - error: format!("failed to parse binary update: {err}"), - } - .into(), - )?; - send_text(&mut ws_sender, error_json).await?; - continue; - } + )?; + + FeedUpdate { + feed_id: Some(data.price_feed_id.0), + source_timestamp: data.source_timestamp_us.into(), + update: Some(Update::FundingRateUpdate(FundingRateUpdate { + price: data.price.map(|p| p.0.get()), + rate: data.funding_rate.map(|r| r.0), + ..FundingRateUpdate::default() + })), + special_fields: Default::default(), } } }; lazer_publisher.push_feed_update(feed_update).await?; - } + Ok(None) + }) } diff --git a/apps/pyth-lazer-agent/src/relayer_session.rs b/apps/pyth-lazer-agent/src/relayer_session.rs index d89d8b140f..373c67066e 100644 --- a/apps/pyth-lazer-agent/src/relayer_session.rs +++ b/apps/pyth-lazer-agent/src/relayer_session.rs @@ -149,7 +149,7 @@ impl RelayerSessionTask { msg = relayer_ws_receiver.next() => { match msg { Some(Ok(msg)) => { - tracing::debug!("Received message from relayer: {msg:?}"); + tracing::debug!("Received a message from relayer: {msg:?}"); } Some(Err(e)) => { tracing::error!("Error receiving message from at relayer: {e:?}"); @@ -165,6 +165,7 @@ impl RelayerSessionTask { } } +//noinspection DuplicatedCode #[cfg(test)] mod tests { use crate::relayer_session::RelayerSessionTask; @@ -215,7 +216,7 @@ mod tests { while let Some(msg) = read.next().await { if let Ok(msg) = msg { if msg.is_binary() { - tracing::info!("Received binary message: {msg:?}"); + tracing::info!("Received a binary message: {msg:?}"); let transaction = SignedLazerTransaction::parse_from_bytes(msg.into_data().as_ref()) .unwrap(); diff --git a/lazer/Cargo.lock b/lazer/Cargo.lock index 31dcee0703..c406ddadfa 100644 --- a/lazer/Cargo.lock +++ b/lazer/Cargo.lock @@ -2373,6 +2373,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + [[package]] name = "hyper" version = "0.14.31" @@ -3872,6 +3882,7 @@ dependencies = [ "derive_more", "ed25519-dalek 2.1.1", "hex", + "humantime-serde", "itertools 0.13.0", "libsecp256k1 0.7.1", "protobuf", diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index c8a0a8fcda..fd7dc153de 100644 --- a/lazer/sdk/rust/protocol/Cargo.toml +++ b/lazer/sdk/rust/protocol/Cargo.toml @@ -16,6 +16,7 @@ itertools = "0.13.0" rust_decimal = "1.36.0" base64 = "0.22.1" protobuf = "3.7.2" +humantime-serde = "1.1.1" [dev-dependencies] bincode = "1.3.3" From f1e15e1e630cfbade6f51e6a4bd80edb19218eb0 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Mon, 7 Jul 2025 16:35:27 +0100 Subject: [PATCH 03/19] add missing dep --- Cargo.lock | 2 +- lazer/sdk/rust/protocol/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17075d2611..5219c1fe64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5600,13 +5600,13 @@ version = "0.7.3" dependencies = [ "alloy-primitives 0.8.25", "anyhow", - "base64 0.22.1", "bincode 1.3.3", "bs58", "byteorder", "derive_more 1.0.0", "ed25519-dalek 2.1.1", "hex", + "humantime-serde", "itertools 0.13.0", "libsecp256k1 0.7.2", "protobuf", diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index 75ba2f0d08..1a467f3cc9 100644 --- a/lazer/sdk/rust/protocol/Cargo.toml +++ b/lazer/sdk/rust/protocol/Cargo.toml @@ -14,8 +14,8 @@ serde_json = "1.0" derive_more = { version = "1.0.0", features = ["from"] } itertools = "0.13.0" rust_decimal = "1.36.0" -base64 = "0.22.1" protobuf = "3.7.2" +humantime-serde = "1.1.1" [dev-dependencies] bincode = "1.3.3" From a42a994afbbb101771b0664c4b92864b2a78815b Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Mon, 7 Jul 2025 16:43:10 +0100 Subject: [PATCH 04/19] make clippy happy --- Cargo.lock | 36 +++++++++++++-------------- lazer/publisher_sdk/rust/src/lib.rs | 2 +- lazer/sdk/rust/protocol/src/jrpc.rs | 4 +-- lazer/sdk/rust/protocol/src/router.rs | 14 +++++------ 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5219c1fe64..7e454154c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5539,8 +5539,8 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol 0.7.2", - "pyth-lazer-publisher-sdk 0.1.5", + "pyth-lazer-protocol 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-publisher-sdk 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "soketto", @@ -5579,15 +5579,19 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9bdf4e2ba853a8b437309487542e742c7d094d8db189db194cb538f2be02ecd" +version = "0.7.3" dependencies = [ + "alloy-primitives 0.8.25", "anyhow", - "base64 0.22.1", + "bincode 1.3.3", + "bs58", "byteorder", "derive_more 1.0.0", + "ed25519-dalek 2.1.1", + "hex", + "humantime-serde", "itertools 0.13.0", + "libsecp256k1 0.7.2", "protobuf", "rust_decimal", "serde", @@ -5597,18 +5601,14 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6445dc5d2f7fff7c677fb8edc5a080a82ef7583c1bdb39daa95421788c23f695" dependencies = [ - "alloy-primitives 0.8.25", "anyhow", - "bincode 1.3.3", - "bs58", + "base64 0.22.1", "byteorder", "derive_more 1.0.0", - "ed25519-dalek 2.1.1", - "hex", - "humantime-serde", "itertools 0.13.0", - "libsecp256k1 0.7.2", "protobuf", "rust_decimal", "serde", @@ -5617,16 +5617,14 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e633db28ca38210de8ab3e99d5bd85ad8cae08a08bb0292506340ee9d1c718" +version = "0.1.6" dependencies = [ "anyhow", "fs-err", "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.7.2", + "pyth-lazer-protocol 0.7.3", "serde-value", "tracing", ] @@ -5634,13 +5632,15 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6ef4052ebf2a7943259b3d52a10b2231ffc346717735c50e44d73fe92019d5" dependencies = [ "anyhow", "fs-err", "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.7.3", + "pyth-lazer-protocol 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde-value", "tracing", ] diff --git a/lazer/publisher_sdk/rust/src/lib.rs b/lazer/publisher_sdk/rust/src/lib.rs index 1a565e0800..1dd1f350f2 100644 --- a/lazer/publisher_sdk/rust/src/lib.rs +++ b/lazer/publisher_sdk/rust/src/lib.rs @@ -195,7 +195,7 @@ impl From for Update { UpdateParams::FundingRateUpdate { price, rate } => { Update::FundingRateUpdate(FundingRateUpdate { price: Some(price.0.into()), - rate: Some(rate.0.into()), + rate: Some(rate.0), special_fields: Default::default(), }) } diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index be5459aaf6..4747071c53 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -1,6 +1,6 @@ -use std::time::Duration; use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; use serde::{Deserialize, Serialize}; +use std::time::Duration; #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct PythLazerAgentJrpcV1 { @@ -65,7 +65,7 @@ pub struct JrpcResponse { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct ErrorResponse { - pub message: String + pub message: String, } #[derive(Serialize, Deserialize)] diff --git a/lazer/sdk/rust/protocol/src/router.rs b/lazer/sdk/rust/protocol/src/router.rs index 41d7bf7dc7..29dda4f298 100644 --- a/lazer/sdk/rust/protocol/src/router.rs +++ b/lazer/sdk/rust/protocol/src/router.rs @@ -38,23 +38,23 @@ impl TryFrom<&Timestamp> for TimestampUs { } } -impl Into for TimestampUs { - fn into(self) -> Timestamp { +impl From for Timestamp { + fn from(value: TimestampUs) -> Self { Timestamp { #[allow( clippy::cast_possible_wrap, reason = "u64 to i64 after this division can never overflow because the value cannot be too big" )] - seconds: (self.0 / 1_000_000) as i64, - nanos: (self.0 % 1_000_000) as i32 * 1000, + seconds: (value.0 / 1_000_000) as i64, + nanos: (value.0 % 1_000_000) as i32 * 1000, special_fields: Default::default(), } } } -impl Into> for TimestampUs { - fn into(self) -> MessageField { - MessageField::some(self.into()) +impl From for MessageField { + fn from(value: TimestampUs) -> Self { + MessageField::some(value.into()) } } From ca3c7a1af4577e59a90fbb78b8355642fe7c588b Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 00:49:20 +0100 Subject: [PATCH 05/19] simplify the ws socket handling --- apps/pyth-lazer-agent/config/config.toml | 5 +- apps/pyth-lazer-agent/src/config.rs | 1 + apps/pyth-lazer-agent/src/http_server.rs | 132 ++----- apps/pyth-lazer-agent/src/jrpc_handle.rs | 372 ++++++++++++++++-- apps/pyth-lazer-agent/src/lazer_publisher.rs | 1 + apps/pyth-lazer-agent/src/publisher_handle.rs | 179 +++++++-- apps/pyth-lazer-agent/src/relayer_session.rs | 1 + 7 files changed, 502 insertions(+), 189 deletions(-) diff --git a/apps/pyth-lazer-agent/config/config.toml b/apps/pyth-lazer-agent/config/config.toml index d75a708880..c87a5f3e25 100644 --- a/apps/pyth-lazer-agent/config/config.toml +++ b/apps/pyth-lazer-agent/config/config.toml @@ -1,4 +1,5 @@ -relayer_urls = ["ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction", "ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction"] -publish_keypair_path = "/path/to/solana/id.json" +relayer_urls = ["ws://localhost:10001"] +publish_keypair_path = "/Users/bartplatak/workspace/pyth-crosschain/apps/pyth-lazer-agent/config/test_keypair.json" listen_address = "0.0.0.0:8910" publish_interval_duration = "25ms" +authorization_token="token1" diff --git a/apps/pyth-lazer-agent/src/config.rs b/apps/pyth-lazer-agent/src/config.rs index 57b0652be7..8ffb990eb5 100644 --- a/apps/pyth-lazer-agent/src/config.rs +++ b/apps/pyth-lazer-agent/src/config.rs @@ -17,6 +17,7 @@ pub struct Config { pub publish_keypair_path: PathBuf, #[serde(with = "humantime_serde", default = "default_publish_interval")] pub publish_interval_duration: Duration, + pub history_service_url: Option } fn default_publish_interval() -> Duration { diff --git a/apps/pyth-lazer-agent/src/http_server.rs b/apps/pyth-lazer-agent/src/http_server.rs index a0923b3b6e..f35756aa58 100644 --- a/apps/pyth-lazer-agent/src/http_server.rs +++ b/apps/pyth-lazer-agent/src/http_server.rs @@ -1,29 +1,23 @@ -use crate::jrpc_handle::jrpc_handler_inner; -use crate::publisher_handle::publisher_inner_handler; -use crate::websocket_utils::{handle_websocket_error, send_text}; +use crate::jrpc_handle::{JrpcConnectionContext, handle_jrpc}; +use crate::publisher_handle::handle_publisher; use crate::{ - config::Config, lazer_publisher::LazerPublisher, publisher_handle::PublisherConnectionContext, + config::Config, http_server, lazer_publisher::LazerPublisher, + publisher_handle::PublisherConnectionContext, }; -use anyhow::{Context, Result, bail}; -use futures_util::io::{BufReader, BufWriter}; +use anyhow::{Context, Result}; use hyper::body::Incoming; use hyper::{Response, StatusCode, body::Bytes, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; -use pyth_lazer_protocol::publisher::{ServerResponse, UpdateDeserializationErrorResponse}; use soketto::{ BoxedError, handshake::http::{Server, is_upgrade_request}, }; use std::fmt::Debug; -use std::pin::Pin; use std::{io, net::SocketAddr}; use tokio::net::{TcpListener, TcpStream}; -use tokio::{pin, select}; -use tokio_util::compat::TokioAsyncReadCompatExt; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, info, instrument, warn}; type FullBody = http_body_util::Full; -pub type InnerHandlerResult = Pin>> + Send>>; #[derive(Debug, Copy, Clone)] pub enum PublisherRequest { @@ -53,8 +47,10 @@ pub async fn run(config: Config, lazer_publisher: LazerPublisher) -> Result<()> let stream_addr = listener.accept().await; let lazer_publisher_clone = lazer_publisher.clone(); let config = config.clone(); - tokio::spawn(async { - if let Err(err) = try_handle_connection(config, stream_addr, lazer_publisher_clone).await { + tokio::spawn(async move { + if let Err(err) = + try_handle_connection(config, stream_addr, lazer_publisher_clone).await + { warn!("error while handling connection: {err:?}"); } }); @@ -74,7 +70,12 @@ async fn try_handle_connection( TokioIo::new(stream), service_fn(move |r| { let request = RelayerRequest(r); - request_handler(config.clone(), request, remote_addr, lazer_publisher.clone()) + request_handler( + config.clone(), + request, + remote_addr, + lazer_publisher.clone(), + ) }), ) .with_upgrades() @@ -136,27 +137,23 @@ async fn request_handler( request_type: publisher_request_type, _remote_addr: remote_addr, }; - - tokio::spawn(handle_ws( - config, + tokio::spawn(handle_publisher( server, request.0, - lazer_publisher, publisher_connection_context, - publisher_inner_handler, + lazer_publisher, )); Ok(response.map(|()| FullBody::default())) } Request::JrpcV1 => { - tokio::spawn(handle_ws( - config, + let publisher_connection_context = JrpcConnectionContext {}; + tokio::spawn(handle_jrpc( + config.clone(), server, request.0, + publisher_connection_context, lazer_publisher, - (), - jrpc_handler_inner, )); - Ok(response.map(|()| FullBody::default())) } } @@ -170,88 +167,3 @@ async fn request_handler( } } } - -#[instrument( - skip(server, request, lazer_publisher), - fields(component = "publisher_ws") -)] -async fn handle_ws( - config: Config, - server: Server, - request: http::Request, - lazer_publisher: LazerPublisher, - context: T, - inner_handler: fn(Config, Vec, LazerPublisher, T) -> InnerHandlerResult, -) { - if let Err(err) = try_handle_ws(config, server, request, lazer_publisher, context, inner_handler).await - { - handle_websocket_error(err); - } -} - -#[instrument( - skip(server, request, lazer_publisher), - fields(component = "publisher_ws") -)] -async fn try_handle_ws( - config: Config, - server: Server, - request: http::Request, - lazer_publisher: LazerPublisher, - context: T, - inner_handler: fn(Config, Vec, LazerPublisher, T) -> InnerHandlerResult, -) -> Result<()> { - let stream = hyper::upgrade::on(request).await?; - let io = TokioIo::new(stream); - let stream = BufReader::new(BufWriter::new(io.compat())); - let (mut ws_sender, mut ws_receiver) = server.into_builder(stream).finish(); - - let mut receive_buf = Vec::new(); - - let mut error_count = 0u32; - const MAX_ERROR_LOG: u32 = 10u32; - const MAX_ERROR_DISCONNECT: u32 = 100u32; - - loop { - receive_buf.clear(); - { - // soketto is not cancel-safe, so we need to store the future and poll it - // in the inner loop. - let receive = async { ws_receiver.receive(&mut receive_buf).await }; - pin!(receive); - loop { - select! { - _result = &mut receive => { - break - } - } - } - } - - match inner_handler(config.clone(), receive_buf.clone(), lazer_publisher.clone(), context).await { - Ok(response) => { - if let Some(response) = response { - send_text(&mut ws_sender, &response).await?; - } - } - Err(err) => { - error_count += 1; - if error_count <= MAX_ERROR_LOG { - warn!("Error decoding message error: {err}"); - } - if error_count >= MAX_ERROR_DISCONNECT { - error!("Error threshold reached; disconnecting"); - bail!("Error threshold reached"); - } - let error_json = &serde_json::to_string::( - &UpdateDeserializationErrorResponse { - error: format!("failed to parse a binary message: {err}"), - } - .into(), - )?; - send_text(&mut ws_sender, error_json).await?; - continue; - } - } - } -} diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index 9600dd535a..54905100bc 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -1,59 +1,345 @@ -use anyhow::{bail, Error}; -use futures_util::future::err; -use reqwest::Client; -use pyth_lazer_protocol::jrpc::{GetMetadataParams, JrpcResponse, JsonRpcVersion, JrpcParams, PythLazerAgentJrpcV1}; use crate::config::Config; -use crate::http_server::InnerHandlerResult; use crate::lazer_publisher::LazerPublisher; +use crate::websocket_utils::{handle_websocket_error, send_text}; +use anyhow::Error; +use futures::{AsyncRead, AsyncWrite}; +use futures_util::io::{BufReader, BufWriter}; +use hyper_util::rt::TokioIo; +use pyth_lazer_protocol::jrpc::{ + Filter, JrpcError, JrpcErrorResponse, JrpcParams, JrpcResponse, JrpcSuccessResponse, + JsonRpcVersion, PythLazerAgentJrpcV1, SymbolMetadata, +}; +use serde::{Deserialize, Serialize}; +use soketto::Sender; +use soketto::handshake::http::Server; +use std::str::FromStr; +use tokio::{pin, select}; +use tokio_util::compat::TokioAsyncReadCompatExt; +use tracing::field::debug; +use tracing::{debug, instrument}; +use url::Url; -pub fn jrpc_handler_inner(config: Config, receive_buf: Vec, lazer_publisher: LazerPublisher, _context: ()) -> InnerHandlerResult { - Box::pin(async move { - let (data, _) = bincode::serde::decode_from_slice::( - &receive_buf, - bincode::config::legacy(), - )?; - - match data.params { - JrpcParams::SendUpdates(update_params) => { - lazer_publisher - .push_feed_update(update_params.into()) - .await?; +const DEFAULT_HISTORY_SERVICE_URL: &str = + "https://history.pyth-lazer.dourolabs.app/history/v1/symbols"; + +pub struct JrpcConnectionContext {} + +#[instrument( + skip(server, request, lazer_publisher, context), + fields(component = "jrpc_ws") +)] +pub async fn handle_jrpc( + config: Config, + server: Server, + request: hyper::Request, + context: JrpcConnectionContext, + lazer_publisher: LazerPublisher, +) { + if let Err(err) = try_handle_jrpc(config, server, request, context, lazer_publisher).await { + handle_websocket_error(err); + } +} + +#[instrument( + skip(server, request, lazer_publisher, context), + fields(component = "jrpc_ws") +)] +async fn try_handle_jrpc( + config: Config, + server: Server, + request: hyper::Request, + context: JrpcConnectionContext, + lazer_publisher: LazerPublisher, +) -> anyhow::Result<()> { + let stream = hyper::upgrade::on(request).await?; + let io = TokioIo::new(stream); + let stream = BufReader::new(BufWriter::new(io.compat())); + let (mut ws_sender, mut ws_receiver) = server.into_builder(stream).finish(); - return Ok(Some(serde_json::to_string::>(&JrpcResponse { - jsonrpc: JsonRpcVersion::V2, - result: (), - id: data.id, - })?)) + let mut receive_buf = Vec::new(); + + loop { + receive_buf.clear(); + { + // soketto is not cancel-safe, so we need to store the future and poll it + // in the inner loop. + let receive = async { ws_receiver.receive(&mut receive_buf).await }; + pin!(receive); + #[allow(clippy::never_loop)] // false positive + loop { + select! { + _result = &mut receive => { + break + } + } } - JrpcParams::GetMetadata(params) => { - match get_metadata(config, params).await { - Ok(result) => { - return Ok(Some(serde_json::to_string::>(&JrpcResponse { + } + + match handle_jrpc_inner(&config, &mut ws_sender, &mut receive_buf, &lazer_publisher).await { + Ok(_) => {} + Err(err) => { + debug!("Error handling JRPC request: {}", err); + send_text( + &mut ws_sender, + serde_json::to_string::>(&JrpcResponse::Error( + JrpcErrorResponse { jsonrpc: JsonRpcVersion::V2, - result: result, - id: data.id, - })?)) + error: JrpcError::InternalError.into(), + id: None, + }, + ))? + .as_str(), + ) + .await?; + } + } + } +} + +async fn handle_jrpc_inner( + config: &Config, + sender: &mut Sender, + receive_buf: &mut Vec, + lazer_publisher: &LazerPublisher, +) -> anyhow::Result<()> { + match serde_json::from_slice::(receive_buf.as_slice()) { + Ok(jrpc_request) => match jrpc_request.params { + JrpcParams::SendUpdates(request_params) => { + match lazer_publisher + .push_feed_update(request_params.into()) + .await + { + Ok(_) => { + send_text( + sender, + serde_json::to_string::>(&JrpcResponse::Success( + JrpcSuccessResponse:: { + jsonrpc: JsonRpcVersion::V2, + result: "success".to_string(), + id: jrpc_request.id, + }, + ))? + .as_str(), + ) + .await?; + } + Err(err) => { + debug!("error while sending updates: {:?}", err); + send_text( + sender, + serde_json::to_string::>(&JrpcResponse::Error( + JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcError::InternalError.into(), + id: Some(jrpc_request.id), + }, + ))? + .as_str(), + ) + .await?; } - Err(_) => {} } } + JrpcParams::GetMetadata(request_params) => match get_metadata(config.clone()).await { + Ok(symbols) => { + let mut symbols = symbols.clone(); + + if request_params.filters.is_some() { + symbols = filter_symbols(symbols, request_params.filters.unwrap()); + } + + send_text( + sender, + serde_json::to_string::>>( + &JrpcResponse::Success(JrpcSuccessResponse::> { + jsonrpc: JsonRpcVersion::V2, + result: symbols, + id: jrpc_request.id, + }), + )? + .as_str(), + ) + .await?; + } + Err(err) => { + debug!("error while retrieving metadata: {:?}", err); + send_text( + sender, + serde_json::to_string::>(&JrpcResponse::Error( + JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + // note: right now specifying an invalid method results in a parse error + error: JrpcError::ParseError.into(), + id: None, + }, + ))? + .as_str(), + ) + .await?; + } + }, + }, + Err(err) => { + debug!("Error parsing JRPC request: {}", err); + send_text( + sender, + serde_json::to_string::>(&JrpcResponse::Error( + JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcError::ParseError.into(), + id: None, + }, + ))? + .as_str(), + ) + .await?; } + } + Ok(()) +} - Ok(None) - }) +async fn get_metadata(config: Config) -> Result, anyhow::Error> { + let result = reqwest::get( + config + .history_service_url + .unwrap_or(Url::from_str(DEFAULT_HISTORY_SERVICE_URL)?), + ) + .await?; + + if result.status().is_success() { + Ok(serde_json::from_str::>( + &result.text().await?, + )?) + } else { + Err(anyhow::anyhow!( + "Error getting metadata (status_code={}, body={})", + result.status(), + result.text().await.unwrap_or("none".to_string()) + )) + } } -async fn get_metadata(config: Config, params: GetMetadataParams) -> Result, anyhow::Error> { - let client = Client::new(); +fn filter_symbols(symbols: Vec, filters: Vec) -> Vec { + let res: Vec = symbols + .into_iter() + .filter(|symbol| { + for filter in &filters { + if filter.name.is_some() && filter.name.clone().unwrap() != symbol.name { + return false; + } + if filter.asset_type.is_some() + && filter.asset_type.clone().unwrap() != symbol.asset_type + { + return false; + } + } + + return true; + }) + .collect(); + + res +} - let resp = client - .post("https://httpbin.org/post") - .json(&payload) - .send() - .await? - .json::() - .await?; +#[cfg(test)] +pub mod tests { + use super::*; + use pyth_lazer_protocol::router::{Channel, FixedRate, PriceFeedId}; + use pyth_lazer_protocol::symbol_state::SymbolState; + use std::net::SocketAddr; - let - Ok(None) + fn gen_test_symbol(name: String, asset_type: String) -> SymbolMetadata { + SymbolMetadata { + pyth_lazer_id: PriceFeedId(1), + name, + symbol: "".to_string(), + description: "".to_string(), + asset_type, + exponent: 0, + cmc_id: None, + funding_rate_interval: None, + min_publishers: 0, + min_channel: Channel::FixedRate(FixedRate::MIN), + state: SymbolState::Stable, + hermes_id: None, + quote_currency: None, + } + } + + #[tokio::test] + #[ignore] + async fn test_try_get_metadata() { + let config = Config { + listen_address: SocketAddr::from(([127, 0, 0, 1], 0)), + relayer_urls: vec![], + authorization_token: None, + publish_keypair_path: Default::default(), + publish_interval_duration: Default::default(), + history_service_url: None, + }; + + println!("{:?}", get_metadata(config).await.unwrap()); + } + + #[test] + fn test_filter_symbols() { + let symbol1 = gen_test_symbol("BTC".to_string(), "crypto".to_string()); + let symbol2 = gen_test_symbol("XMR".to_string(), "crypto".to_string()); + let symbol3 = gen_test_symbol("BTCUSDT".to_string(), "funding-rate".to_string()); + let symbols = vec![symbol1.clone(), symbol2.clone(), symbol3.clone()]; + + // just a name filter + assert_eq!( + filter_symbols( + symbols.clone(), + vec![Filter { + name: Some("XMR".to_string()), + asset_type: None, + }], + ), + vec![symbol2.clone()] + ); + + // just an asset type filter + assert_eq!( + filter_symbols( + symbols.clone(), + vec![Filter { + name: None, + asset_type: Some("crypto".to_string()), + }], + ), + vec![symbol1.clone(), symbol2.clone()] + ); + + // name and asset type + assert_eq!( + filter_symbols( + symbols.clone(), + vec![Filter { + name: Some("BTC".to_string()), + asset_type: Some("crypto".to_string()), + }], + ), + vec![symbol1.clone()] + ); + + // and as two separate filters + assert_eq!( + filter_symbols( + symbols.clone(), + vec![ + Filter { + name: None, + asset_type: Some("crypto".to_string()), + }, + Filter { + name: Some("BTC".to_string()), + asset_type: None, + } + ], + ), + vec![symbol1] + ); + } } diff --git a/apps/pyth-lazer-agent/src/lazer_publisher.rs b/apps/pyth-lazer-agent/src/lazer_publisher.rs index b275489b88..72a3520728 100644 --- a/apps/pyth-lazer-agent/src/lazer_publisher.rs +++ b/apps/pyth-lazer-agent/src/lazer_publisher.rs @@ -223,6 +223,7 @@ mod tests { authorization_token: None, publish_keypair_path: PathBuf::from(signing_key_file.path()), publish_interval_duration: Duration::from_millis(25), + history_service_url: None, }; let (relayer_sender, mut relayer_receiver) = broadcast::channel(CHANNEL_CAPACITY); diff --git a/apps/pyth-lazer-agent/src/publisher_handle.rs b/apps/pyth-lazer-agent/src/publisher_handle.rs index 672d37a6ac..59497b96d0 100644 --- a/apps/pyth-lazer-agent/src/publisher_handle.rs +++ b/apps/pyth-lazer-agent/src/publisher_handle.rs @@ -1,62 +1,173 @@ use std::net::SocketAddr; -use pyth_lazer_protocol::publisher::{PriceFeedDataV1, PriceFeedDataV2}; +use anyhow::bail; +use futures_util::io::{BufReader, BufWriter}; +use hyper_util::rt::TokioIo; +use protobuf::MessageField; +use protobuf::well_known_types::timestamp::Timestamp; +use pyth_lazer_protocol::publisher::{ + PriceFeedDataV1, PriceFeedDataV2, ServerResponse, UpdateDeserializationErrorResponse, +}; use pyth_lazer_publisher_sdk::publisher_update::feed_update::Update; use pyth_lazer_publisher_sdk::publisher_update::{FeedUpdate, FundingRateUpdate, PriceUpdate}; +use soketto::handshake::http::Server; +use tokio::{pin, select}; +use tokio_util::compat::TokioAsyncReadCompatExt; +use tracing::{error, instrument, warn}; -use crate::{http_server, lazer_publisher::LazerPublisher}; -use crate::config::Config; -use crate::http_server::InnerHandlerResult; +use crate::{ + http_server, + lazer_publisher::LazerPublisher, + websocket_utils::{handle_websocket_error, send_text}, +}; -#[derive(Debug, Copy, Clone)] pub struct PublisherConnectionContext { pub request_type: http_server::PublisherRequest, pub _remote_addr: SocketAddr, } +#[instrument( + skip(server, request, lazer_publisher, context), + fields(component = "publisher_ws") +)] +pub async fn handle_publisher( + server: Server, + request: hyper::Request, + context: PublisherConnectionContext, + lazer_publisher: LazerPublisher, +) { + if let Err(err) = try_handle_publisher(server, request, context, lazer_publisher).await { + handle_websocket_error(err); + } +} + +#[instrument( + skip(server, request, lazer_publisher, context), + fields(component = "publisher_ws") +)] +async fn try_handle_publisher( + server: Server, + request: hyper::Request, + context: PublisherConnectionContext, + lazer_publisher: LazerPublisher, +) -> anyhow::Result<()> { + let stream = hyper::upgrade::on(request).await?; + let io = TokioIo::new(stream); + let stream = BufReader::new(BufWriter::new(io.compat())); + let (mut ws_sender, mut ws_receiver) = server.into_builder(stream).finish(); + + let mut receive_buf = Vec::new(); + + let mut error_count = 0u32; + const MAX_ERROR_LOG: u32 = 10u32; + const MAX_ERROR_DISCONNECT: u32 = 100u32; + + loop { + receive_buf.clear(); + { + // soketto is not cancel-safe, so we need to store the future and poll it + // in the inner loop. + let receive = async { ws_receiver.receive(&mut receive_buf).await }; + pin!(receive); + #[allow(clippy::never_loop)] // false positive + loop { + select! { + _result = &mut receive => { + break + } + } + } + } -pub(crate) fn publisher_inner_handler(_: Config, receive_buf: Vec, lazer_publisher: LazerPublisher, context: PublisherConnectionContext) -> InnerHandlerResult { - Box::pin(async move { // reply with an error if we can't parse the binary update let feed_update: FeedUpdate = match context.request_type { http_server::PublisherRequest::PublisherV1 => { - let (data, _) = bincode::serde::decode_from_slice::( + match bincode::serde::decode_from_slice::( &receive_buf, bincode::config::legacy(), - )?; - - FeedUpdate { - feed_id: Some(data.price_feed_id.0), - source_timestamp: data.source_timestamp_us.into(), - update: Some(Update::PriceUpdate(PriceUpdate { - price: data.price.map(|p| p.0.get()), - best_bid_price: data.best_bid_price.map(|p| p.0.get()), - best_ask_price: data.best_ask_price.map(|p| p.0.get()), - ..PriceUpdate::default() - })), - special_fields: Default::default(), + ) { + Ok((data, _)) => { + let source_timestamp = MessageField::some(Timestamp { + seconds: (data.source_timestamp_us.0 / 1_000_000) as i64, + nanos: (data.source_timestamp_us.0 % 1_000_000 * 1000) as i32, + special_fields: Default::default(), + }); + FeedUpdate { + feed_id: Some(data.price_feed_id.0), + source_timestamp, + update: Some(Update::PriceUpdate(PriceUpdate { + price: data.price.map(|p| p.0.get()), + best_bid_price: data.best_bid_price.map(|p| p.0.get()), + best_ask_price: data.best_ask_price.map(|p| p.0.get()), + ..PriceUpdate::default() + })), + special_fields: Default::default(), + } + } + Err(err) => { + error_count += 1; + if error_count <= MAX_ERROR_LOG { + warn!("Error decoding v1 update error: {:?}", err); + } + if error_count >= MAX_ERROR_DISCONNECT { + error!("Error threshold reached; disconnecting",); + bail!("Error threshold reached"); + } + let error_json = &serde_json::to_string::( + &UpdateDeserializationErrorResponse { + error: format!("failed to parse binary update: {err}"), + } + .into(), + )?; + send_text(&mut ws_sender, error_json).await?; + continue; + } } } http_server::PublisherRequest::PublisherV2 => { - let (data, _) = bincode::serde::decode_from_slice::( + match bincode::serde::decode_from_slice::( &receive_buf, bincode::config::legacy(), - )?; - - FeedUpdate { - feed_id: Some(data.price_feed_id.0), - source_timestamp: data.source_timestamp_us.into(), - update: Some(Update::FundingRateUpdate(FundingRateUpdate { - price: data.price.map(|p| p.0.get()), - rate: data.funding_rate.map(|r| r.0), - ..FundingRateUpdate::default() - })), - special_fields: Default::default(), + ) { + Ok((data, _)) => { + let source_timestamp = MessageField::some(Timestamp { + seconds: (data.source_timestamp_us.0 / 1_000_000) as i64, + nanos: (data.source_timestamp_us.0 % 1_000_000 * 1000) as i32, + special_fields: Default::default(), + }); + FeedUpdate { + feed_id: Some(data.price_feed_id.0), + source_timestamp, + update: Some(Update::FundingRateUpdate(FundingRateUpdate { + price: data.price.map(|p| p.0.get()), + rate: data.funding_rate.map(|r| r.0), + ..FundingRateUpdate::default() + })), + special_fields: Default::default(), + } + } + Err(err) => { + error_count += 1; + if error_count <= MAX_ERROR_LOG { + warn!("Error decoding v2 update error: {:?}", err); + } + if error_count >= MAX_ERROR_DISCONNECT { + error!("Error threshold reached; disconnecting"); + bail!("Error threshold reached"); + } + let error_json = &serde_json::to_string::( + &UpdateDeserializationErrorResponse { + error: format!("failed to parse binary update: {err}"), + } + .into(), + )?; + send_text(&mut ws_sender, error_json).await?; + continue; + } } } }; lazer_publisher.push_feed_update(feed_update).await?; - Ok(None) - }) + } } diff --git a/apps/pyth-lazer-agent/src/relayer_session.rs b/apps/pyth-lazer-agent/src/relayer_session.rs index 373c67066e..33423da33b 100644 --- a/apps/pyth-lazer-agent/src/relayer_session.rs +++ b/apps/pyth-lazer-agent/src/relayer_session.rs @@ -35,6 +35,7 @@ async fn connect_to_relayer( HeaderValue::from_str(&format!("Bearer {token}"))?, ); let (ws_stream, _) = connect_async_with_config(req, None, true).await?; + tracing::info!("connected to the relayer at {}", url); Ok(ws_stream.split()) } From 6361828473a2ea9163565f48d4e1bdb414560bf2 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 00:53:20 +0100 Subject: [PATCH 06/19] add error response formats --- lazer/sdk/rust/protocol/src/jrpc.rs | 132 ++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 25 deletions(-) diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index 4747071c53..bccc2869a9 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -1,6 +1,7 @@ -use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; -use serde::{Deserialize, Serialize}; use std::time::Duration; +use crate::router::{Channel, Price, PriceFeedId, Rate, TimestampUs}; +use serde::{Deserialize, Serialize}; +use crate::symbol_state::SymbolState; #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct PythLazerAgentJrpcV1 { @@ -41,13 +42,13 @@ pub enum UpdateParams { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Filter { - name: Option, - asset_type: Option, + pub name: Option, + pub asset_type: Option, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct GetMetadataParams { - filters: Option>, + pub filters: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -57,42 +58,78 @@ pub enum JsonRpcVersion { } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct JrpcResponse { +pub enum JrpcResponse { + Success(JrpcSuccessResponse), + Error(JrpcErrorResponse), +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct JrpcSuccessResponse { pub jsonrpc: JsonRpcVersion, pub result: T, pub id: i64, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ErrorResponse { +pub struct JrpcErrorResponse { + pub jsonrpc: JsonRpcVersion, + pub error: JrpcErrorObject, + pub id: Option, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct JrpcErrorObject { + pub code: i64, pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, } -#[derive(Serialize, Deserialize)] -struct SymbolMetadata { - pub asset_type: String, - pub cmc_id: i64, - pub description: String, - pub exponent: i64, - pub hermes_id: String, - #[serde(default, with = "humantime_serde", alias = "interval")] - pub interval: Option, - pub min_channel: String, - pub min_publishers: i64, +#[derive(Debug, Eq, PartialEq)] +pub enum JrpcError { + ParseError, + InternalError, +} + +impl From for JrpcErrorObject { + fn from(error: JrpcError) -> Self { + match error { + JrpcError::ParseError => JrpcErrorObject { + code: -32700, + message: "Parse error".to_string(), + data: None, + }, + JrpcError::InternalError => JrpcErrorObject { + code: -32603, + message: "Internal error".to_string(), + data: None, + }, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +pub struct SymbolMetadata { + pub pyth_lazer_id: PriceFeedId, pub name: String, - pub pyth_lazer_id: i64, - pub schedule: String, - pub state: String, pub symbol: String, + pub description: String, + pub asset_type: String, + pub exponent: i16, + pub cmc_id: Option, + #[serde(default, with = "humantime_serde", alias = "interval")] + pub funding_rate_interval: Option, + pub min_publishers: u16, + pub min_channel: Channel, + pub state: SymbolState, + pub hermes_id: Option, + pub quote_currency: Option, } #[cfg(test)] mod tests { use crate::jrpc::JrpcParams::{GetMetadata, SendUpdates}; - use crate::jrpc::{ - FeedUpdateParams, Filter, GetMetadataParams, JsonRpcVersion, PythLazerAgentJrpcV1, - UpdateParams, - }; + use crate::jrpc::{FeedUpdateParams, Filter, GetMetadataParams, JrpcErrorObject, JrpcErrorResponse, JrpcSuccessResponse, JsonRpcVersion, PythLazerAgentJrpcV1, UpdateParams}; use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; #[test] @@ -235,4 +272,49 @@ mod tests { expected ); } + + #[test] + fn test_response_format_error() { + let response = serde_json::from_str::( + r#" + { + "jsonrpc": "2.0", + "id": 2, + "error": { + "message": "Internal error", + "code": -32603 + } + } + "# + ).unwrap(); + + assert_eq!(response, JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcErrorObject { + code: -32603, + message: "Internal error".to_string(), + data: None, + }, + id: Some(2), + }); + } + + #[test] + pub fn test_response_format_success() { + let response = serde_json::from_str::>( + r#" + { + "jsonrpc": "2.0", + "id": 2, + "result": "success" + } + "# + ).unwrap(); + + assert_eq!(response, JrpcSuccessResponse:: { + jsonrpc: JsonRpcVersion::V2, + result: "success".to_string(), + id: 2, + }); + } } From 95176458b2da6785d1b972383aecb21df8c76723 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 01:00:50 +0100 Subject: [PATCH 07/19] review --- lazer/sdk/rust/protocol/src/jrpc.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index bccc2869a9..58fcdc02ba 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -7,15 +7,15 @@ use crate::symbol_state::SymbolState; pub struct PythLazerAgentJrpcV1 { pub jsonrpc: JsonRpcVersion, #[serde(flatten)] - pub params: JrpcParams, + pub params: JrpcCall, pub id: i64, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(tag = "method", content = "params")] -pub enum JrpcParams { - #[serde(rename = "send_updates")] - SendUpdates(FeedUpdateParams), +pub enum JrpcCall { + #[serde(rename_all = "snake_case")] + PushUpdate(FeedUpdateParams), #[serde(rename = "get_symbols")] GetMetadata(GetMetadataParams), } @@ -128,7 +128,7 @@ pub struct SymbolMetadata { #[cfg(test)] mod tests { - use crate::jrpc::JrpcParams::{GetMetadata, SendUpdates}; + use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate}; use crate::jrpc::{FeedUpdateParams, Filter, GetMetadataParams, JrpcErrorObject, JrpcErrorResponse, JrpcSuccessResponse, JsonRpcVersion, PythLazerAgentJrpcV1, UpdateParams}; use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; From 1116639da68f8858c5a861573c4ce160c37defc6 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 01:06:49 +0100 Subject: [PATCH 08/19] review --- lazer/sdk/rust/protocol/src/jrpc.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index 58fcdc02ba..11bfd3eb40 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -48,7 +48,8 @@ pub struct Filter { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct GetMetadataParams { - pub filters: Option>, + pub names: Option>, + pub asset_types: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] From 478004dd9bdd69b2113bbbe7c01b1ec3772d1c89 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 16:49:11 +0100 Subject: [PATCH 09/19] fix tests --- Cargo.lock | 11 ++++++++ lazer/sdk/rust/protocol/Cargo.toml | 1 + lazer/sdk/rust/protocol/src/jrpc.rs | 43 +++++++++++------------------ 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e454154c7..95bc4d9a42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4143,6 +4143,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonrpc-types" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d203b1b00d03d1cee9ddcaf970d2ccdd40d8036d8fafef34a0b1b841486b232" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -5591,6 +5601,7 @@ dependencies = [ "hex", "humantime-serde", "itertools 0.13.0", + "jsonrpc-types", "libsecp256k1 0.7.2", "protobuf", "rust_decimal", diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index 1a467f3cc9..70c8a43f9e 100644 --- a/lazer/sdk/rust/protocol/Cargo.toml +++ b/lazer/sdk/rust/protocol/Cargo.toml @@ -16,6 +16,7 @@ itertools = "0.13.0" rust_decimal = "1.36.0" protobuf = "3.7.2" humantime-serde = "1.1.1" +jsonrpc-types = "0.3.3" [dev-dependencies] bincode = "1.3.3" diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index 11bfd3eb40..07b86609d9 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -13,10 +13,9 @@ pub struct PythLazerAgentJrpcV1 { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(tag = "method", content = "params")] +#[serde(rename_all = "snake_case")] pub enum JrpcCall { - #[serde(rename_all = "snake_case")] PushUpdate(FeedUpdateParams), - #[serde(rename = "get_symbols")] GetMetadata(GetMetadataParams), } @@ -134,11 +133,11 @@ mod tests { use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; #[test] - fn test_send_updates_price() { + fn test_push_update_price() { let json = r#" { "jsonrpc": "2.0", - "method": "send_updates", + "method": "push_update", "params": { "feed_id": 1, "source_timestamp": 124214124124, @@ -156,7 +155,7 @@ mod tests { let expected = PythLazerAgentJrpcV1 { jsonrpc: JsonRpcVersion::V2, - params: SendUpdates(FeedUpdateParams { + params: PushUpdate(FeedUpdateParams { feed_id: PriceFeedId(1), source_timestamp: TimestampUs(124214124124), update: UpdateParams::PriceUpdate { @@ -175,11 +174,11 @@ mod tests { } #[test] - fn test_send_updates_funding_rate() { + fn test_push_update_funding_rate() { let json = r#" { "jsonrpc": "2.0", - "method": "send_updates", + "method": "push_update", "params": { "feed_id": 1, "source_timestamp": 124214124124, @@ -196,7 +195,7 @@ mod tests { let expected = PythLazerAgentJrpcV1 { jsonrpc: JsonRpcVersion::V2, - params: SendUpdates(FeedUpdateParams { + params: PushUpdate(FeedUpdateParams { feed_id: PriceFeedId(1), source_timestamp: TimestampUs(124214124124), update: UpdateParams::FundingRateUpdate { @@ -213,16 +212,14 @@ mod tests { ); } #[test] - fn test_send_get_symbols() { + fn test_send_get_metadata() { let json = r#" { "jsonrpc": "2.0", - "method": "get_symbols", + "method": "get_metadata", "params": { - "filters": [ - {"name": "BTC/USD"}, - {"asset_type": "crypto"} - ] + "names": ["BTC/USD"], + "asset_types": ["crypto"] }, "id": 1 } @@ -231,16 +228,8 @@ mod tests { let expected = PythLazerAgentJrpcV1 { jsonrpc: JsonRpcVersion::V2, params: GetMetadata(GetMetadataParams { - filters: Some(vec![ - Filter { - name: Some("BTC/USD".to_string()), - asset_type: None, - }, - Filter { - name: None, - asset_type: Some("crypto".to_string()), - }, - ]), + names: Some(vec!["BTC/USD".to_string()]), + asset_types: Some(vec!["crypto".to_string()]), }), id: 1, }; @@ -252,11 +241,11 @@ mod tests { } #[test] - fn test_get_symbols_without_filters() { + fn test_get_metadata_without_filters() { let json = r#" { "jsonrpc": "2.0", - "method": "get_symbols", + "method": "get_metadata", "params": {}, "id": 1 } @@ -264,7 +253,7 @@ mod tests { let expected = PythLazerAgentJrpcV1 { jsonrpc: JsonRpcVersion::V2, - params: GetMetadata(GetMetadataParams { filters: None }), + params: GetMetadata(GetMetadataParams { names: None, asset_types: None }), id: 1, }; From 3df3efd65217f7f51e50ed53c61edeecf97c768c Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 17:23:21 +0100 Subject: [PATCH 10/19] fix cargo checks --- lazer/sdk/rust/protocol/src/jrpc.rs | 61 +++++++++++++++++------------ 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index 07b86609d9..8f1b98c94b 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -1,7 +1,7 @@ -use std::time::Duration; use crate::router::{Channel, Price, PriceFeedId, Rate, TimestampUs}; -use serde::{Deserialize, Serialize}; use crate::symbol_state::SymbolState; +use serde::{Deserialize, Serialize}; +use std::time::Duration; #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct PythLazerAgentJrpcV1 { @@ -91,6 +91,7 @@ pub enum JrpcError { InternalError, } +// note: error codes can be found in the rfc https://www.jsonrpc.org/specification#error_object impl From for JrpcErrorObject { fn from(error: JrpcError) -> Self { match error { @@ -128,9 +129,8 @@ pub struct SymbolMetadata { #[cfg(test)] mod tests { + use super::*; use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate}; - use crate::jrpc::{FeedUpdateParams, Filter, GetMetadataParams, JrpcErrorObject, JrpcErrorResponse, JrpcSuccessResponse, JsonRpcVersion, PythLazerAgentJrpcV1, UpdateParams}; - use crate::router::{Price, PriceFeedId, Rate, TimestampUs}; #[test] fn test_push_update_price() { @@ -218,8 +218,8 @@ mod tests { "jsonrpc": "2.0", "method": "get_metadata", "params": { - "names": ["BTC/USD"], - "asset_types": ["crypto"] + "names": ["BTC/USD"], + "asset_types": ["crypto"] }, "id": 1 } @@ -253,7 +253,10 @@ mod tests { let expected = PythLazerAgentJrpcV1 { jsonrpc: JsonRpcVersion::V2, - params: GetMetadata(GetMetadataParams { names: None, asset_types: None }), + params: GetMetadata(GetMetadataParams { + names: None, + asset_types: None, + }), id: 1, }; @@ -275,18 +278,22 @@ mod tests { "code": -32603 } } - "# - ).unwrap(); + "#, + ) + .unwrap(); - assert_eq!(response, JrpcErrorResponse { - jsonrpc: JsonRpcVersion::V2, - error: JrpcErrorObject { - code: -32603, - message: "Internal error".to_string(), - data: None, - }, - id: Some(2), - }); + assert_eq!( + response, + JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcErrorObject { + code: -32603, + message: "Internal error".to_string(), + data: None, + }, + id: Some(2), + } + ); } #[test] @@ -298,13 +305,17 @@ mod tests { "id": 2, "result": "success" } - "# - ).unwrap(); + "#, + ) + .unwrap(); - assert_eq!(response, JrpcSuccessResponse:: { - jsonrpc: JsonRpcVersion::V2, - result: "success".to_string(), - id: 2, - }); + assert_eq!( + response, + JrpcSuccessResponse:: { + jsonrpc: JsonRpcVersion::V2, + result: "success".to_string(), + id: 2, + } + ); } } From ea69d88083f9bb879f5662c3b773494fd1cdc51a Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 17:29:39 +0100 Subject: [PATCH 11/19] remove unused dept, make some params optional, bump ver --- Cargo.lock | 41 +++++-------- lazer/sdk/rust/protocol/Cargo.toml | 3 +- lazer/sdk/rust/protocol/src/jrpc.rs | 89 +++++++++++++++++++++++++++-- 3 files changed, 99 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95bc4d9a42..3f84f62804 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4143,16 +4143,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "jsonrpc-types" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d203b1b00d03d1cee9ddcaf970d2ccdd40d8036d8fafef34a0b1b841486b232" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -5549,7 +5539,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-protocol 0.7.3", "pyth-lazer-publisher-sdk 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", @@ -5578,7 +5568,7 @@ dependencies = [ "futures-util", "hex", "libsecp256k1 0.7.2", - "pyth-lazer-protocol 0.7.3", + "pyth-lazer-protocol 0.7.4", "serde", "serde_json", "tokio", @@ -5590,19 +5580,14 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6445dc5d2f7fff7c677fb8edc5a080a82ef7583c1bdb39daa95421788c23f695" dependencies = [ - "alloy-primitives 0.8.25", "anyhow", - "bincode 1.3.3", - "bs58", + "base64 0.22.1", "byteorder", "derive_more 1.0.0", - "ed25519-dalek 2.1.1", - "hex", - "humantime-serde", "itertools 0.13.0", - "jsonrpc-types", - "libsecp256k1 0.7.2", "protobuf", "rust_decimal", "serde", @@ -5611,15 +5596,19 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6445dc5d2f7fff7c677fb8edc5a080a82ef7583c1bdb39daa95421788c23f695" +version = "0.7.4" dependencies = [ + "alloy-primitives 0.8.25", "anyhow", - "base64 0.22.1", + "bincode 1.3.3", + "bs58", "byteorder", "derive_more 1.0.0", + "ed25519-dalek 2.1.1", + "hex", + "humantime-serde", "itertools 0.13.0", + "libsecp256k1 0.7.2", "protobuf", "rust_decimal", "serde", @@ -5635,7 +5624,7 @@ dependencies = [ "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.7.3", + "pyth-lazer-protocol 0.7.4", "serde-value", "tracing", ] @@ -5651,7 +5640,7 @@ dependencies = [ "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-protocol 0.7.3", "serde-value", "tracing", ] diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index 70c8a43f9e..d0ef7aa904 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.7.3" +version = "0.8.0" edition = "2021" description = "Pyth Lazer SDK - protocol types." license = "Apache-2.0" @@ -16,7 +16,6 @@ itertools = "0.13.0" rust_decimal = "1.36.0" protobuf = "3.7.2" humantime-serde = "1.1.1" -jsonrpc-types = "0.3.3" [dev-dependencies] bincode = "1.3.3" diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index 8f1b98c94b..fa3fdce72c 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -32,11 +32,11 @@ pub enum UpdateParams { #[serde(rename = "price")] PriceUpdate { price: Price, - best_bid_price: Price, - best_ask_price: Price, + best_bid_price: Option, + best_ask_price: Option, }, #[serde(rename = "funding_rate")] - FundingRateUpdate { price: Price, rate: Rate }, + FundingRateUpdate { price: Option, rate: Rate }, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -160,8 +160,47 @@ mod tests { source_timestamp: TimestampUs(124214124124), update: UpdateParams::PriceUpdate { price: Price::from_integer(1234567890, 0).unwrap(), - best_bid_price: Price::from_integer(1234567891, 0).unwrap(), - best_ask_price: Price::from_integer(1234567892, 0).unwrap(), + best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()), + best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()), + }, + }), + id: 1, + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } + + #[test] + fn test_push_update_price_without_bid_ask() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "push_update", + "params": { + "feed_id": 1, + "source_timestamp": 124214124124, + + "update": { + "type": "price", + "price": 1234567890 + } + }, + "id": 1 + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: PushUpdate(FeedUpdateParams { + feed_id: PriceFeedId(1), + source_timestamp: TimestampUs(124214124124), + update: UpdateParams::PriceUpdate { + price: Price::from_integer(1234567890, 0).unwrap(), + best_bid_price: None, + best_ask_price: None, }, }), id: 1, @@ -199,7 +238,7 @@ mod tests { feed_id: PriceFeedId(1), source_timestamp: TimestampUs(124214124124), update: UpdateParams::FundingRateUpdate { - price: Price::from_integer(1234567890, 0).unwrap(), + price: Some(Price::from_integer(1234567890, 0).unwrap()), rate: Rate::from_integer(1234567891, 0).unwrap(), }, }), @@ -211,6 +250,44 @@ mod tests { expected ); } + #[test] + fn test_push_update_funding_rate_without_price() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "push_update", + "params": { + "feed_id": 1, + "source_timestamp": 124214124124, + + "update": { + "type": "funding_rate", + "rate": 1234567891 + } + }, + "id": 1 + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: PushUpdate(FeedUpdateParams { + feed_id: PriceFeedId(1), + source_timestamp: TimestampUs(124214124124), + update: UpdateParams::FundingRateUpdate { + price: None, + rate: Rate::from_integer(1234567891, 0).unwrap(), + }, + }), + id: 1, + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } + #[test] fn test_send_get_metadata() { let json = r#" From 9b40549fcad312fd5f63bbcd126dad41511552cc Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 17:41:25 +0100 Subject: [PATCH 12/19] remove unused dept, make some params optional, bump ver --- Cargo.lock | 6 +++--- .../solana/programs/pyth-lazer-solana-contract/Cargo.toml | 2 +- lazer/publisher_sdk/rust/Cargo.toml | 2 +- lazer/publisher_sdk/rust/src/lib.rs | 6 +++--- lazer/sdk/rust/client/Cargo.toml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f84f62804..47aa66fc91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5568,7 +5568,7 @@ dependencies = [ "futures-util", "hex", "libsecp256k1 0.7.2", - "pyth-lazer-protocol 0.7.4", + "pyth-lazer-protocol 0.8.0", "serde", "serde_json", "tokio", @@ -5596,7 +5596,7 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.7.4" +version = "0.8.0" dependencies = [ "alloy-primitives 0.8.25", "anyhow", @@ -5624,7 +5624,7 @@ dependencies = [ "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.7.4", + "pyth-lazer-protocol 0.8.0", "serde-value", "tracing", ] diff --git a/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml index aa3e2a9f85..4888f6669e 100644 --- a/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml +++ b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml @@ -19,7 +19,7 @@ no-log-ix-name = [] idl-build = ["anchor-lang/idl-build"] [dependencies] -pyth-lazer-protocol = { path = "../../../../sdk/rust/protocol", version = "0.7.2" } +pyth-lazer-protocol = { path = "../../../../sdk/rust/protocol", version = "0.8.0" } anchor-lang = "0.30.1" bytemuck = "1.20.0" diff --git a/lazer/publisher_sdk/rust/Cargo.toml b/lazer/publisher_sdk/rust/Cargo.toml index 13f8bb6c61..25c00f42c3 100644 --- a/lazer/publisher_sdk/rust/Cargo.toml +++ b/lazer/publisher_sdk/rust/Cargo.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" repository = "https://github.com/pyth-network/pyth-crosschain" [dependencies] -pyth-lazer-protocol = { version = "0.7.2", path = "../../sdk/rust/protocol" } +pyth-lazer-protocol = { version = "0.8.0", path = "../../sdk/rust/protocol" } anyhow = "1.0.98" protobuf = "3.7.2" serde-value = "0.7.0" diff --git a/lazer/publisher_sdk/rust/src/lib.rs b/lazer/publisher_sdk/rust/src/lib.rs index 1dd1f350f2..d027371f97 100644 --- a/lazer/publisher_sdk/rust/src/lib.rs +++ b/lazer/publisher_sdk/rust/src/lib.rs @@ -188,13 +188,13 @@ impl From for Update { best_ask_price, } => Update::PriceUpdate(PriceUpdate { price: Some(price.0.into()), - best_bid_price: Some(best_bid_price.0.into()), - best_ask_price: Some(best_ask_price.0.into()), + best_bid_price: best_bid_price.map(|p| p.0.into()), + best_ask_price: best_ask_price.map(|p| p.0.into()), special_fields: Default::default(), }), UpdateParams::FundingRateUpdate { price, rate } => { Update::FundingRateUpdate(FundingRateUpdate { - price: Some(price.0.into()), + price: price.map(|p| p.0.into()), rate: Some(rate.0), special_fields: Default::default(), }) diff --git a/lazer/sdk/rust/client/Cargo.toml b/lazer/sdk/rust/client/Cargo.toml index 4670ea301b..405556457f 100644 --- a/lazer/sdk/rust/client/Cargo.toml +++ b/lazer/sdk/rust/client/Cargo.toml @@ -6,7 +6,7 @@ description = "A Rust client for Pyth Lazer" license = "Apache-2.0" [dependencies] -pyth-lazer-protocol = { path = "../protocol", version = "0.7.2" } +pyth-lazer-protocol = { path = "../protocol", version = "0.8.0" } tokio = { version = "1", features = ["full"] } tokio-tungstenite = { version = "0.20", features = ["native-tls"] } futures-util = "0.3" From 6ca713f3d074457b387c5a2f64bdadcb00b63b2b Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 18:51:31 +0100 Subject: [PATCH 13/19] update protocol to 0.8.1 --- Cargo.lock | 134 ++++++++++++++++------- apps/pyth-lazer-agent/Cargo.toml | 2 +- apps/pyth-lazer-agent/src/http_server.rs | 2 +- apps/pyth-lazer-agent/src/jrpc_handle.rs | 85 ++++++-------- 4 files changed, 125 insertions(+), 98 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc6a2f6bc6..18d3294c1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -3383,6 +3389,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -3659,7 +3684,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -3682,6 +3707,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -3737,6 +3763,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.14" @@ -3756,9 +3798,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -5540,8 +5584,9 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol 0.7.3", - "pyth-lazer-publisher-sdk 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-protocol", + "pyth-lazer-publisher-sdk", + "reqwest 0.12.22", "serde", "serde_json", "soketto", @@ -5569,7 +5614,7 @@ dependencies = [ "futures-util", "hex", "libsecp256k1 0.7.2", - "pyth-lazer-protocol 0.8.1", + "pyth-lazer-protocol", "serde", "serde_json", "tokio", @@ -5578,23 +5623,6 @@ dependencies = [ "url", ] -[[package]] -name = "pyth-lazer-protocol" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6445dc5d2f7fff7c677fb8edc5a080a82ef7583c1bdb39daa95421788c23f695" -dependencies = [ - "anyhow", - "base64 0.22.1", - "byteorder", - "derive_more 1.0.0", - "itertools 0.13.0", - "protobuf", - "rust_decimal", - "serde", - "serde_json", -] - [[package]] name = "pyth-lazer-protocol" version = "0.8.1" @@ -5625,23 +5653,7 @@ dependencies = [ "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.8.1", - "serde-value", - "tracing", -] - -[[package]] -name = "pyth-lazer-publisher-sdk" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6ef4052ebf2a7943259b3d52a10b2231ffc346717735c50e44d73fe92019d5" -dependencies = [ - "anyhow", - "fs-err", - "humantime", - "protobuf", - "protobuf-codegen", - "pyth-lazer-protocol 0.7.3", + "pyth-lazer-protocol", "serde-value", "tracing", ] @@ -6102,12 +6114,12 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", "hyper-rustls 0.24.2", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -6122,7 +6134,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", @@ -6144,17 +6156,22 @@ dependencies = [ "async-compression", "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", "hyper-rustls 0.27.7", + "hyper-tls 0.6.0", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -6165,6 +6182,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.2", "tokio-util", "tower 0.5.2", @@ -9644,7 +9662,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -9657,6 +9686,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -10743,6 +10782,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index 8f9b4aa818..2130a78b96 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] pyth-lazer-publisher-sdk = { path = "../../lazer/publisher_sdk/rust" } -pyth-lazer-protocol = {path = "../../lazer/sdk/rust/protocol"} +pyth-lazer-protocol = {path = "../../lazer/sdk/rust/protocol", version = "0.8.1"} anyhow = "1.0.98" backoff = "0.4.0" diff --git a/apps/pyth-lazer-agent/src/http_server.rs b/apps/pyth-lazer-agent/src/http_server.rs index f35756aa58..050fb7bc60 100644 --- a/apps/pyth-lazer-agent/src/http_server.rs +++ b/apps/pyth-lazer-agent/src/http_server.rs @@ -1,7 +1,7 @@ use crate::jrpc_handle::{JrpcConnectionContext, handle_jrpc}; use crate::publisher_handle::handle_publisher; use crate::{ - config::Config, http_server, lazer_publisher::LazerPublisher, + config::Config, lazer_publisher::LazerPublisher, publisher_handle::PublisherConnectionContext, }; use anyhow::{Context, Result}; diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index 54905100bc..60592de3ac 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -5,17 +5,12 @@ use anyhow::Error; use futures::{AsyncRead, AsyncWrite}; use futures_util::io::{BufReader, BufWriter}; use hyper_util::rt::TokioIo; -use pyth_lazer_protocol::jrpc::{ - Filter, JrpcError, JrpcErrorResponse, JrpcParams, JrpcResponse, JrpcSuccessResponse, - JsonRpcVersion, PythLazerAgentJrpcV1, SymbolMetadata, -}; -use serde::{Deserialize, Serialize}; +use pyth_lazer_protocol::jrpc::{GetMetadataParams, JrpcCall, JrpcError, JrpcErrorResponse, JrpcResponse, JrpcSuccessResponse, JsonRpcVersion, PythLazerAgentJrpcV1, SymbolMetadata}; use soketto::Sender; use soketto::handshake::http::Server; use std::str::FromStr; use tokio::{pin, select}; use tokio_util::compat::TokioAsyncReadCompatExt; -use tracing::field::debug; use tracing::{debug, instrument}; use url::Url; @@ -41,14 +36,14 @@ pub async fn handle_jrpc( } #[instrument( - skip(server, request, lazer_publisher, context), + skip(server, request, lazer_publisher, _context), fields(component = "jrpc_ws") )] async fn try_handle_jrpc( config: Config, server: Server, request: hyper::Request, - context: JrpcConnectionContext, + _context: JrpcConnectionContext, lazer_publisher: LazerPublisher, ) -> anyhow::Result<()> { let stream = hyper::upgrade::on(request).await?; @@ -104,7 +99,7 @@ async fn handle_jrpc_inner( ) -> anyhow::Result<()> { match serde_json::from_slice::(receive_buf.as_slice()) { Ok(jrpc_request) => match jrpc_request.params { - JrpcParams::SendUpdates(request_params) => { + JrpcCall::PushUpdate(request_params) => { match lazer_publisher .push_feed_update(request_params.into()) .await @@ -140,13 +135,9 @@ async fn handle_jrpc_inner( } } } - JrpcParams::GetMetadata(request_params) => match get_metadata(config.clone()).await { + JrpcCall::GetMetadata(request_params) => match get_metadata(config.clone()).await { Ok(symbols) => { - let mut symbols = symbols.clone(); - - if request_params.filters.is_some() { - symbols = filter_symbols(symbols, request_params.filters.unwrap()); - } + let symbols = filter_symbols(symbols.clone(), request_params); send_text( sender, @@ -169,7 +160,7 @@ async fn handle_jrpc_inner( JrpcErrorResponse { jsonrpc: JsonRpcVersion::V2, // note: right now specifying an invalid method results in a parse error - error: JrpcError::ParseError.into(), + error: JrpcError::ParseError(err.to_string()).into(), id: None, }, ))? @@ -186,7 +177,7 @@ async fn handle_jrpc_inner( serde_json::to_string::>(&JrpcResponse::Error( JrpcErrorResponse { jsonrpc: JsonRpcVersion::V2, - error: JrpcError::ParseError.into(), + error: JrpcError::ParseError(err.to_string()).into(), id: None, }, ))? @@ -198,7 +189,7 @@ async fn handle_jrpc_inner( Ok(()) } -async fn get_metadata(config: Config) -> Result, anyhow::Error> { +async fn get_metadata(config: Config) -> Result, Error> { let result = reqwest::get( config .history_service_url @@ -219,17 +210,21 @@ async fn get_metadata(config: Config) -> Result, anyhow::Err } } -fn filter_symbols(symbols: Vec, filters: Vec) -> Vec { +fn filter_symbols(symbols: Vec, get_metadata_params: GetMetadataParams) -> Vec { + let names = &get_metadata_params.names.clone(); + let asset_types = &get_metadata_params.asset_types.clone(); + let res: Vec = symbols .into_iter() .filter(|symbol| { - for filter in &filters { - if filter.name.is_some() && filter.name.clone().unwrap() != symbol.name { + if let Some(names) = names { + if !names.contains(&symbol.name) { return false; } - if filter.asset_type.is_some() - && filter.asset_type.clone().unwrap() != symbol.asset_type - { + } + + if let Some(asset_types) = asset_types { + if !asset_types.contains(&symbol.asset_type) { return false; } } @@ -292,10 +287,10 @@ pub mod tests { assert_eq!( filter_symbols( symbols.clone(), - vec![Filter { - name: Some("XMR".to_string()), - asset_type: None, - }], + GetMetadataParams { + names: Some(vec!["XMR".to_string()]), + asset_types: None, + }, ), vec![symbol2.clone()] ); @@ -304,10 +299,10 @@ pub mod tests { assert_eq!( filter_symbols( symbols.clone(), - vec![Filter { - name: None, - asset_type: Some("crypto".to_string()), - }], + GetMetadataParams { + names: None, + asset_types: Some(vec!["crypto".to_string()]), + }, ), vec![symbol1.clone(), symbol2.clone()] ); @@ -316,30 +311,12 @@ pub mod tests { assert_eq!( filter_symbols( symbols.clone(), - vec![Filter { - name: Some("BTC".to_string()), - asset_type: Some("crypto".to_string()), - }], + GetMetadataParams { + names: Some(vec!["BTC".to_string()]), + asset_types: Some(vec!["crypto".to_string()]), + }, ), vec![symbol1.clone()] ); - - // and as two separate filters - assert_eq!( - filter_symbols( - symbols.clone(), - vec![ - Filter { - name: None, - asset_type: Some("crypto".to_string()), - }, - Filter { - name: Some("BTC".to_string()), - asset_type: None, - } - ], - ), - vec![symbol1] - ); } } From edd823070059a17dfa4239cdcdc08b1299c75050 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 18:54:30 +0100 Subject: [PATCH 14/19] update protocol to 0.8.1 --- Cargo.lock | 58 ++++++++++++++++++++++-- apps/pyth-lazer-agent/Cargo.toml | 4 +- apps/pyth-lazer-agent/config/config.toml | 4 +- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18d3294c1e..0eced152c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5584,8 +5584,8 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol", - "pyth-lazer-publisher-sdk", + "pyth-lazer-protocol 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-publisher-sdk 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.12.22", "serde", "serde_json", @@ -5614,7 +5614,7 @@ dependencies = [ "futures-util", "hex", "libsecp256k1 0.7.2", - "pyth-lazer-protocol", + "pyth-lazer-protocol 0.8.1", "serde", "serde_json", "tokio", @@ -5623,6 +5623,23 @@ dependencies = [ "url", ] +[[package]] +name = "pyth-lazer-protocol" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6445dc5d2f7fff7c677fb8edc5a080a82ef7583c1bdb39daa95421788c23f695" +dependencies = [ + "anyhow", + "base64 0.22.1", + "byteorder", + "derive_more 1.0.0", + "itertools 0.13.0", + "protobuf", + "rust_decimal", + "serde", + "serde_json", +] + [[package]] name = "pyth-lazer-protocol" version = "0.8.1" @@ -5644,6 +5661,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "pyth-lazer-protocol" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1258b8770756a82a39b7b02a296c10a91b93aa58c0cded47950defe4d9377644" +dependencies = [ + "anyhow", + "byteorder", + "derive_more 1.0.0", + "humantime-serde", + "itertools 0.13.0", + "protobuf", + "rust_decimal", + "serde", + "serde_json", +] + [[package]] name = "pyth-lazer-publisher-sdk" version = "0.1.6" @@ -5653,7 +5687,23 @@ dependencies = [ "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol", + "pyth-lazer-protocol 0.8.1", + "serde-value", + "tracing", +] + +[[package]] +name = "pyth-lazer-publisher-sdk" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6ef4052ebf2a7943259b3d52a10b2231ffc346717735c50e44d73fe92019d5" +dependencies = [ + "anyhow", + "fs-err", + "humantime", + "protobuf", + "protobuf-codegen", + "pyth-lazer-protocol 0.7.3", "serde-value", "tracing", ] diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index 2130a78b96..00ca5862af 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.3" edition = "2024" [dependencies] -pyth-lazer-publisher-sdk = { path = "../../lazer/publisher_sdk/rust" } -pyth-lazer-protocol = {path = "../../lazer/sdk/rust/protocol", version = "0.8.1"} +pyth-lazer-publisher-sdk = "0.1.5" +pyth-lazer-protocol = "0.8.1" anyhow = "1.0.98" backoff = "0.4.0" diff --git a/apps/pyth-lazer-agent/config/config.toml b/apps/pyth-lazer-agent/config/config.toml index c87a5f3e25..7459a196ab 100644 --- a/apps/pyth-lazer-agent/config/config.toml +++ b/apps/pyth-lazer-agent/config/config.toml @@ -1,5 +1,5 @@ -relayer_urls = ["ws://localhost:10001"] -publish_keypair_path = "/Users/bartplatak/workspace/pyth-crosschain/apps/pyth-lazer-agent/config/test_keypair.json" +relayer_urls = ["ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction", "ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction"] +publish_keypair_path = "/path/to/solana/id.json" listen_address = "0.0.0.0:8910" publish_interval_duration = "25ms" authorization_token="token1" From 57908a6abfb989ba8f88e179513aa706071788e2 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 19:16:53 +0100 Subject: [PATCH 15/19] fix clippy errors --- Cargo.lock | 25 +++---------------- apps/pyth-lazer-agent/Cargo.toml | 2 +- apps/pyth-lazer-agent/src/config.rs | 2 +- apps/pyth-lazer-agent/src/http_server.rs | 3 +-- apps/pyth-lazer-agent/src/jrpc_handle.rs | 10 ++++++-- apps/pyth-lazer-agent/src/main.rs | 2 +- apps/pyth-lazer-agent/src/publisher_handle.rs | 4 +-- 7 files changed, 18 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0eced152c2..0160a179de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5585,7 +5585,7 @@ dependencies = [ "hyper-util", "protobuf", "pyth-lazer-protocol 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "pyth-lazer-publisher-sdk 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-publisher-sdk 0.1.7", "reqwest 0.12.22", "serde", "serde_json", @@ -5623,23 +5623,6 @@ dependencies = [ "url", ] -[[package]] -name = "pyth-lazer-protocol" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6445dc5d2f7fff7c677fb8edc5a080a82ef7583c1bdb39daa95421788c23f695" -dependencies = [ - "anyhow", - "base64 0.22.1", - "byteorder", - "derive_more 1.0.0", - "itertools 0.13.0", - "protobuf", - "rust_decimal", - "serde", - "serde_json", -] - [[package]] name = "pyth-lazer-protocol" version = "0.8.1" @@ -5694,16 +5677,16 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6ef4052ebf2a7943259b3d52a10b2231ffc346717735c50e44d73fe92019d5" +checksum = "b8d52a515b21b77a89266d584da4363fcd1e121213ac3065ab7ff0dab1172006" dependencies = [ "anyhow", "fs-err", "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.7.3", + "pyth-lazer-protocol 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde-value", "tracing", ] diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index 00ca5862af..bf73676e22 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.3" edition = "2024" [dependencies] -pyth-lazer-publisher-sdk = "0.1.5" +pyth-lazer-publisher-sdk = "0.1.7" pyth-lazer-protocol = "0.8.1" anyhow = "1.0.98" diff --git a/apps/pyth-lazer-agent/src/config.rs b/apps/pyth-lazer-agent/src/config.rs index 8ffb990eb5..d0721939e9 100644 --- a/apps/pyth-lazer-agent/src/config.rs +++ b/apps/pyth-lazer-agent/src/config.rs @@ -17,7 +17,7 @@ pub struct Config { pub publish_keypair_path: PathBuf, #[serde(with = "humantime_serde", default = "default_publish_interval")] pub publish_interval_duration: Duration, - pub history_service_url: Option + pub history_service_url: Option, } fn default_publish_interval() -> Duration { diff --git a/apps/pyth-lazer-agent/src/http_server.rs b/apps/pyth-lazer-agent/src/http_server.rs index 050fb7bc60..8e5b5a2bec 100644 --- a/apps/pyth-lazer-agent/src/http_server.rs +++ b/apps/pyth-lazer-agent/src/http_server.rs @@ -1,8 +1,7 @@ use crate::jrpc_handle::{JrpcConnectionContext, handle_jrpc}; use crate::publisher_handle::handle_publisher; use crate::{ - config::Config, lazer_publisher::LazerPublisher, - publisher_handle::PublisherConnectionContext, + config::Config, lazer_publisher::LazerPublisher, publisher_handle::PublisherConnectionContext, }; use anyhow::{Context, Result}; use hyper::body::Incoming; diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index 60592de3ac..ac92df1420 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -5,7 +5,10 @@ use anyhow::Error; use futures::{AsyncRead, AsyncWrite}; use futures_util::io::{BufReader, BufWriter}; use hyper_util::rt::TokioIo; -use pyth_lazer_protocol::jrpc::{GetMetadataParams, JrpcCall, JrpcError, JrpcErrorResponse, JrpcResponse, JrpcSuccessResponse, JsonRpcVersion, PythLazerAgentJrpcV1, SymbolMetadata}; +use pyth_lazer_protocol::jrpc::{ + GetMetadataParams, JrpcCall, JrpcError, JrpcErrorResponse, JrpcResponse, JrpcSuccessResponse, + JsonRpcVersion, PythLazerAgentJrpcV1, SymbolMetadata, +}; use soketto::Sender; use soketto::handshake::http::Server; use std::str::FromStr; @@ -210,7 +213,10 @@ async fn get_metadata(config: Config) -> Result, Error> { } } -fn filter_symbols(symbols: Vec, get_metadata_params: GetMetadataParams) -> Vec { +fn filter_symbols( + symbols: Vec, + get_metadata_params: GetMetadataParams, +) -> Vec { let names = &get_metadata_params.names.clone(); let asset_types = &get_metadata_params.asset_types.clone(); diff --git a/apps/pyth-lazer-agent/src/main.rs b/apps/pyth-lazer-agent/src/main.rs index 2c6ae842b5..7d319a7c4c 100644 --- a/apps/pyth-lazer-agent/src/main.rs +++ b/apps/pyth-lazer-agent/src/main.rs @@ -8,11 +8,11 @@ use { mod config; mod http_server; +mod jrpc_handle; mod lazer_publisher; mod publisher_handle; mod relayer_session; mod websocket_utils; -mod jrpc_handle; #[derive(Parser)] #[command(version)] diff --git a/apps/pyth-lazer-agent/src/publisher_handle.rs b/apps/pyth-lazer-agent/src/publisher_handle.rs index 32a2821049..a75ddc6d87 100644 --- a/apps/pyth-lazer-agent/src/publisher_handle.rs +++ b/apps/pyth-lazer-agent/src/publisher_handle.rs @@ -125,7 +125,7 @@ async fn try_handle_publisher( &UpdateDeserializationErrorResponse { error: format!("failed to parse binary update: {err}"), } - .into(), + .into(), )?; send_text(&mut ws_sender, error_json).await?; continue; @@ -175,7 +175,7 @@ async fn try_handle_publisher( &UpdateDeserializationErrorResponse { error: format!("failed to parse binary update: {err}"), } - .into(), + .into(), )?; send_text(&mut ws_sender, error_json).await?; continue; From 93c3fa2fe18955b6136b190a60d9debbd6c8afe5 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 19:20:29 +0100 Subject: [PATCH 16/19] fix incorrect error type and update cargo.lock --- Cargo.lock | 127 +++++++++++++++++++---- apps/pyth-lazer-agent/src/jrpc_handle.rs | 2 +- 2 files changed, 106 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aab5e1e099..d4e79a1803 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -3383,6 +3389,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -3659,7 +3684,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -3682,6 +3707,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -3737,6 +3763,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.14" @@ -3756,9 +3798,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -5540,8 +5584,9 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol 0.7.3", - "pyth-lazer-publisher-sdk 0.1.6", + "pyth-lazer-protocol 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-publisher-sdk 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.12.22", "serde", "serde_json", "soketto", @@ -5580,15 +5625,19 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6445dc5d2f7fff7c677fb8edc5a080a82ef7583c1bdb39daa95421788c23f695" +version = "0.8.1" dependencies = [ + "alloy-primitives 0.8.25", "anyhow", - "base64 0.22.1", + "bincode 1.3.3", + "bs58", "byteorder", "derive_more 1.0.0", + "ed25519-dalek 2.1.1", + "hex", + "humantime-serde", "itertools 0.13.0", + "libsecp256k1 0.7.2", "protobuf", "rust_decimal", "serde", @@ -5598,18 +5647,14 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1258b8770756a82a39b7b02a296c10a91b93aa58c0cded47950defe4d9377644" dependencies = [ - "alloy-primitives 0.8.25", "anyhow", - "bincode 1.3.3", - "bs58", "byteorder", "derive_more 1.0.0", - "ed25519-dalek 2.1.1", - "hex", "humantime-serde", "itertools 0.13.0", - "libsecp256k1 0.7.2", "protobuf", "rust_decimal", "serde", @@ -5618,16 +5663,14 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6ef4052ebf2a7943259b3d52a10b2231ffc346717735c50e44d73fe92019d5" +version = "0.1.7" dependencies = [ "anyhow", "fs-err", "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.7.3", + "pyth-lazer-protocol 0.8.1", "serde-value", "tracing", ] @@ -5635,13 +5678,15 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d52a515b21b77a89266d584da4363fcd1e121213ac3065ab7ff0dab1172006" dependencies = [ "anyhow", "fs-err", "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.8.1", + "pyth-lazer-protocol 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde-value", "tracing", ] @@ -6102,12 +6147,12 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", "hyper-rustls 0.24.2", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -6122,7 +6167,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", @@ -6144,17 +6189,22 @@ dependencies = [ "async-compression", "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", "hyper-rustls 0.27.7", + "hyper-tls 0.6.0", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -6165,6 +6215,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.2", "tokio-util", "tower 0.5.2", @@ -9644,7 +9695,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -9657,6 +9719,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -10743,6 +10815,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index ac92df1420..a26c16910a 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -163,7 +163,7 @@ async fn handle_jrpc_inner( JrpcErrorResponse { jsonrpc: JsonRpcVersion::V2, // note: right now specifying an invalid method results in a parse error - error: JrpcError::ParseError(err.to_string()).into(), + error: JrpcError::InternalError.into(), id: None, }, ))? From 88ea9c011dcededd190241ee48b6c9feee556e01 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 19:22:50 +0100 Subject: [PATCH 17/19] make the error message more verbose --- apps/pyth-lazer-agent/src/jrpc_handle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index a26c16910a..05609a3aa5 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -14,7 +14,7 @@ use soketto::handshake::http::Server; use std::str::FromStr; use tokio::{pin, select}; use tokio_util::compat::TokioAsyncReadCompatExt; -use tracing::{debug, instrument}; +use tracing::{debug, error, instrument}; use url::Url; const DEFAULT_HISTORY_SERVICE_URL: &str = @@ -156,7 +156,7 @@ async fn handle_jrpc_inner( .await?; } Err(err) => { - debug!("error while retrieving metadata: {:?}", err); + error!("error while retrieving metadata: {:?}", err); send_text( sender, serde_json::to_string::>(&JrpcResponse::Error( From d4d59733da94cc6348e1f6771e687cf53446bcba Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 19:26:05 +0100 Subject: [PATCH 18/19] fix clippy errors --- apps/pyth-lazer-agent/src/jrpc_handle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index 05609a3aa5..f1790e18c3 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -63,7 +63,7 @@ async fn try_handle_jrpc( // in the inner loop. let receive = async { ws_receiver.receive(&mut receive_buf).await }; pin!(receive); - #[allow(clippy::never_loop)] // false positive + #[allow(clippy::never_loop, reason="false positive")] // false positive loop { select! { _result = &mut receive => { @@ -235,7 +235,7 @@ fn filter_symbols( } } - return true; + true }) .collect(); From eb6761ce4fa301624310f7544b64eda151f30137 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Wed, 9 Jul 2025 19:29:22 +0100 Subject: [PATCH 19/19] fix clippy errors --- apps/pyth-lazer-agent/src/jrpc_handle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index f1790e18c3..b0c752910c 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -63,7 +63,7 @@ async fn try_handle_jrpc( // in the inner loop. let receive = async { ws_receiver.receive(&mut receive_buf).await }; pin!(receive); - #[allow(clippy::never_loop, reason="false positive")] // false positive + #[allow(clippy::never_loop, reason = "false positive")] // false positive loop { select! { _result = &mut receive => {