From e315e4e7374010518794280168c04637f29cbc58 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Thu, 24 Jul 2025 21:52:51 +0100 Subject: [PATCH 1/7] feat(pyth-lazer-agent) Allow conditionally disabling success responses --- Cargo.lock | 62 +----- apps/pyth-lazer-agent/Cargo.toml | 6 +- apps/pyth-lazer-agent/README.md | 17 +- apps/pyth-lazer-agent/config/config.toml | 2 +- apps/pyth-lazer-agent/src/jrpc_handle.rs | 182 ++++++++++-------- apps/pyth-lazer-agent/src/publisher_handle.rs | 31 +-- apps/pyth-lazer-agent/src/websocket_utils.rs | 7 + lazer/sdk/rust/protocol/src/jrpc.rs | 70 +++++-- 8 files changed, 184 insertions(+), 193 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f41a51eba4..3c794b826b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5023,15 +5023,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-multimap" version = "0.7.3" @@ -5617,7 +5608,7 @@ dependencies = [ [[package]] name = "pyth-lazer-agent" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "backoff", @@ -5635,8 +5626,8 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol 0.8.1", - "pyth-lazer-publisher-sdk 0.1.7", + "pyth-lazer-protocol", + "pyth-lazer-publisher-sdk", "reqwest 0.12.22", "serde", "serde_json", @@ -5666,7 +5657,7 @@ dependencies = [ "futures-util", "hex", "libsecp256k1 0.7.2", - "pyth-lazer-protocol 0.9.1", + "pyth-lazer-protocol", "serde", "serde_json", "tokio", @@ -5677,23 +5668,6 @@ dependencies = [ "url", ] -[[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-protocol" version = "0.9.1" @@ -5718,22 +5692,6 @@ dependencies = [ "serde_json", ] -[[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", - "serde-value", - "tracing", -] - [[package]] name = "pyth-lazer-publisher-sdk" version = "0.2.0" @@ -5745,7 +5703,7 @@ dependencies = [ "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.9.1", + "pyth-lazer-protocol", "serde", "serde_json", "tracing", @@ -7025,16 +6983,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_bytes" version = "0.11.17" diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index 7ef3c99512..f612ca01ed 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "pyth-lazer-agent" -version = "0.3.3" +version = "0.4.0" edition = "2024" description = "Pyth Lazer Agent" license = "Apache-2.0" repository = "https://github.com/pyth-network/pyth-crosschain" [dependencies] -pyth-lazer-publisher-sdk = "0.1.7" -pyth-lazer-protocol = "0.8.1" +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" diff --git a/apps/pyth-lazer-agent/README.md b/apps/pyth-lazer-agent/README.md index f67387a7d2..f54aba622e 100644 --- a/apps/pyth-lazer-agent/README.md +++ b/apps/pyth-lazer-agent/README.md @@ -1,12 +1,12 @@ # pyth-lazer-agent pyth-lazer-agent is intended to be run by Lazer publishers analogous to [pyth-agent](https://github.com/pyth-network/pyth-agent) -for pythnet publishers. Currently it retains [the existing Lazer publishing interface](https://github.com/pyth-network/pyth-examples/tree/main/lazer/publisher), +for pythnet publishers. Currently, it retains [the existing Lazer publishing interface](https://github.com/pyth-network/pyth-examples/tree/main/lazer/publisher), but will batch and sign transactions before publishing them to Lazer. ## Keypair -You will need to generate an ed25519 keypair and provide the pubkey to the Lazer team. `solana-keygen` is the recommended utility. +You will need to generate an ed25519 keypair and provide the pubkey to the Lazer team. `solana-keygen` it is the recommended utility. ```bash solana-keygen new -o /path/to/keypair.json solana-keygen pubkey /path/to/keypair.json @@ -17,8 +17,8 @@ pyth-lazer-agent will need to configure access to this keypair file to sign tran ## Build and run ### From source -Please check [rust-toolchain](rust-toolchain.toml) to see the version of Rust needed to build (currently 1.88). -You will also need SSL and CA certificates. `cargo build` should then work as usual. +Please check [rust-toolchain](/rust-toolchain.toml) to see the version of Rust needed to build (currently 1.88). +You will also need SSL and CA certificates. `cargo build` it should then work as usual. ### Docker See the included [Dockerfile](Dockerfile) to build an image yourself. @@ -32,7 +32,7 @@ The agent takes a single `--config` CLI option, pointing at `config/config.toml` by default. Configuration is currently minimal: ```toml -relayer_urls = ["ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction", "ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction"] +relayer_urls = ["wss://relayer.pyth-lazer-staging.dourolabs.app/v1/transaction", "wss://relayer-1.pyth-lazer-staging.dourolabs.app/v1/transaction"] publish_keypair_path = "/path/to/keypair.json" authorization_token = "your_token" listen_address = "0.0.0.0:8910" @@ -42,10 +42,5 @@ publish_interval_duration = "25ms" - `relayers_urls`: The Lazer team will provide these. - `publish_keypair_path`: The keypair file generated with `solana-keygen` or similar. - `authorization_token`: The Lazer team will provide this or instruct that it can be omitted. -- `listen_address`: The local port the agent will be listening on; can be anything you want. +- `listen_address`: The local port the agent will be listening to on; can be anything you want. - `publisher_interval`: The agent will batch and send transaction bundles at this interval. The Lazer team will provide guidance here. - -## Publish - -Please use the `/v1/publisher` or `/v2/publisher` endpoints and the corresponding `PriceFeedDataV1` and `PriceFeedDataV2` -schemas as defined in [the sdk](https://github.com/pyth-network/pyth-crosschain/blob/main/lazer/sdk/rust/protocol/src/publisher.rs). diff --git a/apps/pyth-lazer-agent/config/config.toml b/apps/pyth-lazer-agent/config/config.toml index 263904ccb5..4bf417c25a 100644 --- a/apps/pyth-lazer-agent/config/config.toml +++ b/apps/pyth-lazer-agent/config/config.toml @@ -1,4 +1,4 @@ -relayer_urls = ["wss://relayer-0.pyth-lazer.dourolabs.app/v1/transaction", "wss://relayer-0.pyth-lazer.dourolabs.app/v1/transaction"] +relayer_urls = ["wss://relayer.pyth-lazer-staging.dourolabs.app/v1/transaction", "wss://relayer-1.pyth-lazer-staging.dourolabs.app/v1/transaction"] publish_keypair_path = "/path/to/solana/id.json" listen_address = "0.0.0.0:8910" publish_interval_duration = "25ms" diff --git a/apps/pyth-lazer-agent/src/jrpc_handle.rs b/apps/pyth-lazer-agent/src/jrpc_handle.rs index b0c752910c..dea9d337a0 100644 --- a/apps/pyth-lazer-agent/src/jrpc_handle.rs +++ b/apps/pyth-lazer-agent/src/jrpc_handle.rs @@ -1,13 +1,13 @@ use crate::config::Config; use crate::lazer_publisher::LazerPublisher; -use crate::websocket_utils::{handle_websocket_error, send_text}; +use crate::websocket_utils::{handle_websocket_error, send_json, 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::{ - GetMetadataParams, JrpcCall, JrpcError, JrpcErrorResponse, JrpcResponse, JrpcSuccessResponse, - JsonRpcVersion, PythLazerAgentJrpcV1, SymbolMetadata, + FeedUpdateParams, GetMetadataParams, JrpcCall, JrpcError, JrpcErrorResponse, JrpcResponse, + JrpcSuccessResponse, JsonRpcVersion, PythLazerAgentJrpcV1, SymbolMetadata, }; use soketto::Sender; use soketto::handshake::http::Server; @@ -82,7 +82,7 @@ async fn try_handle_jrpc( serde_json::to_string::>(&JrpcResponse::Error( JrpcErrorResponse { jsonrpc: JsonRpcVersion::V2, - error: JrpcError::InternalError.into(), + error: JrpcError::InternalError(err.to_string()).into(), id: None, }, ))? @@ -103,93 +103,40 @@ async fn handle_jrpc_inner( match serde_json::from_slice::(receive_buf.as_slice()) { Ok(jrpc_request) => match jrpc_request.params { JrpcCall::PushUpdate(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?; - } - } + handle_push_update(sender, lazer_publisher, request_params, jrpc_request.id).await } - JrpcCall::GetMetadata(request_params) => match get_metadata(config.clone()).await { - Ok(symbols) => { - let symbols = filter_symbols(symbols.clone(), request_params); - - send_text( - sender, - serde_json::to_string::>>( - &JrpcResponse::Success(JrpcSuccessResponse::> { - jsonrpc: JsonRpcVersion::V2, - result: symbols, - id: jrpc_request.id, - }), - )? - .as_str(), - ) - .await?; - } - Err(err) => { - error!("error while retrieving metadata: {:?}", err); - send_text( + JrpcCall::GetMetadata(request_params) => { + if let Some(request_id) = jrpc_request.id { + handle_get_metadata(sender, config, request_params, request_id).await + } else { + send_json( 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::InternalError.into(), - id: None, - }, - ))? - .as_str(), + &JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcError::ParseError( + "The request to method 'get_metadata' requires an 'id'".to_string(), + ) + .into(), + id: None, + }, ) - .await?; + .await } - }, + } }, Err(err) => { debug!("Error parsing JRPC request: {}", err); - send_text( + send_json( sender, - serde_json::to_string::>(&JrpcResponse::Error( - JrpcErrorResponse { - jsonrpc: JsonRpcVersion::V2, - error: JrpcError::ParseError(err.to_string()).into(), - id: None, - }, - ))? - .as_str(), + &JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcError::ParseError(err.to_string()).into(), + id: None, + }, ) - .await?; + .await } } - Ok(()) } async fn get_metadata(config: Config) -> Result, Error> { @@ -242,6 +189,81 @@ fn filter_symbols( res } +async fn handle_push_update( + sender: &mut Sender, + lazer_publisher: &LazerPublisher, + request_params: FeedUpdateParams, + request_id: Option, +) -> anyhow::Result<()> { + match lazer_publisher + .push_feed_update(request_params.clone().into()) + .await + { + Ok(_) => { + if let Some(request_id) = request_id { + send_json( + sender, + &JrpcSuccessResponse:: { + jsonrpc: JsonRpcVersion::V2, + result: "success".to_string(), + id: request_id, + }, + ) + .await + } else { + Ok(()) + } + } + Err(err) => { + debug!("error while sending updates: {:?}", err); + send_json( + sender, + &JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcError::SendUpdateError(request_params).into(), + id: request_id, + }, + ) + .await + } + } +} + +async fn handle_get_metadata( + sender: &mut Sender, + config: &Config, + request_params: GetMetadataParams, + request_id: i64, +) -> anyhow::Result<()> { + match get_metadata(config.clone()).await { + Ok(symbols) => { + let symbols = filter_symbols(symbols.clone(), request_params); + + send_json( + sender, + &JrpcSuccessResponse::> { + jsonrpc: JsonRpcVersion::V2, + result: symbols, + id: request_id, + }, + ) + .await + } + Err(err) => { + error!("error while retrieving metadata: {:?}", err); + send_json( + sender, + &JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcError::InternalError(err.to_string()).into(), + id: Some(request_id), + }, + ) + .await + } + } +} + #[cfg(test)] pub mod tests { use super::*; diff --git a/apps/pyth-lazer-agent/src/publisher_handle.rs b/apps/pyth-lazer-agent/src/publisher_handle.rs index a75ddc6d87..46a57e743e 100644 --- a/apps/pyth-lazer-agent/src/publisher_handle.rs +++ b/apps/pyth-lazer-agent/src/publisher_handle.rs @@ -4,7 +4,6 @@ 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, }; @@ -87,22 +86,9 @@ async fn try_handle_publisher( bincode::config::legacy(), ) { Ok((data, _)) => { - let source_timestamp = MessageField::some(Timestamp { - #[allow( - clippy::cast_possible_wrap, - reason = "Unix seconds won't wrap any time soon" - )] - seconds: (data.source_timestamp_us.0 / 1_000_000) as i64, - #[allow( - clippy::cast_possible_truncation, - reason = "this value will always be less than one billion" - )] - 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, + source_timestamp: MessageField::some(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()), @@ -138,22 +124,9 @@ async fn try_handle_publisher( bincode::config::legacy(), ) { Ok((data, _)) => { - let source_timestamp = MessageField::some(Timestamp { - #[allow( - clippy::cast_possible_wrap, - reason = "Unix seconds won't wrap any time soon" - )] - seconds: (data.source_timestamp_us.0 / 1_000_000) as i64, - #[allow( - clippy::cast_possible_truncation, - reason = "this value will always be less than one billion" - )] - 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, + source_timestamp: MessageField::some(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), diff --git a/apps/pyth-lazer-agent/src/websocket_utils.rs b/apps/pyth-lazer-agent/src/websocket_utils.rs index 040d1c602b..b9aa07fb20 100644 --- a/apps/pyth-lazer-agent/src/websocket_utils.rs +++ b/apps/pyth-lazer-agent/src/websocket_utils.rs @@ -41,3 +41,10 @@ pub async fn send_text( }) .await? } + +pub async fn send_json( + sender: &mut Sender, + value: &U +) -> anyhow::Result<()> { + send_text(sender, &serde_json::to_string(value)?).await +} diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index 5dca3e6362..1ac6ca39b2 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -9,7 +9,7 @@ pub struct PythLazerAgentJrpcV1 { pub jsonrpc: JsonRpcVersion, #[serde(flatten)] pub params: JrpcCall, - pub id: i64, + pub id: Option, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -20,14 +20,14 @@ pub enum JrpcCall { GetMetadata(GetMetadataParams), } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct FeedUpdateParams { pub feed_id: PriceFeedId, pub source_timestamp: TimestampUs, pub update: UpdateParams, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] pub enum UpdateParams { #[serde(rename = "price")] @@ -89,7 +89,8 @@ pub struct JrpcErrorObject { #[derive(Debug, Eq, PartialEq)] pub enum JrpcError { ParseError(String), - InternalError, + InternalError(String), + SendUpdateError(FeedUpdateParams), } // note: error codes can be found in the rfc https://www.jsonrpc.org/specification#error_object @@ -101,10 +102,15 @@ impl From for JrpcErrorObject { message: "Parse error".to_string(), data: Some(error_message.into()), }, - JrpcError::InternalError => JrpcErrorObject { + JrpcError::InternalError(error_message) => JrpcErrorObject { code: -32603, message: "Internal error".to_string(), - data: None, + data: Some(error_message.into()), + }, + JrpcError::SendUpdateError(feed_update_params) => JrpcErrorObject { + code: -32000, + message: "Internal error".to_string(), + data: Some(serde_json::to_value(feed_update_params).unwrap()), }, } } @@ -165,7 +171,47 @@ mod tests { best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()), }, }), - id: 1, + id: Some(1), + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } + + #[test] + fn test_push_update_price_without_id() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "push_update", + "params": { + "feed_id": 1, + "source_timestamp": 745214124124, + + "update": { + "type": "price", + "price": 5432, + "best_bid_price": 5432, + "best_ask_price": 5432 + } + } + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: PushUpdate(FeedUpdateParams { + feed_id: PriceFeedId(1), + source_timestamp: TimestampUs::from_micros(745214124124), + update: UpdateParams::PriceUpdate { + price: Price::from_integer(5432, 0).unwrap(), + best_bid_price: Some(Price::from_integer(5432, 0).unwrap()), + best_ask_price: Some(Price::from_integer(5432, 0).unwrap()), + }, + }), + id: None, }; assert_eq!( @@ -204,7 +250,7 @@ mod tests { best_ask_price: None, }, }), - id: 1, + id: Some(1), }; assert_eq!( @@ -243,7 +289,7 @@ mod tests { rate: Rate::from_integer(1234567891, 0).unwrap(), }, }), - id: 1, + id: Some(1), }; assert_eq!( @@ -280,7 +326,7 @@ mod tests { rate: Rate::from_integer(1234567891, 0).unwrap(), }, }), - id: 1, + id: Some(1), }; assert_eq!( @@ -309,7 +355,7 @@ mod tests { names: Some(vec!["BTC/USD".to_string()]), asset_types: Some(vec!["crypto".to_string()]), }), - id: 1, + id: Some(1), }; assert_eq!( @@ -335,7 +381,7 @@ mod tests { names: None, asset_types: None, }), - id: 1, + id: Some(1), }; assert_eq!( From 53f67d9c30dc3a372d0c51b49f6f8b157728d2c4 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Thu, 24 Jul 2025 22:33:07 +0100 Subject: [PATCH 2/7] fix formatting --- apps/pyth-lazer-agent/src/publisher_handle.rs | 46 +++++++++---------- apps/pyth-lazer-agent/src/websocket_utils.rs | 2 +- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/apps/pyth-lazer-agent/src/publisher_handle.rs b/apps/pyth-lazer-agent/src/publisher_handle.rs index 46a57e743e..a139e1d22d 100644 --- a/apps/pyth-lazer-agent/src/publisher_handle.rs +++ b/apps/pyth-lazer-agent/src/publisher_handle.rs @@ -85,19 +85,17 @@ async fn try_handle_publisher( &receive_buf, bincode::config::legacy(), ) { - Ok((data, _)) => { - FeedUpdate { - feed_id: Some(data.price_feed_id.0), - source_timestamp: MessageField::some(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, _)) => FeedUpdate { + feed_id: Some(data.price_feed_id.0), + source_timestamp: MessageField::some(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(), + }, Err(err) => { error_count += 1; if error_count <= MAX_ERROR_LOG { @@ -123,18 +121,16 @@ async fn try_handle_publisher( &receive_buf, bincode::config::legacy(), ) { - Ok((data, _)) => { - FeedUpdate { - feed_id: Some(data.price_feed_id.0), - source_timestamp: MessageField::some(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, _)) => FeedUpdate { + feed_id: Some(data.price_feed_id.0), + source_timestamp: MessageField::some(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(), + }, Err(err) => { error_count += 1; if error_count <= MAX_ERROR_LOG { diff --git a/apps/pyth-lazer-agent/src/websocket_utils.rs b/apps/pyth-lazer-agent/src/websocket_utils.rs index b9aa07fb20..53b2d763da 100644 --- a/apps/pyth-lazer-agent/src/websocket_utils.rs +++ b/apps/pyth-lazer-agent/src/websocket_utils.rs @@ -44,7 +44,7 @@ pub async fn send_text( pub async fn send_json( sender: &mut Sender, - value: &U + value: &U, ) -> anyhow::Result<()> { send_text(sender, &serde_json::to_string(value)?).await } From 1e01ca082c15887ae3e551b569a9a5a393748b08 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Thu, 24 Jul 2025 22:37:49 +0100 Subject: [PATCH 3/7] update error response --- lazer/sdk/rust/protocol/src/jrpc.rs | 119 +++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/lazer/sdk/rust/protocol/src/jrpc.rs b/lazer/sdk/rust/protocol/src/jrpc.rs index 5dca3e6362..ff01907535 100644 --- a/lazer/sdk/rust/protocol/src/jrpc.rs +++ b/lazer/sdk/rust/protocol/src/jrpc.rs @@ -9,7 +9,7 @@ pub struct PythLazerAgentJrpcV1 { pub jsonrpc: JsonRpcVersion, #[serde(flatten)] pub params: JrpcCall, - pub id: i64, + pub id: Option, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -20,14 +20,14 @@ pub enum JrpcCall { GetMetadata(GetMetadataParams), } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct FeedUpdateParams { pub feed_id: PriceFeedId, pub source_timestamp: TimestampUs, pub update: UpdateParams, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] pub enum UpdateParams { #[serde(rename = "price")] @@ -59,6 +59,7 @@ pub enum JsonRpcVersion { } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(untagged)] pub enum JrpcResponse { Success(JrpcSuccessResponse), Error(JrpcErrorResponse), @@ -89,7 +90,8 @@ pub struct JrpcErrorObject { #[derive(Debug, Eq, PartialEq)] pub enum JrpcError { ParseError(String), - InternalError, + InternalError(String), + SendUpdateError(FeedUpdateParams), } // note: error codes can be found in the rfc https://www.jsonrpc.org/specification#error_object @@ -101,10 +103,15 @@ impl From for JrpcErrorObject { message: "Parse error".to_string(), data: Some(error_message.into()), }, - JrpcError::InternalError => JrpcErrorObject { + JrpcError::InternalError(error_message) => JrpcErrorObject { code: -32603, message: "Internal error".to_string(), - data: None, + data: Some(error_message.into()), + }, + JrpcError::SendUpdateError(feed_update_params) => JrpcErrorObject { + code: -32000, + message: "Internal error".to_string(), + data: Some(serde_json::to_value(feed_update_params).unwrap()), }, } } @@ -165,7 +172,47 @@ mod tests { best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()), }, }), - id: 1, + id: Some(1), + }; + + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); + } + + #[test] + fn test_push_update_price_without_id() { + let json = r#" + { + "jsonrpc": "2.0", + "method": "push_update", + "params": { + "feed_id": 1, + "source_timestamp": 745214124124, + + "update": { + "type": "price", + "price": 5432, + "best_bid_price": 5432, + "best_ask_price": 5432 + } + } + } + "#; + + let expected = PythLazerAgentJrpcV1 { + jsonrpc: JsonRpcVersion::V2, + params: PushUpdate(FeedUpdateParams { + feed_id: PriceFeedId(1), + source_timestamp: TimestampUs::from_micros(745214124124), + update: UpdateParams::PriceUpdate { + price: Price::from_integer(5432, 0).unwrap(), + best_bid_price: Some(Price::from_integer(5432, 0).unwrap()), + best_ask_price: Some(Price::from_integer(5432, 0).unwrap()), + }, + }), + id: None, }; assert_eq!( @@ -204,7 +251,7 @@ mod tests { best_ask_price: None, }, }), - id: 1, + id: Some(1), }; assert_eq!( @@ -243,7 +290,7 @@ mod tests { rate: Rate::from_integer(1234567891, 0).unwrap(), }, }), - id: 1, + id: Some(1), }; assert_eq!( @@ -280,7 +327,7 @@ mod tests { rate: Rate::from_integer(1234567891, 0).unwrap(), }, }), - id: 1, + id: Some(1), }; assert_eq!( @@ -309,7 +356,7 @@ mod tests { names: Some(vec!["BTC/USD".to_string()]), asset_types: Some(vec!["crypto".to_string()]), }), - id: 1, + id: Some(1), }; assert_eq!( @@ -335,7 +382,7 @@ mod tests { names: None, asset_types: None, }), - id: 1, + id: Some(1), }; assert_eq!( @@ -396,4 +443,52 @@ mod tests { } ); } + + #[test] + pub fn test_parse_response() { + let success_response = serde_json::from_str::>( + r#" + { + "jsonrpc": "2.0", + "id": 2, + "result": "success" + }"#, + ) + .unwrap(); + + assert_eq!( + success_response, + JrpcResponse::Success(JrpcSuccessResponse:: { + jsonrpc: JsonRpcVersion::V2, + result: "success".to_string(), + id: 2, + }) + ); + + let error_response = serde_json::from_str::>( + r#" + { + "jsonrpc": "2.0", + "id": 3, + "error": { + "code": -32603, + "message": "Internal error" + } + }"#, + ) + .unwrap(); + + assert_eq!( + error_response, + JrpcResponse::Error(JrpcErrorResponse { + jsonrpc: JsonRpcVersion::V2, + error: JrpcErrorObject { + code: -32603, + message: "Internal error".to_string(), + data: None, + }, + id: Some(3), + }) + ); + } } From 6137c25a1fc9c2eb9314d60ab629331cc734a2da Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Mon, 28 Jul 2025 22:36:19 +0100 Subject: [PATCH 4/7] fix dep versions --- Cargo.lock | 42 +++++++++++++++++++++++++++++--- apps/pyth-lazer-agent/Cargo.toml | 4 +-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f0cad8785..570deef77f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5626,8 +5626,8 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol", - "pyth-lazer-publisher-sdk", + "pyth-lazer-protocol 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-publisher-sdk 0.2.1", "reqwest 0.12.22", "serde", "serde_json", @@ -5657,7 +5657,7 @@ dependencies = [ "futures-util", "hex", "libsecp256k1 0.7.2", - "pyth-lazer-protocol", + "pyth-lazer-protocol 0.10.0", "serde", "serde_json", "tokio", @@ -5692,6 +5692,26 @@ dependencies = [ "serde_json", ] +[[package]] +name = "pyth-lazer-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f5e115a57556ec979a4206a12920e3a5e8aa27a4944eb5832f8801c4b25f20e" +dependencies = [ + "anyhow", + "byteorder", + "chrono", + "derive_more 1.0.0", + "hex", + "humantime", + "humantime-serde", + "itertools 0.13.0", + "protobuf", + "rust_decimal", + "serde", + "serde_json", +] + [[package]] name = "pyth-lazer-publisher-sdk" version = "0.2.0" @@ -5703,12 +5723,26 @@ dependencies = [ "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol", + "pyth-lazer-protocol 0.10.0", "serde", "serde_json", "tracing", ] +[[package]] +name = "pyth-lazer-publisher-sdk" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07277d6d7a932a15a5d3be34fd9e813b40aa275cc8683c8a62cf3ab72a93de0b" +dependencies = [ + "anyhow", + "fs-err", + "protobuf", + "protobuf-codegen", + "pyth-lazer-protocol 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json", +] + [[package]] name = "pyth-sdk" version = "0.5.0" diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index f612ca01ed..b8c58481a8 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -7,8 +7,8 @@ license = "Apache-2.0" repository = "https://github.com/pyth-network/pyth-crosschain" [dependencies] -pyth-lazer-publisher-sdk = { path = "../../lazer/publisher_sdk/rust" } -pyth-lazer-protocol = { path = "../../lazer/sdk/rust/protocol"} +pyth-lazer-publisher-sdk = "0.2.1" +pyth-lazer-protocol = "0.10.0" anyhow = "1.0.98" backoff = "0.4.0" From 03bf830ef5a53b9b1c82f074a0560c367bc5316d Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Mon, 28 Jul 2025 22:42:52 +0100 Subject: [PATCH 5/7] update readme --- Cargo.lock | 2 +- apps/pyth-lazer-agent/README.md | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 33bb1d4fee..9e68576f19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5627,7 +5627,7 @@ dependencies = [ "hyper-util", "protobuf", "pyth-lazer-protocol 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pyth-lazer-publisher-sdk 0.2.1", + "pyth-lazer-publisher-sdk 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.12.22", "serde", "serde_json", diff --git a/apps/pyth-lazer-agent/README.md b/apps/pyth-lazer-agent/README.md index f54aba622e..3e8a3b21d8 100644 --- a/apps/pyth-lazer-agent/README.md +++ b/apps/pyth-lazer-agent/README.md @@ -16,6 +16,18 @@ pyth-lazer-agent will need to configure access to this keypair file to sign tran ## Build and run +### From cargo +``` +# Download the cargo package +cargo install pyth-lazer-agent + +# Add .cargo/bin to PATH +export PATH="$PATH:~/.cargo/bin" + +# Run the agent +pyth-lazer-agent --help +``` + ### From source Please check [rust-toolchain](/rust-toolchain.toml) to see the version of Rust needed to build (currently 1.88). You will also need SSL and CA certificates. `cargo build` it should then work as usual. From afce38ae7569ed4d71359fc1ab2551631285867d Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Tue, 29 Jul 2025 22:59:25 +0100 Subject: [PATCH 6/7] revert bad lint fixes --- apps/pyth-lazer-agent/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/pyth-lazer-agent/README.md b/apps/pyth-lazer-agent/README.md index 3e8a3b21d8..cd6455e4c7 100644 --- a/apps/pyth-lazer-agent/README.md +++ b/apps/pyth-lazer-agent/README.md @@ -6,7 +6,7 @@ but will batch and sign transactions before publishing them to Lazer. ## Keypair -You will need to generate an ed25519 keypair and provide the pubkey to the Lazer team. `solana-keygen` it is the recommended utility. +You will need to generate an ed25519 keypair and provide the pubkey to the Lazer team. `solana-keygen` is the recommended utility. ```bash solana-keygen new -o /path/to/keypair.json solana-keygen pubkey /path/to/keypair.json @@ -29,8 +29,8 @@ pyth-lazer-agent --help ``` ### From source -Please check [rust-toolchain](/rust-toolchain.toml) to see the version of Rust needed to build (currently 1.88). -You will also need SSL and CA certificates. `cargo build` it should then work as usual. +Please check [rust-toolchain](rust-toolchain.toml) to see the version of Rust needed to build (currently 1.88). +You will also need SSL and CA certificates. `cargo build` should then work as usual. ### Docker See the included [Dockerfile](Dockerfile) to build an image yourself. @@ -54,5 +54,5 @@ publish_interval_duration = "25ms" - `relayers_urls`: The Lazer team will provide these. - `publish_keypair_path`: The keypair file generated with `solana-keygen` or similar. - `authorization_token`: The Lazer team will provide this or instruct that it can be omitted. -- `listen_address`: The local port the agent will be listening to on; can be anything you want. +- `listen_address`: The local port the agent will be listening on; can be anything you want. - `publisher_interval`: The agent will batch and send transaction bundles at this interval. The Lazer team will provide guidance here. From 6604d0cc35448e2e4a649f351f4f3965fb7f0817 Mon Sep 17 00:00:00 2001 From: Bart Platak Date: Tue, 29 Jul 2025 23:04:37 +0100 Subject: [PATCH 7/7] update deps --- Cargo.lock | 60 +++++++++++--------------------- apps/pyth-lazer-agent/Cargo.toml | 4 +-- 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c05101956..258bceb114 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5023,15 +5023,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-multimap" version = "0.7.3" @@ -5617,7 +5608,7 @@ dependencies = [ [[package]] name = "pyth-lazer-agent" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "backoff", @@ -5635,8 +5626,8 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "protobuf", - "pyth-lazer-protocol 0.8.1", - "pyth-lazer-publisher-sdk 0.1.7", + "pyth-lazer-protocol 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pyth-lazer-publisher-sdk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.12.22", "serde", "serde_json", @@ -5679,15 +5670,22 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1258b8770756a82a39b7b02a296c10a91b93aa58c0cded47950defe4d9377644" +version = "0.10.1" dependencies = [ + "alloy-primitives 0.8.25", "anyhow", + "bincode 1.3.3", + "bs58", "byteorder", + "chrono", "derive_more 1.0.0", + "ed25519-dalek 2.1.1", + "hex", + "humantime", "humantime-serde", "itertools 0.13.0", + "libsecp256k1 0.7.2", + "mry", "protobuf", "rust_decimal", "serde", @@ -5697,21 +5695,17 @@ dependencies = [ [[package]] name = "pyth-lazer-protocol" version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d321e49be0315d68f07d097d240701a05e003e05eff5ac9f2d0457d4a606dd92" dependencies = [ - "alloy-primitives 0.8.25", "anyhow", - "bincode 1.3.3", - "bs58", "byteorder", "chrono", "derive_more 1.0.0", - "ed25519-dalek 2.1.1", "hex", "humantime", "humantime-serde", "itertools 0.13.0", - "libsecp256k1 0.7.2", - "mry", "protobuf", "rust_decimal", "serde", @@ -5720,29 +5714,27 @@ dependencies = [ [[package]] name = "pyth-lazer-publisher-sdk" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d52a515b21b77a89266d584da4363fcd1e121213ac3065ab7ff0dab1172006" +version = "0.3.0" dependencies = [ "anyhow", "fs-err", - "humantime", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.8.1", - "serde-value", - "tracing", + "pyth-lazer-protocol 0.10.1", + "serde_json", ] [[package]] name = "pyth-lazer-publisher-sdk" version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bebeacbc58d9e0143e03a397b08becbed1dacf5baad6a245bc00f74ca5cc50d" dependencies = [ "anyhow", "fs-err", "protobuf", "protobuf-codegen", - "pyth-lazer-protocol 0.10.1", + "pyth-lazer-protocol 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json", ] @@ -7020,16 +7012,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_bytes" version = "0.11.17" diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index b8c58481a8..1dcae42e12 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -7,8 +7,8 @@ license = "Apache-2.0" repository = "https://github.com/pyth-network/pyth-crosschain" [dependencies] -pyth-lazer-publisher-sdk = "0.2.1" -pyth-lazer-protocol = "0.10.0" +pyth-lazer-publisher-sdk = "0.3.0" +pyth-lazer-protocol = "0.10.1" anyhow = "1.0.98" backoff = "0.4.0"