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
160 changes: 88 additions & 72 deletions crates/nexum-sdk/src/wit_bindgen_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!({
Expand All @@ -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<chain-log> for nexum_sdk::events::Log` is also emitted so
//! // `on_event` maps a chain-logs batch straight to `Vec<Log>`.
//! // `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<chain-log> for nexum_sdk::events::Log` is also
//! // emitted so `on_event` maps a chain-logs batch straight to
//! // `Vec<Log>`.
//! ```

/// 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 {
() => {
Expand All @@ -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)
}
}

Expand All @@ -69,67 +71,77 @@ macro_rules! bind_host_via_wit_bindgen {
::core::option::Option<::std::vec::Vec<u8>>,
$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<nexum::host::chain::ChainError> 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),
})
}
}
}
}

/// 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<nexum::host::types::Fault> 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),
}
}

Expand All @@ -142,40 +154,44 @@ 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)),
}
}
}

/// Translate a `tracing_core::Level` into the 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
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/shepherd-sdk/src/wit_bindgen_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 3 additions & 4 deletions modules/ethflow-watcher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -69,8 +69,7 @@ impl Guest for EthFlowWatcher {
if let types::Event::ChainLogs(batch) = event {
let logs: Vec<nexum_sdk::events::Log> =
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(())
Expand Down
10 changes: 5 additions & 5 deletions modules/examples/balance-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<strategy::Settings> = OnceLock::new();
Expand All @@ -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(),
Expand All @@ -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(())
}
Expand Down
11 changes: 5 additions & 6 deletions modules/examples/http-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<strategy::Settings> = OnceLock::new();
Expand All @@ -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,
Expand All @@ -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(())
}
Expand Down
7 changes: 3 additions & 4 deletions modules/examples/price-alert/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();

Expand All @@ -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,
Expand All @@ -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(())
}
Expand Down
6 changes: 3 additions & 3 deletions modules/examples/stop-loss/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();

Expand All @@ -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,
Expand All @@ -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(())
}
Expand Down
Loading
Loading