From 249a49c41ac1ea9be24268b083aceceaf0ec028e Mon Sep 17 00:00:00 2001 From: Mike Rolish Date: Sun, 29 Jun 2025 23:53:45 -0500 Subject: [PATCH 1/2] pyth-lazer-agent readme and clippy config --- apps/pyth-lazer-agent/Cargo.lock | 10 ++-- apps/pyth-lazer-agent/Cargo.toml | 42 ++++++++++++++- apps/pyth-lazer-agent/README.md | 51 +++++++++++++++++++ apps/pyth-lazer-agent/clippy.toml | 3 ++ apps/pyth-lazer-agent/rust-toolchain.toml | 2 +- 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 | 6 ++- 8 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 apps/pyth-lazer-agent/README.md create mode 100644 apps/pyth-lazer-agent/clippy.toml diff --git a/apps/pyth-lazer-agent/Cargo.lock b/apps/pyth-lazer-agent/Cargo.lock index 081a98b5ac..cf07a8c9fb 100644 --- a/apps/pyth-lazer-agent/Cargo.lock +++ b/apps/pyth-lazer-agent/Cargo.lock @@ -436,9 +436,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -1146,9 +1146,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -1643,7 +1643,7 @@ dependencies = [ [[package]] name = "pyth-lazer-agent" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "backoff", diff --git a/apps/pyth-lazer-agent/Cargo.toml b/apps/pyth-lazer-agent/Cargo.toml index 188b2149a5..b457a2cdb8 100644 --- a/apps/pyth-lazer-agent/Cargo.toml +++ b/apps/pyth-lazer-agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyth-lazer-agent" -version = "0.1.2" +version = "0.1.3" edition = "2024" [dependencies] @@ -36,3 +36,43 @@ url = { version = "2.5.4", features = ["serde"] } [dev-dependencies] tempfile = "3.20.0" + +[lints.rust] +unsafe_code = "deny" + +[lints.clippy] +wildcard_dependencies = "deny" + +collapsible_if = "allow" +collapsible_else_if = "allow" + +allow_attributes_without_reason = "warn" + +# Panics +expect_used = "warn" +fallible_impl_from = "warn" +indexing_slicing = "warn" +panic = "warn" +panic_in_result_fn = "warn" +string_slice = "warn" +todo = "warn" +unchecked_duration_subtraction = "warn" +unreachable = "warn" +unwrap_in_result = "warn" +unwrap_used = "warn" + +# Correctness +cast_lossless = "warn" +cast_possible_truncation = "warn" +cast_possible_wrap = "warn" +cast_sign_loss = "warn" +collection_is_never_read = "warn" +match_wild_err_arm = "warn" +path_buf_push_overwrite = "warn" +read_zero_byte_vec = "warn" +same_name_method = "warn" +suspicious_operation_groupings = "warn" +suspicious_xor_used_as_pow = "warn" +unused_self = "warn" +used_underscore_binding = "warn" +while_float = "warn" diff --git a/apps/pyth-lazer-agent/README.md b/apps/pyth-lazer-agent/README.md new file mode 100644 index 0000000000..f67387a7d2 --- /dev/null +++ b/apps/pyth-lazer-agent/README.md @@ -0,0 +1,51 @@ +# 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), +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. +```bash +solana-keygen new -o /path/to/keypair.json +solana-keygen pubkey /path/to/keypair.json +``` + +pyth-lazer-agent will need to configure access to this keypair file to sign transactions. + +## 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. + +### Docker +See the included [Dockerfile](Dockerfile) to build an image yourself. + +### Container +We also publish images to the [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#pulling-container-images). +The latest will be available at `ghcr.io/pyth-network/pyth-lazer-agent:latest`. + +## Configure +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"] +publish_keypair_path = "/path/to/keypair.json" +authorization_token = "your_token" +listen_address = "0.0.0.0:8910" +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. +- `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/clippy.toml b/apps/pyth-lazer-agent/clippy.toml new file mode 100644 index 0000000000..3909e5dc3b --- /dev/null +++ b/apps/pyth-lazer-agent/clippy.toml @@ -0,0 +1,3 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-indexing-slicing-in-tests = true diff --git a/apps/pyth-lazer-agent/rust-toolchain.toml b/apps/pyth-lazer-agent/rust-toolchain.toml index c05d49c8d4..fdda5b9efd 100644 --- a/apps/pyth-lazer-agent/rust-toolchain.toml +++ b/apps/pyth-lazer-agent/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.87.0" +channel = "1.88.0" profile = "minimal" components = ["rustfmt", "clippy"] diff --git a/apps/pyth-lazer-agent/src/lazer_publisher.rs b/apps/pyth-lazer-agent/src/lazer_publisher.rs index b556f2d765..b5c2d304cd 100644 --- a/apps/pyth-lazer-agent/src/lazer_publisher.rs +++ b/apps/pyth-lazer-agent/src/lazer_publisher.rs @@ -49,12 +49,12 @@ impl LazerPublisher { .context("Failed to create signing key from keypair") } + #[allow(clippy::panic, reason = "can't run agent without keypair")] pub async fn new(config: &Config) -> Self { let signing_key = match Self::load_signing_key(&config.publish_keypair_path) { Ok(signing_key) => signing_key, Err(e) => { tracing::error!("Failed to load signing key: {e:?}"); - // Can't proceed on key failure panic!("Failed to load signing key: {e:?}"); } }; diff --git a/apps/pyth-lazer-agent/src/main.rs b/apps/pyth-lazer-agent/src/main.rs index a6082b747c..6f17e34857 100644 --- a/apps/pyth-lazer-agent/src/main.rs +++ b/apps/pyth-lazer-agent/src/main.rs @@ -22,6 +22,7 @@ struct Cli { #[tokio::main] async fn main() -> anyhow::Result<()> { + #[allow(clippy::expect_used, reason = "application can fail on invalid RUST_LOG")] tracing_subscriber::fmt() .with_env_filter( EnvFilter::builder() diff --git a/apps/pyth-lazer-agent/src/publisher_handle.rs b/apps/pyth-lazer-agent/src/publisher_handle.rs index 2e0ee35876..d53c1cee4e 100644 --- a/apps/pyth-lazer-agent/src/publisher_handle.rs +++ b/apps/pyth-lazer-agent/src/publisher_handle.rs @@ -69,7 +69,7 @@ async fn try_handle_publisher( // 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")] loop { select! { _result = &mut receive => { @@ -88,7 +88,9 @@ async fn try_handle_publisher( ) { 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(), }); @@ -131,7 +133,9 @@ async fn try_handle_publisher( ) { 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(), }); From 0bb6f67eeb93d2ef311c584396ff6a6c83ca2642 Mon Sep 17 00:00:00 2001 From: Mike Rolish Date: Mon, 30 Jun 2025 00:06:20 -0500 Subject: [PATCH 2/2] fmt, allow panic in tests --- apps/pyth-lazer-agent/clippy.toml | 1 + apps/pyth-lazer-agent/src/main.rs | 5 ++++- apps/pyth-lazer-agent/src/publisher_handle.rs | 20 +++++++++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/pyth-lazer-agent/clippy.toml b/apps/pyth-lazer-agent/clippy.toml index 3909e5dc3b..4d90be6e07 100644 --- a/apps/pyth-lazer-agent/clippy.toml +++ b/apps/pyth-lazer-agent/clippy.toml @@ -1,3 +1,4 @@ allow-unwrap-in-tests = true allow-expect-in-tests = true allow-indexing-slicing-in-tests = true +allow-panic-in-tests = true diff --git a/apps/pyth-lazer-agent/src/main.rs b/apps/pyth-lazer-agent/src/main.rs index 6f17e34857..8cbe81c927 100644 --- a/apps/pyth-lazer-agent/src/main.rs +++ b/apps/pyth-lazer-agent/src/main.rs @@ -22,7 +22,10 @@ struct Cli { #[tokio::main] async fn main() -> anyhow::Result<()> { - #[allow(clippy::expect_used, reason = "application can fail on invalid RUST_LOG")] + #[allow( + clippy::expect_used, + reason = "application can fail on invalid RUST_LOG" + )] tracing_subscriber::fmt() .with_env_filter( EnvFilter::builder() diff --git a/apps/pyth-lazer-agent/src/publisher_handle.rs b/apps/pyth-lazer-agent/src/publisher_handle.rs index d53c1cee4e..45c2b65b4e 100644 --- a/apps/pyth-lazer-agent/src/publisher_handle.rs +++ b/apps/pyth-lazer-agent/src/publisher_handle.rs @@ -88,9 +88,15 @@ async fn try_handle_publisher( ) { Ok((data, _)) => { let source_timestamp = MessageField::some(Timestamp { - #[allow(clippy::cast_possible_wrap, reason = "Unix seconds won't wrap any time soon")] + #[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")] + #[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(), }); @@ -133,9 +139,15 @@ async fn try_handle_publisher( ) { Ok((data, _)) => { let source_timestamp = MessageField::some(Timestamp { - #[allow(clippy::cast_possible_wrap, reason = "Unix seconds won't wrap any time soon")] + #[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")] + #[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(), });