Skip to content
Closed
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
22 changes: 20 additions & 2 deletions crates/nexum-engine/src/engine_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,26 @@ use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use serde::Deserialize;
use thiserror::Error;
use tracing::{info, warn};

/// Errors surfaced by [`load_or_default`].
///
/// Library-side modules must not propagate `anyhow::Error`; the rust
/// idiomatic rubric reserves `anyhow` for `main.rs` and
/// `supervisor.rs` top-level dispatch. The variants carry the
/// upstream error via `#[from]` so the caller in `main.rs` (which
/// uses `anyhow`) gets a free conversion through `?`.
#[derive(Debug, Error)]
pub enum EngineConfigError {
/// Failed to read the config file from disk.
#[error("read engine config: {0}")]
Io(#[from] std::io::Error),
/// Config file was unparseable as TOML.
#[error("parse engine config: {0}")]
Toml(#[from] toml::de::Error),
}

/// Engine-side configuration loaded from `engine.toml`.
#[derive(Debug, Default, Deserialize)]
pub struct EngineConfig {
Expand Down Expand Up @@ -131,8 +149,8 @@ fn default_log_level() -> String {
}

/// Read an engine config from disk, returning defaults if the file is
/// missing. Parse errors propagate.
pub fn load_or_default(path: Option<&Path>) -> anyhow::Result<EngineConfig> {
/// missing. Parse errors propagate via [`EngineConfigError`].
pub fn load_or_default(path: Option<&Path>) -> Result<EngineConfig, EngineConfigError> {
let path = match path {
Some(p) => p.to_path_buf(),
None => PathBuf::from("engine.toml"),
Expand Down
16 changes: 1 addition & 15 deletions crates/nexum-engine/src/host/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Small constructors that wrap the WIT `HostError` shape, used by
//! every `Host` trait impl, plus the lowercase hex encoder shared by
//! the `cow-api` submission path.
//! every `Host` trait impl.

use crate::bindings::HostError;
use crate::bindings::nexum::host::types::HostErrorKind;
Expand All @@ -27,16 +26,3 @@ pub(crate) fn internal_error(domain: &str, detail: impl Into<String>) -> HostErr
data: None,
}
}

/// Lowercase hex encoder. Kept in the engine binary rather than
/// pulling a `hex` crate just for one call site. Writes into the
/// pre-allocated buffer to avoid the per-byte `String` allocation
/// `format!("{b:02x}")` would do.
pub(crate) fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
write!(s, "{b:02x}").expect("writing to String never fails");
}
s
}
8 changes: 4 additions & 4 deletions crates/nexum-engine/src/host/impls/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ 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())),
Expand Down
4 changes: 2 additions & 2 deletions crates/nexum-engine/src/host/impls/cow_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::time::Instant;
use crate::bindings::nexum::host::types::HostErrorKind;
use crate::bindings::{HostError, shepherd};
use crate::host::cow_orderbook::CowApiError;
use crate::host::error::{hex_encode, internal_error, unimplemented};
use crate::host::error::{internal_error, unimplemented};
use crate::host::state::HostState;

impl shepherd::cow::cow_api::Host for HostState {
Expand Down Expand Up @@ -58,7 +58,7 @@ impl shepherd::cow::cow_api::Host for HostState {
let start = Instant::now();
tracing::debug!(chain_id, bytes = order_data.len(), "cow-api::submit-order");
let result = match self.cow.submit_order_json(chain_id, &order_data).await {
Ok(uid) => Ok(format!("0x{}", hex_encode(uid.as_slice()))),
Ok(uid) => Ok(alloy_primitives::hex::encode_prefixed(uid.as_slice())),
Err(CowApiError::UnknownChain(id)) => Err(unimplemented(
"cow-api",
format!("chain {id} not in cowprotocol"),
Expand Down
13 changes: 6 additions & 7 deletions crates/nexum-engine/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
//! - [`state`]: `HostState` struct + `WasiView` impl, the receiver
//! every WIT `Host` trait is implemented for.
//! - [`error`]: small constructors that build the WIT `HostError`
//! shape (`unimplemented`, `internal_error`) plus the lowercase
//! `hex_encode` shared by the `cow-api` submission path.
//! shape (`unimplemented`, `internal_error`).
//! - [`cow_orderbook`], [`provider_pool`], [`local_store_redb`]:
//! capability backends. Pure code with no bindgen types, so each
//! can be unit-tested without spinning up a wasmtime store.
//! - [`impls`] (private): the bindgen-side trait impls, one file per
//! WIT interface, that dispatch to the backends above.

pub mod cow_orderbook;
pub mod error;
pub(crate) mod cow_orderbook;
pub(crate) mod error;
mod impls;
pub mod local_store_redb;
pub mod provider_pool;
pub mod state;
pub(crate) mod local_store_redb;
pub(crate) mod provider_pool;
pub(crate) mod state;
58 changes: 34 additions & 24 deletions crates/nexum-engine/src/host/provider_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,16 @@ impl ProviderPool {
ProviderBuilder::new()
.connect_ws(WsConnect::new(url))
.await
.map_err(|e| ProviderError::Connect {
.map_err(|source| ProviderError::Connect {
chain_id: *chain_id,
detail: e.to_string(),
source,
})?
.erased()
} else {
let parsed: url::Url =
url.parse()
.map_err(|e: url::ParseError| ProviderError::Connect {
chain_id: *chain_id,
detail: e.to_string(),
})?;
let parsed: url::Url = url.parse().map_err(|source| ProviderError::ConnectUrl {
chain_id: *chain_id,
source,
})?;
ProviderBuilder::new().connect_http(parsed).erased()
};
providers.insert(*chain_id, provider);
Expand Down Expand Up @@ -87,9 +85,9 @@ impl ProviderPool {
let sub = provider
.subscribe_blocks()
.await
.map_err(|e| ProviderError::Rpc {
.map_err(|source| ProviderError::Rpc {
method: "eth_subscribe(newHeads)".into(),
detail: e.to_string(),
source,
})?;
let stream = sub.into_stream().map(Ok::<_, ProviderError>);
Ok(Box::pin(stream))
Expand All @@ -108,9 +106,9 @@ impl ProviderPool {
let sub = provider
.subscribe_logs(&filter)
.await
.map_err(|e| ProviderError::Rpc {
.map_err(|source| ProviderError::Rpc {
method: "eth_subscribe(logs)".into(),
detail: e.to_string(),
source,
})?;
let stream = sub.into_stream().map(Ok::<_, ProviderError>);
Ok(Box::pin(stream))
Expand All @@ -132,9 +130,9 @@ impl ProviderPool {
// Pass the params through as a raw JSON value so alloy does
// not re-encode them on the way to the node.
let params: Box<RawValue> =
RawValue::from_string(params_json).map_err(|e| ProviderError::InvalidParams {
RawValue::from_string(params_json).map_err(|source| ProviderError::InvalidParams {
method: method.clone(),
detail: e.to_string(),
source,
})?;
// `raw_request` consumes the method name; clone once for the
// error branch so the success path moves the original string
Expand All @@ -144,9 +142,9 @@ impl ProviderPool {
provider
.raw_request(method.into(), params)
.await
.map_err(|e| ProviderError::Rpc {
.map_err(|source| ProviderError::Rpc {
method: method_for_err,
detail: e.to_string(),
source,
})?;
Ok(result.get().to_owned())
}
Expand All @@ -164,28 +162,40 @@ pub enum ProviderError {
#[error("unknown chain {0} (no engine.toml entry)")]
UnknownChain(u64),
/// Could not open the underlying transport.
#[error("connect chain {chain_id}: {detail}")]
#[error("connect chain {chain_id}: {source}")]
Connect {
/// Chain id we failed to dial.
chain_id: u64,
/// Transport-side error string.
detail: String,
/// Transport-side error.
#[source]
source: alloy_transport::TransportError,
},
/// HTTP RPC URL did not parse as a [`url::Url`].
#[error("connect chain {chain_id}: invalid URL: {source}")]
ConnectUrl {
/// Chain id whose `rpc_url` was malformed.
chain_id: u64,
/// Underlying parse failure.
#[source]
source: url::ParseError,
},
/// The guest-supplied JSON params did not parse.
#[error("invalid params JSON for `{method}`: {detail}")]
#[error("invalid params JSON for `{method}`: {source}")]
InvalidParams {
/// RPC method name.
method: String,
/// JSON-parser detail.
detail: String,
#[source]
source: serde_json::Error,
},
/// The node returned an error for the dispatched call.
#[error("rpc `{method}` failed: {detail}")]
#[error("rpc `{method}` failed: {source}")]
Rpc {
/// RPC method name.
method: String,
/// Transport-side error string.
detail: String,
/// Transport-side error.
#[source]
source: alloy_transport::TransportError,
},
}

Expand Down
21 changes: 17 additions & 4 deletions crates/nexum-engine/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,15 @@ async fn main() -> anyhow::Result<()> {
return Ok(());
}

let block_streams =
runtime::event_loop::open_block_streams(&provider_pool, &block_chains).await;
let log_streams = runtime::event_loop::open_log_streams(&provider_pool, log_subs).await;
let mut reconnect_tasks = tokio::task::JoinSet::new();
let block_streams = runtime::event_loop::open_block_streams(
&provider_pool,
&block_chains,
&mut reconnect_tasks,
)
.await;
let log_streams =
runtime::event_loop::open_log_streams(&provider_pool, log_subs, &mut reconnect_tasks).await;

let shutdown = async {
match runtime::event_loop::wait_for_shutdown_signal().await {
Expand All @@ -169,7 +175,14 @@ async fn main() -> anyhow::Result<()> {
}
};

runtime::event_loop::run(&mut supervisor, block_streams, log_streams, shutdown).await;
runtime::event_loop::run(
&mut supervisor,
block_streams,
log_streams,
reconnect_tasks,
shutdown,
)
.await;
info!("done");
Ok(())
}
55 changes: 21 additions & 34 deletions crates/nexum-engine/src/manifest/error.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,38 @@
//! Error types for manifest parsing and capability enforcement.

use thiserror::Error;

use super::types::KNOWN_CAPABILITIES;

/// Errors returned while loading or validating a manifest.
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum ParseError {
Io(std::io::Error),
Toml(toml::de::Error),
/// Failed to read the manifest file from disk.
#[error("manifest: i/o: {0}")]
Io(#[from] std::io::Error),
/// Manifest file was not valid TOML.
#[error("manifest: parse: {0}")]
Toml(#[from] toml::de::Error),
/// `[capabilities].required` or `.optional` listed a capability
/// the engine does not recognise.
#[error(
"manifest: unknown capability {name:?} in [capabilities].required (known: {})",
KNOWN_CAPABILITIES.join(", "),
name = .0,
)]
UnknownCapability(String),
}

impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "manifest: i/o: {e}"),
Self::Toml(e) => write!(f, "manifest: parse: {e}"),
Self::UnknownCapability(name) => write!(
f,
"manifest: unknown capability {:?} in [capabilities].required (known: {})",
name,
KNOWN_CAPABILITIES.join(", ")
),
}
}
}

impl std::error::Error for ParseError {}

/// Error returned when a component's WIT imports exceed its declared capabilities.
#[derive(Debug)]
#[derive(Debug, Error)]
#[error(
"component imports `{capability}` ({wit_import}) but it is not listed in \
[capabilities].required or [capabilities].optional"
)]
pub struct CapabilityViolation {
/// Capability name (e.g. `"remote-store"`).
pub capability: String,
/// Full WIT import name as it appeared in the component (e.g.
/// `"nexum:host/remote-store@0.2.0"`).
pub wit_import: String,
}

impl std::fmt::Display for CapabilityViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"component imports `{}` ({}) but it is not listed in \
[capabilities].required or [capabilities].optional",
self.capability, self.wit_import
)
}
}

impl std::error::Error for CapabilityViolation {}
Loading
Loading