Skip to content
Open
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
87 changes: 87 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[workspace]
members = [
".",
"crates/mock-collector"
]

[package]
name = "gas-agent"
version = "0.1.1"
Expand Down
13 changes: 13 additions & 0 deletions crates/mock-collector/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "mock-collector"
version = "0.1.0"
edition = "2021"

[dependencies]
gas-agent = { path = "../.." }
tokio = { version = "1.44", features = ["full"] }
axum = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
90 changes: 90 additions & 0 deletions crates/mock-collector/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use axum::{
extract::rejection::JsonRejection, http::StatusCode, response::IntoResponse, routing::post,
Router,
};
use gas_agent::AgentPayload;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use tracing::{info, warn};

/// The payload structure sent by the gas agent
#[derive(Debug, Deserialize, Serialize)]
struct AgentSubmission {
payload: AgentPayload,
signature: String,
network_signature: String,
}

async fn handle_agent_publish(
payload: Result<axum::Json<AgentSubmission>, JsonRejection>,
) -> impl IntoResponse {
match payload {
Ok(axum::Json(submission)) => {
info!("═══════════════════════════════════════════════════════════════");
info!("RECEIVED AGENT SUBMISSION");
info!("═══════════════════════════════════════════════════════════════");
info!("System: {:?}", submission.payload.system);
info!("Network: {:?}", submission.payload.network);
info!("From Block: {}", submission.payload.from_block);
info!("Settlement: {:?}", submission.payload.settlement);
info!(
"Price: {} {:?}",
submission.payload.price, submission.payload.unit
);
info!("Timestamp: {}", submission.payload.timestamp);
info!("Schema Version: {}", submission.payload.schema_version);
info!("───────────────────────────────────────────────────────────────");
info!(
"Signature: {}...",
&submission.signature[..20.min(submission.signature.len())]
);
info!(
"Network Signature: {}...",
&submission.network_signature[..20.min(submission.network_signature.len())]
);
info!("═══════════════════════════════════════════════════════════════");
(StatusCode::OK, "OK".to_string())
}
Err(rejection) => {
warn!("═══════════════════════════════════════════════════════════════");
warn!("INVALID PAYLOAD RECEIVED");
warn!("═══════════════════════════════════════════════════════════════");
warn!("Error: {}", rejection);
if let JsonRejection::JsonDataError(ref err) = rejection {
warn!("Details: {}", err.body_text());
} else if let JsonRejection::JsonSyntaxError(ref err) = rejection {
warn!("Details: {}", err.body_text());
}
warn!("═══════════════════════════════════════════════════════════════");
(
StatusCode::BAD_REQUEST,
format!("Invalid payload: {}", rejection),
)
}
}
}

#[tokio::main]
async fn main() {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()),
)
.init();

let app = Router::new()
.route("/v0/agents", post(handle_agent_publish))
.fallback(|req: axum::http::Request<axum::body::Body>| async move {
warn!("Unhandled request: {} {}", req.method(), req.uri());
(StatusCode::NOT_FOUND, "Not Found")
});

let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
info!("Mock Collector listening on http://{}", addr);
info!("Expecting POST requests at http://{}/v0/agents", addr);

let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
7 changes: 2 additions & 5 deletions src/chain/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,12 @@ mod tests {
use alloy::primitives::aliases::{U240, U48};

use super::*;
use crate::logs::init_logs;

// This ensures metrics are initialized exactly once
// This ensures tracing is initialized exactly once for tests
static INIT: Once = Once::new();

fn setup() {
INIT.call_once(|| {
// Initialize metrics for testing
init_logs();
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
});
}

Expand Down
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! Gas Agent library - shared types for the gas agent ecosystem.

mod chain;
mod types;

pub use types::AgentPayload;
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The library exports only AgentPayload, but this struct has public fields of types Settlement, System, Network, and PriceUnit. These types must also be exported from the library for consumers (like the mock-collector) to deserialize AgentPayload instances. Without these exports, the mock-collector won't be able to compile and use the AgentPayload type properly.

Suggested change
pub use types::AgentPayload;
pub use types::{AgentPayload, Settlement, System, Network, PriceUnit};

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lnbc1QWFyb24 The collector seems to work without these since it only imports AgentPayload. Is the collector able to use those elements of the payload because it's just treating them as strings written to console?

            info!("System:         {:?}", submission.payload.system);
            info!("Network:        {:?}", submission.payload.network);
            info!("From Block:     {}", submission.payload.from_block);
            info!("Settlement:     {:?}", submission.payload.settlement);
            info!(
                "Price:          {} {:?}",
                submission.payload.price, submission.payload.unit
            );
            info!("Timestamp:      {}", submission.payload.timestamp);
            info!("Schema Version: {}", submission.payload.schema_version);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this co-pilot recommendation does not make a lot of sense in this context. If you needed to create an AgentPayload then you would need those enums, but since you are just logging public fields on the struct it is fine as it is.

3 changes: 3 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
use strum_macros::{Display, EnumString};

#[allow(dead_code)] // Used by binary
#[derive(Debug, Clone, EnumString, Display, Deserialize, Serialize)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
Expand All @@ -25,6 +26,7 @@ pub enum ModelKind {
PendingFloor,
}

#[allow(dead_code)] // Used by binary
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[serde(from = "String")]
Expand Down Expand Up @@ -265,6 +267,7 @@ impl SystemNetworkKey {
}
}

#[allow(dead_code)] // Used by agent binary only
pub fn to_block_time(&self) -> u64 {
match self {
SystemNetworkKey {
Expand Down