From 0148cd6b79e33f5518d20c8df5aa798b320daf45 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Wed, 15 Jul 2026 16:36:27 -0300 Subject: [PATCH] refactor(sdk): convert bind-macro error/level shims to From impls nexum-sdk's bind_host_via_wit_bindgen! emitted convert_chain_err, convert_fault, sdk_fault_into_wit, and convert_level as named functions for pure, infallible type-to-type conversions, so call sites read .map_err(sdk_fault_into_wit) instead of plain ?. All four become From impls instead - orphan-rule-allowed because the macro always expands in the per-cdylib module crate, where the wit-bindgen type is local and covers the From type parameter. Ripples to every module lib.rs call site (twap-monitor, ethflow-watcher, http-probe, stop-loss, price-alert, balance-tracker) and to shepherd-sdk's own bind_cow_host_via_wit_bindgen!, which called convert_fault internally. No behavior change. Fixes #264. --- crates/nexum-sdk/src/wit_bindgen_macro.rs | 160 ++++++++++--------- crates/shepherd-sdk/src/wit_bindgen_macro.rs | 2 +- modules/ethflow-watcher/src/lib.rs | 7 +- modules/examples/balance-tracker/src/lib.rs | 10 +- modules/examples/http-probe/src/lib.rs | 11 +- modules/examples/price-alert/src/lib.rs | 7 +- modules/examples/stop-loss/src/lib.rs | 6 +- modules/twap-monitor/src/lib.rs | 8 +- 8 files changed, 112 insertions(+), 99 deletions(-) diff --git a/crates/nexum-sdk/src/wit_bindgen_macro.rs b/crates/nexum-sdk/src/wit_bindgen_macro.rs index 3e33c68c..1a2451c5 100644 --- a/crates/nexum-sdk/src/wit_bindgen_macro.rs +++ b/crates/nexum-sdk/src/wit_bindgen_macro.rs @@ -3,10 +3,10 @@ //! //! Before this macro existed, each module hand-rolled ~80 lines of //! mechanical glue: the `struct WitBindgenHost;` plus the core trait -//! impls (`ChainHost`, `LocalStoreHost`, `LoggingHost`) plus -//! `convert_chain_err` / `convert_fault` / `sdk_fault_into_wit` / -//! `convert_level`. The code differed across modules in zero places -//! that were not bugs. +//! impls (`ChainHost`, `LocalStoreHost`, `LoggingHost`) plus the +//! `ChainError` / `Fault` / `Level` conversions (as `From` impls, so +//! call sites use `?` / `.into()`, never a named function). The code +//! differed across modules in zero places that were not bugs. //! //! The macro assumes the module compiles against a world that //! includes `nexum:host/event-module` with `wit_bindgen::generate!({ @@ -24,23 +24,24 @@ //! ```ignore //! wit_bindgen::generate!({ /* ... */ }); //! nexum_sdk::bind_host_via_wit_bindgen!(); -//! // `WitBindgenHost`, `convert_chain_err`, `convert_fault`, -//! // `sdk_fault_into_wit`, `convert_level`, `HostLogSink`, and -//! // `install_tracing` are now in -//! // scope, with the wit-bindgen and SDK types tied together through -//! // identifier resolution. Call `install_tracing()` once at the top -//! // of `Guest::init` to route `tracing::info!(...)` to the host. A -//! // `From for nexum_sdk::events::Log` is also emitted so -//! // `on_event` maps a chain-logs batch straight to `Vec`. +//! // `WitBindgenHost`, `HostLogSink`, and `install_tracing` are now in +//! // scope, along with `From` impls tying the wit-bindgen and SDK +//! // error/level types together (`?` / `.into()` at call sites, never +//! // a named conversion function). Call `install_tracing()` once at +//! // the top of `Guest::init` to route `tracing::info!(...)` to the +//! // host. A `From for nexum_sdk::events::Log` is also +//! // emitted so `on_event` maps a chain-logs batch straight to +//! // `Vec`. //! ``` /// Generate `WitBindgenHost` + the core `*Host` trait impls + the -/// error / level converters. See module docs. +/// error / level `From` conversions. See module docs. /// /// Macro hygiene note: `macro_rules!` is not hygienic for type names -/// or function items, so the names `WitBindgenHost`, `convert_chain_err`, -/// `convert_fault`, `sdk_fault_into_wit`, `convert_level`, `HostLogSink`, -/// and `install_tracing` are intentionally visible in the caller's scope. +/// or item names, so `WitBindgenHost`, `HostLogSink`, and +/// `install_tracing` are intentionally visible in the caller's scope +/// (the error/level conversions are anonymous trait impls, so they +/// need no name). #[macro_export] macro_rules! bind_host_via_wit_bindgen { () => { @@ -57,7 +58,8 @@ macro_rules! bind_host_via_wit_bindgen { method: &str, params: &str, ) -> ::core::result::Result<::std::string::String, $crate::host::ChainError> { - nexum::host::chain::request(chain_id, method, params).map_err(convert_chain_err) + nexum::host::chain::request(chain_id, method, params) + .map_err(::core::convert::Into::into) } } @@ -69,47 +71,49 @@ macro_rules! bind_host_via_wit_bindgen { ::core::option::Option<::std::vec::Vec>, $crate::host::Fault, > { - nexum::host::local_store::get(key).map_err(convert_fault) + nexum::host::local_store::get(key).map_err(::core::convert::Into::into) } fn set( &self, key: &str, value: &[u8], ) -> ::core::result::Result<(), $crate::host::Fault> { - nexum::host::local_store::set(key, value).map_err(convert_fault) + nexum::host::local_store::set(key, value).map_err(::core::convert::Into::into) } fn delete(&self, key: &str) -> ::core::result::Result<(), $crate::host::Fault> { - nexum::host::local_store::delete(key).map_err(convert_fault) + nexum::host::local_store::delete(key).map_err(::core::convert::Into::into) } fn list_keys( &self, prefix: &str, ) -> ::core::result::Result<::std::vec::Vec<::std::string::String>, $crate::host::Fault> { - nexum::host::local_store::list_keys(prefix).map_err(convert_fault) + nexum::host::local_store::list_keys(prefix).map_err(::core::convert::Into::into) } } impl $crate::host::LoggingHost for WitBindgenHost { fn log(&self, level: $crate::Level, message: &str) { - nexum::host::logging::log(convert_level(level), message); + nexum::host::logging::log(level.into(), message); } } /// Lift the wit-bindgen `chain.chain-error` (per-cdylib) into /// the SDK's host-neutral `ChainError`. Exhaustive on both the /// `Fault` vocabulary and the `RpcError` shape. - fn convert_chain_err(e: nexum::host::chain::ChainError) -> $crate::host::ChainError { - match e { - nexum::host::chain::ChainError::Fault(f) => { - $crate::host::ChainError::Fault(convert_fault(f)) - } - nexum::host::chain::ChainError::Rpc(r) => { - $crate::host::ChainError::Rpc($crate::host::RpcError { - code: r.code, - message: r.message, - data: r.data.map(::core::convert::Into::into), - }) + impl ::core::convert::From for $crate::host::ChainError { + fn from(e: nexum::host::chain::ChainError) -> Self { + match e { + nexum::host::chain::ChainError::Fault(f) => { + $crate::host::ChainError::Fault(f.into()) + } + nexum::host::chain::ChainError::Rpc(r) => { + $crate::host::ChainError::Rpc($crate::host::RpcError { + code: r.code, + message: r.message, + data: r.data.map(::core::convert::Into::into), + }) + } } } } @@ -117,19 +121,27 @@ macro_rules! bind_host_via_wit_bindgen { /// Lift the wit-bindgen `types.fault` (per-cdylib) into the /// SDK's `Fault`. Exhaustive on the seven-case vocabulary; the /// `rate-limited` backoff record maps field for field. - fn convert_fault(f: nexum::host::types::Fault) -> $crate::host::Fault { - match f { - nexum::host::types::Fault::Unsupported(s) => $crate::host::Fault::Unsupported(s), - nexum::host::types::Fault::Unavailable(s) => $crate::host::Fault::Unavailable(s), - nexum::host::types::Fault::Denied(s) => $crate::host::Fault::Denied(s), - nexum::host::types::Fault::RateLimited(rl) => { - $crate::host::Fault::RateLimited($crate::host::RateLimit { - retry_after_ms: rl.retry_after_ms, - }) + impl ::core::convert::From for $crate::host::Fault { + fn from(f: nexum::host::types::Fault) -> Self { + match f { + nexum::host::types::Fault::Unsupported(s) => { + $crate::host::Fault::Unsupported(s) + } + nexum::host::types::Fault::Unavailable(s) => { + $crate::host::Fault::Unavailable(s) + } + nexum::host::types::Fault::Denied(s) => $crate::host::Fault::Denied(s), + nexum::host::types::Fault::RateLimited(rl) => { + $crate::host::Fault::RateLimited($crate::host::RateLimit { + retry_after_ms: rl.retry_after_ms, + }) + } + nexum::host::types::Fault::Timeout => $crate::host::Fault::Timeout, + nexum::host::types::Fault::InvalidInput(s) => { + $crate::host::Fault::InvalidInput(s) + } + nexum::host::types::Fault::Internal(s) => $crate::host::Fault::Internal(s), } - nexum::host::types::Fault::Timeout => $crate::host::Fault::Timeout, - nexum::host::types::Fault::InvalidInput(s) => $crate::host::Fault::InvalidInput(s), - nexum::host::types::Fault::Internal(s) => $crate::host::Fault::Internal(s), } } @@ -142,21 +154,23 @@ macro_rules! bind_host_via_wit_bindgen { /// `#[non_exhaustive]`: a future SDK-side case must compile in /// module crates without source changes. Falls back to /// `internal` carrying the `Display` detail. - fn sdk_fault_into_wit(f: $crate::host::Fault) -> nexum::host::types::Fault { - use nexum::host::types::{Fault as WitFault, RateLimit as WitRateLimit}; - match f { - $crate::host::Fault::Unsupported(s) => WitFault::Unsupported(s), - $crate::host::Fault::Unavailable(s) => WitFault::Unavailable(s), - $crate::host::Fault::Denied(s) => WitFault::Denied(s), - $crate::host::Fault::RateLimited(rl) => WitFault::RateLimited(WitRateLimit { - retry_after_ms: rl.retry_after_ms, - }), - $crate::host::Fault::Timeout => WitFault::Timeout, - $crate::host::Fault::InvalidInput(s) => WitFault::InvalidInput(s), - $crate::host::Fault::Internal(s) => WitFault::Internal(s), - // `$crate::host::Fault` is `#[non_exhaustive]`; a future - // SDK case lands here as `internal`. - other => WitFault::Internal(::std::string::ToString::to_string(&other)), + impl ::core::convert::From<$crate::host::Fault> for nexum::host::types::Fault { + fn from(f: $crate::host::Fault) -> Self { + use nexum::host::types::{Fault as WitFault, RateLimit as WitRateLimit}; + match f { + $crate::host::Fault::Unsupported(s) => WitFault::Unsupported(s), + $crate::host::Fault::Unavailable(s) => WitFault::Unavailable(s), + $crate::host::Fault::Denied(s) => WitFault::Denied(s), + $crate::host::Fault::RateLimited(rl) => WitFault::RateLimited(WitRateLimit { + retry_after_ms: rl.retry_after_ms, + }), + $crate::host::Fault::Timeout => WitFault::Timeout, + $crate::host::Fault::InvalidInput(s) => WitFault::InvalidInput(s), + $crate::host::Fault::Internal(s) => WitFault::Internal(s), + // `$crate::host::Fault` is `#[non_exhaustive]`; a future + // SDK case lands here as `internal`. + other => WitFault::Internal(::std::string::ToString::to_string(&other)), + } } } @@ -164,18 +178,20 @@ macro_rules! bind_host_via_wit_bindgen { /// `logging::Level` wire enum. `Level` is a set of associated /// consts, not a matchable enum, so compare rather than match; /// the five tiers are total, so the final arm is `Trace`. - fn convert_level(level: $crate::Level) -> nexum::host::logging::Level { - use $crate::Level; - if level == Level::ERROR { - nexum::host::logging::Level::Error - } else if level == Level::WARN { - nexum::host::logging::Level::Warn - } else if level == Level::INFO { - nexum::host::logging::Level::Info - } else if level == Level::DEBUG { - nexum::host::logging::Level::Debug - } else { - nexum::host::logging::Level::Trace + impl ::core::convert::From<$crate::Level> for nexum::host::logging::Level { + fn from(level: $crate::Level) -> Self { + use $crate::Level; + if level == Level::ERROR { + nexum::host::logging::Level::Error + } else if level == Level::WARN { + nexum::host::logging::Level::Warn + } else if level == Level::INFO { + nexum::host::logging::Level::Info + } else if level == Level::DEBUG { + nexum::host::logging::Level::Debug + } else { + nexum::host::logging::Level::Trace + } } } diff --git a/crates/shepherd-sdk/src/wit_bindgen_macro.rs b/crates/shepherd-sdk/src/wit_bindgen_macro.rs index 0a4c9b14..63f4d70f 100644 --- a/crates/shepherd-sdk/src/wit_bindgen_macro.rs +++ b/crates/shepherd-sdk/src/wit_bindgen_macro.rs @@ -37,7 +37,7 @@ macro_rules! bind_cow_host_via_wit_bindgen { fn convert_cow_err(e: shepherd::cow::cow_api::CowApiError) -> $crate::cow::CowApiError { match e { shepherd::cow::cow_api::CowApiError::Fault(f) => { - $crate::cow::CowApiError::Fault(convert_fault(f)) + $crate::cow::CowApiError::Fault(f.into()) } shepherd::cow::cow_api::CowApiError::Http(h) => { $crate::cow::CowApiError::Http($crate::cow::HttpFailure { diff --git a/modules/ethflow-watcher/src/lib.rs b/modules/ethflow-watcher/src/lib.rs index 3286a566..7057f61c 100644 --- a/modules/ethflow-watcher/src/lib.rs +++ b/modules/ethflow-watcher/src/lib.rs @@ -44,8 +44,8 @@ wit_bindgen::generate!({ pub mod strategy; -// `WitBindgenHost`, `sdk_fault_into_wit`, `convert_level` -// are generated below. Single source of truth in `nexum-sdk` + `shepherd-sdk`. +// `WitBindgenHost` and its error/level `From` conversions are +// generated below. Single source of truth in `nexum-sdk` + `shepherd-sdk`. // Gated on `wasm32` so the strategy can be reused in native targets // (e.g. the backtest replay harness in `crates/shepherd-backtest`). #[cfg(target_arch = "wasm32")] @@ -69,8 +69,7 @@ impl Guest for EthFlowWatcher { if let types::Event::ChainLogs(batch) = event { let logs: Vec = batch.logs.into_iter().map(Into::into).collect(); - strategy::on_chain_logs(&WitBindgenHost, batch.chain_id, &logs) - .map_err(sdk_fault_into_wit)?; + strategy::on_chain_logs(&WitBindgenHost, batch.chain_id, &logs)?; } // Block / Tick / Message are not used by this module. Ok(()) diff --git a/modules/examples/balance-tracker/src/lib.rs b/modules/examples/balance-tracker/src/lib.rs index 55aec121..c55f5c0a 100644 --- a/modules/examples/balance-tracker/src/lib.rs +++ b/modules/examples/balance-tracker/src/lib.rs @@ -39,9 +39,9 @@ use std::sync::OnceLock; use nexum::host::types; -// `WitBindgenHost`, `sdk_fault_into_wit`, `convert_level`, -// `HostLogSink`, `install_tracing` are generated below. Single source -// of truth in `nexum-sdk`. +// `WitBindgenHost`, its error/level `From` conversions, `HostLogSink`, +// and `install_tracing` are generated below. Single source of truth +// in `nexum-sdk`. nexum_sdk::bind_host_via_wit_bindgen!(); static SETTINGS: OnceLock = OnceLock::new(); @@ -51,7 +51,7 @@ struct BalanceTracker; impl Guest for BalanceTracker { fn init(config: Vec<(String, String)>) -> Result<(), Fault> { install_tracing(); - let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?; + let cfg = strategy::parse_config(&config)?; tracing::info!( "balance-tracker init: {} addresses, threshold={} wei", cfg.addresses.len(), @@ -66,7 +66,7 @@ impl Guest for BalanceTracker { return Ok(()); }; if let types::Event::Block(block) = event { - strategy::on_block(&WitBindgenHost, block.chain_id, cfg).map_err(sdk_fault_into_wit)?; + strategy::on_block(&WitBindgenHost, block.chain_id, cfg)?; } Ok(()) } diff --git a/modules/examples/http-probe/src/lib.rs b/modules/examples/http-probe/src/lib.rs index bb9ab42e..4d9b516b 100644 --- a/modules/examples/http-probe/src/lib.rs +++ b/modules/examples/http-probe/src/lib.rs @@ -48,9 +48,9 @@ use std::sync::OnceLock; use nexum::host::types; -// `WitBindgenHost`, `sdk_fault_into_wit`, `convert_level`, -// `HostLogSink`, `install_tracing` are generated below. Single source -// of truth in `nexum-sdk`. +// `WitBindgenHost`, its error/level `From` conversions, `HostLogSink`, +// and `install_tracing` are generated below. Single source of truth +// in `nexum-sdk`. nexum_sdk::bind_host_via_wit_bindgen!(); static SETTINGS: OnceLock = OnceLock::new(); @@ -60,7 +60,7 @@ struct HttpProbe; impl Guest for HttpProbe { fn init(config: Vec<(String, String)>) -> Result<(), Fault> { install_tracing(); - let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?; + let cfg = strategy::parse_config(&config)?; tracing::info!( "http-probe init: probe_url={} denied_url={} every_n_blocks={}", cfg.probe_url, @@ -76,8 +76,7 @@ impl Guest for HttpProbe { return Ok(()); }; if let types::Event::Block(block) = event { - strategy::on_block(&nexum_sdk::http::WasiFetch, cfg, block.number) - .map_err(sdk_fault_into_wit)?; + strategy::on_block(&nexum_sdk::http::WasiFetch, cfg, block.number)?; } Ok(()) } diff --git a/modules/examples/price-alert/src/lib.rs b/modules/examples/price-alert/src/lib.rs index e9d922d0..23cc088b 100644 --- a/modules/examples/price-alert/src/lib.rs +++ b/modules/examples/price-alert/src/lib.rs @@ -54,7 +54,7 @@ use std::sync::OnceLock; use nexum::host::types; -// `WitBindgenHost`, `sdk_fault_into_wit`, `convert_level` are generated +// `WitBindgenHost` and its error/level `From` conversions are generated // below. Single source of truth in `nexum-sdk`. nexum_sdk::bind_host_via_wit_bindgen!(); @@ -65,7 +65,7 @@ struct PriceAlert; impl Guest for PriceAlert { fn init(config: Vec<(String, String)>) -> Result<(), Fault> { install_tracing(); - let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?; + let cfg = strategy::parse_config(&config)?; tracing::info!( "price-alert init: oracle={:#x} threshold={} direction={:?} every_n_blocks={}", cfg.oracle_address, @@ -82,8 +82,7 @@ impl Guest for PriceAlert { return Ok(()); }; if let types::Event::Block(block) = event { - strategy::on_block(&WitBindgenHost, block.chain_id, cfg, block.number) - .map_err(sdk_fault_into_wit)?; + strategy::on_block(&WitBindgenHost, block.chain_id, cfg, block.number)?; } Ok(()) } diff --git a/modules/examples/stop-loss/src/lib.rs b/modules/examples/stop-loss/src/lib.rs index b5df56f9..92fa2d89 100644 --- a/modules/examples/stop-loss/src/lib.rs +++ b/modules/examples/stop-loss/src/lib.rs @@ -34,7 +34,7 @@ use std::sync::OnceLock; use nexum::host::types; -// `WitBindgenHost`, `sdk_fault_into_wit`, `convert_level` are generated +// `WitBindgenHost` and its error/level `From` conversions are generated // below. Single source of truth in `nexum-sdk` + `shepherd-sdk`. shepherd_sdk::bind_cow_host_via_wit_bindgen!(); @@ -45,7 +45,7 @@ struct StopLoss; impl Guest for StopLoss { fn init(config: Vec<(String, String)>) -> Result<(), Fault> { install_tracing(); - let cfg = strategy::parse_config(&config).map_err(sdk_fault_into_wit)?; + let cfg = strategy::parse_config(&config)?; tracing::info!( "stop-loss init: owner={:#x} trigger={} sell={:#x} buy={:#x}", cfg.owner, @@ -62,7 +62,7 @@ impl Guest for StopLoss { return Ok(()); }; if let types::Event::Block(block) = event { - strategy::on_block(&WitBindgenHost, block.chain_id, cfg).map_err(sdk_fault_into_wit)?; + strategy::on_block(&WitBindgenHost, block.chain_id, cfg)?; } Ok(()) } diff --git a/modules/twap-monitor/src/lib.rs b/modules/twap-monitor/src/lib.rs index e6f4fa62..c18ce96d 100644 --- a/modules/twap-monitor/src/lib.rs +++ b/modules/twap-monitor/src/lib.rs @@ -33,8 +33,8 @@ mod strategy; use nexum::host::types; -// `WitBindgenHost`, `sdk_fault_into_wit`, `convert_level` -// are generated below. Single source of truth in `nexum-sdk` + `shepherd-sdk`. +// `WitBindgenHost` and its error/level `From` conversions are +// generated below. Single source of truth in `nexum-sdk` + `shepherd-sdk`. shepherd_sdk::bind_cow_host_via_wit_bindgen!(); struct TwapMonitor; @@ -51,7 +51,7 @@ impl Guest for TwapMonitor { types::Event::ChainLogs(batch) => { let logs: Vec = batch.logs.into_iter().map(Into::into).collect(); - strategy::on_chain_logs(&WitBindgenHost, &logs).map_err(sdk_fault_into_wit)?; + strategy::on_chain_logs(&WitBindgenHost, &logs)?; } types::Event::Block(block) => { let info = strategy::BlockInfo { @@ -59,7 +59,7 @@ impl Guest for TwapMonitor { number: block.number, timestamp: block.timestamp, }; - strategy::on_block(&WitBindgenHost, info).map_err(sdk_fault_into_wit)?; + strategy::on_block(&WitBindgenHost, info)?; } // Tick / Message are not used by this module. _ => {}