Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
568d8ce
feat(sdk): mark HostErrorKind + LogLevel #[non_exhaustive] (COW-1029)
brunota20 Jun 18, 2026
be4fe97
test(resource-limits): 2 evil fixtures + 3 trap-isolation tests (COW-…
brunota20 Jun 18, 2026
1c1d9b8
feat(logging): JSON formatter + structured dispatch fields (COW-1035)
brunota20 Jun 18, 2026
a789dfa
feat(metrics): Prometheus /metrics endpoint + 4 recording sites (COW-…
brunota20 Jun 18, 2026
5edd14c
feat(supervisor): exponential-backoff restart with component reinstan…
brunota20 Jun 18, 2026
59dcd40
feat(event-loop): WS reconnect with exponential backoff per stream (C…
brunota20 Jun 18, 2026
fc3a755
feat(supervisor): poison-pill detection + quarantine (COW-1032)
brunota20 Jun 18, 2026
f918dc3
feat(event-loop+supervisor): graceful shutdown + last-block persisten…
brunota20 Jun 18, 2026
a609a0d
test(supervisor): multi-chain isolation regression tests (COW-1073)
brunota20 Jun 18, 2026
3f616e7
feat(ops): E2E testnet integration scaffold (COW-1064)
brunota20 Jun 18, 2026
4b65db3
docs(ops): production deployment guide (COW-1030)
brunota20 Jun 18, 2026
9617be1
ops(e2e): pin module configs + run-prep punch list for 2026-06-18 COW…
brunota20 Jun 18, 2026
dc88e4f
ops(e2e): automation scripts for COW-1064 run (.env-file pattern)
brunota20 Jun 18, 2026
168af8a
fix(scripts): match JSON-shape supervisor-ready log line (COW-1064)
brunota20 Jun 18, 2026
fcf8fcf
fix(scripts): macOS bash 3.2 compatibility (COW-1064)
brunota20 Jun 18, 2026
110f37e
fix(scripts): idempotent on-chain submission + python deps pre-flight…
brunota20 Jun 18, 2026
537c4a7
fix(scripts): EthFlow quote uses WETH not native-ETH sentinel (COW-1064)
brunota20 Jun 18, 2026
26572b6
fix(scripts): report-gen handles flat JSON + per-module marker patter…
brunota20 Jun 18, 2026
dca0cbc
feat(sdk + twap-monitor): resolve non-empty app_data via orderbook lo…
brunota20 Jun 18, 2026
0d38b70
fix(ethflow-watcher): apply resolve_app_data to submit_placement (COW…
brunota20 Jun 18, 2026
b007997
ops(e2e): COW-1064 run report 2026-06-18
brunota20 Jun 18, 2026
6268bde
fix(cow-api): forward orderbook ApiError envelope to HostError.data (…
brunota20 Jun 19, 2026
11080ea
feat(ethflow-watcher): downgrade ExcessiveValidTo drops to Info (COW-…
brunota20 Jun 19, 2026
98e74ca
fix(scripts): derive TWAP calldata with t0=now-60 (COW-1077)
brunota20 Jun 19, 2026
512c669
chore(sdk + twap-monitor): hex helpers via alloy_primitives::hex::encode
brunota20 Jun 19, 2026
a736362
feat(load-test): Anvil fork + mock orderbook + load-gen (COW-1079)
brunota20 Jun 19, 2026
fd294b5
ops(load): baseline 5x5 report - engine clean, load-gen needs calibra…
brunota20 Jun 19, 2026
25b9127
fix(scripts): load-run.sh REPORTS_DIR set after lib.sh source (COW-1079)
brunota20 Jun 19, 2026
655d1e4
fix(load-gen): explicit nonce + unique EthFlow sellAmount (COW-1080)
brunota20 Jun 19, 2026
85b60b2
ops(load): medium + saturation reports - engine clean at 300 watches …
brunota20 Jun 19, 2026
163c32d
feat(load-gen): --parallel mode + aggressive saturation report (COW-1…
brunota20 Jun 19, 2026
7213052
chore(rust-idiomatic): M4 compliance pass (blockers + majors) (#66)
brunota20 Jun 24, 2026
071dabf
fix(shepherd-sdk): add cow_api_request to chainlink StubHost + appDat…
brunota20 Jun 25, 2026
21580b9
refactor(sdk): replace [u8; 32] with B256 across resolve_app_data sur…
brunota20 Jun 25, 2026
00e0891
refactor(cow-orderbook): extract DEFAULT_CHAINS const
brunota20 Jun 25, 2026
c65b0fe
chore(engine.e2e.toml): replace em-dash with ASCII hyphen
brunota20 Jun 25, 2026
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ skills-lock.json

# Engine runtime state (default state_dir from engine.toml).
data/

# E2E automation: rendered configs with embedded RPC keys + script state
# never get committed.
*.local.toml
scripts/.state
scripts/.env

# Generated reports under e2e-reports/ (operator commits the filled-in ones
# manually via `git add -f`).
docs/operations/e2e-reports/engine-*.log
docs/operations/e2e-reports/metrics-*.txt
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ members = [
"modules/examples/balance-tracker",
"modules/examples/price-alert",
"modules/examples/stop-loss",
"modules/fixtures/flaky-bomb",
"modules/fixtures/fuel-bomb",
"modules/fixtures/memory-bomb",
"modules/twap-monitor",
"tools/load-gen",
"tools/orderbook-mock",
]
resolver = "2"

Expand Down
8 changes: 7 additions & 1 deletion crates/nexum-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ serde_json.workspace = true
# Observability. `tracing` replaces the prior `eprintln!` debug log
# so the engine can drop into a structured log pipeline in production.
tracing.workspace = true
tracing-subscriber = { workspace = true, default-features = false, features = ["fmt", "env-filter", "ansi"] }
tracing-subscriber.workspace = true

# Prometheus exporter (COW-1034). `metrics` is the facade every
# recording site (dispatch, host backends) calls; the exporter
# crate installs the recorder + binds the `/metrics` HTTP listener.
metrics = "0.24"
metrics-exporter-prometheus = { version = "0.17", default-features = false, features = ["http-listener"] }

# `cow-api` backend. cowprotocol pulls `OrderBookApi`, `OrderCreation`,
# `OrderUid`, the orderbook base URL table per `Chain`, and the typed
Expand Down
17 changes: 16 additions & 1 deletion crates/nexum-engine/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,17 @@ use clap::Parser;

/// Parsed CLI surface.
///
/// `nexum-engine [<wasm-path> [<manifest-path>]] [--engine-config <path>]`
/// `nexum-engine [<wasm-path> [<manifest-path>]] [--engine-config <path>] [--pretty-logs]`
///
/// Positional `<wasm-path>` is a backwards-compat shortcut that
/// synthesises a one-module engine config. Production deployments pass
/// `--engine-config` and declare modules in TOML.
///
/// `--pretty-logs` selects the human-readable tracing formatter (the
/// historical 0.1 default). Without the flag the engine emits JSON
/// log lines per the COW-1035 structured-logging contract: a single
/// `jq` / Loki / Grafana stream reconstructs the full timeline of
/// any dispatch, host call, or order submission.
#[derive(Parser, Debug, Default)]
#[command(
name = "nexum-engine",
Expand All @@ -35,4 +45,9 @@ pub struct Cli {
/// documented in `engine_config::load_or_default`.
#[arg(long = "engine-config")]
pub engine_config: Option<PathBuf>,

/// Use the human-readable tracing formatter instead of the
/// default JSON formatter (COW-1035 structured-logging contract).
#[arg(long = "pretty-logs")]
pub pretty_logs: bool,
}
40 changes: 40 additions & 0 deletions crates/nexum-engine/src/engine_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,63 @@ pub struct EngineSection {
/// `info` when absent; `RUST_LOG` overrides at process start.
#[serde(default = "default_log_level")]
pub log_level: String,
/// Prometheus metrics exporter wiring (COW-1034). Absent table =
/// disabled (the engine still installs the recorder so call sites
/// stay live but no HTTP listener binds).
#[serde(default)]
pub metrics: MetricsSection,
}

impl Default for EngineSection {
fn default() -> Self {
Self {
state_dir: default_state_dir(),
log_level: default_log_level(),
metrics: MetricsSection::default(),
}
}
}

/// `[engine.metrics]` config. When `enabled = true` the engine starts
/// a Prometheus HTTP exporter on `bind_addr` and serves `/metrics`.
///
/// Default: disabled. Operators opt in explicitly so the M3 / M4
/// runbook smoke runs do not bind a port unintentionally.
#[derive(Debug, Deserialize)]
pub struct MetricsSection {
#[serde(default)]
pub enabled: bool,
/// IPv4 / IPv6 socket address to bind. Default `127.0.0.1:9100`.
#[serde(default = "default_metrics_bind")]
pub bind_addr: String,
}

impl Default for MetricsSection {
fn default() -> Self {
Self {
enabled: false,
bind_addr: default_metrics_bind(),
}
}
}

fn default_metrics_bind() -> String {
"127.0.0.1:9100".to_owned()
}

#[derive(Debug, Deserialize)]
pub struct ChainConfig {
/// JSON-RPC endpoint. `ws://` and `wss://` engage alloy's pubsub
/// transport (required for `eth_subscribe`); `http://` and `https://`
/// engage the HTTP transport (request/response only).
pub rpc_url: String,
/// Optional CoW orderbook base URL override for this chain. When
/// absent (the common case), the host uses the canonical
/// `api.cow.fi/{slug}/api/v1` URL from `cowprotocol::Chain`. Set
/// this to point at a staging/barn instance or a local mock (e.g.
/// `tools/orderbook-mock` for the COW-1079 load test).
#[serde(default)]
pub orderbook_url: Option<String>,
}

fn default_state_dir() -> PathBuf {
Expand Down
53 changes: 45 additions & 8 deletions crates/nexum-engine/src/host/cow_orderbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,28 @@ pub struct OrderBookPool {
http: reqwest::Client,
}

/// Canonical CoW Protocol chain set the engine ships clients for.
///
/// Both `Default::default()` and `OrderBookPool::from_config` walk
/// this single source of truth so a new chain joining CoW protocol
/// only needs a one-line addition here instead of two parallel
/// arrays.
const DEFAULT_CHAINS: &[Chain] = &[
Chain::Mainnet,
Chain::Gnosis,
Chain::Sepolia,
Chain::ArbitrumOne,
Chain::Base,
];

impl Default for OrderBookPool {
/// Build a pool covering every `cowprotocol::Chain` variant. Each entry
/// uses the canonical `api.cow.fi/{slug}/api/v1` base URL from the SDK.
/// Override individual entries via `OrderBookApi::new_with_base_url` for
/// barn or staging targets.
fn default() -> Self {
let http = reqwest::Client::new();
let chains = [
Chain::Mainnet,
Chain::Gnosis,
Chain::Sepolia,
Chain::ArbitrumOne,
Chain::Base,
];
let clients = chains
let clients = DEFAULT_CHAINS
.iter()
.map(|c| (c.id(), OrderBookApi::new(*c)))
.collect();
Expand All @@ -52,6 +59,36 @@ impl Default for OrderBookPool {
}

impl OrderBookPool {
/// Build a pool from engine config, honouring any
/// `[chains.<id>] orderbook_url = "..."` overrides. Chains
/// without an override fall back to the canonical
/// `cowprotocol::Chain` URLs (same as [`OrderBookPool::default`]).
///
/// Used by the load test (COW-1079) to point all submissions at
/// `tools/orderbook-mock`, and by staging/barn deployments that
/// run against a non-production orderbook.
pub fn from_config(cfg: &crate::engine_config::EngineConfig) -> Self {
let http = reqwest::Client::new();
let mut clients: BTreeMap<u64, OrderBookApi> = DEFAULT_CHAINS
.iter()
.map(|c| (c.id(), OrderBookApi::new(*c)))
.collect();
for (chain_id, chain_cfg) in &cfg.chains {
if let Some(url) = chain_cfg.orderbook_url.as_deref() {
match url.parse::<url::Url>() {
Ok(parsed) => {
tracing::info!(chain_id, url, "cow-api: orderbook URL override");
clients.insert(*chain_id, OrderBookApi::new_with_base_url(parsed));
}
Err(e) => {
tracing::warn!(chain_id, url, error = %e, "cow-api: bad orderbook_url, falling back to canonical");
}
}
}
}
Self { clients, http }
}

/// Look up the client for a chain.
pub fn get(&self, chain_id: u64) -> Result<&OrderBookApi, CowApiError> {
self.clients
Expand Down
34 changes: 34 additions & 0 deletions crates/nexum-engine/src/host/cow_orderbook/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,40 @@ async fn request_rejects_unknown_chain() {
assert!(matches!(err, CowApiError::UnknownChain(99_999)));
}

#[tokio::test]
async fn submit_order_propagates_orderbook_envelope() {
// The orderbook rejects with a typed envelope. The pool must
// surface `cowprotocol::Error::OrderbookApi { status, api }`
// so the WIT adapter can forward `api` to `HostError.data`
// (COW-1075). The string `DuplicatedOrder` is what the live
// Sepolia orderbook returns for an already-submitted order;
// it parses as `ApiError` even though `OrderPostErrorKind`
// falls back to `Unknown` for the spelling.
let mock = MockServer::start().await;
let envelope = r#"{"errorType":"DuplicatedOrder","description":"order already exists"}"#;
Mock::given(method("POST"))
.and(path("/api/v1/orders"))
.respond_with(ResponseTemplate::new(400).set_body_string(envelope))
.expect(1)
.mount(&mock)
.await;

let pool = pool_with_mainnet_at(&mock);
let err = pool
.submit_order_json(Chain::Mainnet.id(), sample_order_json().as_bytes())
.await
.expect_err("orderbook 400 surfaces as error");

match err {
CowApiError::Orderbook(cowprotocol::Error::OrderbookApi { status, api }) => {
assert_eq!(status, 400);
assert_eq!(api.error_type, "DuplicatedOrder");
assert_eq!(api.description, "order already exists");
}
other => panic!("expected OrderbookApi envelope, got {other:?}"),
}
}

#[tokio::test]
async fn submit_order_propagates_orderbook_response() {
let mock = MockServer::start().await;
Expand Down
17 changes: 13 additions & 4 deletions crates/nexum-engine/src/host/impls/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ impl nexum::host::chain::Host for HostState {
) -> Result<String, HostError> {
let start = Instant::now();
tracing::debug!(chain_id, %method, "chain::request");
let method_label = method.clone();
let result = match self.chain.request(chain_id, method, params).await {
Ok(body) => Ok(body),
Err(ProviderError::UnknownChain(id)) => Err(HostError {
Expand All @@ -27,23 +28,31 @@ impl nexum::host::chain::Host for HostState {
message: format!("chain {id} has no engine.toml RPC entry"),
data: None,
}),
Err(ProviderError::InvalidParams { detail, .. }) => Err(HostError {
Err(err @ ProviderError::InvalidParams { .. }) => Err(HostError {
domain: "chain".into(),
kind: HostErrorKind::InvalidInput,
code: -32602,
message: detail,
message: err.to_string(),
data: None,
}),
Err(ProviderError::Rpc { detail, .. }) => Err(HostError {
Err(err @ ProviderError::Rpc { .. }) => Err(HostError {
domain: "chain".into(),
kind: HostErrorKind::Internal,
code: -32603,
message: detail,
message: err.to_string(),
data: None,
}),
Err(err) => Err(internal_error("chain", err.to_string())),
};
tracing::trace!(elapsed_ms = ?start.elapsed(), "chain::request done");
let outcome = if result.is_ok() { "ok" } else { "err" };
metrics::counter!(
"shepherd_chain_request_total",
"chain_id" => chain_id.to_string(),
"method" => method_label,
"outcome" => outcome,
)
.increment(1);
result
}

Expand Down
Loading