Skip to content

pyth-lazer-agent readme and clippy config #2819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/pyth-lazer-agent/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 41 additions & 1 deletion apps/pyth-lazer-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-lazer-agent"
version = "0.1.2"
version = "0.1.3"
edition = "2024"

[dependencies]
Expand Down Expand Up @@ -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"
51 changes: 51 additions & 0 deletions apps/pyth-lazer-agent/README.md
Original file line number Diff line number Diff line change
@@ -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).
4 changes: 4 additions & 0 deletions apps/pyth-lazer-agent/clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-indexing-slicing-in-tests = true
allow-panic-in-tests = true
2 changes: 1 addition & 1 deletion apps/pyth-lazer-agent/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "1.87.0"
channel = "1.88.0"
profile = "minimal"
components = ["rustfmt", "clippy"]
2 changes: 1 addition & 1 deletion apps/pyth-lazer-agent/src/lazer_publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}");
}
};
Expand Down
4 changes: 4 additions & 0 deletions apps/pyth-lazer-agent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ 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()
Expand Down
18 changes: 17 additions & 1 deletion apps/pyth-lazer-agent/src/publisher_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -88,7 +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"
)]
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(),
});
Expand Down Expand Up @@ -131,7 +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"
)]
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(),
});
Expand Down